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(() => {
keycloakBefore();
loginPage.logIn();

View file

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

View file

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

View file

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

View file

@ -48,12 +48,13 @@ export const MultiValuedListComponent = ({
typeAheadAriaLabel="Select"
onToggle={(isOpen) => setOpen(isOpen)}
selections={value}
onSelect={(_, selectedValue) => {
const option = selectedValue.toString();
const changedValue = value.includes(option)
? value.filter((item: string) => item !== option)
: [...value, option];
onChange(changedValue);
onSelect={(_, v) => {
const option = v.toString();
if (value.includes(option)) {
onChange(value.filter((item: string) => item !== option));
} else {
onChange([...value, option]);
}
}}
onClear={(event) => {
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 { MultiValuedListComponent } from "./MultivaluedListComponent";
export type ComponentProps = Omit<ConfigPropertyRepresentation, "type"> & {
selectedValues?: string[];
parentCallback?: (data: any) => void;
};
export type ComponentProps = Omit<ConfigPropertyRepresentation, "type">;
const ComponentTypes = [
"String",
"boolean",

View file

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

View file

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

View file

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

View file

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

View file

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