From 0565f6a482e1618b09c2a4987ad85191d193b0f8 Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Tue, 9 Nov 2021 13:27:25 -0500 Subject: [PATCH] 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 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 --- .../integration/realm_settings_test.spec.ts | 24 +++ .../realm_settings/RealmSettingsPage.ts | 96 ++++++++- src/components/dynamic/DynamicComponents.tsx | 17 +- .../dynamic/MultivaluedListComponent.tsx | 23 +- src/components/dynamic/components.ts | 5 +- .../multi-line-input/MultiLineInput.tsx | 6 +- .../NewClientPolicyCondition.tsx | 197 ++++++++++++++---- src/realm-settings/NewClientPolicyForm.tsx | 11 +- src/realm-settings/messages.ts | 2 + src/realm-settings/routes.ts | 2 + src/realm-settings/routes/EditCondition.ts | 2 +- 11 files changed, 334 insertions(+), 51 deletions(-) diff --git a/cypress/integration/realm_settings_test.spec.ts b/cypress/integration/realm_settings_test.spec.ts index 88ac61a48b..a1ff5e083d 100644 --- a/cypress/integration/realm_settings_test.spec.ts +++ b/cypress/integration/realm_settings_test.spec.ts @@ -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(); }); diff --git a/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts b/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts index 56e1994b50..485204b908 100644 --- a/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts +++ b/cypress/support/pages/admin_console/manage/realm_settings/RealmSettingsPage.ts @@ -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"); diff --git a/src/components/dynamic/DynamicComponents.tsx b/src/components/dynamic/DynamicComponents.tsx index 5dd8825ca5..516b8a979f 100644 --- a/src/components/dynamic/DynamicComponents.tsx +++ b/src/components/dynamic/DynamicComponents.tsx @@ -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 ; + return ( + + ); } else { console.warn(`There is no editor registered for ${componentType}`); } diff --git a/src/components/dynamic/MultivaluedListComponent.tsx b/src/components/dynamic/MultivaluedListComponent.tsx index ae5e0f0c5a..f98b3c99a7 100644 --- a/src/components/dynamic/MultivaluedListComponent.tsx +++ b/src/components/dynamic/MultivaluedListComponent.tsx @@ -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([defaultValue]); + + useEffect(() => { + if (selectedValues) { + parentCallback!(selectedValues); + setSelectedItems(selectedValues!); + } + }, []); return ( 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) => { diff --git a/src/components/dynamic/components.ts b/src/components/dynamic/components.ts index f09855ee21..17b7bcb0da 100644 --- a/src/components/dynamic/components.ts +++ b/src/components/dynamic/components.ts @@ -10,7 +10,10 @@ import { ClientSelectComponent } from "./ClientSelectComponent"; import { MultiValuedStringComponent } from "./MultivaluedStringComponent"; import { MultiValuedListComponent } from "./MultivaluedListComponent"; -export type ComponentProps = Omit; +export type ComponentProps = Omit & { + selectedValues?: string[]; + parentCallback?: (data: any) => void; +}; const ComponentTypes = [ "String", "boolean", diff --git a/src/components/multi-line-input/MultiLineInput.tsx b/src/components/multi-line-input/MultiLineInput.tsx index 22898b2257..d80ef89f02 100644 --- a/src/components/multi-line-input/MultiLineInput.tsx +++ b/src/components/multi-line-input/MultiLineInput.tsx @@ -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 & { @@ -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} > {t(addButtonLabel || "common:add")} diff --git a/src/realm-settings/NewClientPolicyCondition.tsx b/src/realm-settings/NewClientPolicyCondition.tsx index 5a6f255bae..6558dc99c6 100644 --- a/src/realm-settings/NewClientPolicyCondition.tsx +++ b/src/realm-settings/NewClientPolicyCondition.tsx @@ -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(); const [conditionType, setConditionType] = useState(""); const [conditionProperties, setConditionProperties] = useState< ConfigPropertyRepresentation[] >([]); + const [selectedVals, setSelectedVals] = useState(); + const { policyName } = useParams(); + const { conditionName } = useParams(); 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 ( - + (