import { useCallback, useEffect, useMemo, useState } from "react"; import { AdminPageLayout, Button, Card, DataGrid, Dialog, FilterBar, FormField, FormGrid, MetricCard, MetricGrid, PageActionBar, SelectionList, SelectionListItem, SelectionListItemContent, StatePanel, StatusBadge, TableActionGroup, WorkspaceLayout, formatDateTime, hasScope, useUnsavedChanges, useUnsavedDraftGuard, type ApiSettings, type AuthInfo, type DataGridColumn } from "@govoplan/core-webui"; import { createConnectorConfiguration, executeConnectorRun, listConnectorConfigurations, listConnectorDefinitions, listConnectorRuns, reviewConnectorRun, updateConnectorConfiguration, upsertConnectorDefinition, type ConnectorConfiguration, type ConnectorConfigurationDraft, type ConnectorDefinition, type ConnectorRun } from "../api/governedConnectors"; type Props = { settings: ApiSettings; auth: AuthInfo; }; const EMPTY_DRAFT: ConnectorConfigurationDraft = { name: "", status: "draft", endpoint_url: "", credential_ref: "", local_overrides: "{}", ambiguity_policy: "manual_review" }; const EXAMPLE_DEFINITION = JSON.stringify({ definition_key: "example.reference-data", name: "Example reference data", description: "Locally governed example connector", origin: "local", specification: { provider: "example-provider", protocol: "rest", capabilities: ["discover", "read", "dry_run"], input_schema: { type: "object" }, output_schema: { type: "object" }, mapping: { version: "1", rules: [{ source: "id", target: "record.id", required: true }] }, validation_rules: [{ kind: "unique", field: "record.id", severity: "error", code: "record.id.ambiguous", message: "The record identifier is not unique." }], dry_run: { supported: true, simulation_supported: true, sample_rows: [{ id: "sample-1" }], max_items: 500, redacted_fields: [] }, audit: { event_prefix: "connectors.example", expected_events: ["simulation.completed"], evidence_fields: ["input_hash", "configuration_hash"] }, privacy_classification: "internal", retention_class: "connector-preview-30d", operational_limits: { timeout_seconds: 30 }, retry_policy: { max_attempts: 2 } } }, null, 2); export default function ConnectorGovernancePage({ settings, auth }: Props) { const [definitions, setDefinitions] = useState([]); const [configurations, setConfigurations] = useState([]); const [runs, setRuns] = useState([]); const [selectedId, setSelectedId] = useState(""); const [draft, setDraft] = useState(EMPTY_DRAFT); const [savedKey, setSavedKey] = useState(""); const [search, setSearch] = useState(""); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); const [definitionOpen, setDefinitionOpen] = useState(false); const [definitionJson, setDefinitionJson] = useState(EXAMPLE_DEFINITION); const [configurationOpen, setConfigurationOpen] = useState(false); const [newDefinitionId, setNewDefinitionId] = useState(""); const [newDraft, setNewDraft] = useState(EMPTY_DRAFT); const [sampleJson, setSampleJson] = useState("[]"); const [externalRevision, setExternalRevision] = useState(""); const [reviewRun, setReviewRun] = useState(null); const [reviewReason, setReviewReason] = useState(""); const { requestDiscard } = useUnsavedChanges(); const selected = configurations.find((item) => item.id === selectedId) ?? null; const canAdmin = hasScope(auth, "connectors:source:admin"); const canExecute = canAdmin || hasScope(auth, "connectors:source:write"); const dirty = Boolean(selected && draftKey(draft) !== savedKey); const applyConfiguration = useCallback((item: ConnectorConfiguration | null) => { const next = item ? draftFromConfiguration(item) : EMPTY_DRAFT; setDraft(next); setSavedKey(item ? draftKey(next) : ""); setSampleJson(JSON.stringify( item?.effective_configuration.dry_run.sample_rows ?? [], null, 2 )); }, []); const reload = useCallback(async (preferredId?: string) => { setLoading(true); setError(""); try { const [nextDefinitions, nextConfigurations] = await Promise.all([ listConnectorDefinitions(settings), listConnectorConfigurations(settings) ]); const nextId = preferredId && nextConfigurations.some((item) => item.id === preferredId) ? preferredId : nextConfigurations.some((item) => item.id === selectedId) ? selectedId : nextConfigurations[0]?.id ?? ""; const nextRuns = await listConnectorRuns(settings, nextId || undefined); setDefinitions(nextDefinitions); setConfigurations(nextConfigurations); setRuns(nextRuns); setSelectedId(nextId); applyConfiguration(nextConfigurations.find((item) => item.id === nextId) ?? null); if (!newDefinitionId && nextDefinitions[0]) setNewDefinitionId(nextDefinitions[0].id); } catch (caught) { setError(errorMessage(caught)); } finally { setLoading(false); } }, [applyConfiguration, newDefinitionId, selectedId, settings]); useEffect(() => { void reload(); }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]); const visibleConfigurations = useMemo(() => { const needle = search.trim().toLocaleLowerCase(); return configurations.filter((item) => !needle || `${item.name} ${item.definition_name} ${item.status}` .toLocaleLowerCase() .includes(needle)); }, [configurations, search]); const save = async (): Promise => { if (!selected || !canAdmin) return false; setBusy(true); setError(""); try { const overrides = parseObject(draft.local_overrides, "Local overrides"); const updated = await updateConnectorConfiguration(settings, selected.id, { expected_revision: selected.resource_revision, name: draft.name.trim(), status: draft.status, endpoint_url: draft.endpoint_url.trim() || null, credential_ref: draft.credential_ref.trim() || null, local_overrides: overrides, ambiguity_policy: draft.ambiguity_policy }); setSuccess("Connector configuration saved."); await reload(updated.id); return true; } catch (caught) { setError(errorMessage(caught)); return false; } finally { setBusy(false); } }; useUnsavedDraftGuard({ dirty, onSave: save, onDiscard: () => applyConfiguration(selected), title: "Unsaved connector changes", message: "Save or discard the current connector changes before continuing." }); const selectConfiguration = (item: ConnectorConfiguration) => { if (item.id === selectedId) return; requestDiscard(() => { setSelectedId(item.id); applyConfiguration(item); setRuns([]); setError(""); setSuccess(""); void listConnectorRuns(settings, item.id).then(setRuns).catch((caught) => { setError(errorMessage(caught)); }); }); }; const createDefinition = async () => { setBusy(true); setError(""); try { const payload = parseObject(definitionJson, "Definition"); const created = await upsertConnectorDefinition(settings, payload); setDefinitionOpen(false); setSuccess("Connector definition revision saved."); setNewDefinitionId(created.id); await reload(selectedId); } catch (caught) { setError(errorMessage(caught)); } finally { setBusy(false); } }; const createConfiguration = async () => { if (!newDefinitionId || !newDraft.name.trim()) return; setBusy(true); setError(""); try { const created = await createConnectorConfiguration(settings, { definition_id: newDefinitionId, name: newDraft.name.trim(), status: newDraft.status, endpoint_url: newDraft.endpoint_url.trim() || null, credential_ref: newDraft.credential_ref.trim() || null, local_overrides: parseObject(newDraft.local_overrides, "Local overrides"), ambiguity_policy: newDraft.ambiguity_policy }); setConfigurationOpen(false); setNewDraft(EMPTY_DRAFT); setSuccess("Connector configuration created."); await reload(created.id); } catch (caught) { setError(errorMessage(caught)); } finally { setBusy(false); } }; const adoptUpdate = async () => { if (!selected || dirty || !selected.update_available) return; setBusy(true); setError(""); try { const updated = await updateConnectorConfiguration(settings, selected.id, { expected_revision: selected.resource_revision, adopt_latest_definition: true }); setSuccess("Package revision adopted; protected local overrides were reapplied."); await reload(updated.id); } catch (caught) { setError(errorMessage(caught)); } finally { setBusy(false); } }; const run = async (mode: "dry-runs" | "simulations") => { if (!selected || dirty) return; setBusy(true); setError(""); try { const inputRows = parseRows(sampleJson); const created = await executeConnectorRun(settings, selected.id, mode, { idempotency_key: `${mode}-${crypto.randomUUID()}`, input_rows: inputRows, external_revision: externalRevision.trim() || null }); setSuccess(`${mode === "dry-runs" ? "Dry-run" : "Simulation"} completed with status ${created.status}.`); setRuns(await listConnectorRuns(settings, selected.id)); } catch (caught) { setError(errorMessage(caught)); } finally { setBusy(false); } }; const decideReview = async (decision: "approved" | "rejected") => { if (!reviewRun || reviewReason.trim().length < 5) return; setBusy(true); setError(""); try { await reviewConnectorRun(settings, reviewRun.id, decision, reviewReason.trim()); setReviewRun(null); setReviewReason(""); setSuccess(`Simulation ${decision}.`); setRuns(await listConnectorRuns(settings, selectedId)); } catch (caught) { setError(errorMessage(caught)); } finally { setBusy(false); } }; const runColumns = useMemo[]>(() => [ { id: "created", header: "Run", width: 190, sortable: true, value: (row) => row.created_at, render: (row) => <>{row.mode}
{formatDateTime(row.created_at)} }, { id: "status", header: "Status", width: 150, sortable: true, value: (row) => row.status, render: (row) => }, { id: "summary", header: "Effects", width: "1fr", minWidth: 220, render: (row) => `${row.summary.total ?? 0} total · ${row.summary.ambiguous ?? 0} ambiguous · ${row.summary.errors ?? 0} errors` }, { id: "revision", header: "Evidence", width: 170, render: (row) => r{row.configuration_revision} · {row.input_hash.slice(0, 8)} }, { id: "actions", header: "Actions", width: 90, sticky: "end", align: "right", render: (row) => ✓, applicable: ["pending", "quarantined"].includes(row.review_state), disabled: !canAdmin || busy, disabledReason: !canAdmin ? "Connector administration permission is required." : undefined, onClick: () => setReviewRun(row) }]} /> } ], [busy, canAdmin]); const actionBar = void reload(selectedId), loading }} primaryActions={<> {selected?.update_available ? : null} } discardAction={{ label: "Discard changes", disabled: !selected, onClick: () => applyConfiguration(selected) }} saveAction={{ label: "Save", disabled: !selected || !canAdmin || busy, disabledReason: !canAdmin ? "Connector administration permission is required." : undefined, onClick: () => void save() }} />; return item.update_available).length} tone="warning" /> ["pending", "quarantined"].includes(item.review_state)).length} tone="warning" /> setSearch(event.target.value)} placeholder="Search configurations" aria-label="Search connector configurations" /> {visibleConfigurations.map((item) => selectConfiguration(item)} > )} {!visibleConfigurations.length ? : null} } > {!selected ? :
Definition revision {selected.base_definition_revision} {selected.update_available ? : null} {selected.effective_hash.slice(0, 12)}
setDraft({ ...draft, name: event.target.value })} /> setDraft({ ...draft, endpoint_url: event.target.value })} /> setDraft({ ...draft, credential_ref: event.target.value })} />