fix(databases): apply Flowbite colors to data table viewer
This commit is contained in:
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user