From 9e5f711bea7c6b8f8b5769e7d5fd1c4358d453c3 Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Fri, 29 Oct 2021 10:19:57 -0400 Subject: [PATCH] Client policies(policies): Create, add, delete and list client profiles in existing policy (#1393) * add create client policy form; WIP 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 add profiles modal wip addprofiles wip profiles wip help text wip add help text remove comments remove duplicate message update data test id PR feedback from Jon 1 Apply suggestions from code review Co-authored-by: Jon Koops remove fragment create policy detail attribute type * PR feedback from Jon 1 * PR feedback from Jon 2 * add spinner to prevent loader from being called * remove duplicate identifier * fix and rename route * rename route --- src/realm-settings/AddClientProfileModal.tsx | 137 ++++++++++ src/realm-settings/NewClientPolicyForm.tsx | 237 ++++++++++++++++-- src/realm-settings/PoliciesTab.tsx | 6 +- src/realm-settings/ProfilesTab.tsx | 2 +- src/realm-settings/messages.ts | 13 +- src/realm-settings/routes.ts | 4 +- ...{NewClientPolicy.ts => AddClientPolicy.ts} | 12 +- src/realm-settings/routes/ClientProfile.ts | 2 +- src/user/routes/AddUser.ts | 1 + 9 files changed, 379 insertions(+), 35 deletions(-) create mode 100644 src/realm-settings/AddClientProfileModal.tsx rename src/realm-settings/routes/{NewClientPolicy.ts => AddClientPolicy.ts} (58%) diff --git a/src/realm-settings/AddClientProfileModal.tsx b/src/realm-settings/AddClientProfileModal.tsx new file mode 100644 index 0000000000..0386b5a98c --- /dev/null +++ b/src/realm-settings/AddClientProfileModal.tsx @@ -0,0 +1,137 @@ +import React, { useState } from "react"; +import { + Button, + Label, + Modal, + ModalVariant, + Spinner, +} from "@patternfly/react-core"; +import { useTranslation } from "react-i18next"; +import { useFetch, useAdminClient } from "../context/auth/AdminClient"; +import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation"; +import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; +import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; +import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfileRepresentation"; + +type ClientProfile = ClientProfileRepresentation & { + global: boolean; +}; + +export type AddClientProfileModalProps = { + open: boolean; + toggleDialog: () => void; + onConfirm: (newReps: RoleRepresentation[]) => void; + allProfiles: string[]; +}; + +export const AddClientProfileModal = (props: AddClientProfileModalProps) => { + const { t } = useTranslation("roles"); + const adminClient = useAdminClient(); + const [selectedRows, setSelectedRows] = useState([]); + + const [tableProfiles, setTableProfiles] = useState(); + + useFetch( + () => + adminClient.clientPolicies.listProfiles({ + includeGlobalProfiles: true, + }), + (allProfiles) => { + const globalProfiles = allProfiles.globalProfiles?.map( + (globalProfiles) => ({ + ...globalProfiles, + global: true, + }) + ); + + const profiles = allProfiles.profiles?.map((profiles) => ({ + ...profiles, + global: false, + })); + + setTableProfiles([...(globalProfiles ?? []), ...(profiles ?? [])]); + }, + [] + ); + + const loader = async () => tableProfiles ?? []; + + if (!tableProfiles) { + return ( +
+ +
+ ); + } + + const AliasRenderer = ({ name }: ClientProfile) => ( + <> + {name && } {name} + + ); + + return ( + { + props.toggleDialog(); + props.onConfirm(selectedRows); + }} + > + {t("common:add")} + , + , + ]} + > + + props.allProfiles.includes(value.name!) || false + } + ariaLabelKey="realm-settings:profilesList" + searchPlaceholderKey="realm-settings:searchProfile" + canSelectAll + onSelect={(rows) => { + setSelectedRows([...rows]); + }} + columns={[ + { + name: "name", + displayKey: "realm-settings:clientProfileName", + cellRenderer: AliasRenderer, + }, + { + name: "description", + displayKey: "common:description", + }, + ]} + emptyState={ + + } + /> + + ); +}; diff --git a/src/realm-settings/NewClientPolicyForm.tsx b/src/realm-settings/NewClientPolicyForm.tsx index 367ecb20cf..4913ba46df 100644 --- a/src/realm-settings/NewClientPolicyForm.tsx +++ b/src/realm-settings/NewClientPolicyForm.tsx @@ -38,6 +38,8 @@ import { toClientPolicies } from "./routes/ClientPolicies"; import { toNewClientPolicyCondition } from "./routes/AddCondition"; import { useServerInfo } from "../context/server-info/ServerInfoProvider"; import type { EditClientPolicyParams } from "./routes/EditClientPolicy"; +import { AddClientProfileModal } from "./AddClientProfileModal"; +import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfileRepresentation"; type NewClientPolicyForm = Required; @@ -49,6 +51,11 @@ const defaultValues: NewClientPolicyForm = { profiles: [], }; +type PolicyDetailAttributes = { + idx: number; + name: string; +}; + export const NewClientPolicyForm = () => { const { t } = useTranslation("realm-settings"); const { errors, reset: resetForm } = useForm({ @@ -58,6 +65,10 @@ export const NewClientPolicyForm = () => { const { addAlert, addError } = useAlerts(); const adminClient = useAdminClient(); const [policies, setPolicies] = useState([]); + const [clientProfiles, setClientProfiles] = useState< + ClientProfileRepresentation[] + >([]); + const [currentPolicy, setCurrentPolicy] = useState(); const [ @@ -66,7 +77,12 @@ export const NewClientPolicyForm = () => { ] = useState(false); const [conditionToDelete, setConditionToDelete] = - useState<{ idx: number; name: string }>(); + useState(); + + const [profilesModalOpen, setProfilesModalOpen] = useState(false); + + const [profileToDelete, setProfileToDelete] = + useState(); const { policyName } = useParams(); @@ -78,14 +94,30 @@ export const NewClientPolicyForm = () => { const refresh = () => setKey(new Date().getTime()); useFetch( - () => adminClient.clientPolicies.listPolicies(), - (policies) => { + async () => { + const [policies, profiles] = await Promise.all([ + adminClient.clientPolicies.listPolicies(), + adminClient.clientPolicies.listProfiles({ + includeGlobalProfiles: true, + }), + ]); + + return { policies, profiles }; + }, + ({ policies, profiles }) => { const currentPolicy = policies.policies?.find( (item) => item.name === policyName ); + + const allClientProfiles = [ + ...(profiles.globalProfiles ?? []), + ...(profiles.profiles ?? []), + ]; + setPolicies(policies.policies ?? []); if (currentPolicy) { setupForm(currentPolicy); + setClientProfiles(allClientProfiles); setCurrentPolicy(currentPolicy); setShowAddConditionsAndProfilesForm(true); } @@ -102,6 +134,7 @@ export const NewClientPolicyForm = () => { const policy = policies.filter((policy) => policy.name === policyName); const policyConditions = policy[0]?.conditions || []; + const policyProfiles = policy[0]?.profiles || []; const serverInfo = useServerInfo(); @@ -110,8 +143,10 @@ export const NewClientPolicyForm = () => { "org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProvider" ]; + const formValues = form.getValues(); + const save = async () => { - const createdForm = form.getValues(); + const createdForm = formValues; const createdPolicy = { ...createdForm, profiles: [], @@ -137,9 +172,7 @@ export const NewClientPolicyForm = () => { AlertVariant.success ); history.push( - `/${realm}/realm-settings/clientPolicies/${ - form.getValues().name - }/edit-policy` + `/${realm}/realm-settings/clientPolicies/${formValues.name}/edit-policy` ); setShowAddConditionsAndProfilesForm(true); refresh(); @@ -189,10 +222,9 @@ export const NewClientPolicyForm = () => { }); addAlert(t("deleteConditionSuccess"), AlertVariant.success); history.push( - `/${realm}/realm-settings/clientPolicies/${ - form.getValues().name - }/edit-policy` + `/${realm}/realm-settings/clientPolicies/${formValues.name}/edit-policy` ); + refresh(); } catch (error) { addError(t("deleteConditionError"), error); } @@ -214,15 +246,109 @@ export const NewClientPolicyForm = () => { }, }); + const [toggleDeleteProfileDialog, DeleteProfileConfirm] = useConfirmDialog({ + titleKey: t("deleteClientPolicyProfileConfirmTitle"), + messageKey: t("deleteClientPolicyProfileConfirm", { + profileName: profileToDelete?.name, + policyName, + }), + continueButtonLabel: t("delete"), + continueButtonVariant: ButtonVariant.danger, + onConfirm: async () => { + if (profileToDelete?.name) { + currentPolicy?.profiles?.splice(profileToDelete.idx!, 1); + try { + await adminClient.clientPolicies.updatePolicy({ + policies: policies, + }); + addAlert(t("deleteClientPolicyProfileSuccess"), AlertVariant.success); + history.push( + `/${realm}/realm-settings/clientPolicies/${formValues.name}/edit-policy` + ); + } catch (error) { + addError(t("deleteClientPolicyProfileError"), error); + } + } else { + const updatedPolicies = policies.filter( + (policy) => policy.name !== policyName + ); + + try { + await adminClient.clientPolicies.updatePolicy({ + policies: updatedPolicies, + }); + addAlert(t("deleteClientSuccess"), AlertVariant.success); + history.push(toClientPolicies({ realm })); + } catch (error) { + addError(t("deleteClientError"), error); + } + } + }, + }); + const reset = () => { form.setValue("name", currentPolicy?.name); form.setValue("description", currentPolicy?.description); }; + const toggleModal = () => { + setProfilesModalOpen(!profilesModalOpen); + }; + + const addProfiles = async (profiles: string[]) => { + const createdPolicy = { + ...currentPolicy, + profiles: (currentPolicy?.profiles ?? []).concat(profiles), + conditions: currentPolicy?.conditions, + }; + + const index = policies.findIndex( + (policy) => createdPolicy.name === policy.name + ); + + if (index === -1) { + return; + } + + const newPolicies = [ + ...policies.slice(0, index), + createdPolicy, + ...policies.slice(index + 1), + ]; + + try { + await adminClient.clientPolicies.updatePolicy({ + policies: newPolicies, + }); + setPolicies(newPolicies); + history.push( + `/${realm}/realm-settings/clientPolicies/${formValues.name}/edit-policy` + ); + addAlert( + t("realm-settings:addClientProfileSuccess"), + AlertVariant.success + ); + refresh(); + } catch (error) { + addError("realm-settings:addClientProfileError", error); + } + }; + + console.log("NY", policyConditions); + return ( <> + + { + addProfiles(profiles.map((item) => item.name!)); + }} + allProfiles={policyProfiles} + open={profilesModalOpen} + toggleDialog={toggleModal} + /> { variant="primary" type="submit" data-testid="saveCreatePolicy" + isDisabled={!formValues.name} > {t("common:save")} @@ -325,7 +452,7 @@ export const NewClientPolicyForm = () => { {...props} to={toNewClientPolicyCondition({ realm, - policyName: form.getValues().name!, + policyName: formValues.name!, })} > )} @@ -342,9 +469,10 @@ export const NewClientPolicyForm = () => { {policyConditions.map((condition, idx) => ( { - - - {t("realm-settings:emptyProfiles")} - + {policyProfiles.length > 0 ? ( + + {policyProfiles.map((profile, idx) => ( + + + + {profile && ( + + {profile} + + )} + {policyProfiles + .filter((type) => type === profile) + .map((type) => ( + <> + type === profile.name + )?.description + } + forLabel={profile} + forID={t(`common:helpLabel`, { + label: profile, + })} + /> + + + ))} + , + ]} + /> + + + ))} + + ) : ( + <> + + + {t("realm-settings:emptyProfiles")} + + + )} )} diff --git a/src/realm-settings/PoliciesTab.tsx b/src/realm-settings/PoliciesTab.tsx index e121ae7570..8a63bc42a3 100644 --- a/src/realm-settings/PoliciesTab.tsx +++ b/src/realm-settings/PoliciesTab.tsx @@ -26,7 +26,7 @@ import { useAlerts } from "../components/alert/Alerts"; import "./RealmSettingsSection.css"; import { useRealm } from "../context/realm-context/RealmContext"; -import { toNewClientPolicy } from "./routes/NewClientPolicy"; +import { toAddClientPolicy } from "./routes/AddClientPolicy"; import { toEditClientPolicy } from "./routes/EditClientPolicy"; export const PoliciesTab = () => { const { t } = useTranslation("realm-settings"); @@ -161,7 +161,7 @@ export const PoliciesTab = () => { message={t("realm-settings:noClientPolicies")} instructions={t("realm-settings:noClientPoliciesInstructions")} primaryActionText={t("realm-settings:createClientPolicy")} - onPrimaryAction={() => history.push(toNewClientPolicy({ realm }))} + onPrimaryAction={() => history.push(toAddClientPolicy({ realm }))} /> } ariaLabelKey="realm-settings:clientPolicies" @@ -172,7 +172,7 @@ export const PoliciesTab = () => {