/* eslint-disable */ // @ts-nocheck import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation"; import { HelpItem } from "../../../../shared/keycloak-ui-shared"; import { ActionGroup, Button, Card, CardBody, CardHeader, CardTitle, FormGroup, InputGroup, InputGroupItem, Text, TextContent, TextInput, } from "../../../../shared/@patternfly/react-core"; import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useSearchParams } from "react-router-dom"; import { useAdminClient } from "../../../admin-client"; import { FormAccess } from "../../../components/form/FormAccess"; import { useRealm } from "../../../context/realm-context/RealmContext"; import { addTrailingSlash } from "../../../util"; import { getAuthorizationHeaders } from "../../../utils/getAuthorizationHeaders"; import useFormatDate from "../../../utils/useFormatDate"; type SsfPendingEvent = { jti: string; eventType?: string; deliveryMethod?: string; status?: string; attempts?: number; createdAt?: number; nextAttemptAt?: number; deliveredAt?: number; lastError?: string; streamId?: string; // Decoded JWS payload of the Security Event Token — the full // claim set the receiver will process (iss/iat/jti/aud/txn plus // the subject and event body). Rendered as formatted JSON in the // lookup result so the operator sees exactly what goes on the // wire. decodedSet?: Record; // Resolved Keycloak user UUID — server-side maps the SET's // subject through SubjectResolver. Null for org-only / // unresolvable subjects; the UI then omits the user // click-through. userId?: string; }; export type EventSearchTabProps = { client: ClientRepresentation; }; export const EventSearchTab = ({ client }: EventSearchTabProps) => { const { t } = useTranslation(); const { adminClient } = useAdminClient(); const { realm } = useRealm(); const formatDate = useFormatDate(); const [searchParams, setSearchParams] = useSearchParams(); const [pendingLookupJti, setPendingLookupJti] = useState( () => searchParams.get("jti") ?? "", ); const [pendingLookupResult, setPendingLookupResult] = useState(null); const [pendingLookupError, setPendingLookupError] = useState( null, ); const [pendingActionLoading, setPendingActionLoading] = useState(false); /** * Looks up a single outbox row by jti scoped to this receiver — calls * GET /admin/realms/{realm}/ssf/clients/{uuid}/pending-events/{jti}. * 404 is rendered as a "not found" message rather than an alert; any * other failure surfaces both inline and via addError. */ const handlePendingLookup = useCallback( async (jti: string) => { const lookupJti = jti.trim(); if (!client.id || !lookupJti) { return; } // Don't wipe the previous result/error here — that causes the // result block to unmount during the request, which collapses the // container height and makes the layout jump on repeated clicks. // We update them in place once the new fetch resolves. setPendingActionLoading(true); try { const response = await fetch( `${addTrailingSlash(adminClient.baseUrl)}admin/realms/${realm}/ssf/clients/${client.clientId}/pending-events/${encodeURIComponent(lookupJti)}`, { headers: getAuthorizationHeaders( await adminClient.getAccessToken(), ), }, ); if (response.status === 404) { setPendingLookupError(t("ssfPendingLookupNotFound")); setPendingLookupResult(null); return; } if (!response.ok) { const text = await response.text(); setPendingLookupError(text || `HTTP ${response.status}`); setPendingLookupResult(null); return; } setPendingLookupResult((await response.json()) as SsfPendingEvent); setPendingLookupError(null); } catch (error) { setPendingLookupError(String(error)); setPendingLookupResult(null); } finally { setPendingActionLoading(false); } }, [client.id, client.clientId, adminClient, realm, t], ); // Auto-run the lookup once on mount when the URL carries ?jti=... — // the emit success panel uses this as a one-click handoff into the // search tab. Captured into a ref at first render so the strip // below (which re-triggers this effect via searchParams) doesn't // fire a second lookup. const initialJtiRef = useRef(searchParams.get("jti")); useEffect(() => { const jti = initialJtiRef.current; if (!jti) return; initialJtiRef.current = null; void handlePendingLookup(jti); setSearchParams( (prev) => { const next = new URLSearchParams(prev); next.delete("jti"); return next; }, { replace: true }, ); }, [handlePendingLookup, setSearchParams]); return ( {t("ssfLookupTitle")} {t("ssfLookupTitleHelp")} { e.preventDefault(); void handlePendingLookup(pendingLookupJti); }} > } isRequired > setPendingLookupJti(value)} placeholder={t("ssfPendingLookupJtiPlaceholder")} /> {pendingLookupError && ( {pendingLookupError} )} {pendingLookupResult && ( {t("ssfPendingFieldStatus")}:{" "} {pendingLookupResult.status ?? "-"} {t("ssfPendingFieldEventType")}:{" "} {pendingLookupResult.eventType ?? "-"} {t("ssfPendingFieldDeliveryMethod")}:{" "} {pendingLookupResult.deliveryMethod ?? "-"} {/* Attempts + Next attempt at are PUSH drainer retry state — for POLL the receiver pulls on its own cadence and these fields carry no useful information. Hide them for POLL rows to avoid operator confusion. */} {pendingLookupResult.deliveryMethod !== "POLL" && ( {t("ssfPendingFieldAttempts")}:{" "} {pendingLookupResult.attempts ?? 0} )} {t("ssfPendingFieldCreatedAt")}:{" "} {pendingLookupResult.createdAt ? formatDate(new Date(pendingLookupResult.createdAt * 1000)) : "-"} {pendingLookupResult.deliveryMethod !== "POLL" && ( {t("ssfPendingFieldNextAttemptAt")}:{" "} {pendingLookupResult.nextAttemptAt ? formatDate( new Date(pendingLookupResult.nextAttemptAt * 1000), ) : "-"} )} {t("ssfPendingFieldDeliveredAt")}:{" "} {pendingLookupResult.deliveredAt ? formatDate( new Date(pendingLookupResult.deliveredAt * 1000), ) : "-"} {pendingLookupResult.lastError && ( {t("ssfPendingFieldLastError")}:{" "} {pendingLookupResult.lastError} )} {pendingLookupResult.userId && ( {t("ssfPendingFieldUserId")}:{" "} {pendingLookupResult.userId} )} {pendingLookupResult.decodedSet && ( <> {t("ssfPendingFieldDecodedSet")}:
                      {JSON.stringify(pendingLookupResult.decodedSet, null, 2)}
                    
)}
)}
); };