Implement approved specification #32 and tickets #33-#37. Keep host authentication server-verified and pin session interaction language. Compile scoped base selectors for browser compatibility and retain full gutters during CSS pruning.
98 lines
3.5 KiB
TypeScript
98 lines
3.5 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";
|
|
import { useI18n } from "../i18n";
|
|
|
|
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 { t } = useI18n();
|
|
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)}>{t(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">{t(result.row_sample === 1 ? "{count} row" : "{count} rows", { count: result.row_sample })}</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">{t("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>
|
|
);
|
|
}
|