import { useEffect, useMemo, useRef, useState } from "react"; import { Menu } from "@base-ui/react/menu"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { AgGridReact } from "ag-grid-react"; import type { ColDef, ICellRendererParams } from "ag-grid-community"; import { ArrowLeft, ChevronDown, Columns3, History, Pencil, RefreshCw, Save, Sparkles, Trash2, X } from "lucide-react"; import { toast } from "sonner"; import { Button } from "../../components/ui/button"; import { ApiError, apiErrorMessage } from "../../api/client"; import { consolidateCatalogDescriptions, deleteCatalogTableMetadata, listCatalogTables, startCatalogSync, startDescriptionGenerationRun, updateCatalogTableMetadata, type CatalogDatabase, type CatalogSyncRun, type CatalogTable, type CatalogTableMetadataDeleteTarget, type DescriptionGenerationRun, type SensitiveDataSuggestionRequest, } from "../../api/catalog-databases"; import type { DatabaseNavigationState } from "./model"; import { DatabaseColumns } from "./DatabaseColumns"; import { FleetActionSelector, type FleetActionOption } from "./FleetActionSelector"; import { FleetLedgerDrawer } from "./FleetLedgerShell"; import { NO_METADATA_GENERATION_LLM_MODEL_MESSAGE } from "./MetadataGenerationModelSelector"; interface Props { database: CatalogDatabase; canManage: boolean; activeRun?: CatalogSyncRun; selectedMetadataModel: string | null; descriptionGenerationActive: boolean; onBackToDatabases: () => void; onOpenOverview: () => void; onOpenRelationships: () => void; onNavigationStateChange: (state: DatabaseNavigationState) => void; onNestedNavigationChange?: (active: boolean) => void; onRunStarted: (run: CatalogSyncRun) => void; onOpenSync: () => void; onOpenDescriptionHistory?: () => void; onOpenSensitiveHistory?: () => void; onDescriptionGenerationRunStarted: (run: DescriptionGenerationRun) => void; onSuggestSensitive: (selection: SensitiveDataSuggestionRequest, scopeLabel: string) => Promise; onCatalogMetricsChanged?: () => void | Promise; presentation?: "legacy" | "fleet"; } interface TableGridContext { canManage: boolean; presentation: "legacy" | "fleet"; onOpen: (table: CatalogTable, section: "overview" | "columns", origin: HTMLButtonElement) => void; } function TableActionsCell({ data, context }: ICellRendererParams) { if (!data || !context) return null; return (
event.stopPropagation()}>
); } export function DatabaseTables({ database, canManage, activeRun, selectedMetadataModel, descriptionGenerationActive, onBackToDatabases, onOpenOverview, onOpenRelationships, onNavigationStateChange, onNestedNavigationChange, onRunStarted, onOpenSync, onOpenDescriptionHistory, onOpenSensitiveHistory, onDescriptionGenerationRunStarted, onSuggestSensitive, onCatalogMetricsChanged, presentation = "legacy", }: Props) { const databaseId = database.id!; const queryClient = useQueryClient(); const queryKey = ["catalog-tables", databaseId] as const; const { data = [], error, isError, isLoading, isFetching, refetch } = useQuery({ queryKey, queryFn: () => listCatalogTables(databaseId), retry: false }); const [search, setSearch] = useState(""); const [displayedCount, setDisplayedCount] = useState(0); const [selectedIds, setSelectedIds] = useState([]); const [activeTableId, setActiveTableId] = useState(null); const [tableSection, setTableSection] = useState<"overview" | "columns">("overview"); const [description, setDescription] = useState(""); const [generatedDescription, setGeneratedDescription] = useState(""); const [baseline, setBaseline] = useState(""); const [editorVersion, setEditorVersion] = useState(null); const [stale, setStale] = useState(false); const [staleBannerOpen, setStaleBannerOpen] = useState(true); const [busy, setBusy] = useState<"sync" | "save" | "delete" | "consolidate" | "generate" | "suggest" | null>(null); const [pendingDelete, setPendingDelete] = useState(null); const [columnNavigation, setColumnNavigation] = useState({ dirty: false, busy: false }); const gridRef = useRef>(null); const originRef = useRef(null); const headingRef = useRef(null); const actionsTriggerRef = useRef(null); const fleetActionSelectRef = useRef(null); const searchInputRef = useRef(null); const activeTable = activeTableId ? data.find((table) => table.id === activeTableId) : undefined; const editorFingerprint = JSON.stringify([description, generatedDescription]); const editorDirty = Boolean(activeTableId && editorFingerprint !== baseline); const dirty = tableSection === "columns" ? columnNavigation.dirty : editorDirty; const navigationBusy = busy !== null || (tableSection === "columns" && columnNavigation.busy); const nestedNavigationActive = presentation === "fleet" && Boolean(activeTable) && tableSection === "columns"; const bindingReady = database.connectionStatus === "reachable" && database.testedVersion === database.version; const currentRun = activeRun ?? database.activeSyncRun; useEffect(() => { onNavigationStateChange({ dirty, busy: navigationBusy }); }, [dirty, navigationBusy, onNavigationStateChange]); useEffect(() => { onNestedNavigationChange?.(nestedNavigationActive); }, [nestedNavigationActive, onNestedNavigationChange]); useEffect(() => () => { onNestedNavigationChange?.(false); }, [onNestedNavigationChange]); useEffect(() => { if (!activeTableId || !activeTable || editorVersion === activeTable.version || busy) return; setStale(true); setStaleBannerOpen(true); }, [activeTable, activeTableId, busy, editorVersion]); useEffect(() => { if (!pendingDelete) return; const timer = window.setTimeout(() => document.getElementById("table-cleanup-confirm-button")?.focus(), 0); return () => window.clearTimeout(timer); }, [pendingDelete]); const closeDeleteConfirmation = () => { setPendingDelete(null); window.setTimeout(() => { if (presentation === "fleet") fleetActionSelectRef.current?.focus(); else actionsTriggerRef.current?.focus(); }, 0); }; const openTable = (table: CatalogTable, section: "overview" | "columns", origin: HTMLButtonElement) => { originRef.current = origin; setActiveTableId(table.id); setTableSection(section); setDescription(table.description ?? ""); setGeneratedDescription(table.generatedDescription ?? ""); setBaseline(JSON.stringify([table.description ?? "", table.generatedDescription ?? ""])); setEditorVersion(table.version); setStale(false); setStaleBannerOpen(true); setColumnNavigation({ dirty: false, busy: false }); }; const leaveTable = () => { if (navigationBusy) return; if (dirty && !window.confirm("Discard unsaved metadata?")) return; setActiveTableId(null); setTableSection("overview"); window.setTimeout(() => originRef.current?.focus(), 0); }; const navigateDatabase = (target: "databases" | "overview" | "relationships") => { if (navigationBusy) return; if (dirty && !window.confirm("Discard unsaved metadata?")) return; if (target === "databases") onBackToDatabases(); else if (target === "overview") onOpenOverview(); else onOpenRelationships(); }; const save = async () => { if (!activeTable || editorVersion === null) return; setBusy("save"); try { const updated = await updateCatalogTableMetadata( databaseId, activeTable.id, editorVersion, description.trim() || null, generatedDescription.trim() || null, ); queryClient.setQueryData(queryKey, (current = []) => current.map((table) => table.id === updated.id ? updated : table)); setDescription(updated.description ?? ""); setGeneratedDescription(updated.generatedDescription ?? ""); setBaseline(JSON.stringify([updated.description ?? "", updated.generatedDescription ?? ""])); setEditorVersion(updated.version); setStale(false); void onCatalogMetricsChanged?.(); toast.success("Table metadata saved"); } catch (error) { if (error instanceof ApiError && error.code === "table_stale") { await refetch(); setStale(true); setStaleBannerOpen(true); } else toast.error(apiErrorMessage(error)); } finally { setBusy(null); } }; const reloadTable = async () => { if (!activeTableId) return; setBusy("sync"); try { const result = await refetch(); const latest = result.data?.find((table) => table.id === activeTableId); if (!latest) { leaveTable(); return; } setDescription(latest.description ?? ""); setGeneratedDescription(latest.generatedDescription ?? ""); setBaseline(JSON.stringify([latest.description ?? "", latest.generatedDescription ?? ""])); setEditorVersion(latest.version); setStale(false); setStaleBannerOpen(true); } catch (error) { toast.error(apiErrorMessage(error)); } finally { setBusy(null); } }; const synchronize = async (scope: "tables" | "columns" | "all", tableIds: string[] = []) => { setBusy("sync"); try { const run = await startCatalogSync(databaseId, database.version, scope, tableIds); onRunStarted(run); gridRef.current?.api.deselectAll(); setSelectedIds([]); toast.success(scope === "all" ? "Full schema synchronization started" : scope === "columns" ? "Column synchronization started" : "Table synchronization started"); } catch (error) { toast.error(apiErrorMessage(error)); } finally { setBusy(null); } }; const deleteMetadata = async (target: CatalogTableMetadataDeleteTarget) => { setBusy("delete"); try { const counts = await deleteCatalogTableMetadata(databaseId, selectedIds, target); await Promise.all([ queryClient.invalidateQueries({ queryKey: ["catalog-columns", databaseId] }), queryClient.invalidateQueries({ queryKey: ["catalog-relationships", databaseId] }), queryClient.invalidateQueries({ queryKey: ["catalog-databases"] }), ]); gridRef.current?.api.deselectAll(); setSelectedIds([]); setPendingDelete(null); window.setTimeout(() => searchInputRef.current?.focus(), 0); void onCatalogMetricsChanged?.(); toast.success(target === "columns" ? `${presentation === "fleet" ? "Cleared" : "Deleted"} ${counts.columns} catalog columns` : `${presentation === "fleet" ? "Cleared" : "Deleted"} ${counts.relationships} catalog relationships`); } catch (error) { toast.error(apiErrorMessage(error)); } finally { setBusy(null); } }; const consolidateDescriptions = async () => { setBusy("consolidate"); try { const counts = await consolidateCatalogDescriptions(databaseId, "tables", selectedIds); await queryClient.invalidateQueries({ queryKey, exact: true }); gridRef.current?.api.deselectAll(); setSelectedIds([]); void onCatalogMetricsChanged?.(); toast.success(`Copied ${counts.copied} description${counts.copied === 1 ? "" : "s"}; skipped ${counts.skipped}`); } catch (error) { toast.error(apiErrorMessage(error)); } finally { setBusy(null); } }; const generateDescriptions = async () => { if (selectedIds.length === 0 || !selectedMetadataModel) return; setBusy("generate"); try { const run = await startDescriptionGenerationRun( databaseId, selectedMetadataModel, "selected_tables", selectedIds, ); onDescriptionGenerationRunStarted(run); toast.success(`Description generation started for ${selectedIds.length} table${selectedIds.length === 1 ? "" : "s"}`); } catch (error) { toast.error(apiErrorMessage(error)); } finally { setBusy(null); } }; const suggestSensitive = async () => { if (selectedIds.length === 0) return; setBusy("suggest"); try { await onSuggestSensitive( { scope: "selected_tables", targetIds: selectedIds }, `${selectedIds.length} selected table${selectedIds.length === 1 ? "" : "s"}`, ); } catch { // The page-level request reports the safe, specific reason and preserves the selection. } finally { setBusy(null); } }; type FleetTableAction = | "generate-descriptions" | "sync-tables" | "sync-columns" | "consolidate-descriptions" | "suggest-sensitive" | "clear-columns" | "clear-relationships"; const fleetActions: readonly FleetActionOption[] = [ { id: "generate-descriptions", label: "Generate descriptions", group: "Descriptions", runLabel: "Generate", disabled: !canManage || selectedIds.length === 0 || !selectedMetadataModel || descriptionGenerationActive || busy !== null || Boolean(currentRun), disabledReason: !canManage ? "You do not have permission to generate descriptions." : selectedIds.length === 0 ? "Select at least one table." : !selectedMetadataModel ? NO_METADATA_GENERATION_LLM_MODEL_MESSAGE : descriptionGenerationActive || Boolean(currentRun) ? "Wait for the active catalog operation to finish." : busy !== null ? "Another action is running." : undefined, }, { id: "consolidate-descriptions", label: "Move generated to Description", group: "Descriptions", runLabel: "Move", disabled: !canManage || selectedIds.length === 0 || busy !== null || Boolean(currentRun), disabledReason: !canManage ? "You do not have permission to update metadata." : selectedIds.length === 0 ? "Select at least one table." : busy !== null || Boolean(currentRun) ? "Wait for the active catalog operation to finish." : undefined, }, { id: "sync-tables", label: "Synchronize tables", group: "Synchronization", scopeLabel: "All tables in this database", runLabel: "Synchronize", disabled: !canManage || !bindingReady || busy !== null || Boolean(currentRun), disabledReason: !canManage ? "You do not have permission to synchronize the catalog." : !bindingReady ? "Test the current database binding first." : busy !== null || Boolean(currentRun) ? "Wait for the active catalog operation to finish." : undefined, }, { id: "sync-columns", label: "Synchronize columns", group: "Synchronization", runLabel: "Synchronize", disabled: !canManage || selectedIds.length === 0 || !bindingReady || busy !== null || Boolean(currentRun), disabledReason: !canManage ? "You do not have permission to synchronize the catalog." : selectedIds.length === 0 ? "Select at least one table." : !bindingReady ? "Test the current database binding first." : busy !== null || Boolean(currentRun) ? "Wait for the active catalog operation to finish." : undefined, }, { id: "suggest-sensitive", label: "Suggest sensitive fields", group: "Sensitive data", runLabel: "Suggest", disabled: !canManage || selectedIds.length === 0 || !selectedMetadataModel || descriptionGenerationActive || busy !== null || Boolean(currentRun), disabledReason: !canManage ? "You do not have permission to review sensitive data." : selectedIds.length === 0 ? "Select at least one table." : !selectedMetadataModel ? NO_METADATA_GENERATION_LLM_MODEL_MESSAGE : descriptionGenerationActive || Boolean(currentRun) ? "Wait for the active catalog operation to finish." : busy !== null ? "Another action is running." : undefined, }, { id: "clear-columns", label: "Clear catalog columns", group: "Catalog cleanup", tone: "destructive", runLabel: "Review cleanup", disabled: !canManage || selectedIds.length === 0 || busy !== null || Boolean(currentRun), disabledReason: !canManage ? "You do not have permission to clear catalog metadata." : selectedIds.length === 0 ? "Select at least one table." : busy !== null || Boolean(currentRun) ? "Wait for the active catalog operation to finish." : undefined, }, { id: "clear-relationships", label: "Clear catalog relationships", group: "Catalog cleanup", tone: "destructive", runLabel: "Review cleanup", disabled: !canManage || selectedIds.length === 0 || busy !== null || Boolean(currentRun), disabledReason: !canManage ? "You do not have permission to clear catalog metadata." : selectedIds.length === 0 ? "Select at least one table." : busy !== null || Boolean(currentRun) ? "Wait for the active catalog operation to finish." : undefined, }, ]; const runFleetAction = async (action: FleetTableAction) => { if (action === "generate-descriptions") await generateDescriptions(); else if (action === "sync-tables") await synchronize("tables"); else if (action === "consolidate-descriptions") await consolidateDescriptions(); else if (action === "sync-columns") await synchronize("columns", selectedIds); else if (action === "suggest-sensitive") await suggestSensitive(); else if (action === "clear-columns") setPendingDelete("columns"); else setPendingDelete("relationships"); }; const clearSelection = () => { gridRef.current?.api.deselectAll(); setSelectedIds([]); }; const columns = useMemo[]>(() => [ { field: "name", headerName: "Name", minWidth: 250, flex: 1, cellClass: "font-mono text-xs" }, { field: "sourceComment", headerName: "Source comment", minWidth: 260, flex: 1.25, valueFormatter: ({ value }) => value ?? "" }, { field: "generatedDescription", headerName: "Generated description", minWidth: 280, flex: 1.25, valueFormatter: ({ value }) => value ?? "" }, { field: "description", headerName: "Description", minWidth: 280, flex: 1.25, valueFormatter: ({ value }) => value ?? "" }, { colId: "actions", headerName: "Actions", width: 104, minWidth: 104, maxWidth: 104, pinned: "right", lockPinned: true, sortable: false, filter: false, resizable: false, suppressMovable: true, suppressHeaderMenuButton: true, cellRenderer: TableActionsCell }, ], []); const context = useMemo(() => ({ canManage, presentation, onOpen: openTable }), [canManage, data, presentation]); const visibleSelected = useMemo(() => { const term = search.trim().toLocaleLowerCase(); if (!term) return selectedIds.length; const selected = new Set(selectedIds); return data.filter((table) => selected.has(table.id) && [table.name, table.sourceComment, table.generatedDescription, table.description] .some((value) => value?.toLocaleLowerCase().includes(term))).length; }, [data, search, selectedIds]); const fleetQueryError = presentation === "fleet" && isError ? (

Catalog tables could not be loaded.

{apiErrorMessage(error)}

) : null; const databaseHeader = (

Database management

{database.workspaceName}

{database.databaseName} · {database.schema}

{currentRun ? : null}
); if (activeTable && presentation === "fleet") { if (tableSection === "columns") { return (

Physical table

{activeTable.name}

void synchronize("columns", [activeTable.id])} onSuggestSensitive={onSuggestSensitive} bindingReady={bindingReady} catalogOperationActive={Boolean(currentRun)} onCatalogMetricsChanged={onCatalogMetricsChanged} presentation="fleet" />
); } return (
Catalog tables setSearch(event.target.value)} /> {search.trim() ? `${displayedCount} of ${data.length}` : displayedCount}
{fleetQueryError ??
ref={gridRef} rowData={data} columnDefs={columns} context={context} loading={isLoading} quickFilterText={search} defaultColDef={{ sortable: true, filter: true, resizable: true }} getRowId={({ data: table }) => table.id} rowSelection={{ mode: "multiRow", selectAll: "filtered", enableClickSelection: false }} onSelectionChanged={({ api }) => setSelectedIds(api.getSelectedRows().map((table) => table.id))} onModelUpdated={({ api }) => setDisplayedCount(api.getDisplayedRowCount())} rowHeight={44} headerHeight={38} animateRows={false} overlayNoRowsTemplate="No catalog tables. Test the binding, then sync tables." />
} )} >

Physical table

{activeTable.name}

{stale ? ( staleBannerOpen ? (

A newer table description is available.

Your draft is preserved until you reload the current metadata.

) :
Reload the latest value before this description can be saved.
) : null}