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:
parent
71350cacd5
commit
0b73d51412
13 changed files with 200 additions and 127 deletions
|
@ -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();
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
112
src/components/dynamic/MultivaluedRoleComponent.tsx
Normal file
112
src/components/dynamic/MultivaluedRoleComponent.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export default {
|
||||
dynamic: {
|
||||
addMultivaluedLabel: "Add {{fieldLabel}}",
|
||||
selectARole: "Select a role",
|
||||
usermodel: {
|
||||
prop: {
|
||||
label: "Property",
|
||||
|
|
|
@ -44,7 +44,7 @@ import { groupBy } from "lodash";
|
|||
export type IdPMapperRepresentationWithAttributes =
|
||||
IdentityProviderMapperRepresentation & AttributeForm;
|
||||
|
||||
type Role = RoleRepresentation & {
|
||||
export type Role = RoleRepresentation & {
|
||||
clientId?: string;
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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"],
|
||||
|
|
Loading…
Reference in a new issue