refactor(ui): migrate Workflow Runs table onto shared DataTable

Proof-of-concept consumer for the shared DataTable added in the previous
commit. Swaps the antd Table in the Workflow Runs page for DataTable in
client-pagination mode, keeping the existing cell renderers, row-click
drawer, and empty state. Adds a focused test that the rows render through
DataTable, a row click routes the detail fetch to the correct run, and the
empty state shows.
This commit is contained in:
Yuneng Jiang 2026-07-09 15:21:52 -07:00
parent a987704a48
commit b0ff698add
No known key found for this signature in database
2 changed files with 149 additions and 68 deletions

View file

@ -0,0 +1,81 @@
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import WorkflowRuns from "./WorkflowRuns";
vi.mock("@/components/networking", () => ({ proxyBaseUrl: "" }));
interface FakeRun {
run_id: string;
status: string;
workflow_type: string;
created_at: string;
metadata: { title?: string; state?: string } | null;
}
const RUNS: FakeRun[] = [
{
run_id: "run-aaaaaaaa-1111",
status: "completed",
workflow_type: "grill",
created_at: "2026-01-01T00:00:00Z",
metadata: { title: "First run", state: "done" },
},
{
run_id: "run-bbbbbbbb-2222",
status: "running",
workflow_type: "autofix",
created_at: "2026-01-02T00:00:00Z",
metadata: null,
},
];
function mockFetch(runs: FakeRun[]) {
return vi.fn((url: string) => {
if (url.includes("/runs?limit")) {
return Promise.resolve({ ok: true, json: () => Promise.resolve({ runs }) });
}
if (url.includes("/events")) {
return Promise.resolve({ ok: true, json: () => Promise.resolve({ events: [] }) });
}
if (url.includes("/messages")) {
return Promise.resolve({ ok: true, json: () => Promise.resolve({ messages: [] }) });
}
return Promise.resolve({ ok: false, status: 404, json: () => Promise.resolve({}) });
});
}
afterEach(() => {
vi.unstubAllGlobals();
});
describe("WorkflowRuns (migrated onto shared DataTable)", () => {
it("renders one DataTable row per fetched run", async () => {
vi.stubGlobal("fetch", mockFetch(RUNS));
const { container } = render(<WorkflowRuns accessToken="tok" />);
expect(await screen.findByText("First run")).toBeInTheDocument();
expect(container.querySelectorAll("tr[data-row-id]")).toHaveLength(2);
});
it("opens the detail drawer for the clicked run by firing its detail fetch", async () => {
const user = userEvent.setup();
const fetchSpy = mockFetch(RUNS);
vi.stubGlobal("fetch", fetchSpy);
render(<WorkflowRuns accessToken="tok" />);
await user.click(await screen.findByText("First run"));
await waitFor(() =>
expect(fetchSpy).toHaveBeenCalledWith(expect.stringContaining("run-aaaaaaaa-1111/events"), expect.anything()),
);
});
it("shows the empty state when there are no runs", async () => {
vi.stubGlobal("fetch", mockFetch([]));
render(<WorkflowRuns accessToken="tok" />);
expect(await screen.findByText("No workflow runs yet")).toBeInTheDocument();
});
});

View file

