From b0ff698addc9246a28f0f247669d487f43bb608f Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 9 Jul 2026 15:21:52 -0700 Subject: [PATCH] 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. --- .../workflows/WorkflowRuns.test.tsx | 81 +++++++++++ .../(dashboard)/workflows/WorkflowRuns.tsx | 136 +++++++++--------- 2 files changed, 149 insertions(+), 68 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/workflows/WorkflowRuns.test.tsx diff --git a/ui/litellm-dashboard/src/app/(dashboard)/workflows/WorkflowRuns.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/workflows/WorkflowRuns.test.tsx new file mode 100644 index 00000000000..e73abfe7cd6 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/workflows/WorkflowRuns.test.tsx @@ -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(); + + 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(); + + 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(); + + expect(await screen.findByText("No workflow runs yet")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/workflows/WorkflowRuns.tsx b/ui/litellm-dashboard/src/app/(dashboard)/workflows/WorkflowRuns.tsx index 2aecbece2e3..b668cdee0bd 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/workflows/WorkflowRuns.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/workflows/WorkflowRuns.tsx @@ -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 = ({ accessToken }) => { fetchRuns(); }, [fetchRuns]); - const columns = [ - { - title: "Run", - dataIndex: "run_id", - key: "run", - render: (_: string, run: WorkflowRun) => ( -
- -
-
{runTitle(run)}
-
{shortId(run.run_id)}
-
-
- ), - }, - { - title: "Type", - dataIndex: "workflow_type", - key: "workflow_type", - render: (v: string) => {v}, - }, - { - title: "Status", - dataIndex: "status", - key: "status", - render: (status: RunStatus, run: WorkflowRun) => { - const state = run.metadata?.state; - return ( -
- - {state ?? status} -
- ); + const columns = useMemo[]>( + () => [ + { + id: "run", + header: "Run", + cell: ({ row }) => { + const run = row.original; + return ( +
+ +
+
{runTitle(run)}
+
{shortId(run.run_id)}
+
+
+ ); + }, }, - }, - { - title: "Created", - dataIndex: "created_at", - key: "created_at", - render: (v: string) => {timeAgo(v)}, - }, - ]; + { + accessorKey: "workflow_type", + header: "Type", + cell: ({ row }) => ( + {row.original.workflow_type} + ), + }, + { + id: "status", + header: "Status", + cell: ({ row }) => { + const run = row.original; + return ( +
+ + + {run.metadata?.state ?? run.status} + +
+ ); + }, + }, + { + accessorKey: "created_at", + header: "Created", + cell: ({ row }) => {timeAgo(row.original.created_at)}, + }, + ], + [], + ); return (
= ({ accessToken }) => {
- {/* runs table — matches logs page density */} -
- ({ - onClick: () => fetchRunDetail(run), - style: { cursor: "pointer" }, - })} - locale={{ - emptyText: ( - No workflow runs yet} - image={Empty.PRESENTED_IMAGE_SIMPLE} - /> - ), - }} - className="[&_.ant-table-cell]:py-0.5 [&_.ant-table-thead_.ant-table-cell]:py-1" - style={{ border: "none" }} - /> - + run.run_id} + isLoading={loadingRuns} + loadingMessage="Loading workflow runs…" + noDataMessage={ + No workflow runs yet} + image={Empty.PRESENTED_IMAGE_SIMPLE} + /> + } + paginationMode="client" + pageSizeOptions={[50, 100]} + onRowClick={fetchRunDetail} + size="compact" + /> {/* detail drawer */}