import { useCallback, useEffect, useMemo, useState } from "react"; import { AdminPageLayout, Button, Card, Dialog, FilterBar, FormField, FormGrid, MetricCard, MetricGrid, PageActionBar, SelectionList, SelectionListItem, SelectionListItemContent, StatePanel, StatusBadge, WorkspaceLayout, formatDateTime, hasScope, useUnsavedChanges, useUnsavedDraftGuard, type ApiSettings, type AuthInfo } from "@govoplan/core-webui"; import { createKnowledgeProfile, discoverKnowledgeProfile, listKnowledgeObjects, listKnowledgeProfiles, listKnowledgeRuns, previewKnowledgeMigration, publishKnowledgePage, synchronizeKnowledgeProfile, updateKnowledgeProfile, type KnowledgeMigrationPreview, type KnowledgeObject, type KnowledgeProfile, type KnowledgeRun } from "../api/externalKnowledge"; type Props = { settings: ApiSettings; auth: AuthInfo; }; type ProfileDraft = { status: "active" | "paused"; desired_maturity: KnowledgeProfile["desired_maturity"]; source_authority_mode: KnowledgeProfile["source_authority_mode"]; default_visibility: KnowledgeProfile["default_visibility"]; default_acl_tokens: string; namespace_mappings: string; }; const EMPTY_DRAFT: ProfileDraft = { status: "active", desired_maturity: "migrate", source_authority_mode: "external_mirror", default_visibility: "restricted", default_acl_tokens: "scope:connectors:knowledge:read", namespace_mappings: JSON.stringify([{ source_namespace_id: 0, source_name: "", target_space_ref: "external-knowledge", target_path_prefix: "", include: true, acl_tokens: [] }], null, 2) }; export default function ExternalKnowledgePage({ settings, auth }: Props) { const [profiles, setProfiles] = useState([]); const [objects, setObjects] = 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 [createOpen, setCreateOpen] = useState(false); const [configurationId, setConfigurationId] = useState(""); const [newDraft, setNewDraft] = useState(EMPTY_DRAFT); const [migrationOpen, setMigrationOpen] = useState(false); const [targetSpace, setTargetSpace] = useState("external-knowledge"); const [supportedMacros, setSupportedMacros] = useState(""); const [existingTargets, setExistingTargets] = useState("[]"); const [migration, setMigration] = useState(null); const [publishOpen, setPublishOpen] = useState(false); const [externalPageId, setExternalPageId] = useState(""); const [publishTitle, setPublishTitle] = useState(""); const [publishBody, setPublishBody] = useState(""); const [publishSummary, setPublishSummary] = useState(""); const [publishRevision, setPublishRevision] = useState(""); const { requestDiscard } = useUnsavedChanges(); const selected = profiles.find((item) => item.id === selectedId) ?? null; const canAdmin = hasScope(auth, "connectors:knowledge:admin"); const canSync = hasScope(auth, "connectors:knowledge:sync"); const canMigrate = hasScope(auth, "connectors:knowledge:migrate"); const canPublish = hasScope(auth, "connectors:knowledge:publish"); const dirty = Boolean(selected && draftKey(draft) !== savedKey); const applyProfile = useCallback((profile: KnowledgeProfile | null) => { const next = profile ? draftFromProfile(profile) : EMPTY_DRAFT; setDraft(next); setSavedKey(profile ? draftKey(next) : ""); }, []); const reload = useCallback(async (preferredId?: string) => { setLoading(true); setError(""); try { const nextProfiles = await listKnowledgeProfiles(settings); const nextId = preferredId && nextProfiles.some((item) => item.id === preferredId) ? preferredId : nextProfiles.some((item) => item.id === selectedId) ? selectedId : nextProfiles[0]?.id ?? ""; const [nextObjects, nextRuns] = nextId ? await Promise.all([ listKnowledgeObjects(settings, nextId), listKnowledgeRuns(settings, nextId) ]) : [[], []]; setProfiles(nextProfiles); setSelectedId(nextId); setObjects(nextObjects); setRuns(nextRuns); applyProfile(nextProfiles.find((item) => item.id === nextId) ?? null); } catch (caught) { setError(errorMessage(caught)); } finally { setLoading(false); } }, [applyProfile, selectedId, settings]); useEffect(() => { void reload(); }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]); const save = async (): Promise => { if (!selected || !canAdmin) return false; setBusy(true); setError(""); try { const updated = await updateKnowledgeProfile(settings, selected.id, { expected_resource_revision: selected.resource_revision, status: draft.status, desired_maturity: draft.desired_maturity, source_authority_mode: draft.source_authority_mode, default_visibility: draft.default_visibility, default_acl_tokens: lines(draft.default_acl_tokens), namespace_mappings: parseArray(draft.namespace_mappings, "Namespace mappings") }); setSuccess("External knowledge profile saved; fallback ACLs and Search projections were refreshed."); await reload(updated.id); return true; } catch (caught) { setError(errorMessage(caught)); return false; } finally { setBusy(false); } }; useUnsavedDraftGuard({ dirty, onSave: save, onDiscard: () => applyProfile(selected), title: "Unsaved knowledge profile changes", message: "Save or discard the profile changes before continuing." }); const selectProfile = (profile: KnowledgeProfile) => { if (profile.id === selectedId) return; requestDiscard(() => { setSelectedId(profile.id); applyProfile(profile); setObjects([]); setRuns([]); setMigration(null); void Promise.all([ listKnowledgeObjects(settings, profile.id), listKnowledgeRuns(settings, profile.id) ]).then(([nextObjects, nextRuns]) => { setObjects(nextObjects); setRuns(nextRuns); }).catch((caught) => setError(errorMessage(caught))); }); }; const createProfile = async () => { if (!configurationId.trim()) return; setBusy(true); setError(""); try { const created = await createKnowledgeProfile(settings, { configuration_id: configurationId.trim(), desired_maturity: newDraft.desired_maturity, source_authority_mode: newDraft.source_authority_mode, default_visibility: newDraft.default_visibility, default_acl_tokens: lines(newDraft.default_acl_tokens), namespace_mappings: parseArray(newDraft.namespace_mappings, "Namespace mappings") }); setCreateOpen(false); setConfigurationId(""); setNewDraft(EMPTY_DRAFT); setSuccess("External knowledge profile created. Run discovery before synchronization."); await reload(created.id); } catch (caught) { setError(errorMessage(caught)); } finally { setBusy(false); } }; const discover = async () => { if (!selected || dirty) return; setBusy(true); setError(""); try { const result = await discoverKnowledgeProfile(settings, selected.id); setSuccess(`Discovered ${result.product} ${result.product_version ?? ""} at ${result.maturity} maturity with ${result.diagnostics.length} diagnostics.`); await reload(selected.id); } catch (caught) { setError(errorMessage(caught)); } finally { setBusy(false); } }; const sync = async (forceFull: boolean) => { if (!selected || dirty) return; setBusy(true); setError(""); try { const run = await synchronizeKnowledgeProfile(settings, selected.id, { idempotency_key: `knowledge-${forceFull ? "backfill" : "delta"}-${crypto.randomUUID()}`, force_full: forceFull, limit: 100 }); setSuccess(`${forceFull ? "Backfill" : "Delta"} completed with ${effectTotal(run)} effects.`); await reload(selected.id); } catch (caught) { setError(errorMessage(caught)); } finally { setBusy(false); } }; const previewMigration = async () => { if (!selected || !targetSpace.trim()) return; setBusy(true); setError(""); try { const result = await previewKnowledgeMigration(settings, selected.id, { idempotency_key: `knowledge-migration-${crypto.randomUUID()}`, target_space_ref: targetSpace.trim(), max_items: 100, supported_macros: lines(supportedMacros), existing_targets: parseArray(existingTargets, "Existing targets") }); setMigration(result); setMigrationOpen(false); setSuccess(result.can_apply ? "Migration preview is complete and contains no blocking conflict. It did not write Wiki pages." : "Migration preview found conflicts, errors, or truncation. It did not write Wiki pages."); await reload(selected.id); } catch (caught) { setError(errorMessage(caught)); } finally { setBusy(false); } }; const publish = async () => { if (!selected || !externalPageId.trim() || !publishTitle.trim()) return; setBusy(true); setError(""); try { const result = await publishKnowledgePage( settings, selected.id, externalPageId.trim(), { idempotency_key: `knowledge-publish-${crypto.randomUUID()}`, title: publishTitle.trim(), body: publishBody, summary: publishSummary.trim(), expected_external_revision: publishRevision.trim() || null, minor: false } ); setPublishOpen(false); setSuccess(result.outcome_unknown ? "Publication outcome is unknown. Reconcile the provider revision before retrying." : "Provider accepted the page revision and durable recovery evidence was recorded."); await reload(selected.id); } catch (caught) { setError(errorMessage(caught)); } finally { setBusy(false); } }; const visibleProfiles = useMemo(() => { const needle = search.trim().toLocaleLowerCase(); return profiles.filter((item) => !needle || `${item.product} ${item.product_version ?? ""} ${item.health_status} ${item.configuration_id}` .toLocaleLowerCase() .includes(needle)); }, [profiles, search]); const actionBar = void reload(selectedId), loading }} primaryActions={<> } discardAction={{ label: "Discard changes", disabled: !selected, onClick: () => applyProfile(selected) }} saveAction={{ label: "Save", disabled: !selected || !canAdmin || busy, disabledReason: !canAdmin ? "External knowledge administration permission is required." : undefined, onClick: () => void save() }} />; return item.status !== "deleted").length} /> !["healthy", "unknown"].includes(item.health_status)).length} tone="warning" /> ["failed", "outcome_unknown"].includes(item.status)).length} tone="warning" /> setSearch(event.target.value)} placeholder="Search profiles" aria-label="Search external knowledge profiles" /> {visibleProfiles.map((profile) => selectProfile(profile)}> )} {!visibleProfiles.length ? : null} } > {!selected ? :
Discovered maturity: {selected.discovered_maturity} r{selected.resource_revision}