fix(databases): apply Flowbite colors to data table viewer

This commit is contained in:
2026-06-14 14:15:15 -07:00
parent c5894775f8
commit 103ad8c81f

View File

@@ -7,6 +7,7 @@
*/ */
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { THEME } from "@/components/project/dashboard-ui";
import { Loader2, AlertCircle, Info } from "lucide-react"; import { Loader2, AlertCircle, Info } from "lucide-react";
interface PreviewedTable { interface PreviewedTable {
@@ -37,32 +38,51 @@ export function TableViewer({ projectId, dbUuid, schema, table }: Props) {
setError(null); setError(null);
setData(null); setData(null);
const url = `/api/projects/${projectId}/databases/${dbUuid}/preview` + const url =
`/api/projects/${projectId}/databases/${dbUuid}/preview` +
`?schema=${encodeURIComponent(schema)}&table=${encodeURIComponent(table)}`; `?schema=${encodeURIComponent(schema)}&table=${encodeURIComponent(table)}`;
fetch(url, { credentials: "include", signal: ctrl.signal }) fetch(url, { credentials: "include", signal: ctrl.signal })
.then(async r => { .then(async (r) => {
let body: unknown = {}; let body: unknown = {};
try { body = await r.json(); } catch {/* keep {} */} try {
if (!r.ok) throw new Error((body as { error?: string }).error || `HTTP ${r.status}`); body = await r.json();
} catch {
/* keep {} */
}
if (!r.ok)
throw new Error(
(body as { error?: string }).error || `HTTP ${r.status}`,
);
return body as PreviewedTable; return body as PreviewedTable;
}) })
.then(d => { if (!cancelled) setData(d); }) .then((d) => {
.catch(err => { if (!cancelled) setData(d);
})
.catch((err) => {
if (cancelled) return; if (cancelled) return;
if (err?.name === "AbortError") setError("Timed out after 12s."); if (err?.name === "AbortError") setError("Timed out after 12s.");
else setError(err?.message || "Failed to load preview"); else setError(err?.message || "Failed to load preview");
}) })
.finally(() => { clearTimeout(t); if (!cancelled) setLoading(false); }); .finally(() => {
clearTimeout(t);
if (!cancelled) setLoading(false);
});
return () => { cancelled = true; ctrl.abort(); clearTimeout(t); }; return () => {
cancelled = true;
ctrl.abort();
clearTimeout(t);
};
}, [projectId, dbUuid, schema, table]); }, [projectId, dbUuid, schema, table]);
if (loading) { if (loading) {
return ( return (
<div style={center}> <div style={center}>
<Loader2 size={14} className="animate-spin" /> <Loader2 size={14} className="animate-spin" />
<span style={{ marginLeft: 8 }}>Querying {schema}.{table}</span> <span style={{ marginLeft: 8 }}>
Querying {schema}.{table}
</span>
</div> </div>
); );
} }
@@ -88,23 +108,27 @@ export function TableViewer({ projectId, dbUuid, schema, table }: Props) {
<div style={wrap}> <div style={wrap}>
<div style={meta}> <div style={meta}>
Showing {data.rows.length} row{data.rows.length === 1 ? "" : "s"} Showing {data.rows.length} row{data.rows.length === 1 ? "" : "s"}
{data.truncated && " (truncated to first 50)"} ·{" "} {data.truncated && " (truncated to first 50)"} · {data.columns.length}{" "}
{data.columns.length} column{data.columns.length === 1 ? "" : "s"} ·{" "} column{data.columns.length === 1 ? "" : "s"} ·{" "}
<code style={qual}>{schema}.{table}</code> <code style={qual}>
{schema}.{table}
</code>
</div> </div>
<div style={tableScroll}> <div style={tableScroll}>
<table style={tableEl}> <table style={tableEl}>
<thead> <thead>
<tr> <tr>
{data.columns.map(c => ( {data.columns.map((c) => (
<th key={c} style={th}>{c}</th> <th key={c} style={th}>
{c}
</th>
))} ))}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data.rows.map((row, i) => ( {data.rows.map((row, i) => (
<tr key={i} style={i % 2 === 0 ? trEven : trOdd}> <tr key={i} style={i % 2 === 0 ? trEven : trOdd}>
{data.columns.map(c => ( {data.columns.map((c) => (
<td key={c} style={td} title={row[c]}> <td key={c} style={td} title={row[c]}>
{row[c]} {row[c]}
</td> </td>
@@ -129,52 +153,83 @@ const INK = {
} as const; } as const;
const wrap: React.CSSProperties = { const wrap: React.CSSProperties = {
display: "flex", flexDirection: "column", gap: 8, minHeight: 0, flex: 1, display: "flex",
flexDirection: "column",
gap: 8,
minHeight: 0,
flex: 1,
}; };
const meta: React.CSSProperties = { const meta: React.CSSProperties = {
fontSize: "0.74rem", color: INK.mid, fontSize: "0.74rem",
color: INK.mid,
}; };
const qual: React.CSSProperties = { const qual: React.CSSProperties = {
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
color: INK.ink, color: INK.ink,
}; };
const tableScroll: React.CSSProperties = { const tableScroll: React.CSSProperties = {
flex: 1, minHeight: 0, overflow: "auto", flex: 1,
border: `1px solid ${INK.borderSoft}`, borderRadius: 6, minHeight: 0,
overflow: "auto",
border: `1px solid ${INK.borderSoft}`,
borderRadius: 6,
}; };
const tableEl: React.CSSProperties = { const tableEl: React.CSSProperties = {
borderCollapse: "collapse", borderCollapse: "collapse",
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
fontSize: "0.76rem", fontSize: "0.76rem",
width: "100%", width: "100%",
}; };
const th: React.CSSProperties = { const th: React.CSSProperties = {
position: "sticky", top: 0, position: "sticky",
textAlign: "left", padding: "6px 10px", top: 0,
background: "#fafaf6", color: INK.ink, textAlign: "left",
fontWeight: 600, fontSize: "0.72rem", padding: "6px 10px",
background: "#fafaf6",
color: INK.ink,
fontWeight: 600,
fontSize: "0.72rem",
borderBottom: `1px solid ${INK.border}`, borderBottom: `1px solid ${INK.border}`,
whiteSpace: "nowrap", whiteSpace: "nowrap",
}; };
const td: React.CSSProperties = { const td: React.CSSProperties = {
padding: "5px 10px", color: INK.ink, padding: "5px 10px",
color: INK.ink,
borderBottom: `1px solid ${INK.borderSoft}`, borderBottom: `1px solid ${INK.borderSoft}`,
whiteSpace: "nowrap", maxWidth: 360, whiteSpace: "nowrap",
overflow: "hidden", textOverflow: "ellipsis", maxWidth: 360,
overflow: "hidden",
textOverflow: "ellipsis",
}; };
const trEven: React.CSSProperties = { background: "#fff" }; const trEven: React.CSSProperties = { background: "#fff" };
const trOdd: React.CSSProperties = { background: "#fcfaf3" }; const trOdd: React.CSSProperties = { background: "#fcfaf3" };
const center: React.CSSProperties = { const center: React.CSSProperties = {
flex: 1, display: "flex", alignItems: "center", justifyContent: "center", flex: 1,
color: INK.mid, fontSize: "0.85rem", display: "flex",
alignItems: "center",
justifyContent: "center",
color: INK.mid,
fontSize: "0.85rem",
}; };
const errorBox: React.CSSProperties = { const errorBox: React.CSSProperties = {
display: "flex", alignItems: "center", gap: 6, display: "flex",
padding: "10px 12px", fontSize: "0.82rem", color: "#7a1f15", alignItems: "center",
background: "#fbe9e7", border: `1px solid #f4c2bc`, borderRadius: 8, gap: 6,
padding: "10px 12px",
fontSize: "0.82rem",
color: "#7a1f15",
background: "#fbe9e7",
border: `1px solid #f4c2bc`,
borderRadius: 8,
}; };
const infoBox: React.CSSProperties = { const infoBox: React.CSSProperties = {
display: "flex", alignItems: "center", gap: 6, display: "flex",
padding: "10px 12px", fontSize: "0.82rem", color: INK.mid, alignItems: "center",
background: "#fafaf6", border: `1px dashed ${INK.borderSoft}`, borderRadius: 8, gap: 6,
padding: "10px 12px",
fontSize: "0.82rem",
color: INK.mid,
background: "#fafaf6",
border: `1px dashed ${INK.borderSoft}`,
borderRadius: 8,
}; };