add create client policy form; WIP (#1488)

add client policy tests

checkout realm settings test from master

RealmSettingsPage.ts master

remove comment and add missing translation

fix tests

PR feedback from Jon and Erik

rebase

editClientPolicy

edit client policy

add client policy conditions form

fix bug in create form

remove comment

update help text

fixes

breadcrumbs

add support for adding multiple conditions, deleting conditions, and list conditions in data table

clean up names

add delete functionality to conditions form

PR feedback from Jon

useMemo for conditions

remove comments and logs

remove unused hook

PR feedback from Jon

messages

rename message

rebase

remove duplicate value

fixed multi select bug

conditionConfigs wip

add all config fields, labels, and help text for condition types

wip config POST

config post is working

fix bug in multiline string POST

fix any client empty config

client scopes config wip

fix config settings for client-updater-context

fix client access type config

clean up configuration conditionals

add aria label to resolve critical axe issue

Apply suggestions from code review

Co-authored-by: Jon Koops <jonkoops@gmail.com>

address PR feedback from Jon

fix undef error

wip edit condition configs

fix import

wip edit configs

wip edit condition configs

populate all multiline input fields with data from server

update functionality

condition configs done

update route and delete comment

fix client updater context

add tests

add test for editing condition

remove  comment

fix test

unskip tests

need optional chain for cypress test

fix breadcrumb
This commit is contained in:
Jenny 2021-11-09 13:27:25 -05:00 committed by GitHub
parent 386201595b
commit 0565f6a482
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 334 additions and 51 deletions

View file

@ -566,6 +566,30 @@ describe("Realm settings tests", () => {
realmSettingsPage.shouldSearchClientPolicy();
});
it("Should not have conditions configured by default", () => {
realmSettingsPage.shouldNotHaveConditionsConfigured();
});
it("Should cancel adding a new condition to a client profile", () => {
realmSettingsPage.shouldCancelAddingCondition();
});
it("Should add a new condition to a client profile", () => {
realmSettingsPage.shouldAddCondition();
});
it("Should edit the condition of a client profile", () => {
realmSettingsPage.shouldEditCondition();
});
it("Should cancel deleting condition from a client profile", () => {
realmSettingsPage.shouldCancelDeletingCondition();
});
it("Should delete condition from a client profile", () => {
realmSettingsPage.shouldDeleteCondition();
});
it("Check cancelling the client policy deletion", () => {
realmSettingsPage.shouldDisplayDeleteClientPolicyDialog();
});

View file

@ -176,8 +176,12 @@ 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";
private addExecutor = "addExecutor";
private addExecutorDrpDwn = ".pf-c-select__toggle";
@ -185,6 +189,13 @@ export default class RealmSettingsPage {
private addExecutorCancelBtn = "addExecutor-cancelBtn";
private addExecutorSaveBtn = "addExecutor-saveBtn";
private listingPage = new ListingPage();
private addCondition = "addCondition";
private addConditionDrpDwn = ".pf-c-select__toggle";
private addConditionDrpDwnOption = "conditionType-select";
private addConditionCancelBtn = "addCondition-cancelBtn";
private addConditionSaveBtn = "addCondition-saveBtn";
private conditionTypeLink = "condition-type-link";
private addValue = "addValue";
selectLoginThemeType(themeType: string) {
cy.get(this.selectLoginTheme).click();
@ -843,6 +854,87 @@ export default class RealmSettingsPage {
cy.get("table").should("be.visible").contains("td", "Test");
}
shouldNotHaveConditionsConfigured() {
cy.get(this.clientPolicy).click();
cy.get('h6[class*="kc-emptyConditions"]').should(
"have.text",
"No conditions configured"
);
}
shouldCancelAddingCondition() {
cy.get(this.clientPolicy).click();
cy.findByTestId(this.addCondition).click();
cy.get(this.addConditionDrpDwn).click();
cy.findByTestId(this.addConditionDrpDwnOption)
.contains("any-client")
.click();
cy.findByTestId(this.addConditionCancelBtn).click();
cy.get('h6[class*="kc-emptyConditions"]').should(
"have.text",
"No conditions configured"
);
}
shouldAddCondition() {
cy.get(this.clientPolicy).click();
cy.findByTestId(this.addCondition).click();
cy.get(this.addConditionDrpDwn).click();
cy.findByTestId(this.addConditionDrpDwnOption)
.contains("client-roles")
.click();
cy.get('input[name="config.roles[0].value"]').click().type("role 1");
cy.findByTestId(this.addConditionSaveBtn).click();
cy.get(this.alertMessage).should(
"be.visible",
"Success! Condition created successfully"
);
cy.get('ul[class*="pf-c-data-list"]').should("have.text", "client-roles");
}
shouldEditCondition() {
cy.get(this.clientPolicy).click();
cy.findByTestId(this.conditionTypeLink).contains("client-roles").click();
cy.findByTestId(this.addValue).click();
cy.get('input[name="config.roles[1].value"]').click().type("role 2");
cy.findByTestId(this.addConditionSaveBtn).click();
cy.get(this.alertMessage).should(
"be.visible",
"Success! Condition updated successfully"
);
}
shouldCancelDeletingCondition() {
cy.get(this.clientPolicy).click();
cy.get('svg[class*="kc-conditionType-trash-icon"]').click();
cy.get(this.deleteDialogTitle).contains("Delete condition?");
cy.get(this.deleteDialogBodyText).contains(
"This action will permanently delete client-roles. This cannot be undone."
);
cy.findByTestId("modalConfirm").contains("Delete");
cy.get(this.deleteDialogCancelBtn).contains("Cancel").click();
cy.get('ul[class*="pf-c-data-list"]').should("have.text", "client-roles");
}
shouldDeleteCondition() {
cy.get(this.clientPolicy).click();
cy.get('svg[class*="kc-conditionType-trash-icon"]').click();
cy.get(this.deleteDialogTitle).contains("Delete condition?");
cy.get(this.deleteDialogBodyText).contains(
"This action will permanently delete client-roles. This cannot be undone."
);
cy.findByTestId("modalConfirm").contains("Delete").click();
cy.get('h6[class*="kc-emptyConditions"]').should(
"have.text",
"No conditions configured"
);
}
shouldDeleteClientPolicyDialog() {
this.listingPage.searchItem("Test", false);
this.listingPage.clickRowDetails("Test").clickDetailMenu("Delete");

View file

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

View file

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import {
@ -18,10 +18,20 @@ export const MultiValuedListComponent = ({
helpText,
defaultValue,
options,
selectedValues,
parentCallback,
}: ComponentProps) => {
const { t } = useTranslation("dynamic");
const { control } = useFormContext();
const [open, setOpen] = useState(false);
const [selectedItems, setSelectedItems] = useState<string[]>([defaultValue]);
useEffect(() => {
if (selectedValues) {
parentCallback!(selectedValues);
setSelectedItems(selectedValues!);
}
}, []);
return (
<FormGroup
@ -52,10 +62,19 @@ export const MultiValuedListComponent = ({
const option = v.toString();
if (!value) {
onChange([option]);
parentCallback!([option]);
setSelectedItems([option]);
} else if (value.includes(option)) {
onChange(value.filter((item: string) => item !== option));
const updatedItems = selectedItems.filter(
(item: string) => item !== option
);
setSelectedItems(updatedItems);
onChange(updatedItems);
parentCallback!(updatedItems);
} else {
onChange([...value, option]);
parentCallback!([...value, option]);
setSelectedItems([...selectedItems, option]);
}
}}
onClear={(event) => {

View file

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

View file

@ -15,13 +15,13 @@ export type MultiLine = {
};
export function convertToMultiline(fields: string[]): MultiLine[] {
return (fields && fields.length > 0 ? fields : [""]).map((field) => {
return (fields.length > 0 ? fields : [""]).map((field) => {
return { value: field };
});
}
export function toValue(formValue: MultiLine[]): string[] {
return formValue?.map((field) => field.value);
return formValue.map((field) => field.value);
}
export type MultiLineInputProps = Omit<TextInputProps, "form"> & {
@ -76,6 +76,8 @@ export const MultiLineInput = ({
onClick={() => append({})}
tabIndex={-1}
aria-label={t("common:add")}
data-testid="addValue"
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
isDisabled={rest.isDisabled || !currentValues?.[index]?.value}
>
<PlusCircleIcon /> {t(addButtonLabel || "common:add")}

View file

@ -23,10 +23,15 @@ import { useAlerts } from "../components/alert/Alerts";
import { useHistory, useParams } from "react-router";
import type ComponentTypeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentTypeRepresentation";
import { useRealm } from "../context/realm-context/RealmContext";
import type { EditClientPolicyParams } from "./routes/EditClientPolicy";
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";
export type ItemType = { value: string };
@ -45,12 +50,17 @@ export default function NewClientPolicyCondition() {
const [condition, setCondition] = useState<
ClientPolicyConditionRepresentation[]
>([]);
const [conditionData, setConditionData] =
useState<ClientPolicyConditionRepresentation>();
const [conditionType, setConditionType] = useState("");
const [conditionProperties, setConditionProperties] = useState<
ConfigPropertyRepresentation[]
>([]);
const [selectedVals, setSelectedVals] = useState<any>();
const { policyName } = useParams<EditClientPolicyParams>();
const { conditionName } = useParams<EditClientPolicyConditionParams>();
const serverInfo = useServerInfo();
const form = useForm();
@ -62,48 +72,139 @@ export default function NewClientPolicyCondition() {
const adminClient = useAdminClient();
const setupForm = (condition: ClientPolicyConditionRepresentation) => {
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]);
}
}
form.setValue(key, value);
});
};
useFetch(
() => adminClient.clientPolicies.listPolicies(),
(policies) => {
setPolicies(policies.policies ?? []);
if (conditionName) {
const currentPolicy = policies.policies?.find(
(item) => item.name === policyName
);
const typeAndConfigData = currentPolicy?.conditions?.find(
(item) => item.condition === conditionName
);
const currentCondition = conditionTypes?.find(
(condition) => condition.id === conditionName
);
setConditionData(typeAndConfigData!);
setSelectedVals(Object.values(typeAndConfigData?.configuration!)[0][0]);
setConditionProperties(currentCondition?.properties!);
setupForm(typeAndConfigData!);
}
},
[]
);
const save = async () => {
const formValues = form.getValues();
const configValues = formValues.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 updatedPolicies = policies.map((policy) => {
if (policy.name !== policyName) {
return policy;
}
const formValues = form.getValues();
const configValues = formValues.config;
let conditions = policy.conditions ?? [];
const writeConfig = () => {
if (condition[0]?.condition === "any-client") {
return {};
} else if (condition[0]?.condition === "client-access-type") {
return { type: [formValues["client-accesstype"].label] };
} else if (condition[0]?.condition === "client-updater-context") {
return {
"update-client-source": [Object.values(formValues)[0]],
};
} else if (condition[0]?.condition === "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),
};
};
if (conditionName) {
const createdCondition = {
condition: conditionData?.condition,
configuration: writeConfig(),
};
const conditions = (policy.conditions ?? []).concat({
const index = conditions.findIndex(
(condition) => conditionName === condition.condition
);
if (index === -1) {
return;
}
const newConditions = [
...conditions.slice(0, index),
createdCondition,
...conditions.slice(index + 1),
];
return {
...policy,
conditions: newConditions,
};
}
conditions = conditions.concat({
condition: condition[0].condition,
configuration: writeConfig(),
});
@ -112,7 +213,7 @@ export default function NewClientPolicyCondition() {
...policy,
conditions,
};
});
}) as ClientPolicyRepresentation[];
try {
await adminClient.clientPolicies.updatePolicy({
@ -123,7 +224,9 @@ export default function NewClientPolicyCondition() {
`/${realm}/realm-settings/clientPolicies/${policyName}/edit-policy`
);
addAlert(
t("realm-settings:createClientConditionSuccess"),
conditionName
? t("realm-settings:updateClientConditionSuccess")
: t("realm-settings:createClientConditionSuccess"),
AlertVariant.success
);
} catch (error) {
@ -131,9 +234,16 @@ export default function NewClientPolicyCondition() {
}
};
const handleCallback = (childData: any) => {
setSelectedVals(childData);
};
return (
<PageSection variant="light">
<FormPanel className="kc-login-screen" title={t("addCondition")}>
<FormPanel
className="kc-login-screen"
title={conditionName ? t("editCondition") : t("addCondition")}
>
<FormAccess
isHorizontal
role="manage-realm"
@ -166,7 +276,9 @@ export default function NewClientPolicyCondition() {
render={({ onChange, value }) => (
<Select
placeholderText={t("selectACondition")}
data-testid="conditionType-select"
toggleId="provider"
isDisabled={!!conditionName}
onToggle={(toggle) => setOpenConditionType(toggle)}
onSelect={(_, value) => {
onChange(value);
@ -181,7 +293,7 @@ export default function NewClientPolicyCondition() {
]);
setOpenConditionType(false);
}}
selections={conditionType}
selections={conditionName ? conditionName : conditionType}
variant={SelectVariant.single}
aria-label={t("conditionType")}
isOpen={openConditionType}
@ -205,23 +317,32 @@ export default function NewClientPolicyCondition() {
/>
</FormGroup>
<FormProvider {...form}>
<DynamicComponents properties={conditionProperties} />
<DynamicComponents
properties={conditionProperties}
selectedValues={
conditionName === "client-access-type"
? selectedVals
: conditionName === "client-updater-context"
? selectedVals?.["update-client-source"]
: []
}
parentCallback={handleCallback}
/>
</FormProvider>
<ActionGroup>
<Button
variant="primary"
type="submit"
data-testid="edit-policy-tab-save"
isDisabled={conditionType === ""}
data-testid="addCondition-saveBtn"
isDisabled={conditionType === "" && !conditionName}
>
{t("common:add")}
{conditionName ? t("common:save") : t("common:add")}
</Button>
<Button
variant="link"
data-testid="addCondition-cancelBtn"
onClick={() =>
history.push(
`/${realm}/realm-settings/clientPolicies/${policyName}/edit-policy`
)
history.push(toEditClientPolicy({ realm, policyName }))
}
>
{t("common:cancel")}

View file

@ -37,6 +37,7 @@ import type ClientPolicyRepresentation from "@keycloak/keycloak-admin-client/lib
import { toClientPolicies } from "./routes/ClientPolicies";
import { toNewClientPolicyCondition } from "./routes/AddCondition";
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { toEditClientPolicyCondition } from "./routes/EditCondition";
import type { EditClientPolicyParams } from "./routes/EditClientPolicy";
import { AddClientProfileModal } from "./AddClientProfileModal";
import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfileRepresentation";
@ -462,7 +463,7 @@ export default function NewClientPolicyForm() {
)}
variant="link"
className="kc-addCondition"
data-testid="cancelCreateProfile"
data-testid="addCondition"
icon={<PlusCircleIcon />}
>
{t("realm-settings:addCondition")}
@ -490,7 +491,11 @@ export default function NewClientPolicyForm() {
<Link
key={condition.condition}
data-testid="condition-type-link"
to={""}
to={toEditClientPolicyCondition({
realm,
conditionName: condition.condition!,
policyName: policyName,
})}
className="kc-condition-link"
>
{condition.condition}
@ -570,7 +575,7 @@ export default function NewClientPolicyForm() {
id="addClientProfile"
variant="link"
className="kc-addClientProfile"
data-testid="cancelCreateProfile"
data-testid="addClientProfile"
icon={<PlusCircleIcon />}
onClick={toggleModal}
>

View file

@ -200,6 +200,7 @@ export default {
createClientPolicySuccess: "New policy created",
createClientConditionSuccess: "Condition created successfully.",
createClientConditionError: "Error creating condition: {{error}}",
updateClientConditionSuccess: "Condition updated successfully.",
deleteClientConditionSuccess: "Condition deleted successfully.",
deleteClientConditionError: "Error creating condition: {{error}}",
clientPolicySearch: "Search client policy",
@ -312,6 +313,7 @@ export default {
clientUpdaterSourceRoles: "Updating entity role",
conditionsHelpItem: "Conditions help item",
addCondition: "Add condition",
editCondition: "Edit condition",
emptyConditions: "No conditions configured",
updateClientPoliciesSuccess:
"The client policies configuration was updated",

View file

@ -13,6 +13,7 @@ import { AddExecutorRoute } from "./routes/AddExecutor";
import { AddClientPolicyRoute } from "./routes/AddClientPolicy";
import { EditClientPolicyRoute } from "./routes/EditClientPolicy";
import { NewClientPolicyConditionRoute } from "./routes/AddCondition";
import { EditClientPolicyConditionRoute } from "./routes/EditCondition";
const routes: RouteDef[] = [
RealmSettingsRoute,
@ -29,6 +30,7 @@ const routes: RouteDef[] = [
AddClientPolicyRoute,
EditClientPolicyRoute,
NewClientPolicyConditionRoute,
EditClientPolicyConditionRoute,
];
export default routes;

View file

@ -12,7 +12,7 @@ export type EditClientPolicyConditionParams = {
export const EditClientPolicyConditionRoute: RouteDef = {
path: "/:realm/realm-settings/clientPolicies/:policyName?/edit-policy/:conditionName/edit-condition",
component: NewClientPolicyCondition,
breadcrumb: (t) => t("realm-settings:addCondition"),
breadcrumb: (t) => t("realm-settings:editCondition"),
access: "manage-clients",
};