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.
220 lines
8.6 KiB
TypeScript
220 lines
8.6 KiB
TypeScript
import { useI18n } from "../i18n";
|
|
import type { ReactNode } from "react";
|
|
import { Card, CardContent, CardHeader } from "../components/ui/card";
|
|
import { Badge } from "../components/ui/badge";
|
|
import type { CtePlanCte, CtePlanFilter, CtePlanV2 } from "./artifactV2";
|
|
|
|
const CHIP =
|
|
"min-w-0 max-w-full whitespace-pre-wrap [overflow-wrap:anywhere] rounded-md bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground/90";
|
|
const FIELD_LABEL = "text-[0.65rem] font-semibold uppercase tracking-[0.08em] text-muted-foreground";
|
|
|
|
function Chip({ children }: { children: ReactNode }) {
|
|
return <span className={CHIP}>{children}</span>;
|
|
}
|
|
|
|
function FilterRow({ filter, index }: { filter: CtePlanFilter; index: number }) {
|
|
const { t } = useI18n();
|
|
return (
|
|
<div
|
|
role="group"
|
|
aria-label={t("Filter {index}", { index })}
|
|
className="min-w-0 space-y-2 px-3 py-1 sm:px-4"
|
|
>
|
|
<div className="grid min-w-0 grid-cols-1 gap-2 md:grid-cols-[minmax(0,1fr)_minmax(5rem,auto)_minmax(0,1fr)]">
|
|
<div className="min-w-0 space-y-1">
|
|
<p className={FIELD_LABEL}>{t("Column")}</p>
|
|
<code className="block min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] font-mono text-xs leading-4 text-foreground">
|
|
{filter.column}
|
|
</code>
|
|
</div>
|
|
<div className="min-w-0 space-y-1 md:text-center">
|
|
<p className={FIELD_LABEL}>{t("Operator")}</p>
|
|
<code className="block min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] font-mono text-xs font-semibold leading-4 text-primary">
|
|
{filter.op}
|
|
</code>
|
|
</div>
|
|
<div className="min-w-0 space-y-1">
|
|
<p className={FIELD_LABEL}>{t("Value")}</p>
|
|
<code className="block min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] font-mono text-xs leading-4 text-foreground">
|
|
{filter.value}
|
|
</code>
|
|
</div>
|
|
</div>
|
|
{(filter.description || filter.rationale) && (
|
|
<div className="grid min-w-0 gap-x-4 gap-y-1 border-t border-border/50 pt-1 md:grid-cols-[5.5rem_minmax(0,1fr)]">
|
|
{filter.description && (
|
|
<>
|
|
<span className={FIELD_LABEL}>{t("Description")}</span>
|
|
<p className="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] text-xs leading-4 text-muted-foreground">
|
|
{filter.description}
|
|
</p>
|
|
</>
|
|
)}
|
|
{filter.rationale && (
|
|
<>
|
|
<span className={FIELD_LABEL}>{t("Rationale")}</span>
|
|
<p className="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] text-xs leading-4 text-muted-foreground">
|
|
{filter.rationale}
|
|
</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function CteCard({ cte, total }: { cte: CtePlanCte; total: number }) {
|
|
const { t } = useI18n();
|
|
const dependsOn = cte.depends_on ?? [];
|
|
|
|
return (
|
|
<Card className="gap-0 rounded-lg border py-0 shadow-none">
|
|
<CardHeader className="gap-2 rounded-t-lg border-b border-border/60 bg-muted/15 px-3 py-2 sm:px-4 [&.border-b]:pb-2">
|
|
<div className="grid min-w-0 grid-cols-[auto_minmax(0,1fr)] items-center gap-2.5">
|
|
<Badge variant="outline" className="font-mono">
|
|
CTE {cte.index}/{total}
|
|
</Badge>
|
|
<h3 className="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] font-mono text-sm font-medium leading-5">
|
|
{cte.name}
|
|
</h3>
|
|
</div>
|
|
{cte.purpose && (
|
|
<p className="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] text-sm leading-5 text-muted-foreground">
|
|
{cte.purpose}
|
|
</p>
|
|
)}
|
|
</CardHeader>
|
|
<CardContent className="flex min-w-0 flex-col gap-3 px-3 py-2 sm:px-4">
|
|
<div className="grid min-w-0 gap-3 md:grid-cols-2">
|
|
<section className="min-w-0">
|
|
<h4 className="thot-label mb-1">{t("Depends on")}</h4>
|
|
{dependsOn.length ? (
|
|
<div className="flex min-w-0 flex-wrap gap-1.5">
|
|
{dependsOn.map((dependency) => (
|
|
<Chip key={dependency}>{dependency}</Chip>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-xs text-muted-foreground">{t("No dependencies")}</p>
|
|
)}
|
|
</section>
|
|
|
|
{cte.keys && cte.keys.length > 0 && (
|
|
<section className="min-w-0">
|
|
<h4 className="thot-label mb-1">{t("Keys")}</h4>
|
|
<div className="flex min-w-0 flex-wrap gap-1.5">
|
|
{cte.keys.map((key) => (
|
|
<Chip key={key}>{key}</Chip>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
</div>
|
|
|
|
{cte.tables && cte.tables.length > 0 && (
|
|
<section className="min-w-0">
|
|
<h4 className="thot-label mb-1">{t("Tables")}</h4>
|
|
<div className="min-w-0 divide-y divide-border/50 rounded-lg border border-border/60">
|
|
{cte.tables.map((table) => (
|
|
<div
|
|
key={table.name}
|
|
className="grid min-w-0 gap-1 px-3 py-1 sm:px-4 md:grid-cols-[minmax(10rem,0.8fr)_minmax(0,1.2fr)] md:gap-4"
|
|
>
|
|
<code className="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] font-mono text-xs leading-4 text-foreground">
|
|
{table.name}
|
|
</code>
|
|
{table.description && (
|
|
<p className="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] text-xs leading-4 text-muted-foreground">
|
|
{table.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{cte.filters && cte.filters.length > 0 && (
|
|
<section className="min-w-0">
|
|
<h4 className="thot-label mb-1">{t("Filters")}</h4>
|
|
<div className="min-w-0 divide-y divide-border/50 rounded-lg border border-border/60">
|
|
{cte.filters.map((f, i) => (
|
|
<FilterRow key={`${f.column}-${i}`} filter={f} index={i + 1} />
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{cte.output_columns && cte.output_columns.length > 0 && (
|
|
<section className="min-w-0">
|
|
<h4 className="thot-label mb-1">{t("Output columns")}</h4>
|
|
<div className="flex min-w-0 flex-wrap gap-1.5">
|
|
{cte.output_columns.map((column) => (
|
|
<Chip key={column}>{column}</Chip>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{cte.rationale && (
|
|
<section className="min-w-0 border-t border-border/60 pt-1">
|
|
<h4 className="thot-label mb-1">{t("Rationale")}</h4>
|
|
<p className="whitespace-pre-wrap [overflow-wrap:anywhere] text-sm leading-5 text-foreground/90">
|
|
{cte.rationale}
|
|
</p>
|
|
</section>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export function CtePlanViewer({ plan }: { plan: CtePlanV2 }) {
|
|
const { t } = useI18n();
|
|
return (
|
|
<div className="flex min-w-0 flex-col gap-6">
|
|
<div className="flex min-w-0 flex-col gap-3">
|
|
{plan.question && (
|
|
<div className="min-w-0 space-y-1.5">
|
|
<p className="thot-label">{t("Question")}</p>
|
|
<p className="whitespace-pre-wrap [overflow-wrap:anywhere] text-sm leading-relaxed text-muted-foreground">
|
|
{plan.question}
|
|
</p>
|
|
</div>
|
|
)}
|
|
{plan.strategy && (
|
|
<div className="min-w-0 space-y-1.5">
|
|
<p className="thot-label">{t("Strategy")}</p>
|
|
<p className="thot-prose whitespace-pre-wrap [overflow-wrap:anywhere] leading-relaxed">
|
|
{plan.strategy}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="min-w-0 space-y-1.5">
|
|
<p className="thot-label">{t("Execution order")}</p>
|
|
<div className="flex min-w-0 flex-wrap items-center gap-1.5 font-mono text-xs text-muted-foreground">
|
|
{plan.ctes.map((c, i) => (
|
|
<span key={c.name} className="flex min-w-0 max-w-full items-center gap-1.5">
|
|
{i > 0 && <span aria-hidden="true">→</span>}
|
|
<span className="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] text-foreground/90">
|
|
{c.name}
|
|
</span>
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<ol aria-label={t("CTE plan steps")} className="flex flex-col gap-3">
|
|
{plan.ctes.map((cte) => (
|
|
<li key={cte.name} className="min-w-0">
|
|
<CteCard cte={cte} total={plan.ctes.length} />
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</div>
|
|
);
|
|
}
|