import { ActionGroup, AlertVariant, Button, ButtonVariant, ExpandableSection, FormGroup, InputGroup, PageSection, Split, SplitItem, Text, ToolbarItem, Tooltip, } from "@patternfly/react-core"; import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation"; import type GlobalRequestResult from "@keycloak/keycloak-admin-client/lib/defs/globalRequestResult"; import moment from "moment"; import React, { useEffect, useRef, useState } from "react"; import { Controller, useFormContext } from "react-hook-form"; import { Trans, useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { useAlerts } from "../components/alert/Alerts"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { FormAccess } from "../components/form-access/FormAccess"; import { HelpItem } from "../components/help-enabler/HelpItem"; import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; import { ScrollForm } from "../components/scroll-form/ScrollForm"; import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; import { TimeSelector } from "../components/time-selector/TimeSelector"; import { KeycloakTextInput } from "../components/keycloak-text-input/KeycloakTextInput"; import { useAdminClient } from "../context/auth/AdminClient"; import { useRealm } from "../context/realm-context/RealmContext"; import { convertToFormValues, toUpperCase } from "../util"; import { AddHostDialog } from "./advanced/AddHostDialog"; import { AdvancedSettings } from "./advanced/AdvancedSettings"; import { AuthenticationOverrides } from "./advanced/AuthenticationOverrides"; import { FineGrainOpenIdConnect } from "./advanced/FineGrainOpenIdConnect"; import { FineGrainSamlEndpointConfig } from "./advanced/FineGrainSamlEndpointConfig"; import { OpenIdConnectCompatibilityModes } from "./advanced/OpenIdConnectCompatibilityModes"; import type { SaveOptions } from "./ClientDetails"; import { toClient } from "./routes/Client"; type AdvancedProps = { save: (options?: SaveOptions) => void; client: ClientRepresentation; }; export const AdvancedTab = ({ save, client: { id, publicClient, registeredNodes, attributes, protocol, authenticationFlowBindingOverrides, adminUrl, }, }: AdvancedProps) => { const { t } = useTranslation("clients"); const adminClient = useAdminClient(); const { realm } = useRealm(); const { addAlert, addError } = useAlerts(); const revocationFieldName = "notBefore"; const openIdConnect = "openid-connect"; const { getValues, setValue, register, control, reset } = useFormContext(); const [expanded, setExpanded] = useState(false); const [selectedNode, setSelectedNode] = useState(""); const [addNodeOpen, setAddNodeOpen] = useState(false); const [key, setKey] = useState(0); const refresh = () => setKey(new Date().getTime()); const [nodes, setNodes] = useState(registeredNodes || {}); const pushRevocationButtonRef = useRef(); const setNotBefore = (time: number, messageKey: string) => { setValue(revocationFieldName, time); save({ messageKey }); }; const parseResult = (result: GlobalRequestResult, prefixKey: string) => { const successCount = result.successRequests?.length || 0; const failedCount = result.failedRequests?.length || 0; if (successCount === 0 && failedCount === 0) { addAlert(t("noAdminUrlSet"), AlertVariant.warning); } else if (failedCount > 0) { addAlert( t(prefixKey + "Success", { successNodes: result.successRequests }), AlertVariant.success ); addAlert( t(prefixKey + "Fail", { failedNodes: result.failedRequests }), AlertVariant.danger ); } else { addAlert( t(prefixKey + "Success", { successNodes: result.successRequests }), AlertVariant.success ); } }; const resetFields = (names: string[]) => { const values: { [name: string]: string } = {}; for (const name of names) { values[`attributes.${name}`] = attributes?.[name]; } reset(values); }; const push = async () => { const result = await adminClient.clients.pushRevocation({ id: id!, }); parseResult(result, "notBeforePush"); }; const testCluster = async () => { const result = await adminClient.clients.testNodesAvailable({ id: id! }); parseResult(result, "testCluster"); }; const [toggleDeleteNodeConfirm, DeleteNodeConfirm] = useConfirmDialog({ titleKey: "clients:deleteNode", messageKey: t("deleteNodeBody", { node: selectedNode, }), continueButtonLabel: "common:delete", continueButtonVariant: ButtonVariant.danger, onConfirm: async () => { try { await adminClient.clients.deleteClusterNode({ id: id!, node: selectedNode, }); setNodes({ ...Object.keys(nodes).reduce((object: any, key) => { if (key !== selectedNode) { object[key] = nodes[key]; } return object; }, {}), }); refresh(); addAlert(t("deleteNodeSuccess"), AlertVariant.success); } catch (error) { addError("clients:deleteNodeFail", error); } }, }); useEffect(() => { register(revocationFieldName); }, [register]); const formatDate = () => { const date = getValues(revocationFieldName); if (date > 0) { return moment(date * 1000).format("LLL"); } else { return t("common:none"); } }; const sections = [ t("revocation"), t("clustering"), protocol === openIdConnect ? t("fineGrainOpenIdConnectConfiguration") : t("fineGrainSamlEndpointConfig"), t("advancedSettings"), t("authenticationOverrides"), ]; if (protocol === openIdConnect) { sections.splice(3, 0, t("openIdConnectCompatibilityModes")); } if (!publicClient) { sections.splice(1, 1); } if (protocol !== openIdConnect) { sections.splice(0, 1); } return ( {protocol === openIdConnect && ( <> In order to successfully push setup url on {t("settings")} tab } > {!adminUrl && ( )} )} {publicClient && ( <> } > ( )} /> <> { nodes[node] = moment.now() / 1000; refresh(); }} onClose={() => setAddNodeOpen(false)} /> Promise.resolve( Object.entries(nodes || {}).map((entry) => { return { host: entry[0], registration: entry[1] }; }) ) } toolbarItem={ <> } actions={[ { title: t("common:delete"), onRowClick: (node) => { setSelectedNode(node.host); toggleDeleteNodeConfirm(); }, }, ]} columns={[ { name: "host", displayKey: "clients:nodeHost", }, { name: "registration", displayKey: "clients:lastRegistration", cellFormatters: [ (value) => value ? moment(parseInt(value.toString()) * 1000).format( "LLL" ) : "", ], }, ]} emptyState={ setAddNodeOpen(true)} /> } /> )} <> {protocol === openIdConnect && ( <> {t("clients-help:fineGrainOpenIdConnectConfiguration")} save()} reset={() => convertToFormValues(attributes, (key, value) => setValue(`attributes.${key}`, value) ) } /> )} {protocol !== openIdConnect && ( <> {t("clients-help:fineGrainSamlEndpointConfig")} save()} reset={() => convertToFormValues(attributes, (key, value) => setValue(`attributes.${key}`, value) ) } /> )} {protocol === openIdConnect && ( <> {t("clients-help:openIdConnectCompatibilityModes")} save()} reset={() => resetFields(["exclude.session.state.from.auth.response"]) } /> )} <> {t("clients-help:advancedSettings" + toUpperCase(protocol || ""))} save()} reset={() => { resetFields([ "saml.assertion.lifespan", "access.token.lifespan", "tls.client.certificate.bound.access.tokens", "pkce.code.challenge.method", ]); }} /> <> {t("clients-help:authenticationOverrides")} save()} reset={() => { setValue( "authenticationFlowBindingOverrides.browser", authenticationFlowBindingOverrides?.browser ); setValue( "authenticationFlowBindingOverrides.direct_grant", authenticationFlowBindingOverrides?.direct_grant ); }} /> ); };