import { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { History, Plus, RefreshCw } from "lucide-react"; import { toast } from "sonner"; import { Button } from "../components/ui/button"; import { ApiError, apiErrorMessage } from "../api/client"; import { createCatalogDatabase, deleteCatalogDatabase, deleteCatalogDatabaseMetadata, listCatalogDatabases, listDescriptionGenerationRuns, listMetadataGenerationModels, replaceCatalogDatabaseSecrets, startCatalogSync, startDescriptionGenerationRun, testCatalogDatabase, updateCatalogDatabase, type CatalogDatabase, type CatalogDatabaseMetadataDeleteTarget, type CatalogSecretName, type CatalogSyncScope, type CatalogSyncRun, type DatabaseBinding, type DatabaseTransport, type DescriptionGenerationRun, } from "../api/catalog-databases"; import { DatabaseGrid } from "./database-management/DatabaseGrid"; import { DatabaseForm } 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 { 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 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", }; interface Props { canManage: boolean; canManageSecrets: boolean; onNavigationStateChange?: (state: DatabaseNavigationState) => void; } 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, }: 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 [screen, setScreen] = useState({ kind: "list" }); const [selectedMetadataModel, setSelectedMetadataModel] = useState(""); const [draft, setDraft] = useState(null); const [baseline, setBaseline] = useState(""); const [formSource, setFormSource] = useState(null); const [search, setSearch] = useState(""); 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 [activeSyncRun, setActiveSyncRun] = useState(null); const [syncDrawerOpen, setSyncDrawerOpen] = useState(false); const [activeDescriptionGenerationRun, setActiveDescriptionGenerationRun] = useState(null); const [descriptionGenerationDrawerOpen, setDescriptionGenerationDrawerOpen] = useState(false); 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 availableWorkspaces = useMemo( () => rows.filter((row) => row.workspaceAvailable && !row.configured), [rows], ); const editable = screen.kind === "add" || screen.kind === "edit"; const configurationDirty = Boolean( editable && draft && configurationFingerprint(draft) !== baseline, ); const dirty = Boolean(editable && draft && (configurationDirty || hasSecretChanges(draft))); const busy = busyAction !== null; const navigationDirty = screen.kind === "tables" ? tablesNavigationState.dirty : dirty; const navigationBusy = screen.kind === "tables" ? 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(() => { window.setTimeout(() => { if (originRef.current?.isConnected) { originRef.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 }); 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, workspaceLocked = false, ) => { 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, ...(mode === "add" ? { workspaceLocked } : {}), }); }, []); const viewRow = useCallback((row: CatalogDatabase, origin: HTMLButtonElement) => { openForm("view", row, origin); }, [openForm]); const editRow = useCallback((row: CatalogDatabase, origin: HTMLButtonElement) => { openForm(row.configured ? "edit" : "add", row, origin, !row.configured); }, [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 }); setScreen({ kind: "tables", workspaceId: row.workspaceId }); }, []); const openRelationships = useCallback((row: CatalogDatabase) => { if (!row.configured || !row.id) return; setDraft(null); setBaseline(""); setFormSource(null); setStale(false); setPartialSecretFailure(null); setTablesNavigationState({ dirty: false, busy: false }); 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 addDatabase = useCallback((origin: HTMLElement) => { const workspace = availableWorkspaces[0]; if (!workspace || !canManage) return; openForm("add", workspace, origin, false); }, [availableWorkspaces, canManage, openForm]); const changeWorkspace = useCallback((workspaceId: string) => { const workspace = availableWorkspaces.find((row) => row.workspaceId === workspaceId); if (!workspace) return; if (dirty && !window.confirm("Discard changes and choose another workspace?")) return; const nextDraft = draftFrom(workspace); setDraft(nextDraft); setBaseline(configurationFingerprint(nextDraft)); setFormSource({ id: workspace.id, version: workspace.version }); setStale(false); setPartialSecretFailure(null); setScreen({ kind: "add", workspaceId, workspaceLocked: false }); }, [availableWorkspaces, dirty]); 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: "add", workspaceId: latest.workspaceId, workspaceLocked: true }); } } 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; } catch (error) { toast.error(apiErrorMessage(error)); } }, [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); 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 run = observedActiveDescriptionGenerationRun ?? descriptionGenerationRuns[0] ?? activeDescriptionGenerationRun; if (!run) return; setActiveDescriptionGenerationRun(run); setDescriptionGenerationDrawerOpen(true); }, [activeDescriptionGenerationRun, descriptionGenerationRuns, observedActiveDescriptionGenerationRun]); 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], }), ]); }, [queryClient]); const openSync = useCallback((row?: CatalogDatabase) => { const run = row?.activeSyncRun ?? activeSyncRun; if (!run) return; setActiveSyncRun(run); setSyncDrawerOpen(true); }, [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" : "Generate Missing"} started for ${database.workspaceName}`); } catch (error) { toast.error(apiErrorMessage(error)); throw error; } }, [rememberDescriptionGenerationRun, selectedMetadataModel]); 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 queryClient.invalidateQueries({ queryKey: DATABASE_QUERY_KEY }); toast.success(target === "tables" ? `Deleted ${counts.tables} catalog tables, ${counts.columns} columns, and ${counts.relationships} relationships` : `Deleted ${counts.relationships} catalog relationships`); } catch (error) { toast.error(apiErrorMessage(error)); throw error; } }, [queryClient]); 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 queryClient.invalidateQueries({ queryKey: DATABASE_QUERY_KEY }); }, [activeSyncRun, 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 && ["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 descriptionGenerationHistoryAvailable = Boolean( descriptionGenerationRuns.length > 0 || activeDescriptionGenerationRun, ); return (

Administration

Database management

One database configuration for each repository workspace.

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

The database catalog is unavailable.

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

) : ( )}
{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)} activeSyncRun={currentActiveRun} /> ) : null} {tablesVisible ? ( openOverview(activeRow)} onOpenRelationships={() => openRelationships(activeRow)} onNavigationStateChange={setTablesNavigationState} onRunStarted={rememberSyncRun} onOpenSync={() => openSync(activeRow)} onDescriptionGenerationRunStarted={rememberDescriptionGenerationRun} /> ) : null} {relationshipsVisible ? ( openOverview(activeRow)} onOpenTables={() => openTables(activeRow)} onRunStarted={rememberSyncRun} onOpenSync={() => openSync(activeRow)} /> ) : 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)} />
); }