@ -1,7 +1,9 @@
import React, { useState, useEffect, useCallback } from "react";
import { Button, Collapse, Drawer, Empty, Spin, Table, Tooltip, Typography } from "antd";
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Button, Collapse, Drawer, Empty, Spin, Tooltip, Typography } from "antd";
import { ReloadOutlined } from "@ant-design/icons";
import type { ColumnDef } from "@tanstack/react-table";
import { proxyBaseUrl } from "@/components/networking";
import { DataTable } from "@/components/shared/DataTable";
const { Text } = Typography;
@ -541,48 +543,54 @@ const WorkflowRuns: React.FC<WorkflowRunsProps> = ({ accessToken }) => {
fetchRuns();
}, [fetchRuns]);
const columns = [
{
title: "Run",
dataIndex: "run_id",
key: "run",
render: (_: string, run: WorkflowRun) => (
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<StatusDot status={run.status} size={7} />
<div>
<div style={{ fontSize: 13, color: "#18181b", fontWeight: 500, lineHeight: 1.4 }}>{runTitle(run)}</div>
<div style={{ fontFamily: "monospace", fontSize: 11, color: "#a1a1aa" }}>{shortId(run.run_id)}</div>
</div>
</div>
),
},
{
title: "Type",
dataIndex: "workflow_type",
key: "workflow_type",
render: (v: string) => <span style={{ fontFamily: "monospace", fontSize: 12, color: "#71717a" }}>{v}</span>,
},
{
title: "Status",
dataIndex: "status",
key: "status",
render: (status: RunStatus, run: WorkflowRun) => {
const state = run.metadata?.state;
return (
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<StatusDot status={status} size={7} />
<span style={{ fontSize: 12, color: "#52525b", textTransform: "capitalize" }}>{state ?? status}</span>
</div>
);
const columns = useMemo<ColumnDef<WorkflowRun, unknown>[]>(
() => [
{
id: "run",
header: "Run",
cell: ({ row }) => {
const run = row.original;
return (
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<StatusDot status={run.status} size={7} />
<div>
<div style={{ fontSize: 13, color: "#18181b", fontWeight: 500, lineHeight: 1.4 }}>{runTitle(run)}</div>
<div style={{ fontFamily: "monospace", fontSize: 11, color: "#a1a1aa" }}>{shortId(run.run_id)}</div>
</div>
</div>
);
},
},
},
{
title: "Created",
dataIndex: "created_at",
key: "created_at",
render: (v: string) => <span style={{ fontSize: 12, color: "#a1a1aa" }}>{timeAgo(v)}</span>,
},
];
{
accessorKey: "workflow_type",
header: "Type",
cell: ({ row }) => (
<span style={{ fontFamily: "monospace", fontSize: 12, color: "#71717a" }}>{row.original.workflow_type}</span>
),
},
{
id: "status",
header: "Status",
cell: ({ row }) => {
const run = row.original;
return (
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<StatusDot status={run.status} size={7} />
<span style={{ fontSize: 12, color: "#52525b", textTransform: "capitalize" }}>
{run.metadata?.state ?? run.status}
</span>
</div>
);
},
},
{
accessorKey: "created_at",
header: "Created",
cell: ({ row }) => <span style={{ fontSize: 12, color: "#a1a1aa" }}>{timeAgo(row.original.created_at)}</span>,
},
],
[],
);
return (
<div
@ -619,31 +627,23 @@ const WorkflowRuns: React.FC<WorkflowRunsProps> = ({ accessToken }) => {
</Button>
</div>
{/* runs table — matches logs page density */}
<div className="rounded-lg custom-border overflow-x-auto w-full">
<Table
dataSource={runs}
columns={columns}
rowKey="run_id"
loading={loadingRuns}
size="small"
pagination={{ pageSize: 50, hideOnSinglePage: true, size: "small" }}
onRow={(run) => ({
onClick: () => fetchRunDetail(run),
style: { cursor: "pointer" },
})}
locale={{
emptyText: (
<Empty
description={<span style={{ color: "#a1a1aa", fontSize: 13 }}>No workflow runs yet</span>}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
),
}}
className="[&_.ant-table-cell]:py-0.5 [&_.ant-table-thead_.ant-table-cell]:py-1"
style={{ border: "none" }}
/>
</div>
<DataTable
data={runs}
columns={columns}
getRowId={(run) => run.run_id}
isLoading={loadingRuns}
loadingMessage="Loading workflow runs…"
noDataMessage={
<Empty
description={<span style={{ color: "#a1a1aa", fontSize: 13 }}>No workflow runs yet</span>}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
}
paginationMode="client"
pageSizeOptions={[50, 100]}
onRowClick={fetchRunDetail}
size="compact"
/>
{/* detail drawer */}
<Drawer