a0c99a7ae0
Closes #30705 Signed-off-by: Jon Koops <jonkoops@gmail.com> Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> Co-authored-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
429 lines
15 KiB
TypeScript
429 lines
15 KiB
TypeScript
import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfileRepresentation";
|
|
import type ClientProfilesRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfilesRepresentation";
|
|
import {
|
|
HelpItem,
|
|
TextAreaControl,
|
|
TextControl,
|
|
} from "@keycloak/keycloak-ui-shared";
|
|
import {
|
|
ActionGroup,
|
|
AlertVariant,
|
|
Button,
|
|
ButtonVariant,
|
|
DataList,
|
|
DataListCell,
|
|
DataListItem,
|
|
DataListItemCells,
|
|
DataListItemRow,
|
|
Divider,
|
|
DropdownItem,
|
|
Flex,
|
|
FlexItem,
|
|
Label,
|
|
PageSection,
|
|
Text,
|
|
TextVariants,
|
|
} from "@patternfly/react-core";
|
|
import { PlusCircleIcon, TrashIcon } from "@patternfly/react-icons";
|
|
import { Fragment, useMemo, useState } from "react";
|
|
import { FormProvider, useFieldArray, useForm } from "react-hook-form";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Link, useNavigate } from "react-router-dom";
|
|
import { useAdminClient } from "../admin-client";
|
|
import { useAlerts } from "@keycloak/keycloak-ui-shared";
|
|
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
|
import { FormAccess } from "../components/form/FormAccess";
|
|
import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner";
|
|
import { ViewHeader } from "../components/view-header/ViewHeader";
|
|
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
|
|
import { useFetch } from "../utils/useFetch";
|
|
import { useParams } from "../utils/useParams";
|
|
import { toAddExecutor } from "./routes/AddExecutor";
|
|
import { toClientPolicies } from "./routes/ClientPolicies";
|
|
import { ClientProfileParams, toClientProfile } from "./routes/ClientProfile";
|
|
import { toExecutor } from "./routes/Executor";
|
|
|
|
import "./realm-settings-section.css";
|
|
|
|
type ClientProfileForm = Required<ClientProfileRepresentation>;
|
|
|
|
const defaultValues: ClientProfileForm = {
|
|
name: "",
|
|
description: "",
|
|
executors: [],
|
|
};
|
|
|
|
export default function ClientProfileForm() {
|
|
const { adminClient } = useAdminClient();
|
|
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
const form = useForm<ClientProfileForm>({
|
|
defaultValues,
|
|
mode: "onChange",
|
|
});
|
|
|
|
const {
|
|
handleSubmit,
|
|
setValue,
|
|
getValues,
|
|
formState: { isDirty },
|
|
control,
|
|
} = form;
|
|
|
|
const { fields: profileExecutors, remove } = useFieldArray({
|
|
name: "executors",
|
|
control,
|
|
});
|
|
|
|
const { addAlert, addError } = useAlerts();
|
|
const [profiles, setProfiles] = useState<ClientProfilesRepresentation>();
|
|
const [isGlobalProfile, setIsGlobalProfile] = useState(false);
|
|
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(key + 1);
|
|
|
|
useFetch(
|
|
() =>
|
|
adminClient.clientPolicies.listProfiles({ includeGlobalProfiles: true }),
|
|
(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 = form;
|
|
|
|
try {
|
|
await adminClient.clientPolicies.createProfiles({
|
|
...profiles,
|
|
profiles: [...(profiles?.profiles || []), updatedProfiles],
|
|
});
|
|
|
|
addAlert(
|
|
editMode
|
|
? t("updateClientProfileSuccess")
|
|
: t("createClientProfileSuccess"),
|
|
AlertVariant.success,
|
|
);
|
|
|
|
navigate(toClientProfile({ realm, profileName: form.name }));
|
|
} catch (error) {
|
|
addError(
|
|
editMode ? "updateClientProfileError" : "createClientProfileError",
|
|
error,
|
|
);
|
|
}
|
|
};
|
|
|
|
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!) {
|
|
remove(executorToDelete.idx);
|
|
try {
|
|
await adminClient.clientPolicies.createProfiles({
|
|
...profiles,
|
|
profiles: [...(profiles!.profiles || []), getValues()],
|
|
});
|
|
addAlert(t("deleteExecutorSuccess"), AlertVariant.success);
|
|
navigate(toClientProfile({ realm, profileName }));
|
|
} catch (error) {
|
|
addError(t("deleteExecutorError"), error);
|
|
}
|
|
} else {
|
|
try {
|
|
await adminClient.clientPolicies.createProfiles(profiles);
|
|
addAlert(t("deleteClientSuccess"), AlertVariant.success);
|
|
navigate(toClientPolicies({ realm, tab: "profiles" }));
|
|
} catch (error) {
|
|
addError(t("deleteClientError"), error);
|
|
}
|
|
}
|
|
},
|
|
});
|
|
|
|
if (!profiles) {
|
|
return <KeycloakSpinner />;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<DeleteConfirm />
|
|
<ViewHeader
|
|
titleKey={editMode ? profileName : t("newClientProfile")}
|
|
badges={[
|
|
{
|
|
id: "global-client-profile-badge",
|
|
text: isGlobalProfile ? (
|
|
<Label color="blue">{t("global")}</Label>
|
|
) : (
|
|
""
|
|
),
|
|
},
|
|
]}
|
|
divider
|
|
dropdownItems={
|
|
editMode && !isGlobalProfile
|
|
? [
|
|
<DropdownItem
|
|
key="delete"
|
|
value="delete"
|
|
onClick={toggleDeleteDialog}
|
|
data-testid="deleteClientProfileDropdown"
|
|
>
|
|
{t("deleteClientProfile")}
|
|
</DropdownItem>,
|
|
]
|
|
: undefined
|
|
}
|
|
/>
|
|
<PageSection variant="light">
|
|
<FormProvider {...form}>
|
|
<FormAccess isHorizontal role="view-realm" className="pf-v5-u-mt-lg">
|
|
<TextControl
|
|
name="name"
|
|
label={t("newClientProfileName")}
|
|
helperText={t("createClientProfileNameHelperText")}
|
|
readOnly={isGlobalProfile}
|
|
rules={{
|
|
required: t("required"),
|
|
}}
|
|
/>
|
|
<TextAreaControl
|
|
name="description"
|
|
label={t("description")}
|
|
readOnly={isGlobalProfile}
|
|
/>
|
|
<ActionGroup>
|
|
{!isGlobalProfile && (
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => handleSubmit(save)()}
|
|
data-testid="saveCreateProfile"
|
|
isDisabled={!isDirty}
|
|
>
|
|
{t("save")}
|
|
</Button>
|
|
)}
|
|
{editMode && !isGlobalProfile && (
|
|
<Button
|
|
id={"reloadProfile"}
|
|
variant="link"
|
|
data-testid={"reloadProfile"}
|
|
isDisabled={!isDirty}
|
|
onClick={reload}
|
|
>
|
|
{t("reload")}
|
|
</Button>
|
|
)}
|
|
{!editMode && !isGlobalProfile && (
|
|
<Button
|
|
id={"cancelCreateProfile"}
|
|
variant="link"
|
|
component={(props) => (
|
|
<Link
|
|
{...props}
|
|
to={toClientPolicies({ realm, tab: "profiles" })}
|
|
/>
|
|
)}
|
|
data-testid={"cancelCreateProfile"}
|
|
>
|
|
{t("cancel")}
|
|
</Button>
|
|
)}
|
|
</ActionGroup>
|
|
{editMode && (
|
|
<>
|
|
<Flex>
|
|
<FlexItem>
|
|
<Text className="kc-executors" component={TextVariants.h1}>
|
|
{t("executors")}
|
|
<HelpItem
|
|
helpText={t("executorsHelpText")}
|
|
fieldLabelId="executors"
|
|
/>
|
|
</Text>
|
|
</FlexItem>
|
|
{!isGlobalProfile && (
|
|
<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("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) => (
|
|
<Fragment key={type.id}>
|
|
<HelpItem
|
|
key={type.id}
|
|
helpText={type.helpText}
|
|
fieldLabelId="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,
|
|
});
|
|
}}
|
|
aria-label={t("remove")}
|
|
/>
|
|
)}
|
|
</Fragment>
|
|
))}
|
|
</DataListCell>,
|
|
]}
|
|
/>
|
|
</DataListItemRow>
|
|
</DataListItem>
|
|
))}
|
|
</DataList>
|
|
{isGlobalProfile && (
|
|
<Button
|
|
id="backToClientPolicies"
|
|
component={(props) => (
|
|
<Link
|
|
{...props}
|
|
to={toClientPolicies({ realm, tab: "profiles" })}
|
|
/>
|
|
)}
|
|
variant="primary"
|
|
className="kc-backToPolicies"
|
|
data-testid="backToClientPolicies"
|
|
>
|
|
{t("back")}
|
|
</Button>
|
|
)}
|
|
</>
|
|
)}
|
|
{profileExecutors.length === 0 && (
|
|
<>
|
|
<Divider />
|
|
<Text
|
|
className="kc-emptyExecutors"
|
|
component={TextVariants.h2}
|
|
>
|
|
{t("emptyExecutors")}
|
|
</Text>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
</FormAccess>
|
|
</FormProvider>
|
|
</PageSection>
|
|
</>
|
|
);
|
|
}
|