Added user profile attributes to user detail screen (#3762)
This commit is contained in:
parent
b5698be23c
commit
bce8270e7f
8 changed files with 411 additions and 403 deletions
|
@ -17,13 +17,14 @@ import {
|
|||
|
||||
import type { IndexedValidations } from "../../NewAttributeSettings";
|
||||
import { AddValidatorRoleDialog } from "./AddValidatorRoleDialog";
|
||||
import { Validator, validators as allValidator } from "./Validators";
|
||||
import useToggle from "../../../utils/useToggle";
|
||||
import { useServerInfo } from "../../../context/server-info/ServerInfoProvider";
|
||||
import ComponentTypeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentTypeRepresentation";
|
||||
|
||||
export type AddValidatorDialogProps = {
|
||||
selectedValidators: IndexedValidations[];
|
||||
toggleDialog: () => void;
|
||||
onConfirm: (newValidator: Validator) => void;
|
||||
onConfirm: (newValidator: ComponentTypeRepresentation) => void;
|
||||
};
|
||||
|
||||
export const AddValidatorDialog = ({
|
||||
|
@ -32,10 +33,13 @@ export const AddValidatorDialog = ({
|
|||
onConfirm,
|
||||
}: AddValidatorDialogProps) => {
|
||||
const { t } = useTranslation("realm-settings");
|
||||
const [selectedValidator, setSelectedValidator] = useState<Validator>();
|
||||
const [validators, setValidators] = useState(() =>
|
||||
const [selectedValidator, setSelectedValidator] =
|
||||
useState<ComponentTypeRepresentation>();
|
||||
const allValidator: ComponentTypeRepresentation[] =
|
||||
useServerInfo().componentTypes?.["org.keycloak.validate.Validator"] || [];
|
||||
const [validators, setValidators] = useState(
|
||||
allValidator.filter(
|
||||
({ name }) => !selectedValidators.map(({ key }) => key).includes(name)
|
||||
({ id }) => !selectedValidators.map(({ key }) => key).includes(id)
|
||||
)
|
||||
);
|
||||
const [addValidatorRoleModalOpen, toggleModal] = useToggle();
|
||||
|
@ -47,7 +51,7 @@ export const AddValidatorDialog = ({
|
|||
onConfirm={(newValidator) => {
|
||||
onConfirm(newValidator);
|
||||
setValidators(
|
||||
validators.filter(({ name }) => name !== newValidator.name)
|
||||
validators.filter(({ id }) => id !== newValidator.id)
|
||||
);
|
||||
}}
|
||||
open={addValidatorRoleModalOpen}
|
||||
|
@ -70,9 +74,9 @@ export const AddValidatorDialog = ({
|
|||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{validators.map((validator) => (
|
||||
{allValidator.map((validator) => (
|
||||
<Tr
|
||||
key={validator.name}
|
||||
key={validator.id}
|
||||
onRowClick={() => {
|
||||
setSelectedValidator(validator);
|
||||
toggleModal();
|
||||
|
@ -80,10 +84,10 @@ export const AddValidatorDialog = ({
|
|||
isHoverable
|
||||
>
|
||||
<Td dataLabel={t("validatorDialogColNames.colName")}>
|
||||
{validator.name}
|
||||
{validator.id}
|
||||
</Td>
|
||||
<Td dataLabel={t("validatorDialogColNames.colDescription")}>
|
||||
{validator.description}
|
||||
{validator.helpText}
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Modal, ModalVariant } from "@patternfly/react-core";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { Button, Form, Modal, ModalVariant } from "@patternfly/react-core";
|
||||
|
||||
import type ComponentTypeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentTypeRepresentation";
|
||||
import { DynamicComponents } from "../../../components/dynamic/DynamicComponents";
|
||||
import type { Validator } from "./Validators";
|
||||
|
||||
export type AddValidatorRoleDialogProps = {
|
||||
open: boolean;
|
||||
toggleDialog: () => void;
|
||||
onConfirm: (newValidator: Validator) => void;
|
||||
selected: Validator;
|
||||
onConfirm: (newValidator: ComponentTypeRepresentation) => void;
|
||||
selected: ComponentTypeRepresentation;
|
||||
};
|
||||
|
||||
export const AddValidatorRoleDialog = ({
|
||||
|
@ -22,8 +23,8 @@ export const AddValidatorRoleDialog = ({
|
|||
const { handleSubmit } = form;
|
||||
const selectedRoleValidator = selected;
|
||||
|
||||
const save = (newValidator: Validator) => {
|
||||
onConfirm({ ...newValidator, name: selected.name });
|
||||
const save = (newValidator: ComponentTypeRepresentation) => {
|
||||
onConfirm({ ...newValidator, id: selected.id });
|
||||
toggleDialog();
|
||||
};
|
||||
|
||||
|
@ -31,9 +32,9 @@ export const AddValidatorRoleDialog = ({
|
|||
<Modal
|
||||
variant={ModalVariant.small}
|
||||
title={t("addValidatorRole", {
|
||||
validatorName: selectedRoleValidator.name,
|
||||
validatorName: selectedRoleValidator.id,
|
||||
})}
|
||||
description={selectedRoleValidator.description}
|
||||
description={selectedRoleValidator.helpText}
|
||||
isOpen={open}
|
||||
onClose={toggleDialog}
|
||||
actions={[
|
||||
|
@ -55,9 +56,11 @@ export const AddValidatorRoleDialog = ({
|
|||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form>
|
||||
<FormProvider {...form}>
|
||||
<DynamicComponents properties={selectedRoleValidator.config!} />
|
||||
<DynamicComponents properties={selectedRoleValidator.properties} />
|
||||
</FormProvider>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -67,7 +67,7 @@ export const AttributeValidations = () => {
|
|||
onConfirm={(newValidator) => {
|
||||
setValue("validations", [
|
||||
...validators,
|
||||
{ key: newValidator.name, value: newValidator.config },
|
||||
{ key: newValidator.id, value: newValidator.properties },
|
||||
]);
|
||||
}}
|
||||
toggleDialog={toggleModal}
|
||||
|
|
|
@ -1,171 +0,0 @@
|
|||
export type Validator = {
|
||||
name: string;
|
||||
description?: string;
|
||||
config?: ValidatorConfig[];
|
||||
};
|
||||
|
||||
export type ValidatorConfig = {
|
||||
name?: string;
|
||||
label?: string;
|
||||
helpText?: string;
|
||||
type?: string;
|
||||
defaultValue?: any;
|
||||
options?: string[];
|
||||
secret?: boolean;
|
||||
};
|
||||
|
||||
export const validators: Validator[] = [
|
||||
{
|
||||
name: "double",
|
||||
description:
|
||||
"Check if the value is a double and within a lower and/or upper range. If no range is defined, the validator only checks whether the value is a valid number.",
|
||||
config: [
|
||||
{
|
||||
type: "String",
|
||||
defaultValue: "",
|
||||
helpText: "The minimal allowed value - this config is optional.",
|
||||
label: "Minimum",
|
||||
name: "min",
|
||||
},
|
||||
{
|
||||
type: "String",
|
||||
defaultValue: "",
|
||||
helpText: "The maximal allowed value - this config is optional.",
|
||||
label: "Maximum",
|
||||
name: "max",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
description: "Check if the value has a valid e-mail format.",
|
||||
config: [],
|
||||
},
|
||||
{
|
||||
name: "integer",
|
||||
description:
|
||||
"Check if the value is an integer and within a lower and/or upper range. If no range is defined, the validator only checks whether the value is a valid number.",
|
||||
config: [
|
||||
{
|
||||
type: "String",
|
||||
defaultValue: "",
|
||||
helpText: "The minimal allowed value - this config is optional.",
|
||||
label: "Minimum",
|
||||
name: "min",
|
||||
},
|
||||
{
|
||||
type: "String",
|
||||
defaultValue: "",
|
||||
helpText: "The maximal allowed value - this config is optional.",
|
||||
label: "Maximum",
|
||||
name: "max",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "length",
|
||||
description:
|
||||
"Check the length of a string value based on a minimum and maximum length.",
|
||||
config: [
|
||||
{
|
||||
type: "String",
|
||||
defaultValue: "",
|
||||
helpText: "The minimum length",
|
||||
label: "Minimum length",
|
||||
name: "min",
|
||||
},
|
||||
{
|
||||
type: "String",
|
||||
defaultValue: "",
|
||||
helpText: "The maximum length",
|
||||
label: "Maximum length",
|
||||
name: "max",
|
||||
},
|
||||
{
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
helpText:
|
||||
"Disable trimming of the String value before the length check",
|
||||
label: "Trimming disabled",
|
||||
name: "trim-disabled",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "local-date",
|
||||
description:
|
||||
"Check if the value has a valid format based on the realm and/or user locale.",
|
||||
config: [],
|
||||
},
|
||||
{
|
||||
name: "options",
|
||||
description:
|
||||
"Check if the value is from the defined set of allowed values. Useful to validate values entered through select and multiselect fields.",
|
||||
config: [
|
||||
{
|
||||
type: "MultivaluedString",
|
||||
defaultValue: "",
|
||||
helpText: "List of allowed options",
|
||||
label: "Options",
|
||||
name: "options",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "pattern",
|
||||
description: "Check if the value matches a specific RegEx pattern.",
|
||||
config: [
|
||||
{
|
||||
type: "String",
|
||||
defaultValue: "",
|
||||
helpText:
|
||||
"RegExp pattern the value must match. Java Pattern syntax is used.",
|
||||
label: "RegExp pattern",
|
||||
name: "pattern",
|
||||
},
|
||||
{
|
||||
type: "String",
|
||||
defaultValue: "",
|
||||
helpText:
|
||||
"Key of the error message in i18n bundle. Dafault message key is error-pattern-no-match",
|
||||
label: "Error message key",
|
||||
name: "error-message",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "person-name-prohibited-characters",
|
||||
description:
|
||||
"Check if the value is a valid person name as an additional barrier for attacks such as script injection. The validation is based on a default RegEx pattern that blocks characters not common in person names.",
|
||||
config: [
|
||||
{
|
||||
type: "String",
|
||||
defaultValue: "",
|
||||
helpText:
|
||||
"Key of the error message in i18n bundle. Dafault message key is error-person-name-invalid-character",
|
||||
label: "Error message key",
|
||||
name: "error-message",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "uri",
|
||||
description: "Check if the value is a valid URI.",
|
||||
config: [],
|
||||
},
|
||||
{
|
||||
name: "username-prohibited-characters",
|
||||
description:
|
||||
"Check if the value is a valid username as an additional barrier for attacks such as script injection. The validation is based on a default RegEx pattern that blocks characters not common in usernames.",
|
||||
config: [
|
||||
{
|
||||
type: "String",
|
||||
defaultValue: "",
|
||||
helpText:
|
||||
"Key of the error message in i18n bundle. Dafault message key is error-username-invalid-character",
|
||||
label: "Error message key",
|
||||
name: "error-message",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
|
@ -19,6 +19,7 @@ import {
|
|||
keyValueToArray,
|
||||
} from "../components/key-value-form/key-value-convert";
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
import { useUserProfile } from "../realm-settings/user-profile/UserProfileContext";
|
||||
|
||||
type UserAttributesProps = {
|
||||
user: UserRepresentation;
|
||||
|
@ -30,9 +31,12 @@ export const UserAttributes = ({ user: defaultUser }: UserAttributesProps) => {
|
|||
const { addAlert, addError } = useAlerts();
|
||||
const [user, setUser] = useState<UserRepresentation>(defaultUser);
|
||||
const form = useForm<AttributeForm>({ mode: "onChange" });
|
||||
const { config } = useUserProfile();
|
||||
|
||||
const convertAttributes = (attr?: Record<string, any>) => {
|
||||
return arrayToKeyValue(attr || user.attributes!);
|
||||
const convertAttributes = () => {
|
||||
return arrayToKeyValue(user.attributes!).filter(
|
||||
(a) => !config?.attributes?.some((attribute) => attribute.name === a.key)
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -41,7 +45,11 @@ export const UserAttributes = ({ user: defaultUser }: UserAttributesProps) => {
|
|||
|
||||
const save = async (attributeForm: AttributeForm) => {
|
||||
try {
|
||||
const attributes = keyValueToArray(attributeForm.attributes!);
|
||||
const attributes = Object.assign(
|
||||
{},
|
||||
user.attributes || {},
|
||||
keyValueToArray(attributeForm.attributes!)
|
||||
);
|
||||
await adminClient.users.update({ id: user.id! }, { ...user, attributes });
|
||||
|
||||
setUser({ ...user, attributes });
|
||||
|
|
|
@ -29,6 +29,8 @@ import { GroupPickerDialog } from "../components/group/GroupPickerDialog";
|
|||
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
|
||||
import type RequiredActionProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/requiredActionProviderRepresentation";
|
||||
import { useAccess } from "../context/access/Access";
|
||||
import useIsFeatureEnabled, { Feature } from "../utils/useIsFeatureEnabled";
|
||||
import { UserProfileFields } from "./UserProfileFields";
|
||||
|
||||
export type BruteForced = {
|
||||
isBruteForceProtected?: boolean;
|
||||
|
@ -54,6 +56,7 @@ export const UserForm = ({
|
|||
const { t } = useTranslation("users");
|
||||
const { realm: realmName } = useRealm();
|
||||
const formatDate = useFormatDate();
|
||||
const isFeatureEnabled = useIsFeatureEnabled();
|
||||
|
||||
const [
|
||||
isRequiredUserActionsDropdownOpen,
|
||||
|
@ -190,129 +193,6 @@ export const UserForm = ({
|
|||
</FormGroup>
|
||||
</>
|
||||
)}
|
||||
{!realm?.registrationEmailAsUsername && (
|
||||
<FormGroup
|
||||
label={t("username")}
|
||||
fieldId="kc-username"
|
||||
isRequired
|
||||
validated={errors.username ? "error" : "default"}
|
||||
helperTextInvalid={t("common:required")}
|
||||
>
|
||||
<KeycloakTextInput
|
||||
ref={register()}
|
||||
type="text"
|
||||
id="kc-username"
|
||||
aria-label={t("username")}
|
||||
name="username"
|
||||
isReadOnly={
|
||||
!!user?.id &&
|
||||
!realm?.editUsernameAllowed &&
|
||||
realm?.editUsernameAllowed !== undefined
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
<FormGroup
|
||||
label={t("email")}
|
||||
fieldId="kc-description"
|
||||
validated={errors.email ? "error" : "default"}
|
||||
helperTextInvalid={t("users:emailInvalid")}
|
||||
>
|
||||
<KeycloakTextInput
|
||||
ref={register({
|
||||
pattern: emailRegexPattern,
|
||||
})}
|
||||
type="email"
|
||||
id="kc-email"
|
||||
name="email"
|
||||
data-testid="email-input"
|
||||
aria-label={t("emailInput")}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("emailVerified")}
|
||||
fieldId="kc-email-verified"
|
||||
helperTextInvalid={t("common:required")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="users-help:emailVerified"
|
||||
fieldLabelId="users:emailVerified"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="emailVerified"
|
||||
defaultValue={false}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
data-testid="email-verified-switch"
|
||||
id={"kc-user-email-verified"}
|
||||
isDisabled={false}
|
||||
onChange={(value) => onChange(value)}
|
||||
isChecked={value}
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
aria-label={t("emailVerified")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("firstName")}
|
||||
fieldId="kc-firstname"
|
||||
validated={errors.firstName ? "error" : "default"}
|
||||
helperTextInvalid={t("common:required")}
|
||||
>
|
||||
<KeycloakTextInput
|
||||
ref={register()}
|
||||
data-testid="firstName-input"
|
||||
type="text"
|
||||
id="kc-firstname"
|
||||
aria-label={t("firstName")}
|
||||
name="firstName"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("lastName")}
|
||||
fieldId="kc-name"
|
||||
validated={errors.lastName ? "error" : "default"}
|
||||
>
|
||||
<KeycloakTextInput
|
||||
ref={register()}
|
||||
data-testid="lastName-input"
|
||||
type="text"
|
||||
id="kc-lastname"
|
||||
name="lastName"
|
||||
aria-label={t("lastName")}
|
||||
/>
|
||||
</FormGroup>
|
||||
{isBruteForceProtected && (
|
||||
<FormGroup
|
||||
label={t("temporaryLocked")}
|
||||
fieldId="temporaryLocked"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="users-help:temporaryLocked"
|
||||
fieldLabelId="users:temporaryLocked"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
data-testid="user-locked-switch"
|
||||
id={"temporaryLocked"}
|
||||
onChange={(value) => {
|
||||
unLockUser();
|
||||
setLocked(value);
|
||||
}}
|
||||
isChecked={locked}
|
||||
isDisabled={!locked}
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
aria-label={t("temporaryLocked")}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
<FormGroup
|
||||
label={t("requiredUserActions")}
|
||||
fieldId="kc-required-user-actions"
|
||||
|
@ -362,6 +242,129 @@ export const UserForm = ({
|
|||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
{isFeatureEnabled(Feature.DeclarativeUserProfile) &&
|
||||
realm?.attributes?.userProfileEnabled === "true" ? (
|
||||
<UserProfileFields />
|
||||
) : (
|
||||
<>
|
||||
{!realm?.registrationEmailAsUsername && (
|
||||
<FormGroup
|
||||
label={t("username")}
|
||||
fieldId="kc-username"
|
||||
isRequired
|
||||
validated={errors.username ? "error" : "default"}
|
||||
helperTextInvalid={t("common:required")}
|
||||
>
|
||||
<KeycloakTextInput
|
||||
ref={register()}
|
||||
id="kc-username"
|
||||
aria-label={t("username")}
|
||||
name="username"
|
||||
isReadOnly={
|
||||
!!user?.id &&
|
||||
!realm?.editUsernameAllowed &&
|
||||
realm?.editUsernameAllowed !== undefined
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
<FormGroup
|
||||
label={t("email")}
|
||||
fieldId="kc-description"
|
||||
validated={errors.email ? "error" : "default"}
|
||||
helperTextInvalid={t("users:emailInvalid")}
|
||||
>
|
||||
<KeycloakTextInput
|
||||
ref={register({
|
||||
pattern: emailRegexPattern,
|
||||
})}
|
||||
type="email"
|
||||
id="kc-email"
|
||||
name="email"
|
||||
data-testid="email-input"
|
||||
aria-label={t("emailInput")}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("emailVerified")}
|
||||
fieldId="kc-email-verified"
|
||||
helperTextInvalid={t("common:required")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="users-help:emailVerified"
|
||||
fieldLabelId="users:emailVerified"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="emailVerified"
|
||||
defaultValue={false}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
data-testid="email-verified-switch"
|
||||
id="kc-user-email-verified"
|
||||
isDisabled={false}
|
||||
onChange={(value) => onChange(value)}
|
||||
isChecked={value}
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("firstName")}
|
||||
fieldId="kc-firstname"
|
||||
validated={errors.firstName ? "error" : "default"}
|
||||
helperTextInvalid={t("common:required")}
|
||||
>
|
||||
<KeycloakTextInput
|
||||
ref={register()}
|
||||
data-testid="firstName-input"
|
||||
id="kc-firstname"
|
||||
name="firstName"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("lastName")}
|
||||
fieldId="kc-name"
|
||||
validated={errors.lastName ? "error" : "default"}
|
||||
>
|
||||
<KeycloakTextInput
|
||||
ref={register()}
|
||||
data-testid="lastName-input"
|
||||
id="kc-lastname"
|
||||
name="lastName"
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
)}
|
||||
{isBruteForceProtected && (
|
||||
<FormGroup
|
||||
label={t("temporaryLocked")}
|
||||
fieldId="temporaryLocked"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="users-help:temporaryLocked"
|
||||
fieldLabelId="users:temporaryLocked"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
data-testid="user-locked-switch"
|
||||
id="temporaryLocked"
|
||||
onChange={(value) => {
|
||||
unLockUser();
|
||||
setLocked(value);
|
||||
}}
|
||||
isChecked={locked}
|
||||
isDisabled={!locked}
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
{!user?.id && (
|
||||
<FormGroup
|
||||
label={t("common:groups")}
|
||||
|
|
150
apps/admin-ui/src/user/UserProfileFields.tsx
Normal file
150
apps/admin-ui/src/user/UserProfileFields.tsx
Normal file
|
@ -0,0 +1,150 @@
|
|||
import { Fragment } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import {
|
||||
Form,
|
||||
FormGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
Text,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import type { UserProfileAttribute } from "@keycloak/keycloak-admin-client/lib/defs/userProfileConfig";
|
||||
import type { UserProfileAttributeRequired } from "@keycloak/keycloak-admin-client/lib/defs/userProfileConfig";
|
||||
import { ScrollForm } from "../components/scroll-form/ScrollForm";
|
||||
import { KeycloakTextInput } from "../components/keycloak-text-input/KeycloakTextInput";
|
||||
import { useUserProfile } from "../realm-settings/user-profile/UserProfileContext";
|
||||
import useToggle from "../utils/useToggle";
|
||||
|
||||
const ROOT_ATTRIBUTES = ["username", "fistName", "lastName", "email"];
|
||||
const DEFAULT_ROLES = ["admin", "user"];
|
||||
|
||||
type UserProfileFieldsProps = {
|
||||
roles?: string[];
|
||||
};
|
||||
|
||||
export const UserProfileFields = ({
|
||||
roles = ["admin"],
|
||||
}: UserProfileFieldsProps) => {
|
||||
const { t } = useTranslation("realm-settings");
|
||||
const { config } = useUserProfile();
|
||||
|
||||
return (
|
||||
<ScrollForm
|
||||
sections={[{ name: "" }, ...(config?.groups || [])].map((g) => ({
|
||||
title: g.name || t("general"),
|
||||
panel: (
|
||||
<Form>
|
||||
{g.displayDescription && (
|
||||
<Text className="pf-u-pb-lg">{g.displayDescription}</Text>
|
||||
)}
|
||||
{config?.attributes?.map((attribute) => (
|
||||
<Fragment key={attribute.name}>
|
||||
{(attribute.group || "") === g.name &&
|
||||
(attribute.permissions?.view || DEFAULT_ROLES).some((r) =>
|
||||
roles.includes(r)
|
||||
) && <FormField attribute={attribute} roles={roles} />}
|
||||
</Fragment>
|
||||
))}
|
||||
</Form>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type FormFieldProps = {
|
||||
attribute: UserProfileAttribute;
|
||||
roles: string[];
|
||||
};
|
||||
|
||||
const FormField = ({ attribute, roles }: FormFieldProps) => {
|
||||
const { t } = useTranslation("users");
|
||||
const { errors, register, control } = useFormContext();
|
||||
const [open, toggle] = useToggle();
|
||||
|
||||
const isBundleKey = (displayName?: string) => displayName?.includes("${");
|
||||
const unWrap = (key: string) => key.substring(2, key.length - 1);
|
||||
|
||||
const isSelect = (attribute: UserProfileAttribute) =>
|
||||
Object.hasOwn(attribute.validations || {}, "options");
|
||||
|
||||
const isRootAttribute = (attr?: string) =>
|
||||
attr && ROOT_ATTRIBUTES.includes(attr);
|
||||
|
||||
const isRequired = (required: UserProfileAttributeRequired | undefined) =>
|
||||
Object.keys(required || {}).length !== 0;
|
||||
|
||||
const fieldName = (attribute: UserProfileAttribute) =>
|
||||
`${isRootAttribute(attribute.name) ? "" : "attributes."}${attribute.name}`;
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
key={attribute.name}
|
||||
label={
|
||||
(isBundleKey(attribute.displayName)
|
||||
? t(unWrap(attribute.displayName!))
|
||||
: attribute.displayName) || attribute.name
|
||||
}
|
||||
fieldId={attribute.name}
|
||||
isRequired={isRequired(attribute.required)}
|
||||
validated={errors.username ? "error" : "default"}
|
||||
helperTextInvalid={t("common:required")}
|
||||
>
|
||||
{isSelect(attribute) ? (
|
||||
<Controller
|
||||
name={fieldName(attribute)}
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId={attribute.name}
|
||||
onToggle={toggle}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value.toString());
|
||||
toggle();
|
||||
}}
|
||||
selections={value}
|
||||
variant="single"
|
||||
aria-label={t("common:selectOne")}
|
||||
isOpen={open}
|
||||
isDisabled={
|
||||
!(attribute.permissions?.edit || DEFAULT_ROLES).some((r) =>
|
||||
roles.includes(r)
|
||||
)
|
||||
}
|
||||
>
|
||||
{[
|
||||
<SelectOption key="empty" value="">
|
||||
{t("common:choose")}
|
||||
</SelectOption>,
|
||||
...(
|
||||
attribute.validations?.options as { options: string[] }
|
||||
).options.map((option) => (
|
||||
<SelectOption
|
||||
selected={value === option}
|
||||
key={option}
|
||||
value={option}
|
||||
>
|
||||
{option}
|
||||
</SelectOption>
|
||||
)),
|
||||
]}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<KeycloakTextInput
|
||||
ref={register()}
|
||||
id={attribute.name}
|
||||
name={fieldName(attribute)}
|
||||
isDisabled={
|
||||
!(attribute.permissions?.edit || DEFAULT_ROLES).some((r) =>
|
||||
roles.includes(r)
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
|
@ -32,6 +32,7 @@ import { UserGroups } from "./UserGroups";
|
|||
import { UserIdentityProviderLinks } from "./UserIdentityProviderLinks";
|
||||
import { UserRoleMapping } from "./UserRoleMapping";
|
||||
import { UserSessions } from "./UserSessions";
|
||||
import { UserProfileProvider } from "../realm-settings/user-profile/UserProfileContext";
|
||||
|
||||
const UsersTabs = () => {
|
||||
const { t } = useTranslation("users");
|
||||
|
@ -85,17 +86,25 @@ const UsersTabs = () => {
|
|||
setAddedGroups(groups);
|
||||
};
|
||||
|
||||
const save = async (user: UserRepresentation) => {
|
||||
user.username = user.username?.trim();
|
||||
const save = async (formUser: UserRepresentation) => {
|
||||
formUser.username = formUser.username?.trim();
|
||||
|
||||
try {
|
||||
if (id) {
|
||||
await adminClient.users.update({ id }, user);
|
||||
await adminClient.users.update(
|
||||
{ id },
|
||||
{
|
||||
...formUser,
|
||||
attributes: { ...user?.attributes, ...formUser.attributes },
|
||||
}
|
||||
);
|
||||
addAlert(t("userSaved"), AlertVariant.success);
|
||||
refresh();
|
||||
} else {
|
||||
user.groups = addedGroups.map((group) => group.path!);
|
||||
const createdUser = await adminClient.users.create(user);
|
||||
const createdUser = await adminClient.users.create({
|
||||
...formUser,
|
||||
groups: addedGroups.map((group) => group.path!),
|
||||
});
|
||||
|
||||
addAlert(t("userCreated"), AlertVariant.success);
|
||||
navigate(toUser({ id: createdUser.id, realm, tab: "settings" }));
|
||||
|
@ -183,6 +192,7 @@ const UsersTabs = () => {
|
|||
)}
|
||||
/>
|
||||
<PageSection variant="light" className="pf-u-p-0">
|
||||
<UserProfileProvider>
|
||||
<FormProvider {...userForm}>
|
||||
{id && user && (
|
||||
<KeycloakTabs isBox mountOnEnter>
|
||||
|
@ -265,6 +275,7 @@ const UsersTabs = () => {
|
|||
</PageSection>
|
||||
)}
|
||||
</FormProvider>
|
||||
</UserProfileProvider>
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue