mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
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:
parent
a987704a48
commit
b0ff698add
2 changed files with 149 additions and 68 deletions
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue