import { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { History, RefreshCw } from "lucide-react"; import { toast } from "sonner"; import { Button } from "../components/ui/button"; import { ApiError, apiErrorMessage } from "../api/client"; import { createCatalogDatabase, deleteCatalogDatabase, deleteCatalogDatabaseMetadata, getCatalogMetrics, listCatalogDatabases, listDescriptionGenerationRuns, listMetadataGenerationModels, listSensitiveDataSuggestionRuns, replaceCatalogDatabaseSecrets, startCatalogSync, startDescriptionGenerationRun, suggestSensitiveFields, testCatalogDatabase, updateCatalogDatabase, type CatalogDatabase, type CatalogColumn, type CatalogDatabaseMetadataDeleteTarget, type CatalogSecretName, type CatalogSyncScope, type CatalogSyncRun, type DatabaseBinding, type DatabaseTransport, type DescriptionGenerationRun, type SensitiveDataSuggestion, type SensitiveDataSuggestionRequest, type SensitiveDataSuggestionRun, } from "../api/catalog-databases"; import { DatabaseGrid } from "./database-management/DatabaseGrid"; import { DatabaseForm } from "./database-management/DatabaseForm"; import { databaseFormTitle } from "./database-management/DatabaseForm"; import { DatabaseTables } from "./database-management/DatabaseTables"; import { DatabaseRelationships } from "./database-management/DatabaseRelationships"; import { CatalogSyncDrawer } from "./database-management/CatalogSyncDrawer"; import { MetadataGenerationModelSelector } from "./database-management/MetadataGenerationModelSelector"; import { DescriptionGenerationDrawer } from "./database-management/DescriptionGenerationDrawer"; import { SensitiveDataSuggestionHistoryDrawer } from "./database-management/SensitiveDataSuggestionHistoryDrawer"; import { SensitiveDataReviewDrawer } from "./database-management/SensitiveDataReviewDrawer"; import { FleetLedgerBreadcrumb, FleetLedgerContent, FleetLedgerDrawer, FleetLedgerHeader, FleetLedgerKpiStrip, FleetLedgerRealStatus, FleetLedgerShell, } from "./database-management/FleetLedgerShell"; import { configurationFingerprint, configurationFromDraft, draftFrom, hasSecretChanges, secretReplacements, type DatabaseBusyAction, type DatabaseFormDraft, type DatabaseFormMode, type DatabaseNavigationState, type DatabaseScreen, } from "./database-management/model"; const DATABASE_QUERY_KEY = ["catalog-databases"] as const; const DESCRIPTION_GENERATION_HISTORY_QUERY_KEY = ["description-generation-runs", 50] as const; const SENSITIVE_DATA_SUGGESTION_HISTORY_QUERY_KEY = ["sensitive-data-suggestion-runs", 50] as const; const DESCRIPTION_GENERATION_ACTIVE_STATUSES = ["queued", "running"] as const; const SYNC_STARTED_MESSAGES: Record = { tables: "Table synchronization started", columns: "Column synchronization started", relationships: "Relationship synchronization started", all: "Full schema synchronization started", }; const DATABASE_SCREEN_STORAGE_KEY = "thothii:database-management-screen"; function readPersistedDatabaseScreen(): DatabaseScreen { if (import.meta.env.MODE === "test") return { kind: "list" }; try { const parsed = JSON.parse(window.localStorage.getItem(DATABASE_SCREEN_STORAGE_KEY) ?? "null") as { kind?: unknown; workspaceId?: unknown } | null; if ((parsed?.kind === "tables" || parsed?.kind === "relationships") && typeof parsed.workspaceId === "string") { return { kind: parsed.kind, workspaceId: parsed.workspaceId }; } } catch { // Ignore malformed persisted navigation and return to the safe list view. } return { kind: "list" }; } interface Props { canManage: boolean; canManageSecrets: boolean; onNavigationStateChange?: (state: DatabaseNavigationState) => void; presentation?: "legacy" | "fleet"; } interface FormSource { id?: string; version: number; } function isStaleError(error: unknown): boolean { return error instanceof ApiError && error.code === "database_stale"; } function isDescriptionGenerationActive(run?: DescriptionGenerationRun | null): boolean { return Boolean(run && DESCRIPTION_GENERATION_ACTIVE_STATUSES.some( (status) => status === run.status, )); } export function DatabaseManagementPage({ canManage, canManageSecrets, onNavigationStateChange, presentation = "legacy", }: Props) { const queryClient = useQueryClient(); const { data, isLoading, isError, isFetching, refetch, } = useQuery({ queryKey: DATABASE_QUERY_KEY, queryFn: listCatalogDatabases, retry: false, }); const rows = data ?? []; const metadataModelsQuery = useQuery({ queryKey: ["metadata-generation-models"], queryFn: listMetadataGenerationModels, retry: false, }); const metadataModels = metadataModelsQuery.data?.models ?? []; const configuredMetadataDefault = metadataModelsQuery.data?.default ?? ""; const defaultMetadataModel = configuredMetadataDefault && metadataModels.some((model) => model.id === configuredMetadataDefault) ? configuredMetadataDefault : ""; const descriptionGenerationRunsQuery = useQuery({ queryKey: DESCRIPTION_GENERATION_HISTORY_QUERY_KEY, queryFn: () => listDescriptionGenerationRuns(50), enabled: canManage, retry: false, refetchInterval: 5_000, }); const descriptionGenerationRuns = descriptionGenerationRunsQuery.data ?? []; const observedActiveDescriptionGenerationRun = descriptionGenerationRuns.find( (run) => isDescriptionGenerationActive(run), ); const sensitiveDataSuggestionRunsQuery = useQuery({ queryKey: SENSITIVE_DATA_SUGGESTION_HISTORY_QUERY_KEY, queryFn: () => listSensitiveDataSuggestionRuns(50), enabled: canManage, retry: false, refetchInterval: 5_000, }); const sensitiveDataSuggestionRuns = sensitiveDataSuggestionRunsQuery.data ?? []; const observedActiveSensitiveDataSuggestionRun = sensitiveDataSuggestionRuns.find( (run) => run.status === "running", ); const [screen, setScreen] = useState(readPersistedDatabaseScreen); const [selectedMetadataModel, setSelectedMetadataModel] = useState(""); const [draft, setDraft] = useState(null); const [baseline, setBaseline] = useState(""); const [formSource, setFormSource] = useState(null); const [search, setSearch] = useState(""); const [selectedDatabaseId, setSelectedDatabaseId] = useState(null); const [busyAction, setBusyAction] = useState(null); const [stale, setStale] = useState(false); const [staleBannerOpen, setStaleBannerOpen] = useState(true); const [partialSecretFailure, setPartialSecretFailure] = useState(null); const [tablesNavigationState, setTablesNavigationState] = useState({ dirty: false, busy: false, }); const [tableNestedNavigationActive, setTableNestedNavigationActive] = useState(false); const [tableNestedNavigationBack, setTableNestedNavigationBack] = useState<(() => void) | null>(null); const handleTableNestedNavigationChange = useCallback((active: boolean, onBack?: () => void) => { setTableNestedNavigationActive(active); setTableNestedNavigationBack(() => (active ? onBack ?? null : null)); }, []); const [activeSyncRun, setActiveSyncRun] = useState(null); const [syncHistoryDatabaseId, setSyncHistoryDatabaseId] = useState(null); const [syncDrawerOpen, setSyncDrawerOpen] = useState(false); const [activeDescriptionGenerationRun, setActiveDescriptionGenerationRun] = useState(null); const [descriptionGenerationDrawerOpen, setDescriptionGenerationDrawerOpen] = useState(false); const [activeSensitiveDataSuggestionRun, setActiveSensitiveDataSuggestionRun] = useState(null); const [sensitiveDataSuggestionHistoryDrawerOpen, setSensitiveDataSuggestionHistoryDrawerOpen] = useState(false); const [sensitiveReview, setSensitiveReview] = useState<{ databaseId: string; scopeLabel: string; suggestions: SensitiveDataSuggestion[]; } | null>(null); useEffect(() => { if (import.meta.env.MODE === "test") return; const persisted = screen.kind === "tables" || screen.kind === "relationships" ? screen : { kind: "list" }; window.localStorage.setItem(DATABASE_SCREEN_STORAGE_KEY, JSON.stringify(persisted)); }, [screen]); const originRef = useRef(null); const searchInputRef = useRef(null); const formHeadingRef = useRef(null); useEffect(() => { setSelectedMetadataModel(defaultMetadataModel); }, [defaultMetadataModel]); const activeRow = screen.kind === "list" ? undefined : rows.find((row) => row.workspaceId === screen.workspaceId); const metricsDatabaseId = (screen.kind === "tables" || screen.kind === "relationships") ? activeRow?.id : undefined; const catalogMetricsQuery = useQuery({ queryKey: ["catalog-metrics", metricsDatabaseId ?? "global"], queryFn: () => getCatalogMetrics(metricsDatabaseId), enabled: canManage && (metricsDatabaseId === undefined || Boolean(activeRow?.id)), retry: false, refetchInterval: presentation === "fleet" ? 15_000 : false, }); const invalidateCatalogMetrics = useCallback( () => queryClient.invalidateQueries({ queryKey: ["catalog-metrics"] }), [queryClient], ); const editable = screen.kind === "configure" || screen.kind === "edit"; const configurationDirty = Boolean( editable && draft && configurationFingerprint(draft) !== baseline, ); const dirty = Boolean(editable && draft && (configurationDirty || hasSecretChanges(draft))); const busy = busyAction !== null; const catalogChildVisible = screen.kind === "tables" || screen.kind === "relationships"; const navigationDirty = catalogChildVisible ? tablesNavigationState.dirty : dirty; const navigationBusy = catalogChildVisible ? tablesNavigationState.busy : busy; useEffect(() => { onNavigationStateChange?.({ dirty: navigationDirty, busy: navigationBusy }); }, [navigationBusy, navigationDirty, onNavigationStateChange]); useEffect(() => () => { onNavigationStateChange?.({ dirty: false, busy: false }); }, [onNavigationStateChange]); useEffect(() => { const warn = (event: BeforeUnloadEvent) => { if (!navigationDirty && !navigationBusy) return; event.preventDefault(); }; window.addEventListener("beforeunload", warn); return () => window.removeEventListener("beforeunload", warn); }, [navigationBusy, navigationDirty]); useEffect(() => { if (screen.kind === "list" || !activeRow || !formSource || busy) return; if (activeRow.id === formSource.id && activeRow.version === formSource.version) return; setStale(true); setStaleBannerOpen(true); }, [activeRow, busy, formSource, screen.kind]); useEffect(() => { if (screen.kind === "list") return; const timer = window.setTimeout(() => formHeadingRef.current?.focus(), 0); return () => window.clearTimeout(timer); }, [screen.kind, screen.kind === "list" ? "" : screen.workspaceId]); const cacheSavedRow = useCallback((saved: CatalogDatabase) => { queryClient.setQueryData(DATABASE_QUERY_KEY, (current = []) => { const existing = current.findIndex((row) => row.workspaceId === saved.workspaceId); if (existing < 0) return [...current, saved]; return current.map((row, index) => index === existing ? saved : row); }); }, [queryClient]); const restoreListFocus = useCallback(() => { const originLabel = originRef.current?.getAttribute("aria-label"); window.setTimeout(() => { if (originRef.current?.isConnected) { originRef.current.focus(); } else if (originLabel) { const matchingAction = [...document.querySelectorAll("button")] .find((button) => button.getAttribute("aria-label") === originLabel); (matchingAction ?? searchInputRef.current)?.focus(); } else { searchInputRef.current?.focus(); } }, 0); }, []); const showList = useCallback(() => { setScreen({ kind: "list" }); setDraft(null); setBaseline(""); setFormSource(null); setStale(false); setStaleBannerOpen(true); setPartialSecretFailure(null); setTablesNavigationState({ dirty: false, busy: false }); setTableNestedNavigationActive(false); setTableNestedNavigationBack(null); restoreListFocus(); }, [restoreListFocus]); useEffect(() => { if (screen.kind === "list" || isLoading || activeRow) return; showList(); toast.info("This database configuration is no longer available"); }, [activeRow, isLoading, screen.kind, showList]); useEffect(() => { if (!["tables", "relationships"].includes(screen.kind) || !activeRow || (activeRow.configured && activeRow.id)) return; showList(); toast.info("Save the database configuration before managing tables"); }, [activeRow, screen.kind, showList]); const backToList = useCallback(() => { if (busy) return; if (dirty && !window.confirm("Discard unsaved database changes?")) return; showList(); }, [busy, dirty, showList]); const openForm = useCallback(( mode: DatabaseFormMode, row: CatalogDatabase, origin: HTMLElement, ) => { const nextDraft = draftFrom(row); originRef.current = origin; setDraft(nextDraft); setBaseline(configurationFingerprint(nextDraft)); setFormSource({ id: row.id, version: row.version }); setStale(false); setStaleBannerOpen(true); setPartialSecretFailure(null); setScreen({ kind: mode, workspaceId: row.workspaceId, }); }, []); const viewRow = useCallback((row: CatalogDatabase, origin: HTMLButtonElement) => { openForm("view", row, origin); }, [openForm]); const editRow = useCallback((row: CatalogDatabase, origin: HTMLButtonElement) => { openForm(row.configured ? "edit" : "configure", row, origin); }, [openForm]); const deleteRow = useCallback((row: CatalogDatabase, origin: HTMLButtonElement) => { openForm("delete", row, origin); }, [openForm]); const openTables = useCallback((row: CatalogDatabase, origin?: HTMLElement) => { if (!row.configured || !row.id) return; if (origin) originRef.current = origin; setDraft(null); setBaseline(""); setFormSource(null); setStale(false); setPartialSecretFailure(null); setTablesNavigationState({ dirty: false, busy: false }); setTableNestedNavigationActive(false); setTableNestedNavigationBack(null); setScreen({ kind: "tables", workspaceId: row.workspaceId }); }, []); const openRelationships = useCallback((row: CatalogDatabase, origin?: HTMLElement) => { if (!row.configured || !row.id) return; if (origin) originRef.current = origin; setDraft(null); setBaseline(""); setFormSource(null); setStale(false); setPartialSecretFailure(null); setTablesNavigationState({ dirty: false, busy: false }); setTableNestedNavigationActive(false); setTableNestedNavigationBack(null); setScreen({ kind: "relationships", workspaceId: row.workspaceId }); }, []); const openOverview = useCallback((row: CatalogDatabase) => { const nextDraft = draftFrom(row); setDraft(nextDraft); setBaseline(configurationFingerprint(nextDraft)); setFormSource({ id: row.id, version: row.version }); setStale(false); setStaleBannerOpen(true); setPartialSecretFailure(null); setTablesNavigationState({ dirty: false, busy: false }); setScreen({ kind: "view", workspaceId: row.workspaceId }); }, []); const changeField = useCallback((field: "databaseName" | "schema", value: string) => { setDraft((current) => current ? { ...current, [field]: value } : current); }, []); const changeTransport = useCallback((transport: DatabaseTransport) => { setDraft((current) => current ? { ...current, binding: { ...current.binding, transport } } : current); }, []); const changeBinding = useCallback(( key: K, value: DatabaseBinding[K], ) => { setDraft((current) => current ? { ...current, binding: { ...current.binding, [key]: value } } : current); }, []); const changeSecret = useCallback((name: CatalogSecretName, value: string) => { setDraft((current) => current ? { ...current, secrets: { ...current.secrets, [name]: value } } : current); }, []); const markStale = useCallback(() => { setStale(true); setStaleBannerOpen(true); }, []); const save = useCallback(async () => { if (!activeRow || !formSource || !draft || !editable || !canManage || stale || busy) return; setBusyAction("save"); setPartialSecretFailure(null); try { const input = configurationFromDraft(draft); let saved = activeRow; const shouldPersistConfiguration = !formSource.id || configurationDirty; if (shouldPersistConfiguration) { saved = formSource.id ? await updateCatalogDatabase(formSource.id, formSource.version, input) : await createCatalogDatabase(input); setFormSource({ id: saved.id, version: saved.version }); cacheSavedRow(saved); } const replacements = secretReplacements(draft); if (Object.keys(replacements).length > 0) { if (!canManageSecrets) throw new Error("Secret replacement is not permitted"); try { saved = await replaceCatalogDatabaseSecrets( saved.id ?? formSource.id!, shouldPersistConfiguration ? saved.version : formSource.version, replacements, ); setFormSource({ id: saved.id, version: saved.version }); cacheSavedRow(saved); } catch (error) { setBaseline(configurationFingerprint(draft)); setScreen({ kind: "edit", workspaceId: saved.workspaceId }); setPartialSecretFailure(apiErrorMessage(error)); if (isStaleError(error)) { markStale(); } else { await queryClient.invalidateQueries({ queryKey: DATABASE_QUERY_KEY }); } toast.warning("Database configuration saved, but secrets still need attention"); return; } } const nextDraft = draftFrom(saved); setDraft(nextDraft); setBaseline(configurationFingerprint(nextDraft)); setFormSource({ id: saved.id, version: saved.version }); setScreen({ kind: "edit", workspaceId: saved.workspaceId }); setStale(false); setPartialSecretFailure(null); await queryClient.invalidateQueries({ queryKey: DATABASE_QUERY_KEY }); toast.success(activeRow.configured ? "Database configuration saved" : "Database configuration added"); } catch (error) { if (isStaleError(error)) { markStale(); } else { toast.error(apiErrorMessage(error)); } } finally { setBusyAction(null); } }, [ activeRow, busy, cacheSavedRow, canManage, canManageSecrets, configurationDirty, draft, editable, formSource, markStale, queryClient, stale, ]); const retrySecrets = useCallback(async () => { if (!activeRow?.configured || !formSource?.id || !draft || !canManageSecrets || busy || stale) return; const replacements = secretReplacements(draft); if (Object.keys(replacements).length === 0) { setPartialSecretFailure(null); return; } setBusyAction("retry-secrets"); try { const saved = await replaceCatalogDatabaseSecrets(formSource.id, formSource.version, replacements); setFormSource({ id: saved.id, version: saved.version }); cacheSavedRow(saved); setDraft({ ...draft, secrets: {} }); setPartialSecretFailure(null); await queryClient.invalidateQueries({ queryKey: DATABASE_QUERY_KEY }); toast.success("Database secrets saved"); } catch (error) { setPartialSecretFailure(apiErrorMessage(error)); if (isStaleError(error)) markStale(); toast.error(apiErrorMessage(error)); } finally { setBusyAction(null); } }, [activeRow, busy, cacheSavedRow, canManageSecrets, draft, formSource, markStale, queryClient, stale]); const testConnection = useCallback(async () => { if ( !activeRow?.configured || !formSource?.id || !draft || !canManage || dirty || stale || busy ) return; setBusyAction("test"); try { const tested = await testCatalogDatabase(formSource.id, formSource.version); setFormSource({ id: tested.id, version: tested.version }); cacheSavedRow(tested); const nextDraft = draftFrom(tested); setDraft(nextDraft); setBaseline(configurationFingerprint(nextDraft)); await queryClient.invalidateQueries({ queryKey: DATABASE_QUERY_KEY }); if (tested.connectionStatus === "reachable") { toast.success("Database connection is reachable"); } else { toast.error(tested.lastErrorMessage ?? "Database connection failed"); } } catch (error) { if (isStaleError(error)) markStale(); else toast.error(apiErrorMessage(error)); } finally { setBusyAction(null); } }, [activeRow, busy, cacheSavedRow, canManage, dirty, draft, formSource, markStale, queryClient, stale]); const remove = useCallback(async () => { if (!activeRow?.configured || !formSource?.id || !canManage || busy || stale) return; setBusyAction("delete"); try { await deleteCatalogDatabase(formSource.id, formSource.version); queryClient.setQueryData(DATABASE_QUERY_KEY, (current = []) => { if (!activeRow.workspaceAvailable) { return current.filter((row) => row.workspaceId !== activeRow.workspaceId); } return current.map((row) => row.workspaceId === activeRow.workspaceId ? { ...row, id: undefined, configured: false, version: 0, createdAt: "", updatedAt: "", connectionStatus: "untested", testedVersion: undefined, lastTestedAt: undefined, lastErrorCode: undefined, lastErrorMessage: undefined, secrets: { password: false, apiKey: false, sshPrivateKey: false, sshPrivateKeyPassphrase: false, sshKnownHosts: false, tlsCa: false, }, } : row); }); setBusyAction(null); showList(); await queryClient.invalidateQueries({ queryKey: DATABASE_QUERY_KEY }); toast.success("Database configuration deleted"); } catch (error) { if (isStaleError(error)) markStale(); else toast.error(apiErrorMessage(error)); } finally { setBusyAction(null); } }, [activeRow, busy, canManage, formSource, markStale, queryClient, showList, stale]); const reloadLatest = useCallback(async () => { if (busy || screen.kind === "list") return; setBusyAction("reload"); try { const result = await refetch(); if (result.error) throw result.error; const latest = result.data?.find((row) => row.workspaceId === screen.workspaceId); if (!latest) { showList(); toast.info("This database configuration is no longer available"); return; } const nextDraft = draftFrom(latest); setDraft(nextDraft); setBaseline(configurationFingerprint(nextDraft)); setFormSource({ id: latest.id, version: latest.version }); setStale(false); setStaleBannerOpen(true); setPartialSecretFailure(null); if (!latest.configured && screen.kind === "delete") { showList(); toast.info("This database configuration has already been deleted"); } else if (screen.kind === "edit" && !latest.configured) { setScreen({ kind: "configure", workspaceId: latest.workspaceId }); } } catch (error) { toast.error(apiErrorMessage(error)); } finally { setBusyAction(null); } }, [busy, refetch, screen, showList]); const refreshList = useCallback(async () => { if (isFetching) return; try { const result = await refetch(); if (result.error) throw result.error; await invalidateCatalogMetrics(); } catch (error) { toast.error(apiErrorMessage(error)); } }, [invalidateCatalogMetrics, isFetching, refetch]); const updateTrackedSyncRun = useCallback((run: CatalogSyncRun) => { setActiveSyncRun(run); queryClient.setQueryData(DATABASE_QUERY_KEY, (current = []) => current.map((row) => ( row.id === run.databaseId ? { ...row, activeSyncRun: ["queued", "running", "awaiting_confirmation", "applying"].includes(run.state) ? run : undefined } : row ))); }, [queryClient]); const rememberSyncRun = useCallback((run: CatalogSyncRun) => { updateTrackedSyncRun(run); setSyncHistoryDatabaseId(run.databaseId); setSyncDrawerOpen(true); }, [updateTrackedSyncRun]); const rememberDescriptionGenerationRun = useCallback((run: DescriptionGenerationRun) => { setActiveDescriptionGenerationRun(run); queryClient.setQueryData( DESCRIPTION_GENERATION_HISTORY_QUERY_KEY, (current = []) => [run, ...current.filter((item) => item.id !== run.id)], ); setDescriptionGenerationDrawerOpen(true); }, [queryClient]); const openDescriptionGenerationHistory = useCallback(() => { const scopedRuns = activeRow ? descriptionGenerationRuns.filter((item) => item.databaseId === activeRow.id) : descriptionGenerationRuns; const scopedActiveRun = observedActiveDescriptionGenerationRun && (!activeRow || observedActiveDescriptionGenerationRun.databaseId === activeRow.id) ? observedActiveDescriptionGenerationRun : undefined; const scopedSelectedRun = activeDescriptionGenerationRun && (!activeRow || activeDescriptionGenerationRun.databaseId === activeRow.id) ? activeDescriptionGenerationRun : undefined; const run = scopedActiveRun ?? scopedRuns[0] ?? scopedSelectedRun; if (run) setActiveDescriptionGenerationRun(run); setSensitiveDataSuggestionHistoryDrawerOpen(false); setDescriptionGenerationDrawerOpen(true); }, [activeDescriptionGenerationRun, activeRow, descriptionGenerationRuns, observedActiveDescriptionGenerationRun]); const openSensitiveDataSuggestionHistory = useCallback(() => { const scopedRuns = activeRow ? sensitiveDataSuggestionRuns.filter((item) => item.databaseId === activeRow.id) : sensitiveDataSuggestionRuns; const scopedActiveRun = observedActiveSensitiveDataSuggestionRun && (!activeRow || observedActiveSensitiveDataSuggestionRun.databaseId === activeRow.id) ? observedActiveSensitiveDataSuggestionRun : undefined; const scopedSelectedRun = activeSensitiveDataSuggestionRun && (!activeRow || activeSensitiveDataSuggestionRun.databaseId === activeRow.id) ? activeSensitiveDataSuggestionRun : undefined; const run = scopedActiveRun ?? scopedRuns[0] ?? scopedSelectedRun; if (run) setActiveSensitiveDataSuggestionRun(run); setDescriptionGenerationDrawerOpen(false); setSensitiveDataSuggestionHistoryDrawerOpen(true); }, [activeRow, activeSensitiveDataSuggestionRun, observedActiveSensitiveDataSuggestionRun, sensitiveDataSuggestionRuns]); const descriptionGenerationTerminated = useCallback(async (run: DescriptionGenerationRun) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["catalog-tables", run.databaseId], exact: true, }), queryClient.invalidateQueries({ queryKey: ["catalog-columns", run.databaseId], }), invalidateCatalogMetrics(), ]); }, [invalidateCatalogMetrics, queryClient]); const openSync = useCallback((row?: CatalogDatabase) => { const databaseId = row?.id ?? activeSyncRun?.databaseId ?? activeRow?.id ?? null; if (!databaseId) return; const run = row?.activeSyncRun ?? (activeSyncRun?.databaseId === databaseId ? activeSyncRun : null); setActiveSyncRun(run); setSyncHistoryDatabaseId(databaseId); setSyncDrawerOpen(true); }, [activeRow?.id, activeSyncRun]); const testSelected = useCallback(async (selected: CatalogDatabase[]) => { try { const tested = await Promise.all(selected.map((row) => testCatalogDatabase(row.id!, row.version))); for (const row of tested) cacheSavedRow(row); await queryClient.invalidateQueries({ queryKey: DATABASE_QUERY_KEY }); toast.success(`${tested.length} connection${tested.length === 1 ? "" : "s"} tested`); } catch (error) { toast.error(apiErrorMessage(error)); throw error; } }, [cacheSavedRow, queryClient]); const syncSelected = useCallback(async (selected: CatalogDatabase[], scope: CatalogSyncScope) => { try { const runs = await Promise.all(selected.map((row) => startCatalogSync(row.id!, row.version, scope))); queryClient.setQueryData(DATABASE_QUERY_KEY, (current = []) => current.map((row) => { const run = runs.find((candidate) => candidate.databaseId === row.id); return run ? { ...row, activeSyncRun: run } : row; })); if (runs[0]) rememberSyncRun(runs[0]); toast.success(`${runs.length} schema synchronization${runs.length === 1 ? "" : "s"} started`); } catch (error) { toast.error(apiErrorMessage(error)); throw error; } }, [queryClient, rememberSyncRun]); const generateDatabaseDescriptions = useCallback(async ( selected: CatalogDatabase[], scope: "all" | "missing", ) => { const database = selected.length === 1 ? selected[0] : undefined; if (!database?.id || !selectedMetadataModel) return; try { const run = await startDescriptionGenerationRun( database.id, selectedMetadataModel, scope, ); rememberDescriptionGenerationRun(run); toast.success(`${scope === "all" ? "Generate all descriptions" : "Generate missing descriptions"} started for ${database.workspaceName}`); } catch (error) { toast.error(apiErrorMessage(error)); throw error; } }, [rememberDescriptionGenerationRun, selectedMetadataModel]); const requestSensitiveSuggestions = useCallback(async ( database: CatalogDatabase, selection: SensitiveDataSuggestionRequest, scopeLabel: string, ) => { if (!database.id) { toast.error("The selected database is not configured, so sensitive-field suggestions were not requested."); throw new Error("database is not configured"); } if (!selectedMetadataModel) { toast.error("Select a metadata-generation model before requesting sensitive-field suggestions."); throw new Error("metadata-generation model is not selected"); } try { const result = await suggestSensitiveFields(database.id, selectedMetadataModel, selection); if (result.run) { setActiveSensitiveDataSuggestionRun(result.run); queryClient.setQueryData( SENSITIVE_DATA_SUGGESTION_HISTORY_QUERY_KEY, (current = []) => [result.run, ...current.filter((item) => item.id !== result.run.id)], ); } setSensitiveReview({ databaseId: database.id, scopeLabel, suggestions: result.suggestions }); toast.success(`Prepared ${result.suggestions.length} sensitive-field suggestion${result.suggestions.length === 1 ? "" : "s"} for review`); } catch (error) { toast.error(apiErrorMessage(error)); throw error; } finally { await queryClient.invalidateQueries({ queryKey: SENSITIVE_DATA_SUGGESTION_HISTORY_QUERY_KEY }); } }, [queryClient, selectedMetadataModel]); const suggestDatabaseSensitiveFields = useCallback(async (selected: CatalogDatabase[]) => { if (selected.length !== 1) { toast.error("Sensitive-field suggestions can be requested for only one database at a time. Select one database and try again."); throw new Error("more than one database selected"); } const database = selected[0]!; await requestSensitiveSuggestions(database, { scope: "all" }, `Entire database ${database.workspaceName}`); }, [requestSensitiveSuggestions]); const suggestActiveDatabaseSensitiveFields = useCallback(async ( selection: SensitiveDataSuggestionRequest, scopeLabel: string, ) => { if (!activeRow) { toast.error("The database is no longer available, so sensitive-field suggestions were not requested."); throw new Error("database is no longer available"); } await requestSensitiveSuggestions(activeRow, selection, scopeLabel); }, [activeRow, requestSensitiveSuggestions]); const sensitiveColumnsSaved = useCallback((columns: CatalogColumn[]) => { const savedById = new Map(columns.map((column) => [column.id, column])); setSensitiveReview((current) => current ? { ...current, suggestions: current.suggestions.map((suggestion) => { const saved = savedById.get(suggestion.columnId); return saved ? { ...suggestion, version: saved.version, currentSensitive: saved.sensitive, sensitive: saved.sensitive, } : suggestion; }), } : null); const databaseId = sensitiveReview?.databaseId; if (databaseId) { void Promise.all([ queryClient.invalidateQueries({ queryKey: ["catalog-columns", databaseId] }), invalidateCatalogMetrics(), ]); } }, [invalidateCatalogMetrics, queryClient, sensitiveReview?.databaseId]); const deleteSelectedMetadata = useCallback(async ( selected: CatalogDatabase[], target: CatalogDatabaseMetadataDeleteTarget, ) => { try { const databaseIds = selected.map((row) => row.id!); const counts = await deleteCatalogDatabaseMetadata(databaseIds, target); await Promise.all(databaseIds.flatMap((databaseId) => [ queryClient.invalidateQueries({ queryKey: ["catalog-tables", databaseId] }), queryClient.invalidateQueries({ queryKey: ["catalog-columns", databaseId] }), queryClient.invalidateQueries({ queryKey: ["catalog-relationships", databaseId] }), ])); await Promise.all([ queryClient.invalidateQueries({ queryKey: DATABASE_QUERY_KEY }), invalidateCatalogMetrics(), ]); toast.success(target === "tables" ? `Cleared ${counts.tables} catalog tables, ${counts.columns} columns, and ${counts.relationships} relationships. The source database was not changed.` : `Cleared ${counts.relationships} catalog relationships. The source database was not changed.`); } catch (error) { toast.error(apiErrorMessage(error)); throw error; } }, [invalidateCatalogMetrics, queryClient]); const clearActiveCatalogTables = useCallback(async () => { if (!activeRow?.id) return; await deleteSelectedMetadata([activeRow], "tables"); }, [activeRow, deleteSelectedMetadata]); const syncDatabase = useCallback(async (scope: CatalogSyncScope) => { if (!activeRow?.id || !activeRow.configured) return; try { rememberSyncRun(await startCatalogSync(activeRow.id, activeRow.version, scope)); toast.success(SYNC_STARTED_MESSAGES[scope]); } catch (error) { toast.error(apiErrorMessage(error)); } }, [activeRow, rememberSyncRun]); const catalogChanged = useCallback(async () => { const databaseId = activeSyncRun?.databaseId; if (databaseId) { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["catalog-tables", databaseId] }), queryClient.invalidateQueries({ queryKey: ["catalog-relationships", databaseId] }), queryClient.invalidateQueries({ queryKey: ["catalog-sync-runs", databaseId] }), ]); } await Promise.all([ queryClient.invalidateQueries({ queryKey: DATABASE_QUERY_KEY }), invalidateCatalogMetrics(), ]); }, [activeSyncRun, invalidateCatalogMetrics, queryClient]); const formVisible = screen.kind !== "list" && screen.kind !== "tables" && screen.kind !== "relationships" && activeRow && draft; const tablesVisible = screen.kind === "tables" && activeRow?.configured && activeRow.id; const relationshipsVisible = screen.kind === "relationships" && activeRow?.configured && activeRow.id; const currentActiveRun = activeSyncRun && activeSyncRun.databaseId === activeRow?.id && ["queued", "running", "awaiting_confirmation", "applying"].includes(activeSyncRun.state) ? activeSyncRun : activeRow?.activeSyncRun && ["queued", "running", "awaiting_confirmation", "applying"].includes(activeRow.activeSyncRun.state) ? activeRow.activeSyncRun : undefined; const selectedMetadataModelAvailable = Boolean( selectedMetadataModel && metadataModels.some((model) => model.id === selectedMetadataModel), ); const descriptionGenerationActive = isDescriptionGenerationActive( observedActiveDescriptionGenerationRun ?? activeDescriptionGenerationRun, ); const fleetMetadataModelControl = ( ); const metrics = catalogMetricsQuery.data; const metricsUnavailable = catalogMetricsQuery.isError; const metricsValue = (value?: number) => ( catalogMetricsQuery.isLoading ? "…" : metricsUnavailable || value === undefined ? "—" : value.toLocaleString("en-US") ); const latestCatalogUpdate = metrics?.updatedAt ? new Date(metrics.updatedAt).toLocaleString() : "No synchronized metadata"; const operationLabel = currentActiveRun ? `Synchronization: ${currentActiveRun.phase.replaceAll("_", " ")}` : descriptionGenerationActive ? "Description generation active" : observedActiveSensitiveDataSuggestionRun ? "Sensitive analysis active" : "Idle"; const operationTone = currentActiveRun || descriptionGenerationActive || observedActiveSensitiveDataSuggestionRun ? "warning" as const : "neutral" as const; const fleetBreadcrumb = screen.kind === "tables" || screen.kind === "relationships" ? [ { id: "databases", label: "Databases", onSelect: showList }, { id: "database", label: activeRow?.workspaceName ?? "Database" }, { id: screen.kind, label: screen.kind === "tables" ? "Tables" : "Relationships", current: true }, ] : [{ id: "databases", label: "Databases", current: true }]; const catalogDrawers = ( <> setSyncDrawerOpen(false)} onRunChange={rememberSyncRun} onRunUpdate={updateTrackedSyncRun} onCatalogChanged={() => void catalogChanged()} /> model.id === activeDescriptionGenerationRun?.modelId, )?.label ?? activeDescriptionGenerationRun?.modelId ?? ""} onClose={() => setDescriptionGenerationDrawerOpen(false)} onRunUpdate={setActiveDescriptionGenerationRun} onTerminal={(run) => void descriptionGenerationTerminated(run)} /> model.id === activeSensitiveDataSuggestionRun?.modelId, )?.label ?? activeSensitiveDataSuggestionRun?.modelId ?? ""} onClose={() => setSensitiveDataSuggestionHistoryDrawerOpen(false)} onRunUpdate={setActiveSensitiveDataSuggestionRun} /> setSensitiveReview(null)} onSaved={sensitiveColumnsSaved} /> ); if (presentation === "fleet") { const fleetFormMode = formVisible ? screen.kind as DatabaseFormMode : null; const fleetDrawer = formVisible && fleetFormMode ? ( void save()} onTest={() => void testConnection()} onDelete={() => void remove()} onReloadLatest={() => void reloadLatest()} onKeepEditing={() => setStaleBannerOpen(false)} onRetrySecrets={() => void retrySecrets()} onOpenTables={(origin) => openTables(activeRow, origin)} onOpenRelationships={() => openRelationships(activeRow)} onSync={(scope) => void syncDatabase(scope)} onOpenSync={() => openSync(activeRow)} onOpenDescriptionHistory={openDescriptionGenerationHistory} onOpenSensitiveHistory={openSensitiveDataSuggestionHistory} activeSyncRun={currentActiveRun} /> ) : null; const fleetContent = screen.kind === "tables" && tablesVisible ? ( openOverview(activeRow)} onOpenRelationships={() => openRelationships(activeRow)} onNavigationStateChange={setTablesNavigationState} onNestedNavigationChange={handleTableNestedNavigationChange} onRunStarted={rememberSyncRun} onOpenSync={() => openSync(activeRow)} onDescriptionGenerationRunStarted={rememberDescriptionGenerationRun} onSuggestSensitive={suggestActiveDatabaseSensitiveFields} onClearCatalogTables={clearActiveCatalogTables} onCatalogMetricsChanged={invalidateCatalogMetrics} /> ) : screen.kind === "relationships" && relationshipsVisible ? ( openOverview(activeRow)} onOpenTables={() => openTables(activeRow)} onRunStarted={rememberSyncRun} onOpenSync={() => openSync(activeRow)} onOpenDescriptionHistory={openDescriptionGenerationHistory} onOpenSensitiveHistory={openSensitiveDataSuggestionHistory} onCatalogMetricsChanged={invalidateCatalogMetrics} onNavigationStateChange={setTablesNavigationState} /> ) : isError && rows.length === 0 ? (

The database catalog is unavailable.

Verify the catalog service and database migrations, then try again.

) : ( openTables(row, origin)} onOpenRelationships={(row, origin) => openRelationships(row, origin)} onEdit={editRow} onDelete={deleteRow} onOpenSync={openSync} onSelectionChange={(selected) => setSelectedDatabaseId(selected.length === 1 ? selected[0].id ?? null : null)} onTestSelected={testSelected} onSyncSelected={syncSelected} selectedMetadataModel={selectedMetadataModelAvailable ? selectedMetadataModel : null} descriptionGenerationActive={descriptionGenerationActive} onGenerateDescriptions={generateDatabaseDescriptions} onSuggestSensitive={suggestDatabaseSensitiveFields} sensitiveDataSuggestionRuns={sensitiveDataSuggestionRuns} onDeleteMetadataSelected={deleteSelectedMetadata} onRefresh={refreshList} /> ); return ( <>
)} actions={( <> {fleetMetadataModelControl} )} /> )} kpis={( )} navigation={( tableNestedNavigationBack?.(), disabled: navigationBusy || !tableNestedNavigationBack, } : screen.kind === "tables" ? { label: "Back to databases", onBack: showList, disabled: navigationBusy } : undefined} /> )} children={{fleetContent}} drawer={( <> {fleetDrawer} {catalogDrawers} )} />
); } return (

Administration

Database management

One database configuration for each repository workspace.

{screen.kind === "list" ? <> : null}
{screen.kind === "list" ? (
) : null}
{isError && rows.length === 0 ? (

The database catalog is unavailable.

Verify the catalog service and database migrations, then try again.

) : ( setSelectedDatabaseId(selected.length === 1 ? selected[0].id ?? null : null)} onTestSelected={testSelected} onSyncSelected={syncSelected} selectedMetadataModel={selectedMetadataModelAvailable ? selectedMetadataModel : null} descriptionGenerationActive={descriptionGenerationActive} sensitiveDataSuggestionRuns={sensitiveDataSuggestionRuns} onGenerateDescriptions={generateDatabaseDescriptions} onSuggestSensitive={suggestDatabaseSensitiveFields} onDeleteMetadataSelected={deleteSelectedMetadata} /> )}
{formVisible ? ( void save()} onTest={() => void testConnection()} onDelete={() => void remove()} onReloadLatest={() => void reloadLatest()} onKeepEditing={() => setStaleBannerOpen(false)} onRetrySecrets={() => void retrySecrets()} onOpenTables={(origin) => openTables(activeRow, origin)} onOpenRelationships={() => openRelationships(activeRow)} onSync={(scope) => void syncDatabase(scope)} onOpenSync={() => openSync(activeRow)} onOpenDescriptionHistory={openDescriptionGenerationHistory} onOpenSensitiveHistory={openSensitiveDataSuggestionHistory} activeSyncRun={currentActiveRun} /> ) : null} {tablesVisible ? ( openOverview(activeRow)} onOpenRelationships={() => openRelationships(activeRow)} onNavigationStateChange={setTablesNavigationState} onRunStarted={rememberSyncRun} onOpenSync={() => openSync(activeRow)} onDescriptionGenerationRunStarted={rememberDescriptionGenerationRun} onSuggestSensitive={suggestActiveDatabaseSensitiveFields} onClearCatalogTables={clearActiveCatalogTables} onCatalogMetricsChanged={invalidateCatalogMetrics} /> ) : null} {relationshipsVisible ? ( openOverview(activeRow)} onOpenTables={() => openTables(activeRow)} onRunStarted={rememberSyncRun} onOpenSync={() => openSync(activeRow)} onOpenDescriptionHistory={openDescriptionGenerationHistory} onOpenSensitiveHistory={openSensitiveDataSuggestionHistory} onCatalogMetricsChanged={invalidateCatalogMetrics} onNavigationStateChange={setTablesNavigationState} /> ) : null}
setSyncDrawerOpen(false)} onRunChange={rememberSyncRun} onRunUpdate={updateTrackedSyncRun} onCatalogChanged={() => void catalogChanged()} /> model.id === activeDescriptionGenerationRun?.modelId, )?.label ?? activeDescriptionGenerationRun?.modelId ?? ""} onClose={() => setDescriptionGenerationDrawerOpen(false)} onRunUpdate={setActiveDescriptionGenerationRun} onTerminal={(run) => void descriptionGenerationTerminated(run)} /> model.id === activeSensitiveDataSuggestionRun?.modelId, )?.label ?? activeSensitiveDataSuggestionRun?.modelId ?? ""} onClose={() => setSensitiveDataSuggestionHistoryDrawerOpen(false)} onRunUpdate={setActiveSensitiveDataSuggestionRun} /> setSensitiveReview(null)} onSaved={sensitiveColumnsSaved} />
); }