refactor: extract and modularize execution tabs and helpers for enhanced code reusability and maintainability

This commit is contained in:
Shine1i 2026-02-22 06:26:40 +01:00
commit b7dfa2b7e4
7 changed files with 818 additions and 615 deletions

View file

@ -0,0 +1,54 @@
import type { ReactElement } from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import type { AnalysisColumnStat } from "./executions-view-helpers";
type ExecutionColumnsTabProps = {
analysisColumns: AnalysisColumnStat[];
};
export function ExecutionColumnsTab({
analysisColumns,
}: ExecutionColumnsTabProps): ReactElement {
return (
<div className="mt-3 rounded-xl border p-3">
<p className="mb-2 text-sm font-semibold">Column statistics</p>
{analysisColumns.length === 0 ? (
<p className="text-xs text-muted-foreground">No column statistics yet.</p>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Column</TableHead>
<TableHead>Type</TableHead>
<TableHead>Data type</TableHead>
<TableHead>Unique</TableHead>
<TableHead>Nulls</TableHead>
<TableHead>Input tok avg</TableHead>
<TableHead>Output tok avg</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{analysisColumns.map((column) => (
<TableRow key={column.column_name}>
<TableCell>{column.column_name}</TableCell>
<TableCell>{column.column_type}</TableCell>
<TableCell>{column.simple_dtype}</TableCell>
<TableCell>{column.num_unique ?? "--"}</TableCell>
<TableCell>{column.num_null ?? "--"}</TableCell>
<TableCell>{column.input_tokens_mean ?? "--"}</TableCell>
<TableCell>{column.output_tokens_mean ?? "--"}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
);
}

View file

@ -0,0 +1,157 @@
import type { ReactElement } from "react";
import type { ColumnDef } from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import { DataTable } from "@/components/ui/data-table";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { isExecutionInProgress } from "../../executions/execution-helpers";
import type { RecipeExecutionRecord } from "../../execution-types";
import { formatCellValue, isExpandableCellValue } from "./executions-view-helpers";
type ExecutionDataTabProps = {
execution: RecipeExecutionRecord;
datasetColumnNames: string[];
hiddenDatasetColumns: string[];
canPageDataset: boolean;
currentDatasetPage: number;
totalPages: number;
tableColumns: ColumnDef<Record<string, unknown>>[];
datasetRowsForTable: Record<string, unknown>[];
visibleDatasetColumnNames: string[];
expandedDatasetRows: Record<string, boolean>;
selectedExecutionIdSafe: string | null;
onSetHiddenColumns: (updater: (current: string[]) => string[]) => void;
onPrevPage: () => void;
onNextPage: () => void;
onToggleRowExpanded: (rowId: string) => void;
};
export function ExecutionDataTab({
execution,
datasetColumnNames,
hiddenDatasetColumns,
canPageDataset,
currentDatasetPage,
totalPages,
tableColumns,
datasetRowsForTable,
visibleDatasetColumnNames,
expandedDatasetRows,
selectedExecutionIdSafe,
onSetHiddenColumns,
onPrevPage,
onNextPage,
onToggleRowExpanded,
}: ExecutionDataTabProps): ReactElement {
return (
<div className="mt-3">
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
<p className="text-sm font-semibold">Dataset sample</p>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
{datasetColumnNames.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="button" size="sm" variant="outline">
Columns
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Visible columns</DropdownMenuLabel>
{datasetColumnNames.map((columnName) => (
<DropdownMenuCheckboxItem
key={columnName}
checked={!hiddenDatasetColumns.includes(columnName)}
onSelect={(event) => {
event.preventDefault();
}}
onCheckedChange={(checked) => {
onSetHiddenColumns((currentColumns) => {
if (checked) {
return currentColumns.filter((name) => name !== columnName);
}
return [...currentColumns, columnName];
});
}}
>
{columnName}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
{canPageDataset && (
<>
<span>
Page {currentDatasetPage}/{totalPages}
</span>
<Button
type="button"
size="sm"
variant="outline"
disabled={
isExecutionInProgress(execution.status) || currentDatasetPage <= 1
}
onClick={onPrevPage}
>
Prev
</Button>
<Button
type="button"
size="sm"
variant="outline"
disabled={
isExecutionInProgress(execution.status) ||
currentDatasetPage >= totalPages
}
onClick={onNextPage}
>
Next
</Button>
</>
)}
</div>
</div>
{execution.dataset.length === 0 ? (
<p className="text-xs text-muted-foreground">No rows returned.</p>
) : tableColumns.length === 0 ? (
<p className="text-xs text-muted-foreground">
All columns hidden. Use Columns to show at least one.
</p>
) : (
<div className="max-h-[55vh] overflow-auto">
<DataTable
columns={tableColumns}
data={datasetRowsForTable}
getRowClassName={(row, _rowIndex, rowId) => {
const canExpand = visibleDatasetColumnNames.some((columnName) =>
isExpandableCellValue(formatCellValue(row[columnName])),
);
if (!canExpand) {
return undefined;
}
return cn(
"cursor-pointer",
expandedDatasetRows[rowId] ? "bg-primary/[0.05]" : "hover:bg-primary/[0.06]",
);
}}
onRowClick={(row, _rowIndex, rowId) => {
const canExpand = visibleDatasetColumnNames.some((columnName) =>
isExpandableCellValue(formatCellValue(row[columnName])),
);
if (!canExpand || !selectedExecutionIdSafe) {
return;
}
onToggleRowExpanded(rowId);
}}
/>
</div>
)}
</div>
);
}

View file

@ -0,0 +1,226 @@
import type { ReactElement, RefObject, UIEvent } from "react";
import {
Database01Icon,
Database02Icon,
Flag02Icon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { isExecutionInProgress } from "../../executions/execution-helpers";
import type { RecipeExecutionRecord } from "../../execution-types";
import type { ModelUsageRow } from "./executions-view-helpers";
import { formatMetricValue } from "./executions-view-helpers";
type ExecutionOverviewTabProps = {
execution: RecipeExecutionRecord;
showSummaryCards: boolean;
recordsMetric: number | null;
totalMetric: number | null;
runDuration: string;
columnCount: number;
llmColumnCount: number;
nullRate: number | null;
sideEffects: string[];
lowUniquenessColumns: string[];
modelUsageRows: ModelUsageRow[];
totalInputTokens: number;
totalOutputTokens: number;
terminalLines: string[];
terminalRef: RefObject<HTMLDivElement | null>;
onTerminalScroll: (event: UIEvent<HTMLDivElement>) => void;
};
export function ExecutionOverviewTab({
execution,
showSummaryCards,
recordsMetric,
totalMetric,
runDuration,
columnCount,
llmColumnCount,
nullRate,
sideEffects,
lowUniquenessColumns,
modelUsageRows,
totalInputTokens,
totalOutputTokens,
terminalLines,
terminalRef,
onTerminalScroll,
}: ExecutionOverviewTabProps): ReactElement {
return (
<div className="mt-3 space-y-3">
{showSummaryCards && (
<div className="space-y-3">
<div className="grid gap-3 md:grid-cols-2">
<div className="h-full rounded-lg bg-muted/20 p-3">
<div className="mb-2 flex items-center justify-between">
<p className="text-xs text-muted-foreground">Run summary</p>
<HugeiconsIcon
icon={Database01Icon}
className="size-4 text-muted-foreground"
/>
</div>
<div className="space-y-1 text-xs">
<p>
Records:{" "}
<span className="font-semibold">
{formatMetricValue(recordsMetric)} / {formatMetricValue(totalMetric)}
</span>
</p>
<p>
Duration: <span className="font-semibold">{runDuration}</span>
</p>
<p>
Columns analyzed:{" "}
<span className="font-semibold">{formatMetricValue(columnCount)}</span>
</p>
<p>
Final stage:{" "}
<span className="font-semibold">{execution.stage ?? "--"}</span>
</p>
</div>
</div>
<div className="h-full rounded-lg bg-muted/20 p-3">
<div className="mb-2 flex items-center justify-between">
<p className="text-xs text-muted-foreground">Insights</p>
<HugeiconsIcon
icon={Database02Icon}
className="size-4 text-muted-foreground"
/>
</div>
<div className="space-y-2 text-xs">
<p>
LLM columns:{" "}
<span className="font-semibold">{formatMetricValue(llmColumnCount)}</span>
</p>
<p>
Null rate: <span className="font-semibold">{nullRate?.toFixed(1) ?? "--"}%</span>
</p>
<p>
Dropped columns:{" "}
<span className="font-semibold">{formatMetricValue(sideEffects.length)}</span>
</p>
{sideEffects.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{sideEffects.map((name) => (
<Badge key={name} variant="outline">
{name}
</Badge>
))}
</div>
)}
<p>
Low uniqueness flags:{" "}
<span className="font-semibold">
{formatMetricValue(lowUniquenessColumns.length)}
</span>
</p>
{lowUniquenessColumns.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{lowUniquenessColumns.slice(0, 3).map((name) => (
<Badge key={name} variant="secondary">
{name}
</Badge>
))}
{lowUniquenessColumns.length > 3 && (
<Badge variant="secondary">
+{lowUniquenessColumns.length - 3} more
</Badge>
)}
</div>
)}
</div>
</div>
</div>
<div className="rounded-lg bg-muted/20 p-3">
<div className="mb-2 flex items-center justify-between">
<p className="text-xs text-muted-foreground">Model usage</p>
<HugeiconsIcon icon={Flag02Icon} className="size-4 text-muted-foreground" />
</div>
{modelUsageRows.length === 0 ? (
<p className="text-xs text-muted-foreground">No model usage yet.</p>
) : (
<div className="space-y-2 text-xs">
<div className="grid grid-cols-2 gap-2">
<div className="rounded border bg-muted/30 px-2 py-1.5">
<p className="text-muted-foreground">Total input</p>
<p className="text-sm font-semibold">
{formatMetricValue(totalInputTokens)}
</p>
</div>
<div className="rounded border bg-muted/30 px-2 py-1.5">
<p className="text-muted-foreground">Total output</p>
<p className="text-sm font-semibold">
{formatMetricValue(totalOutputTokens)}
</p>
</div>
</div>
<div className="overflow-hidden rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Model</TableHead>
<TableHead className="text-right">Input</TableHead>
<TableHead className="text-right">Output</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{modelUsageRows.map((usage) => (
<TableRow key={usage.model}>
<TableCell className="max-w-[320px] truncate">{usage.model}</TableCell>
<TableCell className="text-right">
{formatMetricValue(usage.input)}
</TableCell>
<TableCell className="text-right">
{formatMetricValue(usage.output)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)}
</div>
</div>
)}
<div className="overflow-hidden rounded-xl corner-squircle border">
<div className="flex items-center justify-between border-b px-3 py-2">
<p className="text-sm font-semibold">Terminal output</p>
<p className="text-xs text-muted-foreground">{terminalLines.length} lines</p>
</div>
<div
ref={terminalRef}
className="max-h-72 overflow-auto bg-zinc-900/80 px-3 py-2 font-mono text-xs text-zinc-200"
onScroll={onTerminalScroll}
>
{terminalLines.length === 0 ? (
<p className="text-zinc-400">
{isExecutionInProgress(execution.status)
? "Waiting for logs..."
: "No logs captured."}
</p>
) : (
terminalLines.map((line, index) => (
<p
key={`${index}-${line.slice(0, 24)}`}
className="whitespace-pre-wrap break-words leading-relaxed"
>
{line}
</p>
))
)}
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,18 @@
import type { ReactElement } from "react";
type ExecutionRawTabProps = {
rawExecution: Record<string, unknown> | null;
};
export function ExecutionRawTab({
rawExecution,
}: ExecutionRawTabProps): ReactElement {
return (
<div className="mt-3 rounded-xl border p-3">
<p className="mb-2 text-sm font-semibold">Raw execution</p>
<pre className="max-h-96 overflow-auto rounded-md bg-muted/40 p-3 text-xs">
{JSON.stringify(rawExecution, null, 2)}
</pre>
</div>
);
}

View file

@ -0,0 +1,70 @@
import type { ReactElement } from "react";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import type { RecipeExecutionRecord } from "../../execution-types";
import {
formatStatus,
formatTimestamp,
statusRightBorder,
statusTone,
} from "./executions-view-helpers";
type ExecutionSidebarProps = {
executions: RecipeExecutionRecord[];
selectedExecutionId: string | null;
onSelectExecution: (id: string) => void;
};
export function ExecutionSidebar({
executions,
selectedExecutionId,
onSelectExecution,
}: ExecutionSidebarProps): ReactElement {
return (
<aside className="w-72 shrink-0 border-r">
<div className="flex items-center justify-between border-b px-3 py-2">
<p className="text-xs font-semibold uppercase text-muted-foreground">
Executions
</p>
</div>
<div className="h-[calc(100%-45px)] overflow-auto p-2">
{executions.length === 0 ? (
<div className="rounded-xl border border-dashed p-3 text-xs text-muted-foreground">
No executions yet.
</div>
) : (
executions.map((execution) => (
<button
key={execution.id}
type="button"
onClick={() => onSelectExecution(execution.id)}
className={cn(
"mb-2 w-full rounded-xl corner-squircle border border-r-4 p-3 text-left",
selectedExecutionId === execution.id
? "border-primary/50 bg-primary/5"
: "hover:bg-muted/40",
statusRightBorder(execution.status),
)}
>
<div className="mb-2 flex items-center justify-between gap-2">
<p className="truncate text-sm font-medium capitalize">
{execution.kind}
</p>
<Badge
variant="secondary"
className={cn("capitalize", statusTone(execution.status))}
>
{formatStatus(execution.status)}
</Badge>
</div>
<p className="text-xs text-muted-foreground">{execution.rows} rows</p>
<p className="text-xs text-muted-foreground">
{formatTimestamp(execution.createdAt)}
</p>
</button>
))
)}
</div>
</aside>
);
}

View file

@ -0,0 +1,175 @@
import type {
RecipeExecutionAnalysis,
RecipeExecutionStatus,
} from "../../execution-types";
import { isExecutionInProgress } from "../../executions/execution-helpers";
export type AnalysisColumnStat = {
column_name: string;
column_type: string;
simple_dtype: string;
num_unique: number | null;
num_null: number | null;
input_tokens_mean: number | null;
output_tokens_mean: number | null;
};
export type ModelUsageRow = {
model: string;
input: number | null;
output: number | null;
};
export const PREVIEW_DATASET_PAGE_SIZE = 20;
export const TERMINAL_STICKY_BOTTOM_THRESHOLD_PX = 24;
export function formatTimestamp(value: number): string {
return new Date(value).toLocaleString();
}
export function formatCellValue(value: unknown): string {
if (value === null || value === undefined) {
return "--";
}
if (typeof value === "string") {
return value;
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
export function isExpandableCellValue(value: string): boolean {
return value.length > 180;
}
export function truncateCellValue(value: string): string {
if (value.length <= 180) {
return value;
}
return `${value.slice(0, 180).trimEnd()}...`;
}
function parseNumber(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function parseString(value: unknown): string {
return typeof value === "string" && value.length > 0 ? value : "--";
}
export function parseAnalysisColumns(
analysis: RecipeExecutionAnalysis | null,
): AnalysisColumnStat[] {
const items = Array.isArray(analysis?.column_statistics)
? analysis.column_statistics
: [];
return items
.map((item) => {
if (!item || typeof item !== "object" || Array.isArray(item)) {
return null;
}
const row = item as Record<string, unknown>;
return {
column_name: parseString(row.column_name),
column_type: parseString(row.column_type),
simple_dtype: parseString(row.simple_dtype),
num_unique: parseNumber(row.num_unique),
num_null: parseNumber(row.num_null),
input_tokens_mean: parseNumber(row.input_tokens_mean),
output_tokens_mean: parseNumber(row.output_tokens_mean),
};
})
.filter((item): item is AnalysisColumnStat => item !== null);
}
export function statusTone(status: RecipeExecutionStatus): string {
if (status === "completed") {
return "bg-emerald-100 text-emerald-700";
}
if (status === "error" || status === "cancelled") {
return "bg-red-100 text-red-700";
}
if (isExecutionInProgress(status)) {
return "bg-amber-100 text-amber-700";
}
return "bg-muted text-muted-foreground";
}
export function statusRightBorder(status: RecipeExecutionStatus): string {
if (status === "completed") {
return "border-r-emerald-500";
}
if (status === "error" || status === "cancelled") {
return "border-r-red-500";
}
if (isExecutionInProgress(status)) {
return "border-r-amber-500";
}
return "border-r-border";
}
export function formatStatus(status: RecipeExecutionStatus): string {
if (status === "cancelled") {
return "cancelled";
}
return status;
}
export function formatPercent(value: number | null | undefined): string {
if (typeof value !== "number" || Number.isNaN(value)) {
return "--";
}
return `${value.toFixed(1)}%`;
}
export function formatDuration(startedAt: number, finishedAt: number | null): string {
if (!finishedAt || finishedAt <= startedAt) {
return "--";
}
const seconds = Math.round((finishedAt - startedAt) / 1000);
return `${seconds}s`;
}
export function formatMetricValue(value: number | null | undefined): string {
if (typeof value !== "number" || Number.isNaN(value)) {
return "--";
}
return value.toLocaleString();
}
export function parseModelUsageRows(
value: Record<string, unknown> | null,
): ModelUsageRow[] {
if (!value) {
return [];
}
return Object.entries(value)
.map(([name, data]) => {
if (!data || typeof data !== "object" || Array.isArray(data)) {
return null;
}
const modelObj = data as Record<string, unknown>;
const tokens =
modelObj.tokens &&
typeof modelObj.tokens === "object" &&
!Array.isArray(modelObj.tokens)
? (modelObj.tokens as Record<string, unknown>)
: null;
const modelName =
typeof modelObj.model === "string" && modelObj.model.length > 0
? modelObj.model
: name;
return {
model: modelName,
input: parseNumber(tokens?.input),
output: parseNumber(tokens?.output),
};
})
.filter((item): item is ModelUsageRow => item !== null);
}

View file

@ -2,38 +2,37 @@ import { useEffect, useMemo, useRef, useState, type ReactElement } from "react";
import type { ColumnDef } from "@tanstack/react-table";
import {
CheckmarkCircle02Icon,
Database01Icon,
Database02Icon,
Flag02Icon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { DataTable } from "@/components/ui/data-table";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Progress } from "@/components/ui/progress";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import type {
RecipeExecutionAnalysis,
RecipeExecutionRecord,
RecipeExecutionStatus,
} from "../../execution-types";
import { isExecutionInProgress } from "../../executions/execution-helpers";
import { ExecutionColumnsTab } from "./execution-columns-tab";
import { ExecutionDataTab } from "./execution-data-tab";
import { ExecutionOverviewTab } from "./execution-overview-tab";
import { ExecutionRawTab } from "./execution-raw-tab";
import { ExecutionSidebar } from "./execution-sidebar";
import {
PREVIEW_DATASET_PAGE_SIZE,
TERMINAL_STICKY_BOTTOM_THRESHOLD_PX,
formatCellValue,
formatDuration,
formatPercent,
formatStatus,
formatTimestamp,
isExpandableCellValue,
parseAnalysisColumns,
parseModelUsageRows,
statusTone,
truncateCellValue,
} from "./executions-view-helpers";
type ExecutionsViewProps = {
executions: RecipeExecutionRecord[];
@ -44,170 +43,6 @@ type ExecutionsViewProps = {
onLoadDatasetPage: (id: string, page: number) => void;
};
type AnalysisColumnStat = {
column_name: string;
column_type: string;
simple_dtype: string;
num_unique: number | null;
num_null: number | null;
input_tokens_mean: number | null;
output_tokens_mean: number | null;
};
type ModelUsageRow = {
model: string;
input: number | null;
output: number | null;
};
const PREVIEW_DATASET_PAGE_SIZE = 20;
const TERMINAL_STICKY_BOTTOM_THRESHOLD_PX = 24;
function formatTimestamp(value: number): string {
return new Date(value).toLocaleString();
}
function formatCellValue(value: unknown): string {
if (value === null || value === undefined) {
return "--";
}
if (typeof value === "string") {
return value;
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
function isExpandableCellValue(value: string): boolean {
return value.length > 180;
}
function truncateCellValue(value: string): string {
if (value.length <= 180) {
return value;
}
return `${value.slice(0, 180).trimEnd()}...`;
}
function parseNumber(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function parseString(value: unknown): string {
return typeof value === "string" && value.length > 0 ? value : "--";
}
function parseAnalysisColumns(analysis: RecipeExecutionAnalysis | null): AnalysisColumnStat[] {
const items = Array.isArray(analysis?.column_statistics)
? analysis.column_statistics
: [];
return items
.map((item) => {
if (!item || typeof item !== "object" || Array.isArray(item)) {
return null;
}
const row = item as Record<string, unknown>;
return {
column_name: parseString(row.column_name),
column_type: parseString(row.column_type),
simple_dtype: parseString(row.simple_dtype),
num_unique: parseNumber(row.num_unique),
num_null: parseNumber(row.num_null),
input_tokens_mean: parseNumber(row.input_tokens_mean),
output_tokens_mean: parseNumber(row.output_tokens_mean),
};
})
.filter((item): item is AnalysisColumnStat => item !== null);
}
function statusTone(status: RecipeExecutionStatus): string {
if (status === "completed") {
return "bg-emerald-100 text-emerald-700";
}
if (status === "error" || status === "cancelled") {
return "bg-red-100 text-red-700";
}
if (isExecutionInProgress(status)) {
return "bg-amber-100 text-amber-700";
}
return "bg-muted text-muted-foreground";
}
function statusRightBorder(status: RecipeExecutionStatus): string {
if (status === "completed") {
return "border-r-emerald-500";
}
if (status === "error" || status === "cancelled") {
return "border-r-red-500";
}
if (isExecutionInProgress(status)) {
return "border-r-amber-500";
}
return "border-r-border";
}
function formatStatus(status: RecipeExecutionStatus): string {
if (status === "cancelled") {
return "cancelled";
}
return status;
}
function formatPercent(value: number | null | undefined): string {
if (typeof value !== "number" || Number.isNaN(value)) {
return "--";
}
return `${value.toFixed(1)}%`;
}
function formatDuration(startedAt: number, finishedAt: number | null): string {
if (!finishedAt || finishedAt <= startedAt) {
return "--";
}
const seconds = Math.round((finishedAt - startedAt) / 1000);
return `${seconds}s`;
}
function formatMetricValue(value: number | null | undefined): string {
if (typeof value !== "number" || Number.isNaN(value)) {
return "--";
}
return value.toLocaleString();
}
function parseModelUsageRows(value: Record<string, unknown> | null): ModelUsageRow[] {
if (!value) {
return [];
}
return Object.entries(value)
.map(([name, data]) => {
if (!data || typeof data !== "object" || Array.isArray(data)) {
return null;
}
const modelObj = data as Record<string, unknown>;
const tokens =
modelObj.tokens && typeof modelObj.tokens === "object" && !Array.isArray(modelObj.tokens)
? (modelObj.tokens as Record<string, unknown>)
: null;
const modelName =
typeof modelObj.model === "string" && modelObj.model.length > 0
? modelObj.model
: name;
return {
model: modelName,
input: parseNumber(tokens?.input),
output: parseNumber(tokens?.output),
};
})
.filter((item): item is ModelUsageRow => item !== null);
}
export function ExecutionsView({
executions,
selectedExecutionId,
@ -476,53 +311,11 @@ export function ExecutionsView({
return (
<div className="flex h-full min-h-0">
<aside className="w-72 shrink-0 border-r">
<div className="flex items-center justify-between border-b px-3 py-2">
<p className="text-xs font-semibold uppercase text-muted-foreground">
Executions
</p>
</div>
<div className="h-[calc(100%-45px)] overflow-auto p-2">
{executions.length === 0 ? (
<div className="rounded-xl border border-dashed p-3 text-xs text-muted-foreground">
No executions yet.
</div>
) : (
executions.map((execution) => (
<button
key={execution.id}
type="button"
onClick={() => onSelectExecution(execution.id)}
className={cn(
"mb-2 w-full rounded-xl corner-squircle border border-r-4 p-3 text-left",
selectedExecutionId === execution.id
? "border-primary/50 bg-primary/5"
: "hover:bg-muted/40",
statusRightBorder(execution.status),
)}
>
<div className="mb-2 flex items-center justify-between gap-2">
<p className="truncate text-sm font-medium capitalize">
{execution.kind}
</p>
<Badge
variant="secondary"
className={cn("capitalize", statusTone(execution.status))}
>
{formatStatus(execution.status)}
</Badge>
</div>
<p className="text-xs text-muted-foreground">
{execution.rows} rows
</p>
<p className="text-xs text-muted-foreground">
{formatTimestamp(execution.createdAt)}
</p>
</button>
))
)}
</div>
</aside>
<ExecutionSidebar
executions={executions}
selectedExecutionId={selectedExecutionId}
onSelectExecution={onSelectExecution}
/>
<section className="min-w-0 flex-1 overflow-auto p-4">
{!selectedExecution ? (
<div className="rounded-xl border border-dashed p-4 text-sm text-muted-foreground">
@ -540,7 +333,9 @@ export function ExecutionsView({
</Badge>
<span>{selectedExecution.rows} rows</span>
<span>Started {formatTimestamp(selectedExecution.createdAt)}</span>
<span>Duration {formatDuration(selectedExecution.createdAt, selectedExecution.finishedAt)}</span>
<span>
Duration {formatDuration(selectedExecution.createdAt, selectedExecution.finishedAt)}
</span>
{selectedExecution.stage && (
<span>
Stage: {selectedExecution.stage}
@ -595,18 +390,10 @@ export function ExecutionsView({
progressComplete ? "text-emerald-900" : "text-amber-900",
)}
>
<p>
Done: {selectedExecution.progress?.done ?? "--"}
</p>
<p>
Total: {selectedExecution.progress?.total ?? "--"}
</p>
<p>
Rate: {selectedExecution.progress?.rate ?? "--"} rec/s
</p>
<p>
ETA: {selectedExecution.progress?.eta_sec ?? "--"} s
</p>
<p>Done: {selectedExecution.progress?.done ?? "--"}</p>
<p>Total: {selectedExecution.progress?.total ?? "--"}</p>
<p>Rate: {selectedExecution.progress?.rate ?? "--"} rec/s</p>
<p>ETA: {selectedExecution.progress?.eta_sec ?? "--"} s</p>
</div>
{selectedExecution.current_column && selectedExecution.column_progress && (
<p
@ -659,380 +446,96 @@ export function ExecutionsView({
</Button>
)}
</div>
<TabsContent value="overview" className="mt-3 space-y-3">
{showSummaryCards && (
<div className="space-y-3">
<div className="grid gap-3 md:grid-cols-2">
<div className="h-full rounded-lg bg-muted/20 p-3">
<div className="mb-2 flex items-center justify-between">
<p className="text-xs text-muted-foreground">Run summary</p>
<HugeiconsIcon
icon={Database01Icon}
className="size-4 text-muted-foreground"
/>
</div>
<div className="space-y-1 text-xs">
<p>
Records:{" "}
<span className="font-semibold">
{formatMetricValue(recordsMetric)} / {formatMetricValue(totalMetric)}
</span>
</p>
<p>
Duration: <span className="font-semibold">{runDuration}</span>
</p>
<p>
Columns analyzed:{" "}
<span className="font-semibold">
{formatMetricValue(columnCount)}
</span>
</p>
<p>
Final stage:{" "}
<span className="font-semibold">
{selectedExecution.stage ?? "--"}
</span>
</p>
</div>
</div>
<div className="h-full rounded-lg bg-muted/20 p-3">
<div className="mb-2 flex items-center justify-between">
<p className="text-xs text-muted-foreground">Insights</p>
<HugeiconsIcon
icon={Database02Icon}
className="size-4 text-muted-foreground"
/>
</div>
<div className="space-y-2 text-xs">
<p>
LLM columns:{" "}
<span className="font-semibold">
{formatMetricValue(llmColumnCount)}
</span>
</p>
<p>
Null rate:{" "}
<span className="font-semibold">{formatPercent(nullRate)}</span>
</p>
<p>
Dropped columns:{" "}
<span className="font-semibold">
{formatMetricValue(sideEffects.length)}
</span>
</p>
{sideEffects.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{sideEffects.map((name) => (
<Badge key={name} variant="outline">
{name}
</Badge>
))}
</div>
)}
<p>
Low uniqueness flags:{" "}
<span className="font-semibold">
{formatMetricValue(lowUniquenessColumns.length)}
</span>
</p>
{lowUniquenessColumns.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{lowUniquenessColumns.slice(0, 3).map((name) => (
<Badge key={name} variant="secondary">
{name}
</Badge>
))}
{lowUniquenessColumns.length > 3 && (
<Badge variant="secondary">
+{lowUniquenessColumns.length - 3} more
</Badge>
)}
</div>
)}
</div>
</div>
</div>
<div className="rounded-lg bg-muted/20 p-3">
<div className="mb-2 flex items-center justify-between">
<p className="text-xs text-muted-foreground">Model usage</p>
<HugeiconsIcon
icon={Flag02Icon}
className="size-4 text-muted-foreground"
/>
</div>
{modelUsageRows.length === 0 ? (
<p className="text-xs text-muted-foreground">No model usage yet.</p>
) : (
<div className="space-y-2 text-xs">
<div className="grid grid-cols-2 gap-2">
<div className="rounded border bg-muted/30 px-2 py-1.5">
<p className="text-muted-foreground">Total input</p>
<p className="text-sm font-semibold">
{formatMetricValue(totalInputTokens)}
</p>
</div>
<div className="rounded border bg-muted/30 px-2 py-1.5">
<p className="text-muted-foreground">Total output</p>
<p className="text-sm font-semibold">
{formatMetricValue(totalOutputTokens)}
</p>
</div>
</div>
<div className="overflow-hidden rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Model</TableHead>
<TableHead className="text-right">Input</TableHead>
<TableHead className="text-right">Output</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{modelUsageRows.map((usage) => (
<TableRow key={usage.model}>
<TableCell className="max-w-[320px] truncate">
{usage.model}
</TableCell>
<TableCell className="text-right">
{formatMetricValue(usage.input)}
</TableCell>
<TableCell className="text-right">
{formatMetricValue(usage.output)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)}
</div>
</div>
)}
<div className="overflow-hidden rounded-xl corner-squircle border">
<div className="flex items-center justify-between border-b px-3 py-2">
<p className="text-sm font-semibold">Terminal output</p>
<p className="text-xs text-muted-foreground">
{terminalLines.length} lines
</p>
</div>
<div
ref={terminalRef}
className="max-h-72 overflow-auto bg-zinc-900/80 px-3 py-2 font-mono text-xs text-zinc-200"
onScroll={(event) => {
const element = event.currentTarget;
const distanceFromBottom =
element.scrollHeight - element.scrollTop - element.clientHeight;
shouldStickTerminalToBottomRef.current =
distanceFromBottom <= TERMINAL_STICKY_BOTTOM_THRESHOLD_PX;
}}
>
{terminalLines.length === 0 ? (
<p className="text-zinc-400">
{isExecutionInProgress(selectedExecution.status)
? "Waiting for logs..."
: "No logs captured."}
</p>
) : (
terminalLines.map((line, index) => (
<p
key={`${index}-${line.slice(0, 24)}`}
className="whitespace-pre-wrap break-words leading-relaxed"
>
{line}
</p>
))
)}
</div>
</div>
<TabsContent value="overview">
<ExecutionOverviewTab
execution={selectedExecution}
showSummaryCards={showSummaryCards}
recordsMetric={recordsMetric}
totalMetric={totalMetric}
runDuration={runDuration}
columnCount={columnCount}
llmColumnCount={llmColumnCount}
nullRate={nullRate}
sideEffects={sideEffects}
lowUniquenessColumns={lowUniquenessColumns}
modelUsageRows={modelUsageRows}
totalInputTokens={totalInputTokens}
totalOutputTokens={totalOutputTokens}
terminalLines={terminalLines}
terminalRef={terminalRef}
onTerminalScroll={(event) => {
const element = event.currentTarget;
const distanceFromBottom =
element.scrollHeight - element.scrollTop - element.clientHeight;
shouldStickTerminalToBottomRef.current =
distanceFromBottom <= TERMINAL_STICKY_BOTTOM_THRESHOLD_PX;
}}
/>
</TabsContent>
<TabsContent value="columns" className="mt-3">
<div className="rounded-xl border p-3">
<p className="mb-2 text-sm font-semibold">Column statistics</p>
{analysisColumns.length === 0 ? (
<p className="text-xs text-muted-foreground">
No column statistics yet.
</p>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Column</TableHead>
<TableHead>Type</TableHead>
<TableHead>Data type</TableHead>
<TableHead>Unique</TableHead>
<TableHead>Nulls</TableHead>
<TableHead>Input tok avg</TableHead>
<TableHead>Output tok avg</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{analysisColumns.map((column) => (
<TableRow key={column.column_name}>
<TableCell>{column.column_name}</TableCell>
<TableCell>{column.column_type}</TableCell>
<TableCell>{column.simple_dtype}</TableCell>
<TableCell>{column.num_unique ?? "--"}</TableCell>
<TableCell>{column.num_null ?? "--"}</TableCell>
<TableCell>{column.input_tokens_mean ?? "--"}</TableCell>
<TableCell>{column.output_tokens_mean ?? "--"}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
<TabsContent value="columns">
<ExecutionColumnsTab analysisColumns={analysisColumns} />
</TabsContent>
<TabsContent value="data" className="mt-3">
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
<p className="text-sm font-semibold">Dataset sample</p>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
{datasetColumnNames.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="button" size="sm" variant="outline">
Columns
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Visible columns</DropdownMenuLabel>
{datasetColumnNames.map((columnName) => (
<DropdownMenuCheckboxItem
key={columnName}
checked={!hiddenDatasetColumns.includes(columnName)}
onSelect={(event) => {
event.preventDefault();
}}
onCheckedChange={(checked) => {
const selectedId = selectedExecution?.id;
if (!selectedId) {
return;
}
setHiddenDatasetColumnsByExecution((current) => {
const currentColumns = current[selectedId] ?? [];
const nextColumns = checked
? currentColumns.filter((name) => name !== columnName)
: [...currentColumns, columnName];
return {
...current,
[selectedId]: nextColumns,
};
});
}}
>
{columnName}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
{canPageDataset && selectedExecution && (
<>
<span>
Page {currentDatasetPage}/{totalPages}
</span>
<Button
type="button"
size="sm"
variant="outline"
disabled={
isExecutionInProgress(selectedExecution.status) ||
currentDatasetPage <= 1
}
onClick={() => {
if (selectedExecution.kind === "preview") {
const selectedId = selectedExecution.id;
setPreviewDatasetPageByExecution((current) => ({
...current,
[selectedId]: Math.max(1, currentDatasetPage - 1),
}));
return;
}
onLoadDatasetPage(selectedExecution.id, currentDatasetPage - 1);
}}
>
Prev
</Button>
<Button
type="button"
size="sm"
variant="outline"
disabled={
isExecutionInProgress(selectedExecution.status) ||
currentDatasetPage >= totalPages
}
onClick={() => {
if (selectedExecution.kind === "preview") {
const selectedId = selectedExecution.id;
setPreviewDatasetPageByExecution((current) => ({
...current,
[selectedId]: Math.min(totalPages, currentDatasetPage + 1),
}));
return;
}
onLoadDatasetPage(selectedExecution.id, currentDatasetPage + 1);
}}
>
Next
</Button>
</>
)}
</div>
</div>
{selectedExecution.dataset.length === 0 ? (
<p className="text-xs text-muted-foreground">No rows returned.</p>
) : tableColumns.length === 0 ? (
<p className="text-xs text-muted-foreground">
All columns hidden. Use Columns to show at least one.
</p>
) : (
<div className="max-h-[55vh] overflow-auto">
<DataTable
columns={tableColumns}
data={datasetRowsForTable}
getRowClassName={(row, _rowIndex, rowId) => {
const canExpand = visibleDatasetColumnNames.some((columnName) =>
isExpandableCellValue(formatCellValue(row[columnName])),
);
if (!canExpand) {
return undefined;
}
return cn(
"cursor-pointer",
expandedDatasetRows[rowId]
? "bg-primary/[0.05]"
: "hover:bg-primary/[0.06]",
);
}}
onRowClick={(row, _rowIndex, rowId) => {
const canExpand = visibleDatasetColumnNames.some((columnName) =>
isExpandableCellValue(formatCellValue(row[columnName])),
);
if (!canExpand || !selectedExecutionIdSafe) {
return;
}
setExpandedDatasetRowsByExecution((current) => {
const rows = current[selectedExecutionIdSafe] ?? {};
return {
...current,
[selectedExecutionIdSafe]: {
...rows,
[rowId]: !rows[rowId],
},
};
});
}}
/>
</div>
)}
<TabsContent value="data">
<ExecutionDataTab
execution={selectedExecution}
datasetColumnNames={datasetColumnNames}
hiddenDatasetColumns={hiddenDatasetColumns}
canPageDataset={canPageDataset}
currentDatasetPage={currentDatasetPage}
totalPages={totalPages}
tableColumns={tableColumns}
datasetRowsForTable={datasetRowsForTable}
visibleDatasetColumnNames={visibleDatasetColumnNames}
expandedDatasetRows={expandedDatasetRows}
selectedExecutionIdSafe={selectedExecutionIdSafe}
onSetHiddenColumns={(updater) => {
const selectedId = selectedExecution.id;
setHiddenDatasetColumnsByExecution((current) => {
const currentColumns = current[selectedId] ?? [];
return {
...current,
[selectedId]: updater(currentColumns),
};
});
}}
onPrevPage={() => {
if (selectedExecution.kind === "preview") {
const selectedId = selectedExecution.id;
setPreviewDatasetPageByExecution((current) => ({
...current,
[selectedId]: Math.max(1, currentDatasetPage - 1),
}));
return;
}
onLoadDatasetPage(selectedExecution.id, currentDatasetPage - 1);
}}
onNextPage={() => {
if (selectedExecution.kind === "preview") {
const selectedId = selectedExecution.id;
setPreviewDatasetPageByExecution((current) => ({
...current,
[selectedId]: Math.min(totalPages, currentDatasetPage + 1),
}));
return;
}
onLoadDatasetPage(selectedExecution.id, currentDatasetPage + 1);
}}
onToggleRowExpanded={(rowId) => {
setExpandedDatasetRowsByExecution((current) => {
const rows = current[selectedExecution.id] ?? {};
return {
...current,
[selectedExecution.id]: {
...rows,
[rowId]: !rows[rowId],
},
};
});
}}
/>
</TabsContent>
<TabsContent value="raw" className="mt-3">
<div className="rounded-xl border p-3">
<p className="mb-2 text-sm font-semibold">Raw execution</p>
<pre className="max-h-96 overflow-auto rounded-md bg-muted/40 p-3 text-xs">
{JSON.stringify(rawExecution, null, 2)}
</pre>
</div>
<TabsContent value="raw">
<ExecutionRawTab rawExecution={rawExecution} />
</TabsContent>
</Tabs>
)}