"use client"; /** * Right-pane table preview. Shows the first ~50 rows of the selected * table as a compact data grid. Cells longer than 200 chars are * truncated server-side; we render the rest as-is. Read-only. */ import { useEffect, useState } from "react"; import { Loader2, AlertCircle, Info } from "lucide-react"; interface PreviewedTable { columns: string[]; rows: Array>; truncated: boolean; unsupported?: boolean; error?: string; } interface Props { projectId: string; dbUuid: string; schema: string; table: string; } export function TableViewer({ projectId, dbUuid, schema, table }: 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); setData(null); const url = `/api/projects/${projectId}/databases/${dbUuid}/preview` + `?schema=${encodeURIComponent(schema)}&table=${encodeURIComponent(table)}`; fetch(url, { 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 PreviewedTable; }) .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 load preview"); }) .finally(() => { clearTimeout(t); if (!cancelled) setLoading(false); }); return () => { cancelled = true; ctrl.abort(); clearTimeout(t); }; }, [projectId, dbUuid, schema, table]); if (loading) { return (
Querying {schema}.{table}…
); } if (error) { return (
{error}
); } if (!data) return null; if (data.rows.length === 0) { return (
Table is empty.
); } return (
Showing {data.rows.length} row{data.rows.length === 1 ? "" : "s"} {data.truncated && " (truncated to first 50)"} ·{" "} {data.columns.length} column{data.columns.length === 1 ? "" : "s"} ·{" "} {schema}.{table}
{data.columns.map(c => ( ))} {data.rows.map((row, i) => ( {data.columns.map(c => ( ))} ))}
{c}
{row[c]}
); } // ────────────────────────────────────────────────── const INK = { ink: "#1a1a1a", mid: "#5f5e5a", muted: "#a09a90", borderSoft: "#efebe1", border: "#e8e4dc", } as const; const wrap: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 8, minHeight: 0, flex: 1, }; const meta: React.CSSProperties = { fontSize: "0.74rem", color: INK.mid, }; const qual: React.CSSProperties = { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', color: INK.ink, }; const tableScroll: React.CSSProperties = { flex: 1, minHeight: 0, overflow: "auto", border: `1px solid ${INK.borderSoft}`, borderRadius: 6, }; const tableEl: React.CSSProperties = { borderCollapse: "collapse", fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: "0.76rem", width: "100%", }; const th: React.CSSProperties = { position: "sticky", top: 0, textAlign: "left", padding: "6px 10px", background: "#fafaf6", color: INK.ink, fontWeight: 600, fontSize: "0.72rem", borderBottom: `1px solid ${INK.border}`, whiteSpace: "nowrap", }; const td: React.CSSProperties = { padding: "5px 10px", color: INK.ink, borderBottom: `1px solid ${INK.borderSoft}`, whiteSpace: "nowrap", maxWidth: 360, overflow: "hidden", textOverflow: "ellipsis", }; const trEven: React.CSSProperties = { background: "#fff" }; const trOdd: React.CSSProperties = { background: "#fcfaf3" }; const center: React.CSSProperties = { flex: 1, display: "flex", alignItems: "center", justifyContent: "center", color: INK.mid, fontSize: "0.85rem", }; const errorBox: React.CSSProperties = { display: "flex", alignItems: "center", gap: 6, padding: "10px 12px", fontSize: "0.82rem", color: "#7a1f15", background: "#fbe9e7", border: `1px solid #f4c2bc`, borderRadius: 8, }; const infoBox: React.CSSProperties = { display: "flex", alignItems: "center", gap: 6, padding: "10px 12px", fontSize: "0.82rem", color: INK.mid, background: "#fafaf6", border: `1px dashed ${INK.borderSoft}`, borderRadius: 8, };