fix(databases): align database tree styles with codebase tree

This commit is contained in:
2026-06-14 14:11:14 -07:00
parent 7a8d13d7e2
commit c004be3b12

View File

@@ -11,7 +11,16 @@
*/
import { useEffect, useState } from "react";
import { Table, ChevronDown, ChevronRight, Loader2, AlertCircle, Info } from "lucide-react";
import {
Table,
ChevronDown,
ChevronRight,
Loader2,
AlertCircle,
Info,
Database,
} from "lucide-react";
import { THEME } from "@/components/project/dashboard-ui";
interface IntrospectedTable {
schema: string;
@@ -34,7 +43,10 @@ interface Props {
}
export function DatabaseTableTree({
projectId, dbUuid, selectedTable, onSelectTable,
projectId,
dbUuid,
selectedTable,
onSelectTable,
}: Props) {
const [data, setData] = useState<ApiResp | null>(null);
const [error, setError] = useState<string | null>(null);
@@ -48,38 +60,54 @@ export function DatabaseTableTree({
setError(null);
fetch(`/api/projects/${projectId}/databases/${dbUuid}/tables`, {
credentials: "include", signal: ctrl.signal,
credentials: "include",
signal: ctrl.signal,
})
.then(async r => {
.then(async (r) => {
let body: unknown = {};
try { body = await r.json(); } catch {/* keep {} */}
try {
body = await r.json();
} catch {
/* keep {} */
}
if (!r.ok) {
throw new Error((body as { error?: string }).error || `HTTP ${r.status}`);
throw new Error(
(body as { error?: string }).error || `HTTP ${r.status}`,
);
}
return body as ApiResp;
})
.then(d => { if (!cancelled) setData(d); })
.catch(err => {
.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); });
.finally(() => {
clearTimeout(t);
if (!cancelled) setLoading(false);
});
return () => { cancelled = true; ctrl.abort(); clearTimeout(t); };
return () => {
cancelled = true;
ctrl.abort();
clearTimeout(t);
};
}, [projectId, dbUuid]);
if (loading) {
return (
<div style={inline}>
<Loader2 size={12} className="animate-spin" /> Inspecting database
<Loader2 size={13} className="animate-spin" /> Inspecting database
</div>
);
}
if (error) {
return (
<div style={errorBox}>
<AlertCircle size={12} /> {error}
<AlertCircle size={13} /> {error}
</div>
);
}
@@ -88,9 +116,9 @@ export function DatabaseTableTree({
if (data.unsupported) {
return (
<div style={infoBox}>
<Info size={12} />
Table inspection isn't wired up for {data.engine} yet — Postgres
is the only engine supported today.
<Info size={13} />
Table inspection isn't wired up for {data.engine} yet Postgres is the
only engine supported today.
</div>
);
}
@@ -98,7 +126,7 @@ export function DatabaseTableTree({
if (data.tables.length === 0) {
return (
<div style={infoBox}>
<Info size={12} />
<Info size={13} />
No user-tables found. (Pre-deploy databases often start empty.)
</div>
);
@@ -114,7 +142,7 @@ export function DatabaseTableTree({
return (
<div style={treeWrap}>
{schemas.map(schema => (
{schemas.map((schema) => (
<SchemaGroup
key={schema}
schema={schema}
@@ -128,7 +156,10 @@ export function DatabaseTableTree({
}
function SchemaGroup({
schema, tables, selectedTable, onSelectTable,
schema,
tables,
selectedTable,
onSelectTable,
}: {
schema: string;
tables: IntrospectedTable[];
@@ -140,8 +171,9 @@ function SchemaGroup({
const items = (
<ul style={list}>
{tables.map(t => {
const active = selectedTable?.schema === t.schema && selectedTable?.name === t.name;
{tables.map((t) => {
const active =
selectedTable?.schema === t.schema && selectedTable?.name === t.name;
return (
<li key={`${t.schema}.${t.name}`}>
<button
@@ -149,12 +181,20 @@ function SchemaGroup({
onClick={() => onSelectTable({ schema: t.schema, name: t.name })}
style={{
...row,
background: active ? "#fffdf8" : "transparent",
borderColor: active ? INK.ink : "transparent",
background: active ? THEME.subtleBg : "transparent",
borderColor: active ? THEME.border : "transparent",
color: active ? THEME.ink : THEME.mid,
fontWeight: active ? 500 : 400,
}}
aria-pressed={active}
>
<Table size={11} style={{ color: INK.mid, flexShrink: 0 }} />
<Table
size={13}
style={{
color: active ? THEME.ink : THEME.muted,
flexShrink: 0,
}}
/>
<span style={tableName}>{t.name}</span>
{t.approxRows != null && t.approxRows > 0 && (
<span style={rowCount}>~{formatCount(t.approxRows)}</span>
@@ -170,7 +210,11 @@ function SchemaGroup({
return (
<div>
<button type="button" onClick={() => setOpen(o => !o)} style={schemaHeader}>
<button
type="button"
onClick={() => setOpen((o) => !o)}
style={schemaHeader}
>
{open ? <ChevronDown size={11} /> : <ChevronRight size={11} />}
{schema}
</button>
@@ -194,45 +238,90 @@ const INK = {
borderSoft: "#efebe1",
} as const;
const treeWrap: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 6 };
const list: React.CSSProperties = { listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 1 };
const treeWrap: React.CSSProperties = {
display: "flex",
flexDirection: "column",
gap: 6,
};
const list: React.CSSProperties = {
listStyle: "none",
margin: 0,
padding: 0,
display: "flex",
flexDirection: "column",
gap: 1,
};
const row: React.CSSProperties = {
display: "flex", alignItems: "center", gap: 6,
width: "100%", padding: "5px 8px",
border: "1px solid transparent", borderRadius: 5,
cursor: "pointer", font: "inherit", color: "inherit",
display: "flex",
alignItems: "center",
gap: 6,
width: "100%",
padding: "5px 8px",
border: "1px solid transparent",
borderRadius: 5,
cursor: "pointer",
font: "inherit",
color: "inherit",
textAlign: "left",
};
const tableName: React.CSSProperties = {
fontSize: "0.78rem", color: INK.ink, flex: 1,
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
fontSize: "0.78rem",
color: INK.ink,
flex: 1,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
};
const rowCount: React.CSSProperties = {
fontSize: "0.68rem", color: INK.muted, flexShrink: 0,
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
fontSize: "0.68rem",
color: INK.muted,
flexShrink: 0,
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
};
const schemaHeader: React.CSSProperties = {
display: "flex", alignItems: "center", gap: 4,
display: "flex",
alignItems: "center",
gap: 4,
padding: "4px 6px",
fontSize: "0.7rem", fontWeight: 600,
letterSpacing: "0.06em", textTransform: "uppercase",
fontSize: "0.7rem",
fontWeight: 600,
letterSpacing: "0.06em",
textTransform: "uppercase",
color: INK.muted,
background: "transparent", border: "none",
cursor: "pointer", font: "inherit",
background: "transparent",
border: "none",
cursor: "pointer",
font: "inherit",
};
const inline: React.CSSProperties = {
display: "flex", alignItems: "center", gap: 6,
padding: "8px 10px", fontSize: "0.76rem", color: INK.mid,
display: "flex",
alignItems: "center",
gap: 6,
padding: "8px 10px",
fontSize: "0.76rem",
color: INK.mid,
};
const infoBox: React.CSSProperties = {
display: "flex", alignItems: "flex-start", gap: 6,
padding: "8px 10px", fontSize: "0.74rem", color: INK.mid,
background: "#fafaf6", border: `1px dashed ${INK.borderSoft}`, borderRadius: 6,
display: "flex",
alignItems: "flex-start",
gap: 6,
padding: "8px 10px",
fontSize: "0.74rem",
color: INK.mid,
background: "#fafaf6",
border: `1px dashed ${INK.borderSoft}`,
borderRadius: 6,
lineHeight: 1.4,
};
const errorBox: React.CSSProperties = {
display: "flex", alignItems: "center", gap: 6,
padding: "8px 10px", fontSize: "0.74rem", color: "#7a1f15",
background: "#fbe9e7", border: `1px solid #f4c2bc`, borderRadius: 6,
display: "flex",
alignItems: "center",
gap: 6,
padding: "8px 10px",
fontSize: "0.74rem",
color: "#7a1f15",
background: "#fbe9e7",
border: `1px solid #f4c2bc`,
borderRadius: 6,
};