SPB Git

spb/akos

Public
5.3 KB · 37 lines tsx
1import { useState } from "react";2import { Link, useParams } from "react-router";3import { api, type DatasetDetail as DD, type Graph } from "../api";4import { useFetch, fmtBytes, fmtDate, num } from "../hooks";5import { Badge, ErrorBox, JobStatus, JsonTree, KV, Loading, Tabs, Td } from "../components/ui";6import { LayeredGraph } from "../components/Graph";78export default function DatasetDetail() {9  const { id } = useParams();10  const { data, error, loading, reload } = useFetch<DD>(`datasets/${id}`);11  const lineage = useFetch<Graph>(`datasets/${id}/lineage`);12  const quality = useFetch<Record<string, unknown>>(`datasets/${id}/quality`);13  const [tab, setTab] = useState("Assessment");14  const [job, setJob] = useState<string | null>(null);15  if (loading) return <Loading />; if (error) return <ErrorBox error={error} />; if (!data) return null;16  const d = data;17  const cols = Object.keys(d.schema_json?.columns ?? {}).slice(0, 12);18  const exportIt = async (verified: boolean) => { const r = await api<{ job_id: string }>(`datasets/${d.id}/export`, { body: { verified } }); setJob(r.job_id); };19  return (20    <div>21      <div className="row between"><div><h1>{d.name} <Badge v={d.verification} /></h1><div className="hint">project {d.project_slug} · {num(d.row_count)} rows · granularity {d.granularity ?? "—"} · hash <code>{d.hash}</code></div></div>22        <div className="row"><button className="btn primary sm" onClick={() => exportIt(true)}>Export (verified)</button><button className="btn sm" onClick={() => exportIt(false)}>Export (unverified)</button></div></div>23      <JobStatus jobId={job} label="export" onDone={reload} />24      <Tabs tabs={["Assessment", "Preview", "Quality", "Lineage", "Sources", "Exports"]} active={tab} onChange={setTab} />25      {tab === "Assessment" && <div className="card"><p className={`alert ${d.assessment.verified ? "ok" : "warn"}`}>{d.assessment.verified ? "All checks pass: a verified export is allowed." : "Some checks fail: verified export is blocked; an unverified export is clearly labelled."}</p>26        {d.assessment.checks.map((c, i) => <div key={i} className={`check ${c.ok ? "ok" : "bad"}`}><span className="ic">{c.ok ? "✓" : "✕"}</span><span><code>{c.check}</code>{c.detail && <span className="hint"> — {c.detail}</span>}</span></div>)}27        <KV items={[["Identifiers", (d.identifiers?.keys ?? []).join(", ") || "—"], ["Units", Object.keys(d.units).length ? Object.entries(d.units).map(([k, v]) => `${k}: ${v}`).join(", ") : "—"], ["License", (d.license?.name as string) ?? "—"], ["Records path", d.schema_json?.records_path ?? "—"], ["Created", `${fmtDate(d.created_at)} by ${d.created_by ?? "system"}`]]} /></div>}28      {tab === "Preview" && <div className="card" style={{ overflowX: "auto" }}>{d.preview.length ? <table className="tbl" style={{ minWidth: 600 }}><thead><tr>{cols.map((c) => <th key={c}>{c}</th>)}</tr></thead><tbody>{d.preview.map((r, i) => <tr key={i}>{cols.map((c) => <Td key={c} l={c}>{String(r[c] ?? "")}</Td>)}</tr>)}</tbody></table> : <div className="empty">No rows.</div>}<p className="hint">first {d.preview.length} rows · {cols.length} of {Object.keys(d.schema_json?.columns ?? {}).length} columns</p></div>}29      {tab === "Quality" && <div className="card">{quality.loading && <Loading />}<ErrorBox error={quality.error} />{quality.data && <JsonTree data={quality.data} open />}</div>}30      {tab === "Lineage" && <div>{lineage.loading && <Loading />}<ErrorBox error={lineage.error} />{lineage.data && <LayeredGraph graph={lineage.data} />}</div>}31      {tab === "Sources" && <div className="card"><h3>Executions</h3>{d.executions.length ? <table className="tbl"><thead><tr><th>Execution</th><th>State</th><th>AKP</th><th>URL</th><th>Hash</th><th>When</th></tr></thead><tbody>{d.executions.map((e) => <tr key={e.id}><Td l="execution"><Link to={`/executions/${e.id}`}>{e.id.slice(0, 8)}</Link></Td><Td l="state"><Badge v={e.state} />{e.observed === false && <Badge v="unobserved" />}</Td><Td l="akp">{e.akp_id ? `${e.akp_id} v${e.akp_version} (${e.akp_lifecycle ?? ""})` : "—"}{(e as { opaque?: boolean }).opaque && <Badge v="critical" label="opaque" />}</Td><Td l="url" wide><span className="truncate mono">{e.url}</span></Td><Td l="hash"><code>{e.response_hash?.slice(0, 12)}</code></Td><Td l="when">{fmtDate(e.finished_at)}</Td></tr>)}</tbody></table> : <span className="hint">none (derived dataset)</span>}32        <h3>Transformations</h3>{d.transformations.length ? d.transformations.map((t) => <details key={t.id} className="card tight"><summary><b>{t.name}</b> ({t.kind}) code sha256 <code>{t.code_hash.slice(0, 12)}</code> · inputs {t.input_dataset_ids.map((i) => <Link key={i} to={`/datasets/${i}`}>{i.slice(0, 8)} </Link>)}</summary><JsonTree data={t.validation} open /></details>) : <span className="hint">none</span>}</div>}33      {tab === "Exports" && <div className="card">{d.exports.length ? d.exports.map((x) => <div key={x.artifact_id} className="row between" style={{ padding: "6px 0", borderBottom: "1px solid var(--border)" }}><span><Badge v={String(x.details?.label ?? "")} /> {fmtDate(x.created_at)} · {fmtBytes(x.size_bytes)} · <code>{x.sha256.slice(0, 12)}</code></span><a className="btn sm" href={`/api/v1/artifacts/${x.artifact_id}`}>download zip</a></div>) : <div className="empty">No exports yet.</div>}</div>}34    </div>35  );36}37
Files · main
Menu

Theme