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

View file

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