Ream-settings -> User Profile -> Edit Attribute (#2343)

* edit attribute - wip

* edit attribute - wip

* edit attribute - wip

* edit attribute - wip

* edit attribute - wip

* edit attribute - wip

* edit attribute - wip

* edit attribute - wip

* edit attribute - wip

* edit attribute - wip

* edit attribute - wip

* edit attribute - wip

* edit attribute - wip

* edit attribute - wip

* edit attribute - wip

* edit attribute - wip

* refactor

* refactor

* refactor

* refactor

* refactor

* refactor

* refactor

* moved form initialisation to central place

* fixed loading values

Co-authored-by: Agnieszka Gancarczyk <agancarc@redhat.com>
Co-authored-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
agagancarczyk 2022-03-31 14:28:48 +01:00 committed by GitHub
parent be2d7e268e
commit 4c70064bd4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 297 additions and 239 deletions

View file

@ -8,33 +8,32 @@ import {
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { FormProvider, useForm, useFormContext } from "react-hook-form"; import { FormProvider, useForm, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link, useHistory } from "react-router-dom"; import { Link, useHistory, useParams } from "react-router-dom";
import { ScrollForm } from "../components/scroll-form/ScrollForm"; import { ScrollForm } from "../components/scroll-form/ScrollForm";
import { useRealm } from "../context/realm-context/RealmContext";
import type UserProfileConfig from "@keycloak/keycloak-admin-client/lib/defs/userProfileConfig"; import type UserProfileConfig from "@keycloak/keycloak-admin-client/lib/defs/userProfileConfig";
import { AttributeGeneralSettings } from "./user-profile/attribute/AttributeGeneralSettings"; import { AttributeGeneralSettings } from "./user-profile/attribute/AttributeGeneralSettings";
import { AttributePermission } from "./user-profile/attribute/AttributePermission"; import { AttributePermission } from "./user-profile/attribute/AttributePermission";
import { AttributeValidations } from "./user-profile/attribute/AttributeValidations"; import { AttributeValidations } from "./user-profile/attribute/AttributeValidations";
import { toUserProfile } from "./routes/UserProfile"; import { toUserProfile } from "./routes/UserProfile";
import "./realm-settings-section.css";
import { ViewHeader } from "../components/view-header/ViewHeader"; import { ViewHeader } from "../components/view-header/ViewHeader";
import { AttributeAnnotations } from "./user-profile/attribute/AttributeAnnotations"; import { AttributeAnnotations } from "./user-profile/attribute/AttributeAnnotations";
import { useAdminClient, useFetch } from "../context/auth/AdminClient"; import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
import { UserProfileProvider } from "./user-profile/UserProfileContext"; import { UserProfileProvider } from "./user-profile/UserProfileContext";
import type { UserProfileAttribute } from "@keycloak/keycloak-admin-client/lib/defs/userProfileConfig"; import type { UserProfileAttribute } from "@keycloak/keycloak-admin-client/lib/defs/userProfileConfig";
import type { AttributeParams } from "./routes/Attribute";
import type { KeyValueType } from "../components/attribute-form/attribute-convert"; import type { KeyValueType } from "../components/attribute-form/attribute-convert";
import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation"; import { convertToFormValues } from "../util";
import { flatten } from "flat";
type UserProfileAttributeType = UserProfileAttribute & import "./realm-settings-section.css";
AttributeRequired &
Permission;
type AttributeRequired = { type UserProfileAttributeType = UserProfileAttribute & Attribute & Permission;
type Attribute = {
roles: string[]; roles: string[];
scopeRequired: string[]; scopes: string[];
enabledWhen: boolean; isRequired: boolean;
requiredWhen: boolean;
}; };
type Permission = { type Permission = {
@ -63,7 +62,8 @@ const CreateAttributeFormContent = ({
}) => { }) => {
const { t } = useTranslation("realm-settings"); const { t } = useTranslation("realm-settings");
const form = useFormContext(); const form = useFormContext();
const { realm } = useRealm(); const { realm, attributeName } = useParams<AttributeParams>();
const editMode = attributeName ? true : false;
return ( return (
<UserProfileProvider> <UserProfileProvider>
@ -87,7 +87,7 @@ const CreateAttributeFormContent = ({
type="submit" type="submit"
data-testid="attribute-create" data-testid="attribute-create"
> >
{t("common:create")} {editMode ? t("common:save") : t("common:create")}
</Button> </Button>
<Link <Link
to={toUserProfile({ realm, tab: "attributes" })} to={toUserProfile({ realm, tab: "attributes" })}
@ -103,51 +103,51 @@ const CreateAttributeFormContent = ({
}; };
export default function NewAttributeSettings() { export default function NewAttributeSettings() {
const { realm: realmName } = useRealm(); const { realm, attributeName } = useParams<AttributeParams>();
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const form = useForm<UserProfileConfig>(); const form = useForm<UserProfileConfig>({ shouldUnregister: false });
const { t } = useTranslation("realm-settings"); const { t } = useTranslation("realm-settings");
const history = useHistory(); const history = useHistory();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const [config, setConfig] = useState<UserProfileConfig | null>(null); const [config, setConfig] = useState<UserProfileConfig | null>(null);
const [clientScopes, setClientScopes] = const editMode = attributeName ? true : false;
useState<ClientScopeRepresentation[]>();
const convert = (obj: Record<string, unknown>[] | undefined) =>
Object.entries(obj || []).map(([key, value]) => ({
key,
value,
}));
useFetch( useFetch(
() => () => adminClient.users.getProfile({ realm }),
Promise.all([ (config) => {
adminClient.users.getProfile({ realm: realmName }),
adminClient.clientScopes.find(),
]),
([config, clientScopes]) => {
setConfig(config); setConfig(config);
setClientScopes(clientScopes); const {
annotations,
validations,
permissions,
selector,
required,
...values
} = config.attributes!.find(
(attribute) => attribute.name === attributeName
)!;
convertToFormValues(values, form.setValue);
Object.entries(
flatten({ permissions, selector, required }, { safe: true })
).map(([key, value]) => form.setValue(key, value));
form.setValue("annotations", convert(annotations));
form.setValue("validations", convert(validations));
form.setValue("isRequired", required !== undefined);
}, },
[] []
); );
const save = async (profileConfig: UserProfileAttributeType) => { const save = async (profileConfig: UserProfileAttributeType) => {
const scopeNames = clientScopes?.map((clientScope) => clientScope.name);
const selector = {
scopes: profileConfig.enabledWhen
? scopeNames
: profileConfig.selector?.scopes,
};
const required = {
roles: profileConfig.roles,
scopes: profileConfig.requiredWhen
? scopeNames
: profileConfig.scopeRequired,
};
const validations = profileConfig.validations?.reduce( const validations = profileConfig.validations?.reduce(
(prevValidations: any, currentValidations: any) => { (prevValidations: any, currentValidations: any) => {
prevValidations[currentValidations.name] = prevValidations[currentValidations.key] =
currentValidations.config.length === 0 currentValidations.value.length === 0 ? {} : currentValidations.value;
? {}
: currentValidations.config;
return prevValidations; return prevValidations;
}, },
{} {}
@ -158,29 +158,55 @@ export default function NewAttributeSettings() {
{} {}
); );
const newAttribute = [ const patchAttributes = () =>
{ config?.attributes!.map((attribute) => {
name: profileConfig.name, if (attribute.name !== attributeName) {
displayName: profileConfig.displayName, return attribute;
required, }
validations,
selector,
permissions: profileConfig.permissions,
annotations,
},
];
const newAttributesList = config?.attributes!.concat( return Object.assign(
newAttribute as UserProfileAttribute {
); ...attribute,
name: attributeName,
displayName: profileConfig.displayName!,
validations,
selector: profileConfig.selector,
permissions: profileConfig.permissions!,
annotations,
},
profileConfig.isRequired
? { required: profileConfig.required }
: undefined
);
});
const addAttribute = () =>
config?.attributes!.concat([
Object.assign(
{
name: profileConfig.name,
displayName: profileConfig.displayName!,
required: profileConfig.isRequired ? profileConfig.required : {},
validations,
selector: profileConfig.selector,
permissions: profileConfig.permissions!,
annotations,
},
profileConfig.isRequired
? { required: profileConfig.required }
: undefined
),
] as UserProfileAttribute);
const updatedAttributes = editMode ? patchAttributes() : addAttribute();
try { try {
await adminClient.users.updateProfile({ await adminClient.users.updateProfile({
attributes: newAttributesList, attributes: updatedAttributes as UserProfileAttribute[],
realm: realmName, realm,
}); });
history.push(toUserProfile({ realm: realmName, tab: "attributes" })); history.push(toUserProfile({ realm, tab: "attributes" }));
addAlert( addAlert(
t("realm-settings:createAttributeSuccess"), t("realm-settings:createAttributeSuccess"),
@ -194,8 +220,8 @@ export default function NewAttributeSettings() {
return ( return (
<FormProvider {...form}> <FormProvider {...form}>
<ViewHeader <ViewHeader
titleKey={t("createAttribute")} titleKey={editMode ? attributeName : t("createAttribute")}
subKey={t("createAttributeSubTitle")} subKey={editMode ? "" : t("createAttributeSubTitle")}
/> />
<PageSection variant="light"> <PageSection variant="light">
<CreateAttributeFormContent save={() => form.handleSubmit(save)()} /> <CreateAttributeFormContent save={() => form.handleSubmit(save)()} />

View file

@ -404,6 +404,7 @@ export default {
updatedUserProfileSuccess: "User Profile configuration has been saved", updatedUserProfileSuccess: "User Profile configuration has been saved",
updatedUserProfileError: "User Profile configuration hasn't been saved", updatedUserProfileError: "User Profile configuration hasn't been saved",
createAttribute: "Create attribute", createAttribute: "Create attribute",
editAttribute: "Edit attribute",
createAttributeSubTitle: "Create a new attribute", createAttributeSubTitle: "Create a new attribute",
createAttributeSuccess: createAttributeSuccess:
"Success! User Profile configuration has been saved.", "Success! User Profile configuration has been saved.",
@ -415,6 +416,8 @@ export default {
"Are you sure you want to permanently delete the attribute {{attributeName}}?", "Are you sure you want to permanently delete the attribute {{attributeName}}?",
deleteAttributeSuccess: "Attribute deleted", deleteAttributeSuccess: "Attribute deleted",
deleteAttributeError: "Attribute not deleted", deleteAttributeError: "Attribute not deleted",
always: "Always",
scopesAsRequested: "Scopes are requested",
generalSettings: "General settings", generalSettings: "General settings",
permission: "Permission", permission: "Permission",
validations: "Validations", validations: "Validations",

View file

@ -11,7 +11,7 @@ export type AttributeParams = {
export const AttributeRoute: RouteDef = { export const AttributeRoute: RouteDef = {
path: "/:realm/realm-settings/userProfile/attributes/:attributeName/edit-attribute", path: "/:realm/realm-settings/userProfile/attributes/:attributeName/edit-attribute",
component: lazy(() => import("../NewAttributeSettings")), component: lazy(() => import("../NewAttributeSettings")),
breadcrumb: (t) => t("realm-settings:createAttribute"), breadcrumb: (t) => t("realm-settings:editAttribute"),
access: "manage-realm", access: "manage-realm",
}; };

View file

@ -59,8 +59,6 @@ export const AttributesTab = () => {
); );
}; };
const goToCreate = () => history.push(toAddAttribute({ realm: realmName }));
const updatedAttributes = config?.attributes!.filter( const updatedAttributes = config?.attributes!.filter(
(attribute) => attribute.name !== attributeToDelete (attribute) => attribute.name !== attributeToDelete
); );
@ -173,7 +171,14 @@ export const AttributesTab = () => {
actions={[ actions={[
{ {
title: t("common:edit"), title: t("common:edit"),
onClick: goToCreate, onClick: (_key, _idx, component) => {
history.push(
toAttribute({
realm: realmName,
attributeName: component.name,
})
);
},
}, },
{ {
title: t("common:delete"), title: t("common:delete"),

View file

@ -1,15 +1,14 @@
import React from "react"; import React from "react";
import { FormGroup, Grid, GridItem } from "@patternfly/react-core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FormGroup, Grid, GridItem } from "@patternfly/react-core";
import { FormAccess } from "../../../components/form-access/FormAccess"; import { FormAccess } from "../../../components/form-access/FormAccess";
import { FormProvider, useFormContext } from "react-hook-form";
import { AttributeInput } from "../../../components/attribute-input/AttributeInput"; import { AttributeInput } from "../../../components/attribute-input/AttributeInput";
import "../../realm-settings-section.css"; import "../../realm-settings-section.css";
export const AttributeAnnotations = () => { export const AttributeAnnotations = () => {
const { t } = useTranslation("realm-settings"); const { t } = useTranslation("realm-settings");
const form = useFormContext();
return ( return (
<FormAccess role="manage-realm" isHorizontal> <FormAccess role="manage-realm" isHorizontal>
@ -21,9 +20,7 @@ export const AttributeAnnotations = () => {
> >
<Grid className="kc-annotations"> <Grid className="kc-annotations">
<GridItem> <GridItem>
<FormProvider {...form}> <AttributeInput name="annotations" />
<AttributeInput name="annotations" />
</FormProvider>
</GridItem> </GridItem>
</Grid> </Grid>
</FormGroup> </FormGroup>

View file

@ -17,9 +17,12 @@ import { Controller, useFormContext, useWatch } from "react-hook-form";
import { FormAccess } from "../../../components/form-access/FormAccess"; import { FormAccess } from "../../../components/form-access/FormAccess";
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient"; import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation"; import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation";
import type { AttributeParams } from "../../routes/Attribute";
import { useParams } from "react-router-dom";
import { isEqual } from "lodash-es";
import "../../realm-settings-section.css"; import "../../realm-settings-section.css";
const ENABLED_REQUIRED_WHEN = ["Always", "Scopes are requested"] as const;
const REQUIRED_FOR = [ const REQUIRED_FOR = [
{ label: "Both users and admins", value: ["admin", "user"] }, { label: "Both users and admins", value: ["admin", "user"] },
{ label: "Only users", value: ["user"] }, { label: "Only users", value: ["user"] },
@ -36,12 +39,24 @@ export const AttributeGeneralSettings = () => {
const [selectRequiredForOpen, setSelectRequiredForOpen] = useState(false); const [selectRequiredForOpen, setSelectRequiredForOpen] = useState(false);
const [isAttributeGroupDropdownOpen, setIsAttributeGroupDropdownOpen] = const [isAttributeGroupDropdownOpen, setIsAttributeGroupDropdownOpen] =
useState(false); useState(false);
const [enabledWhenSelection, setEnabledWhenSelection] = useState("Always"); const { attributeName } = useParams<AttributeParams>();
const [requiredWhenSelection, setRequiredWhenSelection] = useState("Always"); const editMode = attributeName ? true : false;
const requiredToggle = useWatch({ const selectedScopes = useWatch({
control: form.control, control: form.control,
name: "required", name: "selector.scopes",
defaultValue: [],
});
const requiredScopes = useWatch({
control: form.control,
name: "required.scopes",
defaultValue: [],
});
const required = useWatch({
control: form.control,
name: "isRequired",
defaultValue: false, defaultValue: false,
}); });
@ -77,10 +92,11 @@ export const AttributeGeneralSettings = () => {
ref={form.register({ ref={form.register({
required: { required: {
value: true, value: true,
message: `${t("validateName")}`, message: t("validateName"),
}, },
})} })}
data-testid="attribute-name" data-testid="attribute-name"
isDisabled={editMode}
validated={form.errors.name ? "error" : "default"} validated={form.errors.name ? "error" : "default"}
/> />
</FormGroup> </FormGroup>
@ -141,44 +157,49 @@ export const AttributeGeneralSettings = () => {
</FormGroup> </FormGroup>
<Divider /> <Divider />
<FormGroup label={t("enabledWhen")} fieldId="enabledWhen" hasNoPaddingTop> <FormGroup label={t("enabledWhen")} fieldId="enabledWhen" hasNoPaddingTop>
<Controller <Radio
id="always"
data-testid="always"
isChecked={selectedScopes.length === clientScopes?.length}
name="enabledWhen" name="enabledWhen"
data-testid="enabledWhen" label={t("always")}
control={form.control} onChange={(value) => {
defaultValue={ENABLED_REQUIRED_WHEN[0]} if (value) {
render={({ onChange, value }) => ( form.setValue(
<> "selector.scopes",
{ENABLED_REQUIRED_WHEN.map((option) => ( clientScopes?.map((s) => s.name)
<Radio );
id={option} } else {
key={option} form.setValue("selector.scopes", []);
data-testid={option} }
isChecked={value === option} }}
name="enabledWhen" className="pf-u-mb-md"
onChange={() => { />
onChange(option); <Radio
setEnabledWhenSelection(option); id="scopesAsRequested"
}} data-testid="scopesAsRequested"
label={option} isChecked={selectedScopes.length !== clientScopes?.length}
className="pf-u-mb-md" name="enabledWhen"
/> label={t("scopesAsRequested")}
))} onChange={(value) => {
</> if (value) {
)} form.setValue("selector.scopes", []);
} else {
form.setValue(
"selector.scopes",
clientScopes?.map((s) => s.name)
);
}
}}
className="pf-u-mb-md"
/> />
</FormGroup> </FormGroup>
<FormGroup fieldId="kc-scope-enabled-when"> <FormGroup fieldId="kc-scope-enabled-when">
<Controller <Controller
name="scopes" name="selector.scopes"
control={form.control} control={form.control}
defaultValue={[]} defaultValue={[]}
render={({ render={({ onChange, value }) => (
onChange,
value,
}: {
onChange: (newValue: string[]) => void;
value: string[];
}) => (
<Select <Select
name="scopes" name="scopes"
data-testid="enabled-when-scope-field" data-testid="enabled-when-scope-field"
@ -196,7 +217,7 @@ export const AttributeGeneralSettings = () => {
let changedValue = [""]; let changedValue = [""];
if (value) { if (value) {
changedValue = value.includes(option) changedValue = value.includes(option)
? value.filter((item) => item !== option) ? value.filter((item: string) => item !== option)
: [...value, option]; : [...value, option];
} else { } else {
changedValue = [option]; changedValue = [option];
@ -209,7 +230,7 @@ export const AttributeGeneralSettings = () => {
onChange([]); onChange([]);
}} }}
isOpen={selectEnabledWhenOpen} isOpen={selectEnabledWhenOpen}
isDisabled={enabledWhenSelection === "Always"} isDisabled={selectedScopes.length === clientScopes?.length}
aria-labelledby={"scope"} aria-labelledby={"scope"}
> >
{clientScopes?.map((option) => ( {clientScopes?.map((option) => (
@ -232,24 +253,22 @@ export const AttributeGeneralSettings = () => {
hasNoPaddingTop hasNoPaddingTop
> >
<Controller <Controller
name="required" name="isRequired"
data-testid="required"
defaultValue={false} defaultValue={false}
control={form.control} control={form.control}
render={({ onChange, value }) => ( render={({ onChange, value }) => (
<Switch <Switch
id={"kc-required"} id={"kc-required"}
onChange={(value) => { onChange={onChange}
onChange(value); isChecked={value}
form.setValue("required", value);
}}
isChecked={value === true}
label={t("common:on")} label={t("common:on")}
labelOff={t("common:off")} labelOff={t("common:off")}
/> />
)} )}
></Controller> />
</FormGroup> </FormGroup>
{requiredToggle && ( {required && (
<> <>
<FormGroup <FormGroup
label={t("requiredFor")} label={t("requiredFor")}
@ -257,7 +276,7 @@ export const AttributeGeneralSettings = () => {
hasNoPaddingTop hasNoPaddingTop
> >
<Controller <Controller
name="roles" name="required.roles"
data-testid="requiredFor" data-testid="requiredFor"
defaultValue={REQUIRED_FOR[0].value} defaultValue={REQUIRED_FOR[0].value}
control={form.control} control={form.control}
@ -267,10 +286,12 @@ export const AttributeGeneralSettings = () => {
<Radio <Radio
id={option.label} id={option.label}
key={option.label} key={option.label}
data-testid={option} data-testid={option.label}
isChecked={value === option.value} isChecked={isEqual(value, option.value)}
name="roles" name="roles"
onChange={() => onChange(option.value)} onChange={() => {
onChange(option.value);
}}
label={option.label} label={option.label}
className="kc-requiredFor-option" className="kc-requiredFor-option"
/> />
@ -284,44 +305,49 @@ export const AttributeGeneralSettings = () => {
fieldId="requiredWhen" fieldId="requiredWhen"
hasNoPaddingTop hasNoPaddingTop
> >
<Controller <Radio
id="requiredAlways"
data-testid="requiredAlways"
isChecked={requiredScopes.length === clientScopes?.length}
name="requiredWhen" name="requiredWhen"
data-testid="requiredWhen" label={t("always")}
defaultValue={ENABLED_REQUIRED_WHEN[0]} onChange={(value) => {
control={form.control} if (value) {
render={({ onChange, value }) => ( form.setValue(
<> "required.scopes",
{ENABLED_REQUIRED_WHEN.map((option) => ( clientScopes?.map((s) => s.name)
<Radio );
id={option} } else {
key={option} form.setValue("required.scopes", []);
data-testid={option} }
isChecked={value === option} }}
name="requiredWhen" className="pf-u-mb-md"
onChange={() => { />
onChange(option); <Radio
setRequiredWhenSelection(option); id="requiredScopesAsRequested"
}} data-testid="requiredScopesAsRequested"
label={option} isChecked={requiredScopes.length !== clientScopes?.length}
className="pf-u-mb-md" name="requiredWhen"
/> label={t("scopesAsRequested")}
))} onChange={(value) => {
</> if (value) {
)} form.setValue("required.scopes", []);
} else {
form.setValue(
"required.scopes",
clientScopes?.map((s) => s.name)
);
}
}}
className="pf-u-mb-md"
/> />
</FormGroup> </FormGroup>
<FormGroup fieldId="kc-scope-required-when"> <FormGroup fieldId="kc-scope-required-when">
<Controller <Controller
name="scopeRequired" name="required.scopes"
control={form.control} control={form.control}
defaultValue={[]} defaultValue={[]}
render={({ render={({ onChange, value }) => (
onChange,
value,
}: {
onChange: (newValue: string[]) => void;
value: string[];
}) => (
<Select <Select
name="scopeRequired" name="scopeRequired"
data-testid="required-when-scope-field" data-testid="required-when-scope-field"
@ -339,7 +365,7 @@ export const AttributeGeneralSettings = () => {
let changedValue = [""]; let changedValue = [""];
if (value) { if (value) {
changedValue = value.includes(option) changedValue = value.includes(option)
? value.filter((item) => item !== option) ? value.filter((item: string) => item !== option)
: [...value, option]; : [...value, option];
} else { } else {
changedValue = [option]; changedValue = [option];
@ -351,7 +377,7 @@ export const AttributeGeneralSettings = () => {
onChange([]); onChange([]);
}} }}
isOpen={selectRequiredForOpen} isOpen={selectRequiredForOpen}
isDisabled={requiredWhenSelection === "Always"} isDisabled={requiredScopes.length === clientScopes?.length}
aria-labelledby={"scope"} aria-labelledby={"scope"}
> >
{clientScopes?.map((option) => ( {clientScopes?.map((option) => (

View file

@ -9,57 +9,53 @@ import "../../realm-settings-section.css";
const Permissions = ({ name }: { name: string }) => { const Permissions = ({ name }: { name: string }) => {
const { t } = useTranslation("realm-settings"); const { t } = useTranslation("realm-settings");
const { control } = useFormContext(); const { control } = useFormContext();
return ( return (
<Grid> <Grid>
<GridItem lg={4} sm={6}> <Controller
<Controller name={`permissions.${name}`}
name={`permissions.${name}`} control={control}
control={control} defaultValue={[]}
defaultValue={[]} render={({ onChange, value }) => (
render={({ onChange, value }) => ( <>
<Checkbox <GridItem lg={4} sm={6}>
id={`user-${name}`} <Checkbox
label={t("user")} id={`user-${name}`}
value="user" label={t("user")}
data-testid={`user-${name}`} value="user"
isChecked={value.includes("user")} data-testid={`user-${name}`}
onChange={() => { isChecked={value.includes("user")}
const option = "user"; onChange={() => {
const changedValue = value.includes(option) const option = "user";
? value.filter((item: string) => item !== option) const changedValue = value.includes(option)
: [option]; ? value.filter((item: string) => item !== option)
: [option];
onChange(changedValue); onChange(changedValue);
}} }}
isDisabled={value.includes("admin")} isDisabled={value.includes("admin")}
/> />
)} </GridItem>
/> <GridItem lg={8} sm={6}>
</GridItem> <Checkbox
<GridItem lg={8} sm={6}> id={`admin-${name}`}
<Controller label={t("admin")}
name={`permissions.${name}`} value="admin"
control={control} data-testid={`admin-${name}`}
defaultValue={[]} isChecked={value.includes("admin")}
render={({ onChange, value }) => ( onChange={() => {
<Checkbox const option = "admin";
id={`admin-${name}`} const changedValue = value.includes(option)
label={t("admin")} ? value.filter((item: string) => item !== option)
value="admin" : ["user", option];
data-testid={`admin-${name}`}
isChecked={value.includes("admin")}
onChange={() => {
const option = "admin";
const changedValue = value.includes(option)
? value.filter((item: string) => item !== option)
: ["user", option];
onChange(changedValue); onChange(changedValue);
}} }}
/> />
)} </GridItem>
/> </>
</GridItem> )}
/>
</Grid> </Grid>
); );
}; };

View file

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { import {
Button, Button,
@ -22,26 +21,29 @@ import {
Tr, Tr,
} from "@patternfly/react-table"; } from "@patternfly/react-table";
import { useConfirmDialog } from "../../../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../../../components/confirm-dialog/ConfirmDialog";
import type { Validator } from "./Validators";
import useToggle from "../../../utils/useToggle"; import useToggle from "../../../utils/useToggle";
import { useFormContext, useWatch } from "react-hook-form"; import { useFormContext, useWatch } from "react-hook-form";
import type { KeyValueType } from "../../../components/attribute-form/attribute-convert";
import "../../realm-settings-section.css"; import "../../realm-settings-section.css";
export const AttributeValidations = () => { export const AttributeValidations = () => {
const { t } = useTranslation("realm-settings"); const { t } = useTranslation("realm-settings");
const [addValidatorModalOpen, toggleModal] = useToggle(); const [addValidatorModalOpen, toggleModal] = useToggle();
const [validatorToDelete, setValidatorToDelete] = useState<{ const [validatorToDelete, setValidatorToDelete] =
name: string; useState<{ name: string }>();
}>();
const { setValue, control, register } = useFormContext(); const { setValue, control, register } = useFormContext();
const validators = useWatch<Validator[]>({
const validators = useWatch<KeyValueType[]>({
name: "validations", name: "validations",
control, control,
defaultValue: [], defaultValue: [],
}); });
useEffect(() => register("validations"), []); useEffect(() => {
register("validations");
}, []);
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: t("deleteValidatorConfirmTitle"), titleKey: t("deleteValidatorConfirmTitle"),
@ -52,8 +54,9 @@ export const AttributeValidations = () => {
continueButtonVariant: ButtonVariant.danger, continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => { onConfirm: async () => {
const updatedValidators = validators.filter( const updatedValidators = validators.filter(
(validator) => validator.name !== validatorToDelete?.name (validator) => validator.key !== validatorToDelete?.name
); );
setValue("validations", [...updatedValidators]); setValue("validations", [...updatedValidators]);
}, },
}); });
@ -63,7 +66,10 @@ export const AttributeValidations = () => {
{addValidatorModalOpen && ( {addValidatorModalOpen && (
<AddValidatorDialog <AddValidatorDialog
onConfirm={(newValidator) => { onConfirm={(newValidator) => {
setValue("validations", [...validators, newValidator]); setValue("validations", [
...validators,
{ key: newValidator.name, value: newValidator.config },
]);
}} }}
toggleDialog={toggleModal} toggleDialog={toggleModal}
/> />
@ -94,33 +100,32 @@ export const AttributeValidations = () => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{validators.length ? ( {validators.map((validator) => (
validators.map((validator: Validator) => ( <Tr key={validator.key}>
<Tr key={validator.name}> <Td dataLabel={t("validatorColNames.colName")}>
<Td dataLabel={t("validatorColNames.colName")}> {validator.key}
{validator.name} </Td>
</Td> <Td dataLabel={t("validatorColNames.colConfig")}>
<Td dataLabel={t("validatorColNames.colConfig")}> {JSON.stringify(validator.value)}
{JSON.stringify(validator.config)} </Td>
</Td> <Td>
<Td> <Button
<Button key="validator"
key="validator" variant="link"
variant="link" data-testid="deleteValidator"
data-testid="deleteValidator" onClick={() => {
onClick={() => { toggleDeleteDialog();
toggleDeleteDialog(); setValidatorToDelete({
setValidatorToDelete({ name: validator.key,
name: validator.name, });
}); }}
}} >
> {t("common:delete")}
{t("common:delete")} </Button>
</Button> </Td>
</Td> </Tr>
</Tr> ))}
)) {validators.length === 0 && (
) : (
<Text className="kc-emptyValidators" component={TextVariants.h6}> <Text className="kc-emptyValidators" component={TextVariants.h6}>
{t("realm-settings:emptyValidators")} {t("realm-settings:emptyValidators")}
</Text> </Text>