Changed to use form to store executors (#3242)

This commit is contained in:
Erik Jan de Wit 2022-09-05 23:31:39 +02:00 committed by GitHub
parent fe1b602f45
commit df6526311a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 119 additions and 184 deletions

View file

@ -1,4 +1,7 @@
import { Fragment, useEffect, useMemo, useState } from "react"; import { Fragment, useMemo, useState } from "react";
import { useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useForm, useFieldArray } from "react-hook-form";
import { import {
ActionGroup, ActionGroup,
AlertVariant, AlertVariant,
@ -19,26 +22,28 @@ import {
TextVariants, TextVariants,
ValidatedOptions, ValidatedOptions,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form"; import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfileRepresentation";
import type ClientProfilesRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfilesRepresentation";
import type ClientPolicyExecutorRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientPolicyExecutorRepresentation";
import { FormAccess } from "../components/form-access/FormAccess"; import { FormAccess } from "../components/form-access/FormAccess";
import { ViewHeader } from "../components/view-header/ViewHeader"; import { ViewHeader } from "../components/view-header/ViewHeader";
import { useParams } from "react-router-dom";
import { Link, useNavigate } from "react-router-dom-v5-compat"; import { Link, useNavigate } from "react-router-dom-v5-compat";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
import { useAdminClient, useFetch } from "../context/auth/AdminClient"; import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfileRepresentation";
import { HelpItem } from "../components/help-enabler/HelpItem"; import { HelpItem } from "../components/help-enabler/HelpItem";
import { KeycloakTextInput } from "../components/keycloak-text-input/KeycloakTextInput"; import { KeycloakTextInput } from "../components/keycloak-text-input/KeycloakTextInput";
import { KeycloakTextArea } from "../components/keycloak-text-area/KeycloakTextArea"; import { KeycloakTextArea } from "../components/keycloak-text-area/KeycloakTextArea";
import { PlusCircleIcon, TrashIcon } from "@patternfly/react-icons"; import { PlusCircleIcon, TrashIcon } from "@patternfly/react-icons";
import "./realm-settings-section.css";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { toAddExecutor } from "./routes/AddExecutor"; import { toAddExecutor } from "./routes/AddExecutor";
import { useServerInfo } from "../context/server-info/ServerInfoProvider"; import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { ClientProfileParams, toClientProfile } from "./routes/ClientProfile"; import { ClientProfileParams, toClientProfile } from "./routes/ClientProfile";
import { toExecutor } from "./routes/Executor"; import { toExecutor } from "./routes/Executor";
import { toClientPolicies } from "./routes/ClientPolicies"; import { toClientPolicies } from "./routes/ClientPolicies";
import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner";
import "./realm-settings-section.css";
type ClientProfileForm = Required<ClientProfileRepresentation>; type ClientProfileForm = Required<ClientProfileRepresentation>;
@ -54,19 +59,25 @@ export default function ClientProfileForm() {
const { const {
handleSubmit, handleSubmit,
setValue, setValue,
getValues,
register, register,
formState: { isDirty, errors }, formState: { isDirty, errors },
control,
} = useForm<ClientProfileForm>({ } = useForm<ClientProfileForm>({
defaultValues, defaultValues,
mode: "onChange", mode: "onChange",
}); });
const { fields: profileExecutors, remove } =
useFieldArray<ClientPolicyExecutorRepresentation>({
name: "executors",
control,
});
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const { adminClient } = useAdminClient(); const { adminClient } = useAdminClient();
const [globalProfiles, setGlobalProfiles] = useState< const [profiles, setProfiles] = useState<ClientProfilesRepresentation>();
ClientProfileRepresentation[] const [isGlobalProfile, setIsGlobalProfile] = useState(false);
>([]);
const [profiles, setProfiles] = useState<ClientProfileRepresentation[]>([]);
const { realm, profileName } = useParams<ClientProfileParams>(); const { realm, profileName } = useParams<ClientProfileParams>();
const serverInfo = useServerInfo(); const serverInfo = useServerInfo();
const executorTypes = useMemo( const executorTypes = useMemo(
@ -82,25 +93,38 @@ export default function ClientProfileForm() {
}>(); }>();
const editMode = profileName ? true : false; const editMode = profileName ? true : false;
const [key, setKey] = useState(0); const [key, setKey] = useState(0);
const reload = () => setKey(new Date().getTime()); const reload = () => setKey(key + 1);
useFetch( useFetch(
() => () =>
adminClient.clientPolicies.listProfiles({ includeGlobalProfiles: true }), adminClient.clientPolicies.listProfiles({ includeGlobalProfiles: true }),
(profiles) => { (profiles) => {
setGlobalProfiles(profiles.globalProfiles ?? []); setProfiles({
setProfiles(profiles.profiles ?? []); globalProfiles: profiles.globalProfiles,
profiles: profiles.profiles?.filter((p) => p.name !== profileName),
});
const globalProfile = profiles.globalProfiles?.find(
(p) => p.name === profileName
);
const profile = profiles.profiles?.find((p) => p.name === profileName);
setIsGlobalProfile(globalProfile !== undefined);
setValue("name", globalProfile?.name ?? profile?.name);
setValue(
"description",
globalProfile?.description ?? profile?.description
);
setValue("executors", globalProfile?.executors ?? profile?.executors);
}, },
[key] [key]
); );
const save = async (form: ClientProfileForm) => { const save = async (form: ClientProfileForm) => {
const updatedProfiles = editMode ? patchProfiles(form) : addProfile(form); const updatedProfiles = form;
try { try {
await adminClient.clientPolicies.createProfiles({ await adminClient.clientPolicies.createProfiles({
profiles: updatedProfiles, ...profiles,
globalProfiles: globalProfiles, profiles: [...(profiles?.profiles || []), updatedProfiles],
}); });
addAlert( addAlert(
@ -121,25 +145,6 @@ export default function ClientProfileForm() {
} }
}; };
const patchProfiles = (data: ClientProfileRepresentation) =>
profiles.map((profile) => {
if (profile.name !== profileName) {
return profile;
}
return {
...profile,
name: data.name,
description: data.description,
};
});
const addProfile = (data: ClientProfileRepresentation) =>
profiles.concat({
...data,
executors: [],
});
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: executorToDelete?.name! titleKey: executorToDelete?.name!
? t("deleteExecutorProfileConfirmTitle") ? t("deleteExecutorProfileConfirmTitle")
@ -156,11 +161,11 @@ export default function ClientProfileForm() {
onConfirm: async () => { onConfirm: async () => {
if (executorToDelete?.name!) { if (executorToDelete?.name!) {
profileExecutors.splice(executorToDelete.idx!, 1); remove(executorToDelete.idx);
try { try {
await adminClient.clientPolicies.createProfiles({ await adminClient.clientPolicies.createProfiles({
profiles: profiles, ...profiles,
globalProfiles, profiles: [...(profiles!.profiles || []), getValues()],
}); });
addAlert(t("deleteExecutorSuccess"), AlertVariant.success); addAlert(t("deleteExecutorSuccess"), AlertVariant.success);
navigate(toClientProfile({ realm, profileName })); navigate(toClientProfile({ realm, profileName }));
@ -168,15 +173,8 @@ export default function ClientProfileForm() {
addError(t("deleteExecutorError"), error); addError(t("deleteExecutorError"), error);
} }
} else { } else {
const updatedProfiles = profiles.filter(
(profile) => profile.name !== profileName
);
try { try {
await adminClient.clientPolicies.createProfiles({ await adminClient.clientPolicies.createProfiles(profiles);
profiles: updatedProfiles,
globalProfiles,
});
addAlert(t("deleteClientSuccess"), AlertVariant.success); addAlert(t("deleteClientSuccess"), AlertVariant.success);
navigate(toClientPolicies({ realm, tab: "profiles" })); navigate(toClientPolicies({ realm, tab: "profiles" }));
} catch (error) { } catch (error) {
@ -186,17 +184,9 @@ export default function ClientProfileForm() {
}, },
}); });
const profile = profiles.find((profile) => profile.name === profileName); if (!profiles) {
const profileExecutors = profile?.executors || []; return <KeycloakSpinner />;
const globalProfile = globalProfiles.find( }
(globalProfile) => globalProfile.name === profileName
);
const globalProfileExecutors = globalProfile?.executors || [];
useEffect(() => {
setValue("name", globalProfile?.name ?? profile?.name);
setValue("description", globalProfile?.description ?? profile?.description);
}, [profiles]);
return ( return (
<> <>
@ -206,12 +196,12 @@ export default function ClientProfileForm() {
badges={[ badges={[
{ {
id: "global-client-profile-badge", id: "global-client-profile-badge",
text: globalProfile ? t("global") : "", text: isGlobalProfile ? t("global") : "",
}, },
]} ]}
divider divider
dropdownItems={ dropdownItems={
editMode && !globalProfile editMode && !isGlobalProfile
? [ ? [
<DropdownItem <DropdownItem
key="delete" key="delete"
@ -244,7 +234,7 @@ export default function ClientProfileForm() {
id="name" id="name"
aria-label={t("name")} aria-label={t("name")}
data-testid="client-profile-name" data-testid="client-profile-name"
isReadOnly={!!globalProfile} isReadOnly={isGlobalProfile}
/> />
</FormGroup> </FormGroup>
<FormGroup label={t("common:description")} fieldId="kc-description"> <FormGroup label={t("common:description")} fieldId="kc-description">
@ -255,11 +245,11 @@ export default function ClientProfileForm() {
id="description" id="description"
aria-label={t("description")} aria-label={t("description")}
data-testid="client-profile-description" data-testid="client-profile-description"
isReadOnly={!!globalProfile} isReadOnly={isGlobalProfile}
/> />
</FormGroup> </FormGroup>
<ActionGroup> <ActionGroup>
{!globalProfile && ( {!isGlobalProfile && (
<Button <Button
variant="primary" variant="primary"
onClick={() => handleSubmit(save)()} onClick={() => handleSubmit(save)()}
@ -269,7 +259,7 @@ export default function ClientProfileForm() {
{t("common:save")} {t("common:save")}
</Button> </Button>
)} )}
{editMode && !globalProfile && ( {editMode && !isGlobalProfile && (
<Button <Button
id={"reloadProfile"} id={"reloadProfile"}
variant="link" variant="link"
@ -280,7 +270,7 @@ export default function ClientProfileForm() {
{t("realm-settings:reload")} {t("realm-settings:reload")}
</Button> </Button>
)} )}
{!editMode && !globalProfile && ( {!editMode && !isGlobalProfile && (
<Button <Button
id={"cancelCreateProfile"} id={"cancelCreateProfile"}
variant="link" variant="link"
@ -308,7 +298,7 @@ export default function ClientProfileForm() {
/> />
</Text> </Text>
</FlexItem> </FlexItem>
{profile && ( {!isGlobalProfile && (
<FlexItem align={{ default: "alignRight" }}> <FlexItem align={{ default: "alignRight" }}>
<Button <Button
id="addExecutor" id="addExecutor"
@ -332,99 +322,22 @@ export default function ClientProfileForm() {
)} )}
</Flex> </Flex>
{profileExecutors.length > 0 && ( {profileExecutors.length > 0 && (
<DataList aria-label={t("executors")} isCompact>
{profileExecutors.map((executor, idx) => (
<DataListItem
aria-labelledby={"executors-list-item"}
key={executor.executor}
id={executor.executor}
>
<DataListItemRow data-testid="executors-list-row">
<DataListItemCells
dataListCells={[
<DataListCell
key="executor"
data-testid="executor-type"
>
{executor.configuration ? (
<Button
component={(props) => (
<Link
{...props}
to={toExecutor({
realm,
profileName,
executorName: executor.executor!,
})}
/>
)}
variant="link"
data-testid="editExecutor"
>
{executor.executor}
</Button>
) : (
<span className="kc-unclickable-executor">
{executor.executor}
</span>
)}
{executorTypes
?.filter(
(type) => type.id === executor.executor
)
.map((type) => (
<Fragment key={type.id}>
<HelpItem
key={type.id}
helpText={type.helpText}
fieldLabelId="realm-settings:executorTypeTextHelpText"
/>
<Button
variant="link"
isInline
icon={
<TrashIcon
key={`executorType-trash-icon-${type.id}`}
className="kc-executor-trash-icon"
data-testid="deleteExecutor"
/>
}
onClick={() => {
toggleDeleteDialog();
setExecutorToDelete({
idx: idx,
name: type.id,
});
}}
></Button>
</Fragment>
))}
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
))}
</DataList>
)}
{globalProfileExecutors.length > 0 && (
<> <>
<DataList aria-label={t("executors")} isCompact> <DataList aria-label={t("executors")} isCompact>
{globalProfileExecutors.map((executor) => ( {profileExecutors.map((executor, idx) => (
<DataListItem <DataListItem
aria-labelledby={"global-executors-list-item"} aria-labelledby={"executors-list-item"}
key={executor.executor} key={executor.executor}
id={executor.executor} id={executor.executor}
> >
<DataListItemRow data-testid="global-executors-list-row"> <DataListItemRow data-testid="executors-list-row">
<DataListItemCells <DataListItemCells
dataListCells={[ dataListCells={[
<DataListCell <DataListCell
key="executor" key="executor"
data-testid="global-executor-type" data-testid="executor-type"
> >
{Object.keys(executor.configuration!).length !== {executor.configuration ? (
0 ? (
<Button <Button
component={(props) => ( component={(props) => (
<Link <Link
@ -451,11 +364,33 @@ export default function ClientProfileForm() {
(type) => type.id === executor.executor (type) => type.id === executor.executor
) )
.map((type) => ( .map((type) => (
<HelpItem <Fragment key={type.id}>
key={type.id} <HelpItem
helpText={type.helpText} key={type.id}
fieldLabelId="realm-settings:executorTypeTextHelpText" helpText={type.helpText}
/> fieldLabelId="realm-settings:executorTypeTextHelpText"
/>
{!isGlobalProfile && (
<Button
variant="link"
isInline
icon={
<TrashIcon
key={`executorType-trash-icon-${type.id}`}
className="kc-executor-trash-icon"
data-testid="deleteExecutor"
/>
}
onClick={() => {
toggleDeleteDialog();
setExecutorToDelete({
idx: idx,
name: type.id,
});
}}
/>
)}
</Fragment>
))} ))}
</DataListCell>, </DataListCell>,
]} ]}
@ -464,34 +399,35 @@ export default function ClientProfileForm() {
</DataListItem> </DataListItem>
))} ))}
</DataList> </DataList>
<Button {isGlobalProfile && (
id="backToClientPolicies" <Button
component={(props) => ( id="backToClientPolicies"
<Link component={(props) => (
{...props} <Link
to={toClientPolicies({ realm, tab: "profiles" })} {...props}
/> to={toClientPolicies({ realm, tab: "profiles" })}
)} />
variant="primary" )}
className="kc-backToPolicies" variant="primary"
data-testid="backToClientPolicies" className="kc-backToPolicies"
> data-testid="backToClientPolicies"
{t("realm-settings:back")} >
</Button> {t("realm-settings:back")}
</Button>
)}
</>
)}
{profileExecutors.length === 0 && (
<>
<Divider />
<Text
className="kc-emptyExecutors"
component={TextVariants.h6}
>
{t("realm-settings:emptyExecutors")}
</Text>
</> </>
)} )}
{profileExecutors.length === 0 &&
globalProfileExecutors.length === 0 && (
<>
<Divider />
<Text
className="kc-emptyExecutors"
component={TextVariants.h6}
>
{t("realm-settings:emptyExecutors")}
</Text>
</>
)}
</> </>
)} )}
</FormAccess> </FormAccess>

View file

@ -27,7 +27,7 @@ import { DynamicComponents } from "../components/dynamic/DynamicComponents";
import type { ExecutorParams } from "./routes/Executor"; import type { ExecutorParams } from "./routes/Executor";
type ExecutorForm = { type ExecutorForm = {
config: object; config?: object;
executor: string; executor: string;
}; };
@ -89,16 +89,15 @@ export default function ExecutorForm() {
return profile; return profile;
} }
const profileExecutor = profile.executors!.find(
(executor) => executor.executor === executorName
);
const executors = (profile.executors ?? []).concat({ const executors = (profile.executors ?? []).concat({
executor: formValues.executor, executor: formValues.executor,
configuration: formValues.config, configuration: formValues.config || {},
}); });
if (editMode) { if (editMode) {
const profileExecutor = profile.executors!.find(
(executor) => executor.executor === executorName
);
profileExecutor!.configuration = { profileExecutor!.configuration = {
...profileExecutor!.configuration, ...profileExecutor!.configuration,
...formValues.config, ...formValues.config,