Realm settings(Client policies -> conditions): Update client-roles condition to match new design (#1532)

* wip client roles select field

WIP multivalued roles component

wip client roles select: multivalued rows component

wip client roles select

role multi select is working :)

update client roles select to use multivalued select as per new design

delete comments

remove unused css

update cypress tests

revert to original

remove duplicates

add isCreateable

* PR feedback

* small refactor

* change to use orderBy

* use localeCompare

* fix tests

Co-authored-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
Jenny 2021-11-22 07:41:43 -05:00 committed by GitHub
parent 71350cacd5
commit 0b73d51412
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 200 additions and 127 deletions

View file

@ -568,7 +568,7 @@ describe("Realm settings tests", () => {
}); });
}); });
describe.skip("Realm settings client policies tab tests", () => { describe("Realm settings client policies tab tests", () => {
beforeEach(() => { beforeEach(() => {
keycloakBefore(); keycloakBefore();
loginPage.logIn(); loginPage.logIn();

View file

@ -176,8 +176,10 @@ export default class RealmSettingsPage {
private clientPolicyDrpDwn = "action-dropdown"; private clientPolicyDrpDwn = "action-dropdown";
private searchFld = "[id^=realm-settings][id$=profilesinput]"; private searchFld = "[id^=realm-settings][id$=profilesinput]";
private searchFldPolicies = "[id^=realm-settings][id$=clientPoliciesinput]"; private searchFldPolicies = "[id^=realm-settings][id$=clientPoliciesinput]";
private clientProfileOne = 'a[href*="realm-settings/clientPolicies/Test"]'; private clientProfileOne =
private clientProfileTwo = 'a[href*="realm-settings/clientPolicies/Edit"]'; 'a[href*="realm-settings/clientPolicies/Test/edit-profile"]';
private clientProfileTwo =
'a[href*="realm-settings/clientPolicies/Edit/edit-profile"]';
private clientPolicy = private clientPolicy =
'a[href*="realm-settings/clientPolicies/Test/edit-policy"]'; 'a[href*="realm-settings/clientPolicies/Test/edit-policy"]';
private reloadBtn = "reloadProfile"; private reloadBtn = "reloadProfile";
@ -195,7 +197,6 @@ export default class RealmSettingsPage {
private addConditionCancelBtn = "addCondition-cancelBtn"; private addConditionCancelBtn = "addCondition-cancelBtn";
private addConditionSaveBtn = "addCondition-saveBtn"; private addConditionSaveBtn = "addCondition-saveBtn";
private conditionTypeLink = "condition-type-link"; private conditionTypeLink = "condition-type-link";
private addValue = "addValue";
private eventListenersFormLabel = ".pf-c-form__label-text"; private eventListenersFormLabel = ".pf-c-form__label-text";
private eventListenersDrpDwn = ".pf-c-select.kc_eventListeners_select"; private eventListenersDrpDwn = ".pf-c-select.kc_eventListeners_select";
private eventListenersSaveBtn = "saveEventListenerBtn"; private eventListenersSaveBtn = "saveEventListenerBtn";
@ -206,6 +207,7 @@ export default class RealmSettingsPage {
private eventListenersDrwDwnSelect = private eventListenersDrwDwnSelect =
".pf-c-button.pf-c-select__toggle-button.pf-m-plain"; ".pf-c-button.pf-c-select__toggle-button.pf-m-plain";
private eventListenerRemove = '[data-ouia-component-id="Remove"]'; private eventListenerRemove = '[data-ouia-component-id="Remove"]';
private roleSelect = ".pf-c-select.kc-role-select";
selectLoginThemeType(themeType: string) { selectLoginThemeType(themeType: string) {
cy.get(this.selectLoginTheme).click(); cy.get(this.selectLoginTheme).click();
@ -971,7 +973,13 @@ export default class RealmSettingsPage {
cy.findByTestId(this.addConditionDrpDwnOption) cy.findByTestId(this.addConditionDrpDwnOption)
.contains("client-roles") .contains("client-roles")
.click(); .click();
cy.get('input[name="config.roles[0].value"]').click().type("role 1"); cy.get(this.roleSelect).click().contains("impersonation").click();
cy.get(this.roleSelect).contains("manage-realm").click();
cy.get(this.roleSelect).contains("view-users").click();
cy.get(this.roleSelect).click();
cy.findByTestId(this.addConditionSaveBtn).click(); cy.findByTestId(this.addConditionSaveBtn).click();
cy.get(this.alertMessage).should( cy.get(this.alertMessage).should(
@ -986,9 +994,10 @@ export default class RealmSettingsPage {
cy.findByTestId(this.conditionTypeLink).contains("client-roles").click(); cy.findByTestId(this.conditionTypeLink).contains("client-roles").click();
cy.findByTestId(this.addValue).click(); cy.get(this.roleSelect).click();
cy.get(this.roleSelect).contains("create-client").click();
cy.get('input[name="config.roles[1].value"]').click().type("role 2"); cy.get(this.roleSelect).click();
cy.findByTestId(this.addConditionSaveBtn).click(); cy.findByTestId(this.addConditionSaveBtn).click();
cy.get(this.alertMessage).should( cy.get(this.alertMessage).should(

View file

@ -30,7 +30,6 @@ export default {
addMapperExplain: addMapperExplain:
"If you want more fine-grain control, you can create protocol mapper on this client", "If you want more fine-grain control, you can create protocol mapper on this client",
realmRoles: "Realm roles", realmRoles: "Realm roles",
selectARole: "Select a role",
clientRoles: "Client roles", clientRoles: "Client roles",
selectASourceOfRoles: "Select a source of roles", selectASourceOfRoles: "Select a source of roles",
newRoleName: "New role name", newRoleName: "New role name",

View file

@ -9,24 +9,13 @@ type DynamicComponentProps = {
parentCallback?: (data: string[]) => void; parentCallback?: (data: string[]) => void;
}; };
export const DynamicComponents = ({ export const DynamicComponents = ({ properties }: DynamicComponentProps) => (
properties,
selectedValues,
parentCallback,
}: DynamicComponentProps) => (
<> <>
{properties.map((property) => { {properties.map((property) => {
const componentType = property.type!; const componentType = property.type!;
if (isValidComponentType(componentType)) { if (isValidComponentType(componentType)) {
const Component = COMPONENTS[componentType]; const Component = COMPONENTS[componentType];
return ( return <Component key={property.name} {...property} />;
<Component
key={property.name}
selectedValues={selectedValues}
parentCallback={parentCallback}
{...property}
/>
);
} else { } else {
console.warn(`There is no editor registered for ${componentType}`); console.warn(`There is no editor registered for ${componentType}`);
} }

View file

@ -48,12 +48,13 @@ export const MultiValuedListComponent = ({
typeAheadAriaLabel="Select" typeAheadAriaLabel="Select"
onToggle={(isOpen) => setOpen(isOpen)} onToggle={(isOpen) => setOpen(isOpen)}
selections={value} selections={value}
onSelect={(_, selectedValue) => { onSelect={(_, v) => {
const option = selectedValue.toString(); const option = v.toString();
const changedValue = value.includes(option) if (value.includes(option)) {
? value.filter((item: string) => item !== option) onChange(value.filter((item: string) => item !== option));
: [...value, option]; } else {
onChange(changedValue); onChange([...value, option]);
}
}} }}
onClear={(event) => { onClear={(event) => {
event.stopPropagation(); event.stopPropagation();

View file

@ -0,0 +1,112 @@
import React, { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { sortedUniq } from "lodash";
import {
FormGroup,
Select,
SelectOption,
SelectVariant,
} from "@patternfly/react-core";
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
import type { ComponentProps } from "./components";
import type { MultiLine } from "../multi-line-input/MultiLineInput";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { HelpItem } from "../help-enabler/HelpItem";
import { convertToHyphens } from "../../util";
import { useWhoAmI } from "../../context/whoami/WhoAmI";
export const MultivaluedRoleComponent = ({
name,
label,
helpText,
}: ComponentProps) => {
const { t } = useTranslation("dynamic");
const { whoAmI } = useWhoAmI();
const fieldName = `config.${convertToHyphens(name!)}`;
const adminClient = useAdminClient();
const { control } = useFormContext();
const [clientRoles, setClientRoles] = useState<RoleRepresentation[]>([]);
const [open, setOpen] = useState(false);
useFetch(
async () => {
const clients = await adminClient.clients.find();
const clientRoles = await Promise.all(
clients.map(async (client) => {
const roles = await adminClient.clients.listRoles({ id: client.id! });
return roles.map<RoleRepresentation>((role) => ({
...role,
}));
})
);
return clientRoles.flat();
},
(clientRoles) => {
setClientRoles(clientRoles);
},
[]
);
const alphabetizedClientRoles = sortedUniq(
clientRoles.map((item) => item.name)
).sort((a, b) =>
a!.localeCompare(b!, whoAmI.getLocale(), { ignorePunctuation: true })
);
return (
<FormGroup
label={t(label!)}
labelIcon={
<HelpItem helpText={t(helpText!)} forLabel={label!} forID={name!} />
}
fieldId={name!}
>
<Controller
name={fieldName}
defaultValue={[]}
control={control}
rules={{ required: true }}
render={({ onChange, value }) => (
<Select
onToggle={(isExpanded) => setOpen(isExpanded)}
isOpen={open}
className="kc-role-select"
data-testid="multivalued-role-select"
variant={SelectVariant.typeaheadMulti}
placeholderText={t("selectARole")}
chipGroupProps={{
numChips: 5,
expandedText: t("common:hide"),
collapsedText: t("common:showRemaining"),
}}
typeAheadAriaLabel={t("selectARole")}
selections={value.map((v: MultiLine) => v.value)}
isCreatable
onSelect={(_, v) => {
const option = v.toString();
if (value.map((v: MultiLine) => v.value).includes(option)) {
onChange(
value.filter((item: MultiLine) => item.value !== option)
);
} else {
onChange([...value, { value: option }]);
}
}}
maxHeight={200}
onClear={() => onChange([])}
>
{alphabetizedClientRoles.map((option) => (
<SelectOption key={option} value={option} />
))}
</Select>
)}
/>
</FormGroup>
);
};

View file

@ -10,10 +10,7 @@ import { ClientSelectComponent } from "./ClientSelectComponent";
import { MultiValuedStringComponent } from "./MultivaluedStringComponent"; import { MultiValuedStringComponent } from "./MultivaluedStringComponent";
import { MultiValuedListComponent } from "./MultivaluedListComponent"; import { MultiValuedListComponent } from "./MultivaluedListComponent";
export type ComponentProps = Omit<ConfigPropertyRepresentation, "type"> & { export type ComponentProps = Omit<ConfigPropertyRepresentation, "type">;
selectedValues?: string[];
parentCallback?: (data: any) => void;
};
const ComponentTypes = [ const ComponentTypes = [
"String", "String",
"boolean", "boolean",

View file

@ -1,6 +1,7 @@
export default { export default {
dynamic: { dynamic: {
addMultivaluedLabel: "Add {{fieldLabel}}", addMultivaluedLabel: "Add {{fieldLabel}}",
selectARole: "Select a role",
usermodel: { usermodel: {
prop: { prop: {
label: "Property", label: "Property",

View file

@ -44,7 +44,7 @@ import { groupBy } from "lodash";
export type IdPMapperRepresentationWithAttributes = export type IdPMapperRepresentationWithAttributes =
IdentityProviderMapperRepresentation & AttributeForm; IdentityProviderMapperRepresentation & AttributeForm;
type Role = RoleRepresentation & { export type Role = RoleRepresentation & {
clientId?: string; clientId?: string;
}; };

View file

@ -25,26 +25,33 @@ import type ComponentTypeRepresentation from "@keycloak/keycloak-admin-client/li
import { useRealm } from "../context/realm-context/RealmContext"; import { useRealm } from "../context/realm-context/RealmContext";
import type { ConfigPropertyRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigInfoRepresentation"; import type { ConfigPropertyRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigInfoRepresentation";
import type ClientPolicyConditionRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientPolicyConditionRepresentation"; import type ClientPolicyConditionRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientPolicyConditionRepresentation";
import { DynamicComponents } from "../components/dynamic/DynamicComponents";
import { import {
EditClientPolicyParams, EditClientPolicyParams,
toEditClientPolicy, toEditClientPolicy,
} from "./routes/EditClientPolicy"; } from "./routes/EditClientPolicy";
import type { EditClientPolicyConditionParams } from "./routes/EditCondition"; import type { EditClientPolicyConditionParams } from "./routes/EditCondition";
import { convertToMultiline } from "../components/multi-line-input/MultiLineInput"; import {
convertToMultiline,
toValue,
} from "../components/multi-line-input/MultiLineInput";
import { MultivaluedRoleComponent } from "../components/dynamic/MultivaluedRoleComponent";
import {
COMPONENTS,
isValidComponentType,
} from "../components/dynamic/components";
export type ItemType = { value: string }; export type ItemType = { value: string };
type ConfigProperty = ConfigPropertyRepresentation & {
config: any;
};
export default function NewClientPolicyCondition() { export default function NewClientPolicyCondition() {
const { t } = useTranslation("realm-settings"); const { t } = useTranslation("realm-settings");
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const history = useHistory(); const history = useHistory();
const { realm } = useRealm(); const { realm } = useRealm();
const { handleSubmit, control } = useForm<ClientPolicyRepresentation>({
mode: "onChange",
});
const [openConditionType, setOpenConditionType] = useState(false); const [openConditionType, setOpenConditionType] = useState(false);
const [policies, setPolicies] = useState<ClientPolicyRepresentation[]>([]); const [policies, setPolicies] = useState<ClientPolicyRepresentation[]>([]);
const [condition, setCondition] = useState< const [condition, setCondition] = useState<
@ -57,13 +64,13 @@ export default function NewClientPolicyCondition() {
ConfigPropertyRepresentation[] ConfigPropertyRepresentation[]
>([]); >([]);
const [selectedVals, setSelectedVals] = useState<any>();
const { policyName } = useParams<EditClientPolicyParams>(); const { policyName } = useParams<EditClientPolicyParams>();
const { conditionName } = useParams<EditClientPolicyConditionParams>(); const { conditionName } = useParams<EditClientPolicyConditionParams>();
const serverInfo = useServerInfo(); const serverInfo = useServerInfo();
const form = useForm(); const form = useForm<ClientPolicyConditionRepresentation>({
shouldUnregister: false,
});
const conditionTypes = const conditionTypes =
serverInfo.componentTypes?.[ serverInfo.componentTypes?.[
@ -72,36 +79,20 @@ export default function NewClientPolicyCondition() {
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const setupForm = (condition: ClientPolicyConditionRepresentation) => { const setupForm = (
condition: ClientPolicyConditionRepresentation,
properties: ConfigPropertyRepresentation[]
) => {
form.reset(); form.reset();
Object.entries(condition).map(([key, value]) => { Object.entries(condition.configuration!).map(([key, value]) => {
if (key === "configuration") { const formKey = `config.${key}`;
if ( const property = properties.find((p) => p.name === key);
conditionName === "client-roles" || if (property?.type === "MultivaluedString") {
conditionName === "client-updater-source-roles" form.setValue(formKey, convertToMultiline(value));
) { } else {
form.setValue("config.roles", convertToMultiline(value["roles"])); form.setValue(formKey, value);
} else if (conditionName === "client-scopes") {
form.setValue("config.scopes", convertToMultiline(value["scopes"]));
form.setValue("config.type", value["type"]);
} else if (conditionName === "client-updater-source-groups") {
form.setValue("config.groups", convertToMultiline(value["groups"]));
} else if (conditionName === "client-updater-source-host") {
form.setValue(
"config.trusted-hosts",
convertToMultiline(value["trusted-hosts"])
);
} else if (conditionName === "client-updater-context") {
form.setValue(
"config.update-client-source",
value["update-client-source"][0]["update-client-source"]
);
} else if (conditionName === "client-access-type") {
form.setValue("config.type", value.type[0]);
}
} }
form.setValue(key, value);
}); });
}; };
@ -123,53 +114,23 @@ export default function NewClientPolicyCondition() {
); );
setConditionData(typeAndConfigData!); setConditionData(typeAndConfigData!);
setSelectedVals(Object.values(typeAndConfigData?.configuration!)[0][0]);
setConditionProperties(currentCondition?.properties!); setConditionProperties(currentCondition?.properties!);
setupForm(typeAndConfigData!); setupForm(typeAndConfigData!, currentCondition?.properties!);
} }
}, },
[] []
); );
const save = async () => { const save = async (configPolicy: ConfigProperty) => {
const formValues = form.getValues(); const configValues = configPolicy.config;
const configValues = formValues.config;
const writeConfig = () => { const writeConfig = () =>
if ( conditionProperties.reduce((r: any, p) => {
condition[0]?.condition === "any-client" || p.type === "MultivaluedString"
conditionName === "any-client" ? (r[p.name!] = toValue(configValues[p.name!]))
) { : (r[p.name!] = configValues[p.name!]);
return {}; return r;
} else if ( }, {});
condition[0]?.condition === "client-access-type" ||
conditionName === "client-access-type"
) {
return { type: [formValues.config.type] };
} else if (
condition[0]?.condition === "client-updater-context" ||
conditionName === "client-updater-context"
) {
return {
"update-client-source": [Object.values(formValues)[0]],
};
} else if (
condition[0]?.condition === "client-scopes" ||
conditionName === "client-scopes"
) {
return {
type: Object.values(formValues)[0].type,
scopes: (Object.values(formValues)[0].scopes as ItemType[]).map(
(item) => (item as ItemType).value
),
};
} else
return {
[Object.keys(configValues)[0]]: Object.values(
configValues?.[Object.keys(configValues)[0]]
).map((item) => (item as ItemType).value),
};
};
const updatedPolicies = policies.map((policy) => { const updatedPolicies = policies.map((policy) => {
if (policy.name !== policyName) { if (policy.name !== policyName) {
@ -234,10 +195,6 @@ export default function NewClientPolicyCondition() {
} }
}; };
const handleCallback = (childData: any) => {
setSelectedVals(childData);
};
return ( return (
<PageSection variant="light"> <PageSection variant="light">
<FormPanel <FormPanel
@ -248,7 +205,7 @@ export default function NewClientPolicyCondition() {
isHorizontal isHorizontal
role="manage-realm" role="manage-realm"
className="pf-u-mt-lg" className="pf-u-mt-lg"
onSubmit={handleSubmit(save)} onSubmit={form.handleSubmit(save)}
> >
<FormGroup <FormGroup
label={t("conditionType")} label={t("conditionType")}
@ -272,10 +229,11 @@ export default function NewClientPolicyCondition() {
<Controller <Controller
name="conditions" name="conditions"
defaultValue={"any-client"} defaultValue={"any-client"}
control={control} control={form.control}
render={({ onChange, value }) => ( render={({ onChange, value }) => (
<Select <Select
placeholderText={t("selectACondition")} placeholderText={t("selectACondition")}
className="kc-conditionType-select"
data-testid="conditionType-select" data-testid="conditionType-select"
toggleId="provider" toggleId="provider"
isDisabled={!!conditionName} isDisabled={!!conditionName}
@ -316,18 +274,25 @@ export default function NewClientPolicyCondition() {
)} )}
/> />
</FormGroup> </FormGroup>
<FormProvider {...form}> <FormProvider {...form}>
<DynamicComponents {conditionProperties.map((property) => {
properties={conditionProperties} const componentType = property.type!;
selectedValues={ if (
conditionName === "client-access-type" property.name === "roles" &&
? selectedVals (conditionType === "client-roles" ||
: conditionName === "client-updater-context" conditionName === "client-roles")
? selectedVals?.["update-client-source"] ) {
: [] return <MultivaluedRoleComponent {...property} />;
} else if (isValidComponentType(componentType)) {
const Component = COMPONENTS[componentType];
return <Component key={property.name} {...property} />;
} else {
console.warn(
`There is no editor registered for ${componentType}`
);
} }
parentCallback={handleCallback} })}
/>
</FormProvider> </FormProvider>
<ActionGroup> <ActionGroup>
<Button <Button

View file

@ -251,4 +251,4 @@ article.pf-c-card.pf-m-flat.kc-login-settings-template
.kc_eventListeners_select { .kc_eventListeners_select {
width: 35rem; width: 35rem;
} }

View file

@ -10,7 +10,7 @@ export type ClientProfileParams = {
}; };
export const ClientProfileRoute: RouteDef = { export const ClientProfileRoute: RouteDef = {
path: "/:realm/realm-settings/clientPolicies/:profileName", path: "/:realm/realm-settings/clientPolicies/:profileName/edit-profile",
component: lazy(() => import("../ClientProfileForm")), component: lazy(() => import("../ClientProfileForm")),
breadcrumb: () => EditProfileCrumb, breadcrumb: () => EditProfileCrumb,
access: ["view-realm", "view-users"], access: ["view-realm", "view-users"],

View file

@ -11,7 +11,7 @@ export type ExecutorParams = {
}; };
export const ExecutorRoute: RouteDef = { export const ExecutorRoute: RouteDef = {
path: "/:realm/realm-settings/clientPolicies/:profileName/:executorName", path: "/:realm/realm-settings/clientPolicies/:profileName/edit-profile/:executorName",
component: lazy(() => import("../ExecutorForm")), component: lazy(() => import("../ExecutorForm")),
breadcrumb: () => EditExecutorCrumb, breadcrumb: () => EditExecutorCrumb,
access: ["manage-realm"], access: ["manage-realm"],