/* eslint-disable */ // @ts-nocheck import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation"; import { HelpItem, KeycloakSelect, NumberControl, ScrollForm, SelectControl, SelectVariant, TextAreaControl, TextControl, } from "../../../../shared/keycloak-ui-shared"; import { ActionGroup, Button, Card, CardBody, CardHeader, CardTitle, Checkbox, Chip, FormGroup, Label, SelectOption, Split, SplitItem, Stack, StackItem, Text, TextContent, } from "../../../../shared/@patternfly/react-core"; import { CheckCircleIcon, InfoCircleIcon, MinusCircleIcon, PauseCircleIcon, TimesCircleIcon, } from "../../../../shared/@patternfly/react-icons"; import { useState } from "react"; import { Controller, useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { FormAccess } from "../../../components/form/FormAccess"; import { MultiLineInput } from "../../../components/multi-line-input/MultiLineInput"; import { AddRoleButton, AddRoleMappingModal, FilterType, } from "../../../components/role-mapping/AddRoleMappingModal"; import { ServiceRole } from "../../../components/role-mapping/RoleMapping"; import { DefaultSwitchControl } from "../../../components/SwitchControl"; import { TimeSelector } from "../../../components/time-selector/TimeSelector"; import { convertAttributeNameToForm } from "../../../util"; import type { FormFields, SaveOptions } from "../../ClientDetails"; import type { SsfClientStream } from "./StreamTab"; const splitSupportedEvents = (value: unknown): string[] => { if (!value || typeof value !== "string") { return []; } return value .split(",") .map((s) => s.trim()) .filter((s) => s.length > 0); }; /** * Splits a {@code ssf.allowedDeliveryMethods} client attribute value * into its canonical lowercase tokens. The server uses * {@code ##}-separated storage (see {@code Constants.CFG_DELIMITER}); * empty / unset attribute returns an empty array which the UI treats * as "both push and poll allowed". */ const splitDeliveryMethods = (value: unknown): string[] => { if (!value || typeof value !== "string") { return []; } return value .split("##") .map((s) => s.trim().toLowerCase()) .filter((s) => s.length > 0); }; export type ReceiverTabProps = { client: ClientRepresentation; clientStream: SsfClientStream | null; defaultSupportedEvents: string; availableSupportedEvents: string[]; nativelyEmittedEvents: string[]; defaultUserSubjectFormat: string; defaultPushConnectTimeoutMillis: number; defaultPushSocketTimeoutMillis: number; save: (options?: SaveOptions) => void; reset: () => void; }; export const ReceiverTab = ({ client, clientStream, defaultSupportedEvents, availableSupportedEvents, nativelyEmittedEvents, defaultUserSubjectFormat, defaultPushConnectTimeoutMillis, defaultPushSocketTimeoutMillis, save, reset, }: ReceiverTabProps) => { const { t } = useTranslation(); const { control, watch, setValue } = useFormContext(); const [supportedEventsOpen, setSupportedEventsOpen] = useState(false); const [supportedEventsFilter, setSupportedEventsFilter] = useState(""); const [emitOnlyEventsOpen, setEmitOnlyEventsOpen] = useState(false); const [emitOnlyEventsFilter, setEmitOnlyEventsFilter] = useState(""); // --- SSF Required Role picker state --- const [rolePickerOpen, setRolePickerOpen] = useState(false); const [roleFilterType, setRoleFilterType] = useState("clients"); // --- SSF Emit Events Role picker state --- // Independent modal/filter state so both pickers can coexist in the // same form without clobbering each other's open state. const [emitRolePickerOpen, setEmitRolePickerOpen] = useState(false); const [emitRoleFilterType, setEmitRoleFilterType] = useState("clients"); const requiredRoleFieldName = convertAttributeNameToForm( "attributes.ssf.requiredRole", ); const emitEventsRoleFieldName = convertAttributeNameToForm( "attributes.ssf.emitEventsRole", ); const parseRoleValue = (value: string | undefined) => { if (!value?.includes(".")) return ["", value || ""]; return value.split("."); }; // DefaultSwitchControl with stringify persists "true" / "false" — // compare as string so the delay-millis input toggles in sync with // the switch rather than interpreting the raw boolean. const ssfAutoVerifyStream = watch( convertAttributeNameToForm("attributes.ssf.autoVerifyStream"), ); // DefaultSwitchControl with stringify persists "true" / "false" — // compare as string so the role picker toggles in sync with the // switch rather than interpreting the raw boolean. const ssfAllowEmitEvents = watch( convertAttributeNameToForm("attributes.ssf.allowEmitEvents"), ); // Drive the emit-only multi-select options off the live value of // supportedEvents so adding / removing a supported event immediately // adjusts what the operator can mark emit-only. No standalone // registry list — the emit-only set is a strict subset. const ssfSupportedEvents = watch( convertAttributeNameToForm("attributes.ssf.supportedEvents"), ); const ssfEmitOnlyEvents = watch( convertAttributeNameToForm("attributes.ssf.emitOnlyEvents"), ); const supportedEventsSelected = splitSupportedEvents( ssfSupportedEvents ?? defaultSupportedEvents, ); const emitOnlyEventsSelected = splitSupportedEvents(ssfEmitOnlyEvents).filter( (e) => supportedEventsSelected.includes(e), ); // Allowed delivery methods is stored as a ##-separated client attribute // matching ssf.allowedDeliveryMethods on the server. Empty / unset means // "both push and poll allowed" (transmitter default), which is also what // the checkboxes render as the initial state on a freshly registered // receiver. The two checkboxes drive the same form field via setValue — // the conditional reveal of "Valid push URLs" below keys off whether // "push" is selected here. const allowedDeliveryMethodsField = convertAttributeNameToForm( "attributes.ssf.allowedDeliveryMethods", ); const ssfAllowedDeliveryMethodsRaw = watch(allowedDeliveryMethodsField); const allowedDeliveryMethodTokens = splitDeliveryMethods( ssfAllowedDeliveryMethodsRaw, ); const pushDeliveryAllowed = allowedDeliveryMethodTokens.length === 0 || allowedDeliveryMethodTokens.includes("push"); const pollDeliveryAllowed = allowedDeliveryMethodTokens.length === 0 || allowedDeliveryMethodTokens.includes("poll"); const updateAllowedDeliveryMethod = ( method: "push" | "poll", checked: boolean, ) => { const current = allowedDeliveryMethodTokens.length === 0 ? new Set(["push", "poll"]) : new Set(allowedDeliveryMethodTokens); if (checked) { current.add(method); } else { current.delete(method); } // At least one method must remain selected — silently re-enable the // toggled-off method if the user attempted to uncheck both. The // server treats blank as "both allowed" too, but driving the user // through that path makes the UI ambiguous (cleared → defaults). if (current.size === 0) { current.add(method); } setValue( allowedDeliveryMethodsField, // Canonical wire order push,poll → join in the same order so a // round-trip through storage doesn't churn the value. ["push", "poll"].filter((m) => current.has(m)).join("##"), { shouldDirty: true }, ); }; return ( {t("ssfReceiver")} {t("ssfReceiverHelp")} {t("ssfSectionGeneralHelp")} {!clientStream && ( )} {clientStream?.status === "enabled" && ( )} {clientStream?.status === "paused" && ( )} {clientStream?.status === "disabled" && ( )} {clientStream && !clientStream.status && ( )} ( "attributes.ssf.profile", )} label={t("ssfProfile")} labelIcon={t("ssfProfileHelp")} controller={{ defaultValue: "SSF_1_0", }} options={[ { key: "SSF_1_0", value: t("ssfProfile.SSF_1_0") }, { key: "SSE_CAEP", value: t("ssfProfile.SSE_CAEP"), }, ]} /> ( "attributes.ssf.description", )} label={t("ssfDescription")} labelIcon={t("ssfDescriptionHelp")} rules={{ maxLength: { value: 255, message: t("maxLength", { length: 255 }), }, }} /> ( "attributes.ssf.streamAudience", )} label={t("ssfStreamAudience")} labelIcon={t("ssfStreamAudienceHelp")} /> ( "attributes.ssf.defaultSubjects", )} label={t("ssfDefaultSubjects")} labelIcon={t("ssfDefaultSubjectsHelp")} controller={{ defaultValue: "NONE", }} options={[ { key: "ALL", value: t("ssfDefaultSubjects.ALL"), }, { key: "NONE", value: t("ssfDefaultSubjects.NONE"), }, ]} /> ( "attributes.ssf.userSubjectFormat", )} label={t("ssfUserSubjectFormat")} labelIcon={t("ssfUserSubjectFormatHelp")} controller={{ defaultValue: defaultUserSubjectFormat, }} options={[ { key: "iss_sub", value: t("ssfUserSubjectFormat.iss_sub"), }, { key: "email", value: t("ssfUserSubjectFormat.email"), }, { key: "complex.iss_sub+tenant", value: t( "ssfUserSubjectFormat.complex.iss_sub+tenant", ), }, { key: "complex.email+tenant", value: t("ssfUserSubjectFormat.complex.email+tenant"), }, ]} /> ( "attributes.ssf.autoNotifyOnLogin", )} label={t("ssfAutoNotifyOnLogin")} labelIcon={t("ssfAutoNotifyOnLoginHelp")} stringify /> } > ( "attributes.ssf.subjectRemovalGraceSeconds", )} defaultValue="" control={control} render={({ field }) => ( )} /> ( "attributes.ssf.autoVerifyStream", )} label={t("ssfAutoVerifyStream")} labelIcon={t("ssfAutoVerifyStreamHelp")} stringify /> {ssfAutoVerifyStream === "true" && ( ( "attributes.ssf.verificationDelayMillis", )} label={t("ssfVerificationDelay")} labelIcon={t("ssfVerificationDelayHelp")} controller={{ defaultValue: 1500, rules: { min: 0, }, }} /> )} ( "attributes.ssf.minVerificationInterval", )} label={t("ssfMinVerificationInterval")} labelIcon={t("ssfMinVerificationIntervalHelp")} controller={{ defaultValue: "", rules: { min: 0, }, }} /> } > ( "attributes.ssf.maxEventAgeSeconds", )} defaultValue="" control={control} render={({ field }) => ( )} /> } > ( "attributes.ssf.inactivityTimeoutSeconds", )} defaultValue="" control={control} render={({ field }) => ( )} /> ), }, { title: t("ssfSectionAuthentication"), panel: (
{t("ssfSectionAuthenticationHelp")} ( "attributes.ssf.requireServiceAccount", )} label={t("ssfRequireServiceAccount")} labelIcon={t("ssfRequireServiceAccountHelp")} stringify /> } > ( {rolePickerOpen && ( { const row = rows[0]; const value = row.client?.clientId ? `${row.client.clientId}.${row.role.name}` : row.role.name; field.onChange(value); setRolePickerOpen(false); }} onClose={() => setRolePickerOpen(false)} isRadio /> )} {field.value && field.value !== "" && ( field.onChange("")} > )} { setRoleFilterType(type); setRolePickerOpen(true); }} variant="secondary" data-testid="ssfRequiredRoleSelect" isDisabled={false} /> )} />
), }, { title: t("ssfSectionDelivery"), panel: (
{t("ssfSectionDeliveryHelp")} } > updateAllowedDeliveryMethod("push", checked) } /> updateAllowedDeliveryMethod("poll", checked) } /> {pushDeliveryAllowed && ( } > ( "attributes.ssf.validPushUrls", )} aria-label={t("ssfValidPushUrls")} addButtonLabel="ssfValidPushUrls.add" stringify /> )} {pushDeliveryAllowed && ( <> ( "attributes.ssf.pushEndpointConnectTimeoutMillis", )} label={t("ssfPushEndpointConnectTimeout")} labelIcon={t("ssfPushEndpointConnectTimeoutHelp")} controller={{ defaultValue: defaultPushConnectTimeoutMillis, rules: { min: 0, }, }} /> ( "attributes.ssf.pushEndpointSocketTimeoutMillis", )} label={t("ssfPushEndpointSocketTimeout")} labelIcon={t("ssfPushEndpointSocketTimeoutHelp")} controller={{ defaultValue: defaultPushSocketTimeoutMillis, rules: { min: 0, }, }} /> )}
), }, { title: t("ssfSectionEvents"), panel: (
{t("ssfSectionEventsHelp")} } > { const option = value.toString(); if (!option) return; const next = supportedEventsSelected.includes(option) ? supportedEventsSelected.filter( (s) => s !== option, ) : [...supportedEventsSelected, option]; setValue( convertAttributeNameToForm( "attributes.ssf.supportedEvents", ), next.join(","), { shouldDirty: true }, ); setSupportedEventsFilter(""); }} onClear={() => { setValue( convertAttributeNameToForm( "attributes.ssf.supportedEvents", ), "", { shouldDirty: true }, ); setSupportedEventsFilter(""); }} onFilter={setSupportedEventsFilter} > {availableSupportedEvents .filter((event) => event .toLowerCase() .includes(supportedEventsFilter.toLowerCase()), ) .map((event) => ( {event} {nativelyEmittedEvents.includes(event) && ( )} ))} ( "attributes.ssf.allowEmitEvents", )} label={t("ssfAllowEmitEvents")} labelIcon={t("ssfAllowEmitEventsHelp")} stringify /> } > { const option = value.toString(); if (!option) return; const next = emitOnlyEventsSelected.includes(option) ? emitOnlyEventsSelected.filter((s) => s !== option) : [...emitOnlyEventsSelected, option]; setValue( convertAttributeNameToForm( "attributes.ssf.emitOnlyEvents", ), next.join(","), { shouldDirty: true }, ); setEmitOnlyEventsFilter(""); }} onClear={() => { setValue( convertAttributeNameToForm( "attributes.ssf.emitOnlyEvents", ), "", { shouldDirty: true }, ); setEmitOnlyEventsFilter(""); }} onFilter={setEmitOnlyEventsFilter} isDisabled={supportedEventsSelected.length === 0} > {supportedEventsSelected .filter((event) => event .toLowerCase() .includes(emitOnlyEventsFilter.toLowerCase()), ) .map((event) => ( {event} ))} {String(ssfAllowEmitEvents) === "true" && ( } > ( {emitRolePickerOpen && ( { const row = rows[0]; const value = row.client?.clientId ? `${row.client.clientId}.${row.role.name}` : row.role.name; field.onChange(value); setEmitRolePickerOpen(false); }} onClose={() => setEmitRolePickerOpen(false)} isRadio /> )} {field.value && field.value !== "" && ( field.onChange("")} > )} { setEmitRoleFilterType(type); setEmitRolePickerOpen(true); }} variant="secondary" data-testid="ssfEmitEventsRoleSelect" isDisabled={false} /> )} /> )}
), }, ]} />
); };