fix(databases): align database tree styles with codebase tree
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user