import { useEffect, useMemo, useState } from "react"; import { AdminPageLayout, adminErrorMessage, Button, Card, DataGrid, DismissibleAlert, DocumentationHelpLink, MetricCard, StatusBadge, TableActionGroup, type ApiSettings, type DataGridColumn } from "@govoplan/core-webui"; import { Play, RefreshCw, RotateCw } from "lucide-react"; import { continueSearchRebuild, getSearchDiagnostics, listSearchProviders, processSearchChanges, reconcileSearchModules, startSearchRebuild, type SearchDiagnostics, type SearchIndexState, type SearchResourceType } from "../../api/search"; type Props = { settings: ApiSettings; }; type ResourceRow = SearchResourceType & { state?: SearchIndexState; }; const DOCUMENTATION = { topicId: "search.global-and-contextual", documentationType: "admin" as const }; export default function SearchAdminPanel({ settings }: Props) { const [diagnostics, setDiagnostics] = useState(null); const [resources, setResources] = useState([]); const [loading, setLoading] = useState(true); const [busyKey, setBusyKey] = useState(""); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); useEffect(() => { void load(); }, [settings.accessToken, settings.apiBaseUrl, settings.apiKey]); async function load() { setLoading(true); setError(""); try { const [nextDiagnostics, catalogue] = await Promise.all([ getSearchDiagnostics(settings), listSearchProviders(settings) ]); setDiagnostics(nextDiagnostics); setResources(catalogue.resources); } catch (err) { setError(adminErrorMessage(err)); } finally { setLoading(false); } } async function runAction(key: string, action: () => Promise) { setBusyKey(key); setError(""); setSuccess(""); try { setSuccess(await action()); await load(); } catch (err) { setError(adminErrorMessage(err)); } finally { setBusyKey(""); } } const rows = useMemo(() => { const states = new Map( (diagnostics?.states ?? []).map((state) => [ `${state.provider_id}:${state.resource_type}`, state ]) ); return resources.map((resource) => ({ ...resource, state: states.get(`${resource.provider_id}:${resource.resource_type}`) })); }, [diagnostics, resources]); const columns = useMemo[]>(() => [ { id: "resource", header: "Search source", minWidth: 250, resizable: true, sortable: true, filterable: true, value: (row) => row.label, render: (row) => (
{row.label}
{row.module_id} / {row.provider_id}
) }, { id: "status", header: "State", width: 135, minWidth: 115, sortable: true, filterable: true, filterType: "list", value: (row) => row.state?.status ?? "not built", render: (row) => ( ) }, { id: "documents", header: "Documents", width: 125, minWidth: 105, align: "right", value: (row) => row.state?.indexed_documents ?? 0 }, { id: "rejected", header: "Rejected", width: 105, minWidth: 90, align: "right", value: (row) => row.state?.rejected_documents ?? 0 }, { id: "lastSuccess", header: "Last success", width: 190, minWidth: 165, sortable: true, value: (row) => row.state?.last_success_at ?? "", render: (row) => formatDateTime(row.state?.last_success_at) }, { id: "actions", header: "Actions", width: 80, minWidth: 80, sticky: "end", align: "right", render: (row) => { const key = `${row.provider_id}:${row.resource_type}`; const continuing = row.state?.status === "backfilling"; return ( : , disabled: Boolean(busyKey), onClick: () => void runAction(key, async () => { const response = continuing ? await continueSearchRebuild(settings, row.provider_id, row.resource_type) : await startSearchRebuild(settings, row.provider_id, row.resource_type); return response.state.status === "backfilling" ? `${row.label} rebuild advanced to the next checkpoint.` : `${row.label} rebuild completed.`; }) }]} /> ); } } ], [busyKey, settings]); const queue = diagnostics?.queue ?? {}; const pending = (queue.queued ?? 0) + (queue.retrying ?? 0); const quarantined = queue.quarantined ?? 0; return ( )} >
{quarantined > 0 && ( Quarantined changes require source or contract repair followed by a source rebuild. They are never silently discarded. )}
`${row.provider_id}:${row.resource_type}`} initialFit="container" emptyText="No active modules announce searchable resource types." />
{rows.some((row) => row.state?.last_error) && (
{rows.filter((row) => row.state?.last_error).map((row) => ( {row.label}: {row.state?.last_error} ))}
)}
); } function statusTone(status?: string): string { if (["ready", "idle"].includes(status ?? "")) return "success"; if (["failed", "quarantined"].includes(status ?? "")) return "danger"; if (["backfilling", "stale"].includes(status ?? "")) return "warning"; return "neutral"; } function formatDateTime(value?: string | null): string { if (!value) return "-"; const parsed = new Date(value); return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString(); }