- Extract shared statusBadgeClass() helper (src/viewers/statusBadge.ts) so SqlViewer, CteResultViewer and PhaseSummaryViewer can't drift: ok/success/ passed/promoted render green (--success), warn renders amber (--warning), error/failed stay destructive red, everything else stays neutral outline. Previously CteResultViewer/PhaseSummaryViewer mapped "ok"/"promoted" to the default badge variant, which is bg-primary (GSD red) — success states rendered red. - enrich.js: buildCteResultV2 now falls back preview.rows to [] instead of null when last_test.preview_rows is missing (pre-upgrade ok records), and CteResultViewer reads result.preview?.rows?.length with a null-safe fallback so it degrades to the "No preview rows" empty state instead of crashing. - PhaseSummaryViewer: section.items is optional (model-authored sections can be prose-only); render (section.items ?? []) instead of crashing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
96 lines
3.4 KiB
TypeScript
96 lines
3.4 KiB
TypeScript
import { Badge } from "../components/ui/badge";
|
|
import { SqlViewer, type SqlBlock } from "./SqlViewer";
|
|
import { PreviewGrid } from "./PreviewGrid";
|
|
import { statusBadgeClass } from "./statusBadge";
|
|
import type { CteResultV2 } from "./artifactV2";
|
|
|
|
const CHIP = "rounded-md bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground/90";
|
|
|
|
function statusLabel(status: string): string {
|
|
if (status === "ok") return "success";
|
|
if (status === "error") return "error";
|
|
return status;
|
|
}
|
|
|
|
function testStatus(status: string): SqlBlock["testStatus"] {
|
|
if (status === "ok") return "passed";
|
|
if (status === "error") return "failed";
|
|
return "untested";
|
|
}
|
|
|
|
export function CteResultViewer({ result }: { result: CteResultV2 }) {
|
|
const hasPreviewRows = (result.preview?.rows?.length ?? 0) > 0;
|
|
const descriptions = (result.columns ?? []).map((c) => c.description ?? "");
|
|
const showColumns = descriptions.some((d) => d.trim() !== "");
|
|
const dependsOn = result.depends_on ?? [];
|
|
|
|
const block: SqlBlock = {
|
|
name: result.name,
|
|
sql: result.sql,
|
|
testStatus: testStatus(result.status),
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<Badge variant="outline" className="font-mono">
|
|
CTE {result.index}/{result.total}
|
|
</Badge>
|
|
<span className="font-mono text-sm font-semibold">{result.name}</span>
|
|
<Badge variant="outline" className={statusBadgeClass(result.status)}>{statusLabel(result.status)}</Badge>
|
|
{result.execution_ms !== undefined && (
|
|
<span className="text-xs text-muted-foreground">{result.execution_ms} ms</span>
|
|
)}
|
|
{result.row_sample !== undefined && (
|
|
<span className="text-xs text-muted-foreground">{result.row_sample} rows</span>
|
|
)}
|
|
</div>
|
|
|
|
{result.warnings && result.warnings.length > 0 && (
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{result.warnings.map((w, i) => (
|
|
<Badge key={i} variant="outline" className="border-[oklch(var(--warning)/0.4)] text-[oklch(0.5_0.12_79)]">
|
|
{w}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{(result.purpose || result.rationale || dependsOn.length > 0) && (
|
|
<div className="flex flex-col gap-1">
|
|
{result.purpose && <p className="text-sm text-muted-foreground">{result.purpose}</p>}
|
|
{result.rationale && <p className="text-sm text-foreground/90">{result.rationale}</p>}
|
|
{dependsOn.length > 0 && (
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{dependsOn.map((d) => (
|
|
<span key={d} className={CHIP}>{d}</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<SqlViewer blocks={[block]} />
|
|
|
|
{hasPreviewRows ? (
|
|
<PreviewGrid columns={result.preview!.columns} rows={result.preview!.rows} />
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">No preview rows</p>
|
|
)}
|
|
|
|
{showColumns && (
|
|
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs">
|
|
{result.columns!.map((c) => (
|
|
<div key={c.name} className="contents">
|
|
<dt className="font-mono font-medium">{c.name}</dt>
|
|
<dd className="text-muted-foreground">{c.description}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
)}
|
|
|
|
{result.note && <p className="thot-prose">{result.note}</p>}
|
|
</div>
|
|
);
|
|
}
|