Files
ThothII/frontend/src/viewers/CteResultViewer.tsx
T
Codex d8a29bfbdd Add full shell, replaceable Omics adapter and bilingual interaction
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.
2026-09-13 14:26:39 +02:00

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>
);
}