diff --git a/apps/admin-ui/cypress/e2e/realm_settings_user_profile_tab.spec.ts b/apps/admin-ui/cypress/e2e/realm_settings_user_profile_tab.spec.ts index d98d170794..c0554f1c68 100644 --- a/apps/admin-ui/cypress/e2e/realm_settings_user_profile_tab.spec.ts +++ b/apps/admin-ui/cypress/e2e/realm_settings_user_profile_tab.spec.ts @@ -92,7 +92,7 @@ describe("User profile tabs", () => { userProfileTab.cancelRemovingValidator(); userProfileTab.removeValidator(); - cy.get('tbody [class="kc-emptyValidators"]').contains("No validators."); + cy.get(".kc-emptyValidators").contains("No validators."); }); }); diff --git a/apps/admin-ui/cypress/support/pages/admin_console/manage/realm_settings/UserProfile.ts b/apps/admin-ui/cypress/support/pages/admin_console/manage/realm_settings/UserProfile.ts index f80f0b6ecf..0633e6ff32 100644 --- a/apps/admin-ui/cypress/support/pages/admin_console/manage/realm_settings/UserProfile.ts +++ b/apps/admin-ui/cypress/support/pages/admin_console/manage/realm_settings/UserProfile.ts @@ -14,7 +14,7 @@ export default class UserProfile { private newAttributeCheckboxes = 'input[type="checkbox"]'; private newAttributeRequiredFor = 'input[name="roles"]'; private newAttributeRequiredWhen = 'input[name="requiredWhen"]'; - private newAttributeEmptyValidators = 'tbody [class="kc-emptyValidators"]'; + private newAttributeEmptyValidators = ".kc-emptyValidators"; private newAttributeAnnotationKey = 'input[name="annotations[0].key"]'; private newAttributeAnnotationValue = 'input[name="annotations[0].value"]'; private validatorRolesList = 'tbody [data-label="Role name"]'; diff --git a/apps/admin-ui/src/realm-settings/NewAttributeSettings.tsx b/apps/admin-ui/src/realm-settings/NewAttributeSettings.tsx index 0001be6bf8..328023eaec 100644 --- a/apps/admin-ui/src/realm-settings/NewAttributeSettings.tsx +++ b/apps/admin-ui/src/realm-settings/NewAttributeSettings.tsx @@ -23,13 +23,30 @@ import { useAlerts } from "../components/alert/Alerts"; import { UserProfileProvider } from "./user-profile/UserProfileContext"; import type { UserProfileAttribute } from "@keycloak/keycloak-admin-client/lib/defs/userProfileConfig"; import type { AttributeParams } from "./routes/Attribute"; -import type { KeyValueType } from "../components/key-value-form/key-value-convert"; import { convertToFormValues } from "../util"; import { flatten } from "flat"; import "./realm-settings-section.css"; -type UserProfileAttributeType = UserProfileAttribute & Attribute & Permission; +type IndexedAnnotations = { + key: string; + value: unknown; +}; + +export type IndexedValidations = { + key: string; + value?: Record[]; +}; + +type UserProfileAttributeType = Omit< + UserProfileAttribute, + "validations" | "annotations" +> & + Attribute & + Permission & { + validations: IndexedValidations[]; + annotations: IndexedAnnotations[]; + }; type Attribute = { roles: string[]; @@ -56,6 +73,8 @@ type PermissionEdit = [ } ]; +export const USERNAME_EMAIL = ["username", "email"]; + const CreateAttributeFormContent = ({ save, }: { @@ -108,12 +127,6 @@ export default function NewAttributeSettings() { const [config, setConfig] = useState(null); const editMode = attributeName ? true : false; - const convert = (obj: Record[] | undefined) => - Object.entries(obj || []).map(([key, value]) => ({ - key, - value, - })); - useFetch( () => adminClient.users.getProfile(), (config) => { @@ -133,15 +146,38 @@ export default function NewAttributeSettings() { Object.entries( flatten({ permissions, selector, required }, { safe: true }) ).map(([key, value]) => form.setValue(key, value)); - form.setValue("annotations", convert(annotations)); - form.setValue("validations", validations); + form.setValue( + "annotations", + Object.entries(annotations || {}).map(([key, value]) => ({ + key, + value, + })) + ); + form.setValue( + "validations", + Object.entries(validations || {}).map(([key, value]) => ({ + key, + value, + })) + ); form.setValue("isRequired", required !== undefined); }, [] ); const save = async (profileConfig: UserProfileAttributeType) => { - const annotations = (profileConfig.annotations! as KeyValueType[]).reduce( + const validations = profileConfig.validations.reduce( + (prevValidations, currentValidations) => { + prevValidations[currentValidations.key] = + currentValidations.value?.length === 0 + ? {} + : currentValidations.value; + return prevValidations; + }, + {} as Record + ); + + const annotations = profileConfig.annotations.reduce( (obj, item) => Object.assign(obj, { [item.key]: item.value }), {} ); @@ -161,6 +197,7 @@ export default function NewAttributeSettings() { selector: profileConfig.selector, permissions: profileConfig.permissions!, annotations, + validations, }, profileConfig.isRequired ? { required: profileConfig.required } diff --git a/apps/admin-ui/src/realm-settings/user-profile/attribute/AddValidatorDialog.tsx b/apps/admin-ui/src/realm-settings/user-profile/attribute/AddValidatorDialog.tsx index a72322fe17..d533e0e479 100644 --- a/apps/admin-ui/src/realm-settings/user-profile/attribute/AddValidatorDialog.tsx +++ b/apps/admin-ui/src/realm-settings/user-profile/attribute/AddValidatorDialog.tsx @@ -1,6 +1,11 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { Modal, ModalVariant } from "@patternfly/react-core"; +import { + Modal, + ModalVariant, + Text, + TextVariants, +} from "@patternfly/react-core"; import { TableComposable, Tbody, @@ -10,13 +15,13 @@ import { Tr, } from "@patternfly/react-table"; -import type { KeyValueType } from "../../../components/key-value-form/key-value-convert"; +import type { IndexedValidations } from "../../NewAttributeSettings"; import { AddValidatorRoleDialog } from "./AddValidatorRoleDialog"; import { Validator, validators as allValidator } from "./Validators"; import useToggle from "../../../utils/useToggle"; export type AddValidatorDialogProps = { - selectedValidators: KeyValueType[]; + selectedValidators: IndexedValidations[]; toggleDialog: () => void; onConfirm: (newValidator: Validator) => void; }; @@ -56,33 +61,39 @@ export const AddValidatorDialog = ({ isOpen onClose={toggleDialog} > - - - - {t("validatorDialogColNames.colName")} - {t("validatorDialogColNames.colDescription")} - - - - {validators.map((validator) => ( - { - setSelectedValidator(validator); - toggleModal(); - }} - isHoverable - > - - {validator.name} - - - {validator.description} - + {validators.length !== 0 ? ( + + + + {t("validatorDialogColNames.colName")} + {t("validatorDialogColNames.colDescription")} - ))} - - + + + {validators.map((validator) => ( + { + setSelectedValidator(validator); + toggleModal(); + }} + isHoverable + > + + {validator.name} + + + {validator.description} + + + ))} + + + ) : ( + + {t("realm-settings:emptyValidators")} + + )} ); diff --git a/apps/admin-ui/src/realm-settings/user-profile/attribute/AttributeGeneralSettings.tsx b/apps/admin-ui/src/realm-settings/user-profile/attribute/AttributeGeneralSettings.tsx index 90ac34ebf5..6ec6afcece 100644 --- a/apps/admin-ui/src/realm-settings/user-profile/attribute/AttributeGeneralSettings.tsx +++ b/apps/admin-ui/src/realm-settings/user-profile/attribute/AttributeGeneralSettings.tsx @@ -21,6 +21,7 @@ import { useParams } from "react-router-dom"; import { isEqual } from "lodash-es"; import "../../realm-settings-section.css"; +import { USERNAME_EMAIL } from "../../NewAttributeSettings"; const REQUIRED_FOR = [ { label: "requiredForLabel.both", value: ["admin", "user"] }, @@ -151,187 +152,44 @@ export const AttributeGeneralSettings = () => { )} > - - - { - if (value) { - form.setValue( - "selector.scopes", - clientScopes?.map((s) => s.name) - ); - } else { - form.setValue("selector.scopes", []); - } - }} - className="pf-u-mb-md" - /> - { - if (value) { - form.setValue("selector.scopes", []); - } else { - form.setValue( - "selector.scopes", - clientScopes?.map((s) => s.name) - ); - } - }} - className="pf-u-mb-md" - /> - - - ( - - )} - /> - - - - } - fieldId="kc-required" - hasNoPaddingTop - > - ( - - )} - /> - - {required && ( + {!USERNAME_EMAIL.includes(attributeName) && ( <> + - ( -
- {REQUIRED_FOR.map((option) => ( - { - onChange(option.value); - }} - label={t(option.label)} - className="kc-requiredFor-option" - /> - ))} -
- )} - /> -
- { if (value) { form.setValue( - "required.scopes", + "selector.scopes", clientScopes?.map((s) => s.name) ); } else { - form.setValue("required.scopes", []); + form.setValue("selector.scopes", []); } }} className="pf-u-mb-md" /> { if (value) { - form.setValue("required.scopes", []); + form.setValue("selector.scopes", []); } else { form.setValue( - "required.scopes", + "selector.scopes", clientScopes?.map((s) => s.name) ); } @@ -339,15 +197,15 @@ export const AttributeGeneralSettings = () => { className="pf-u-mb-md" /> - + ( setSelectRequiredForOpen(isOpen)} + selections={value} + onSelect={(_, selectedValue) => { + const option = selectedValue.toString(); + let changedValue = [""]; + if (value) { + changedValue = value.includes(option) + ? value.filter((item: string) => item !== option) + : [...value, option]; + } else { + changedValue = [option]; + } + onChange(changedValue); + }} + onClear={(selectedValues) => { + selectedValues.stopPropagation(); + onChange([]); + }} + isOpen={selectRequiredForOpen} + isDisabled={ + requiredScopes.length === clientScopes?.length + } + aria-labelledby={"scope"} + > + {clientScopes?.map((option) => ( + + ))} + + )} + /> + + + )} )} diff --git a/apps/admin-ui/src/realm-settings/user-profile/attribute/AttributeValidations.tsx b/apps/admin-ui/src/realm-settings/user-profile/attribute/AttributeValidations.tsx index 1896c1191e..b660892c76 100644 --- a/apps/admin-ui/src/realm-settings/user-profile/attribute/AttributeValidations.tsx +++ b/apps/admin-ui/src/realm-settings/user-profile/attribute/AttributeValidations.tsx @@ -7,7 +7,6 @@ import { TextVariants, } from "@patternfly/react-core"; import { useTranslation } from "react-i18next"; -import "../../realm-settings-section.css"; import { PlusCircleIcon } from "@patternfly/react-icons"; import { AddValidatorDialog } from "../attribute/AddValidatorDialog"; import { @@ -18,12 +17,12 @@ import { Thead, Tr, } from "@patternfly/react-table"; + +import type { IndexedValidations } from "../../NewAttributeSettings"; import { useConfirmDialog } from "../../../components/confirm-dialog/ConfirmDialog"; import useToggle from "../../../utils/useToggle"; import { useFormContext, useWatch } from "react-hook-form"; -import type { KeyValueType } from "../../../components/key-value-form/key-value-convert"; - import "../../realm-settings-section.css"; export const AttributeValidations = () => { @@ -34,7 +33,7 @@ export const AttributeValidations = () => { }>(); const { setValue, control, register } = useFormContext(); - const validators = useWatch({ + const validators = useWatch({ name: "validations", control, defaultValue: [], @@ -87,47 +86,48 @@ export const AttributeValidations = () => { {t("realm-settings:addValidator")} - - - - {t("validatorColNames.colName")} - {t("validatorColNames.colConfig")} - - - - - {validators.map((validator) => ( - - - {validator.key} - - - {JSON.stringify(validator.value)} - - - - + {validators.length !== 0 ? ( + + + + {t("validatorColNames.colName")} + {t("validatorColNames.colConfig")} + - ))} - {validators.length === 0 && ( - - {t("realm-settings:emptyValidators")} - - )} - - + + + {validators.map((validator) => ( + + + {validator.key} + + + {JSON.stringify(validator.value)} + + + + + + ))} + + + ) : ( + + {t("realm-settings:emptyValidators")} + + )} );