"use client"; /** * Inline tree view of tables inside a Coolify-managed database. * Mirrors the pattern of GiteaFileTree: lazy-fetched on first mount, * one-level deep, click a table to "preview" it on the right. * * Tables are grouped by schema. The default `public` schema is rendered * flat (no schema header) since 95% of small projects only have one * schema and the extra heading is just noise. */ import { useEffect, useState } from "react"; import { Table, ChevronDown, ChevronRight, Loader2, AlertCircle, Info, Database, } from "lucide-react"; import { THEME } from "@/components/project/dashboard-ui"; interface IntrospectedTable { schema: string; name: string; approxRows?: number; } interface ApiResp { engine: string; tables: IntrospectedTable[]; unsupported?: boolean; message?: string; } interface Props { projectId: string; dbUuid: string; selectedTable?: { schema: string; name: string }; onSelectTable: (t: { schema: string; name: string }) => void; } export function DatabaseTableTree({ projectId, dbUuid, selectedTable, onSelectTable, }: Props) { const [data, setData] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { let cancelled = false; const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), 12_000); setLoading(true); setError(null); fetch(`/api/projects/${projectId}/databases/${dbUuid}/tables`, { credentials: "include", signal: ctrl.signal, }) .then(async (r) => { let body: unknown = {}; try { body = await r.json(); } catch { /* keep {} */ } if (!r.ok) { throw new Error( (body as { error?: string }).error || `HTTP ${r.status}`, ); } return body as ApiResp; }) .then((d) => { if (!cancelled) setData(d); }) .catch((err) => { if (cancelled) return; if (err?.name === "AbortError") setError("Timed out after 12s."); else setError(err?.message || "Failed to list tables"); }) .finally(() => { clearTimeout(t); if (!cancelled) setLoading(false); }); return () => { cancelled = true; ctrl.abort(); clearTimeout(t); }; }, [projectId, dbUuid]); if (loading) { return (
Inspecting database…
); } if (error) { return (
{error}
); } if (!data) return null; if (data.unsupported) { return (
Table inspection isn't wired up for {data.engine} yet — Postgres is the only engine supported today.
); } if (data.tables.length === 0) { return (
No user-tables found. (Pre-deploy databases often start empty.)
); } // Group by schema; flatten `public` since most projects only use it. const bySchema = new Map(); for (const t of data.tables) { if (!bySchema.has(t.schema)) bySchema.set(t.schema, []); bySchema.get(t.schema)!.push(t); } const schemas = [...bySchema.keys()].sort(); return (
{schemas.map((schema) => ( ))}
); } function SchemaGroup({ schema, tables, selectedTable, onSelectTable, }: { schema: string; tables: IntrospectedTable[]; selectedTable?: { schema: string; name: string }; onSelectTable: (t: { schema: string; name: string }) => void; }) { const isPublic = schema === "public"; const [open, setOpen] = useState(true); const items = (
    {tables.map((t) => { const active = selectedTable?.schema === t.schema && selectedTable?.name === t.name; return (