keycloak-scim/src/realm-settings/ClientProfileForm.tsx

505 lines
18 KiB
TypeScript

import React, { useEffect, useMemo, useState } from "react";
import {
ActionGroup,
AlertVariant,
Button,
ButtonVariant,
DataList,
DataListCell,
DataListItem,
DataListItemCells,
DataListItemRow,
Divider,
DropdownItem,
Flex,
FlexItem,
FormGroup,
PageSection,
Text,
TextArea,
TextInput,
TextVariants,
ValidatedOptions,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form";
import { FormAccess } from "../components/form-access/FormAccess";
import { ViewHeader } from "../components/view-header/ViewHeader";
import { Link, useHistory, useParams } from "react-router-dom";
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 { PlusCircleIcon, TrashIcon } from "@patternfly/react-icons";
import "./RealmSettingsSection.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";
type ClientProfileForm = Required<ClientProfileRepresentation>;
const defaultValues: ClientProfileForm = {
name: "",
description: "",
executors: [],
};
export default function ClientProfileForm() {
const { t } = useTranslation("realm-settings");
const history = useHistory();
const {
handleSubmit,
setValue,
register,
errors,
formState: { isDirty },
} = useForm<ClientProfileForm>({
defaultValues,
mode: "onChange",
});
const { addAlert, addError } = useAlerts();
const adminClient = useAdminClient();
const [globalProfiles, setGlobalProfiles] = useState<
ClientProfileRepresentation[]
>([]);
const [profiles, setProfiles] = useState<ClientProfileRepresentation[]>([]);
const { realm, profileName } = useParams<ClientProfileParams>();
const serverInfo = useServerInfo();
const executorTypes = useMemo(
() =>
serverInfo.componentTypes?.[
"org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProvider"
],
[]
);
const [executorToDelete, setExecutorToDelete] =
useState<{ idx: number; name: string }>();
const editMode = profileName ? true : false;
const [key, setKey] = useState(0);
const reload = () => setKey(new Date().getTime());
useFetch(
() =>
adminClient.clientPolicies.listProfiles({ includeGlobalProfiles: true }),
(profiles) => {
setGlobalProfiles(profiles.globalProfiles ?? []);
setProfiles(profiles.profiles ?? []);
},
[key]
);
const save = async (form: ClientProfileForm) => {
const updatedProfiles = editMode ? patchProfiles(form) : addProfile(form);
try {
await adminClient.clientPolicies.createProfiles({
profiles: updatedProfiles,
globalProfiles: globalProfiles,
});
addAlert(
editMode
? t("realm-settings:updateClientProfileSuccess")
: t("realm-settings:createClientProfileSuccess"),
AlertVariant.success
);
history.push(toClientProfile({ realm, profileName: form.name }));
} catch (error) {
addError(
editMode
? "realm-settings:updateClientProfileError"
: "realm-settings:createClientProfileError",
error
);
}
};
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")
: t("deleteClientProfileConfirmTitle"),
messageKey: executorToDelete?.name!
? t("deleteExecutorProfileConfirm", {
executorName: executorToDelete.name!,
})
: t("deleteClientProfileConfirm", {
profileName,
}),
continueButtonLabel: t("delete"),
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
if (executorToDelete?.name!) {
profileExecutors.splice(executorToDelete.idx!, 1);
try {
await adminClient.clientPolicies.createProfiles({
profiles: profiles,
globalProfiles,
});
addAlert(t("deleteExecutorSuccess"), AlertVariant.success);
history.push(toClientProfile({ realm, profileName }));
} catch (error) {
addError(t("deleteExecutorError"), error);
}
} else {
const updatedProfiles = profiles.filter(
(profile) => profile.name !== profileName
);
try {
await adminClient.clientPolicies.createProfiles({
profiles: updatedProfiles,
globalProfiles,
});
addAlert(t("deleteClientSuccess"), AlertVariant.success);
history.push(toClientPolicies({ realm }));
} catch (error) {
addError(t("deleteClientError"), error);
}
}
},
});
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]);
return (
<>
<DeleteConfirm />
<ViewHeader
titleKey={editMode ? profileName : t("newClientProfile")}
badges={[
{
id: "global-client-profile-badge",
text: globalProfile ? t("global") : "",
},
]}
divider
dropdownItems={
!globalProfile
? [
<DropdownItem
key="delete"
value="delete"
onClick={toggleDeleteDialog}
data-testid="deleteClientProfileDropdown"
>
{t("deleteClientProfile")}
</DropdownItem>,
]
: undefined
}
/>
<PageSection variant="light">
<FormAccess isHorizontal role="view-realm" className="pf-u-mt-lg">
<FormGroup
label={t("newClientProfileName")}
fieldId="kc-name"
helperText={t("createClientProfileNameHelperText")}
isRequired
helperTextInvalid={t("common:required")}
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
>
<TextInput
ref={register({ required: true })}
name="name"
type="text"
id="name"
aria-label={t("name")}
data-testid="client-profile-name"
isReadOnly={!!globalProfile}
/>
</FormGroup>
<FormGroup label={t("common:description")} fieldId="kc-description">
<TextArea
ref={register()}
name="description"
type="text"
id="description"
aria-label={t("description")}
data-testid="client-profile-description"
isReadOnly={!!globalProfile}
/>
</FormGroup>
<ActionGroup>
{!globalProfile && (
<Button
variant="primary"
onClick={() => handleSubmit(save)()}
data-testid="saveCreateProfile"
isDisabled={!isDirty}
>
{t("common:save")}
</Button>
)}
{editMode && !globalProfile && (
<Button
id={"reloadProfile"}
variant="link"
data-testid={"reloadProfile"}
isDisabled={!isDirty}
onClick={reload}
>
{t("realm-settings:reload")}
</Button>
)}
{!editMode && !globalProfile && (
<Button
id={"cancelCreateProfile"}
component={(props) => (
<Link
{...props}
to={`/${realm}/realm-settings/clientPolicies`}
/>
)}
data-testid={"cancelCreateProfile"}
>
{t("common:cancel")}
</Button>
)}
</ActionGroup>
{editMode && (
<>
<Flex>
<FlexItem>
<Text className="kc-executors" component={TextVariants.h1}>
{t("executors")}
<HelpItem
helpText={t("realm-settings:executorsHelpText")}
forLabel={t("executorsHelpItem")}
forID={t("executors")}
/>
</Text>
</FlexItem>
{profile && (
<FlexItem align={{ default: "alignRight" }}>
<Button
id="addExecutor"
component={(props) => (
<Link
{...props}
to={toAddExecutor({
realm,
profileName,
})}
/>
)}
variant="link"
className="kc-addExecutor"
data-testid="addExecutor"
icon={<PlusCircleIcon />}
>
{t("realm-settings:addExecutor")}
</Button>
</FlexItem>
)}
</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) => (
<>
<HelpItem
key={type.id}
helpText={type.helpText}
forLabel={t("executorTypeTextHelpText")}
forID={t(`common:helpLabel`, {
label: t("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>
</>
))}
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
))}
</DataList>
)}
{globalProfileExecutors.length > 0 && (
<>
<DataList aria-label={t("executors")} isCompact>
{globalProfileExecutors.map((executor) => (
<DataListItem
aria-labelledby={"global-executors-list-item"}
key={executor.executor}
id={executor.executor}
>
<DataListItemRow data-testid="global-executors-list-row">
<DataListItemCells
dataListCells={[
<DataListCell
key="executor"
data-testid="global-executor-type"
>
{Object.keys(executor.configuration!).length !==
0 ? (
<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) => (
<HelpItem
key={type.id}
helpText={type.helpText}
forLabel={t("executorTypeTextHelpText")}
forID={t(`common:helpLabel`, {
label: t("executorTypeTextHelpText"),
})}
/>
))}
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
))}
</DataList>
<Button
id="backToClientPolicies"
component={(props) => (
<Link
{...props}
to={`/${realm}/realm-settings/clientPolicies`}
/>
)}
variant="primary"
className="kc-backToPolicies"
data-testid="backToClientPolicies"
>
{t("realm-settings:back")}
</Button>
</>
)}
{profileExecutors.length === 0 &&
globalProfileExecutors.length === 0 && (
<>
<Divider />
<Text
className="kc-emptyExecutors"
component={TextVariants.h6}
>
{t("realm-settings:emptyExecutors")}
</Text>
</>
)}
</>
)}
</FormAccess>
</PageSection>
</>
);
}