import { IconDatabase, IconLoader2 } from "@tabler/icons-react"; /** * Database admin page — the shell that hosts the table browser, the table * editor, and the SQL editor. * * By default this is gated to Code mode for the core dev route. Trusted hosts * can opt out and point it at their own admin-gated API path. */ import { useEffect, useMemo, useState } from "react"; import type { DbAdminFilter } from "../../db-admin/types.js"; import { useCodeMode } from "../use-dev-mode.js"; import { cn } from "../utils.js"; import { SqlEditor } from "./SqlEditor.js"; import { TableBrowser } from "./TableBrowser.js"; import { TableEditor } from "./TableEditor.js"; import { useDbAdminAgentSync, useNavigateConsumer } from "./useAgentSync.js"; import { useOverview, type DbAdminRequestConfig } from "./useDbAdmin.js"; const DIALECT_LABEL: Record = { postgres: "Postgres", sqlite: "SQLite", d1: "Cloudflare D1", }; export interface DbAdminPageProps { apiBasePath?: string; cacheScope?: string; title?: string; subtitle?: string; codeModeGate?: boolean; syncNavigation?: boolean; } export function DbAdminPage({ apiBasePath, cacheScope, title = "Database", subtitle, codeModeGate = true, syncNavigation = true, }: DbAdminPageProps = {}) { const { canToggle, isLoading: devLoading } = useCodeMode(); const requestConfig = useMemo(() => { if (!apiBasePath && !cacheScope) return undefined; return { basePath: apiBasePath, scopeKey: cacheScope }; }, [apiBasePath, cacheScope]); const { data: overview, isLoading: overviewLoading } = useOverview(requestConfig); const [selectedTable, setSelectedTable] = useState(null); const [mode, setMode] = useState<"table" | "sql">("table"); const [fkFilters, setFkFilters] = useState( undefined, ); const tables = overview?.tables ?? []; const dialect = overview?.dialect ?? "sqlite"; // Default selection to the first table once the overview loads. useEffect(() => { if (selectedTable === null && tables.length > 0) { setSelectedTable(tables[0].name); } }, [selectedTable, tables]); // Keep the agent's in sync, and let it drive navigation. useDbAdminAgentSync({ table: selectedTable, mode, enabled: syncNavigation }); useNavigateConsumer((table) => { setSelectedTable(table); setMode("table"); setFkFilters(undefined); }, syncNavigation); const tableNames = useMemo(() => tables.map((t) => t.name), [tables]); // SqlEditor degrades gracefully without per-table columns; pass an empty map. // (Table-name autocomplete still works; column autocomplete fills in lazily.) const columnsByTable = useMemo>(() => ({}), []); // ─── Code mode gate ────────────────────────────────────────────────────── if (codeModeGate && !devLoading && !canToggle) { return (

Code mode only

Database admin is available in Code mode only.

); } const showInitialLoading = ((codeModeGate && devLoading) || overviewLoading) && !overview; return (
{/* Header */}
{title} {DIALECT_LABEL[dialect] ?? dialect} {tables.length} {tables.length === 1 ? "table" : "tables"} {subtitle ? ( {subtitle} ) : null}
{/* Body: fixed sidebar + flexible main */}
{showInitialLoading ? ( ) : mode === "sql" ? ( ) : selectedTable ? ( { setSelectedTable(t); setMode("table"); setFkFilters(filters); }} /> ) : ( )}
); } function SidebarSkeleton() { return (
{Array.from({ length: 8 }).map((_, i) => (
))}
); } function MainLoading() { return (
); } function NoTableSelected() { return (

No table selected

Pick a table from the sidebar to browse and edit its rows.

); }