import type ClientPolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientPolicyRepresentation"; import { CodeEditor, Language } from "@patternfly/react-code-editor"; import { AlertVariant, Button, ButtonVariant, Divider, Flex, FlexItem, PageSection, Radio, Switch, Title, ToolbarItem, } from "@patternfly/react-core"; import { omit } from "lodash-es"; import { useState } from "react"; import { Controller, useForm, type UseFormReturn } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { Link, useNavigate } from "react-router-dom"; import { useAdminClient } from "../admin-client"; import { translationFormatter } from "../clients/ClientsSection"; import { useAlerts } from "../components/alert/Alerts"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner"; import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; import { Action, KeycloakDataTable, } from "../components/table-toolbar/KeycloakDataTable"; import { useRealm } from "../context/realm-context/RealmContext"; import { prettyPrintJSON } from "../util"; import { useFetch } from "../utils/useFetch"; import { toAddClientPolicy } from "./routes/AddClientPolicy"; import { toClientPolicies } from "./routes/ClientPolicies"; import { toEditClientPolicy } from "./routes/EditClientPolicy"; import "./realm-settings-section.css"; type ClientPolicy = ClientPolicyRepresentation & { global?: boolean; }; export const PoliciesTab = () => { const { adminClient } = useAdminClient(); const { t } = useTranslation(); const { addAlert, addError } = useAlerts(); const { realm } = useRealm(); const navigate = useNavigate(); const [show, setShow] = useState(false); const [policies, setPolicies] = useState(); const [selectedPolicy, setSelectedPolicy] = useState(); const [key, setKey] = useState(0); const [code, setCode] = useState(); const [tablePolicies, setTablePolicies] = useState(); const refresh = () => setKey(key + 1); const form = useForm>({ mode: "onChange" }); useFetch( () => adminClient.clientPolicies.listPolicies({ includeGlobalPolicies: true, }), (allPolicies) => { const globalPolicies = allPolicies.globalPolicies?.map( (globalPolicies) => ({ ...globalPolicies, global: true, }), ); const policies = allPolicies.policies?.map((policies) => ({ ...policies, global: false, })); const allClientPolicies = globalPolicies?.concat(policies ?? []); setPolicies(allClientPolicies), setTablePolicies(allClientPolicies || []), setCode(prettyPrintJSON(allClientPolicies)); }, [key], ); const loader = async () => policies ?? []; const saveStatus = async () => { const switchValues = form.getValues(); const updatedPolicies = policies ?.filter((policy) => { return !policy.global; }) .map((policy) => { const enabled = switchValues[policy.name!]; const enabledPolicy = { ...policy, enabled, }; delete enabledPolicy.global; return enabledPolicy; }); try { await adminClient.clientPolicies.updatePolicy({ policies: updatedPolicies, }); navigate(toClientPolicies({ realm, tab: "policies" })); addAlert(t("updateClientPolicySuccess"), AlertVariant.success); } catch (error) { addError("updateClientPolicyError", error); } }; const normalizePolicy = (policy: ClientPolicy): ClientPolicyRepresentation => omit(policy, "global"); const save = async () => { if (!code) { return; } try { const obj: ClientPolicy[] = JSON.parse(code); const changedPolicies = obj .filter((policy) => !policy.global) .map((policy) => normalizePolicy(policy)); const changedGlobalPolicies = obj .filter((policy) => policy.global) .map((policy) => normalizePolicy(policy)); try { await adminClient.clientPolicies.updatePolicy({ policies: changedPolicies, globalPolicies: changedGlobalPolicies, }); addAlert(t("updateClientPoliciesSuccess"), AlertVariant.success); refresh(); } catch (error) { addError("updateClientPoliciesError", error); } } catch (error) { console.warn("Invalid json, ignoring value using {}"); addError("invalidJsonClientPoliciesError", error); } }; const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ titleKey: t("deleteClientPolicyConfirmTitle"), messageKey: t("deleteClientPolicyConfirm", { policyName: selectedPolicy?.name, }), continueButtonLabel: t("delete"), continueButtonVariant: ButtonVariant.danger, onConfirm: async () => { const updatedPolicies = policies ?.filter((policy) => { return !policy.global && policy.name !== selectedPolicy?.name; }) .map((policy) => { const newPolicy = { ...policy }; delete newPolicy.global; return newPolicy; }); try { await adminClient.clientPolicies.updatePolicy({ policies: updatedPolicies, }); addAlert(t("deleteClientPolicySuccess"), AlertVariant.success); refresh(); } catch (error) { addError(t("deleteClientPolicyError"), error); } }, }); if (!policies) { return ; } return ( <> {t("policiesConfigType")} setShow(false)} label={t("policiesConfigTypes.formView")} id="formView-policiesView" data-testid="formView-policiesView" className="kc-form-radio-btn pf-v5-u-mr-sm pf-v5-u-ml-sm" /> setShow(true)} label={t("policiesConfigTypes.jsonEditor")} id="jsonEditor-policiesView" data-testid="jsonEditor-policiesView" className="kc-editor-radio-btn" /> {!show ? ( navigate(toAddClientPolicy({ realm }))} /> } ariaLabelKey="clientPolicies" searchPlaceholderKey="clientPolicySearch" loader={loader} toolbarItem={ } isRowDisabled={(value) => !!value.global} actions={[ { title: t("delete"), onRowClick: (item) => { toggleDeleteDialog(); setSelectedPolicy(item); }, } as Action, ]} columns={[ { name: "name", cellRenderer: ({ name }: ClientPolicyRepresentation) => ( {name} ), }, { name: "enabled", displayKey: "status", cellRenderer: (clientPolicy) => ( { form.setValue(clientPolicy.name!, false); saveStatus(); }} /> ), }, { name: "description", cellFormatters: [translationFormatter(t)], }, ]} /> ) : ( <>
)} ); }; type SwitchRendererProps = { clientPolicy: ClientPolicy; form: UseFormReturn>; saveStatus: () => void; onConfirm: () => void; }; const SwitchRenderer = ({ clientPolicy, form, saveStatus, onConfirm, }: SwitchRendererProps) => { const { t } = useTranslation(); const [toggleDisableDialog, DisableConfirm] = useConfirmDialog({ titleKey: "disablePolicyConfirmTitle", messageKey: "disablePolicyConfirm", continueButtonLabel: "disable", onConfirm, }); return ( <> ( { if (!value) { toggleDisableDialog(); } else { field.onChange(value); saveStatus(); } }} aria-label={clientPolicy.name!} /> )} /> ); };