From 2a82259034c06827150a4abc9dc61ecf564083d8 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Sun, 5 Jul 2026 15:32:17 -0700 Subject: [PATCH 1/8] test(ui): characterize DataTable behavior before shadcn reskin Pins the shared view_logs DataTable contract with library-agnostic queries ahead of the tremor-to-shadcn table migration: loading and empty states, TanStack column defs with custom cell renderers, onRowClick payload, both expansion render paths (colspan sub-component and sibling child rows), the getRowCanExpand gate, and client-side sorting on and off. These must pass unchanged after the reskin. --- .../src/components/view_logs/table.test.tsx | 157 +++++++++++++++++- 1 file changed, 155 insertions(+), 2 deletions(-) diff --git a/ui/litellm-dashboard/src/components/view_logs/table.test.tsx b/ui/litellm-dashboard/src/components/view_logs/table.test.tsx index da9bcef1455..198f1c9d0da 100644 --- a/ui/litellm-dashboard/src/components/view_logs/table.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/table.test.tsx @@ -1,6 +1,7 @@ import type { ColumnDef } from "@tanstack/react-table"; -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; +import { render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; import { DataTable } from "./table"; type Row = { request_id: string; a: string; b: string }; @@ -17,6 +18,20 @@ const unsizedColumns: ColumnDef[] = [ { header: "B", accessorKey: "b" }, ]; +const expanderColumn: ColumnDef = { + id: "expander", + header: () => null, + cell: ({ row }) => + row.getCanExpand() ? ( + + ) : null, +}; + describe("DataTable column sizing", () => { it("min-widths the table to the column total and sizes every cell when columns declare sizes", () => { render(); @@ -44,3 +59,141 @@ describe("DataTable column sizing", () => { } }); }); + +describe("DataTable states", () => { + it("shows the loading message instead of rows while loading", () => { + render(); + + expect(screen.getByText("Fetching things")).toBeInTheDocument(); + expect(screen.queryByText("alpha")).not.toBeInTheDocument(); + }); + + it("shows the no-data message when there are no rows", () => { + render(); + + expect(screen.getByText("Nothing here")).toBeInTheDocument(); + }); + + it("renders row data through plain TanStack column defs, including custom cell renderers", () => { + const columns: ColumnDef[] = [ + { header: "A", accessorKey: "a" }, + { header: "B", cell: ({ row }) => custom:{row.original.b} }, + ]; + render(); + + expect(screen.getByText("alpha")).toBeInTheDocument(); + expect(screen.getByText("custom:beta")).toBeInTheDocument(); + }); +}); + +describe("DataTable row interaction", () => { + it("fires onRowClick with the row's original data", async () => { + const user = userEvent.setup(); + const onRowClick = vi.fn(); + render(); + + await user.click(screen.getByText("alpha")); + + expect(onRowClick).toHaveBeenCalledExactlyOnceWith(data[0]); + }); +}); + +describe("DataTable expansion", () => { + const rows: Row[] = [ + { request_id: "r1", a: "alpha", b: "beta" }, + { request_id: "r2", a: "gamma", b: "delta" }, + ]; + + it("toggles the sub-component in a full-width cell (colspan path)", async () => { + const user = userEvent.setup(); + render( + true} + renderSubComponent={({ row }) =>
details for {row.original.request_id}
} + />, + ); + + expect(screen.queryByText("details for r1")).not.toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: "expand r1" })); + const details = screen.getByText("details for r1"); + expect(details).toBeInTheDocument(); + expect(screen.queryByText("details for r2")).not.toBeInTheDocument(); + + const detailCell = details.closest("td"); + expect(detailCell).toHaveAttribute("colspan", "3"); + + await user.click(screen.getByRole("button", { name: "collapse r1" })); + expect(screen.queryByText("details for r1")).not.toBeInTheDocument(); + }); + + it("renders child rows as sibling table rows (child-rows path)", async () => { + const user = userEvent.setup(); + render( + true} + renderChildRows={({ row }) => ( + + child of {row.original.request_id} + + )} + />, + ); + + await user.click(screen.getByRole("button", { name: "expand r2" })); + + const childCell = screen.getByText("child of r2"); + expect(childCell.closest("tr")).not.toBeNull(); + expect(within(screen.getByRole("table")).getByText("child of r2")).toBeInTheDocument(); + }); + + it("does not expand rows when getRowCanExpand is missing even if a renderer is provided", () => { + render( +
details for {row.original.request_id}
} + />, + ); + + expect(screen.queryByRole("button", { name: "expand r1" })).not.toBeInTheDocument(); + }); +}); + +describe("DataTable sorting", () => { + const rows: Row[] = [ + { request_id: "r1", a: "bravo", b: "2" }, + { request_id: "r2", a: "alpha", b: "1" }, + { request_id: "r3", a: "charlie", b: "3" }, + ]; + + const firstColumnValues = () => + screen + .getAllByRole("row") + .slice(1) + .map((row) => within(row).getAllByRole("cell")[0].textContent); + + it("leaves row order untouched when sorting is disabled", async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByText("A")); + + expect(firstColumnValues()).toEqual(["bravo", "alpha", "charlie"]); + }); + + it("sorts ascending then descending on header clicks when enabled", async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByText("A")); + expect(firstColumnValues()).toEqual(["alpha", "bravo", "charlie"]); + + await user.click(screen.getByText("A")); + expect(firstColumnValues()).toEqual(["charlie", "bravo", "alpha"]); + }); +}); From 6664c410112541de8579b62c0e75573cbd120a1c Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Sun, 5 Jul 2026 15:46:09 -0700 Subject: [PATCH 2/8] refactor(ui): reskin shared DataTable from tremor onto shadcn table primitives Swaps the view_logs DataTable's presentational layer from @tremor/react to the in-repo components/ui/table primitives and hardens the seam that every later table migration copies: - getRowId is injected instead of hardcoded to request_id through an any cast; identity defaults to the row index and the logs page now passes request_id explicitly, keeping expansion state attached to the right row across refetch reorders - one expansion render path: renderChildRows had zero consumers and is removed; renderSubComponent (colspan cell) is the single path - the four consumers passing dead no-op renderSubComponent and getRowCanExpand boilerplate drop it - loading and empty defaults become generic (Loading... / No results) instead of log-specific The characterization tests from the previous commit pass unchanged except the dead child-rows path test, replaced by a reorder-stability test for injected getRowId plus coverage of the new generic defaults. First tremor removal of the tables track; view_logs/table.tsx no longer imports @tremor/react. --- ui/litellm-dashboard/eslint-metrics.json | 2 +- ui/litellm-dashboard/eslint-suppressions.json | 5 -- .../components/EntityUsage/TopKeyView.tsx | 8 +-- .../components/EntityUsage/TopModelView.tsx | 8 +-- .../components/mcp_tools/MCPToolsetsTab.tsx | 2 - .../src/components/pass_through_settings.tsx | 2 - .../src/components/view_logs/index.tsx | 1 + .../src/components/view_logs/table.test.tsx | 37 ++++++++++---- .../src/components/view_logs/table.tsx | 50 ++++++++----------- 9 files changed, 50 insertions(+), 65 deletions(-) diff --git a/ui/litellm-dashboard/eslint-metrics.json b/ui/litellm-dashboard/eslint-metrics.json index 6d18a36f754..c924405fca8 100644 --- a/ui/litellm-dashboard/eslint-metrics.json +++ b/ui/litellm-dashboard/eslint-metrics.json @@ -1,5 +1,5 @@ { - "@typescript-eslint/no-explicit-any": 1991, + "@typescript-eslint/no-explicit-any": 1989, "complexity": 128, "max-depth": 59, "no-console": 15 diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 1c8f92b720f..67b19471aaf 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -2077,11 +2077,6 @@ "count": 1 } }, - "src/components/view_logs/table.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/view_user_spend.tsx": { "react-hooks/set-state-in-effect": { "count": 2 diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.tsx index d0748583c30..ddafcde3b1b 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.tsx @@ -247,13 +247,7 @@ const TopKeyView: React.FC = ({ topKeys, teams, showTags = fals ) : (
- <>} - getRowCanExpand={() => false} - isLoading={false} - /> +
)} diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopModelView.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopModelView.tsx index c69ba42f182..1bb8ac1d95a 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopModelView.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopModelView.tsx @@ -99,13 +99,7 @@ export default function TopModelView({ topModels, topModelsLimit, setTopModelsLi ) : (
- <>} - getRowCanExpand={() => false} - isLoading={false} - /> +
)} diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx b/ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx index 0198b830229..546df9ebc4b 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx @@ -509,8 +509,6 @@ export function MCPToolsetsTab({ accessToken, userRole }: MCPToolsetsTabProps) {
} - getRowCanExpand={() => false} isLoading={isLoading} noDataMessage="No toolsets yet. Click 'New Toolset' to create one." loadingMessage="Loading toolsets..." diff --git a/ui/litellm-dashboard/src/components/pass_through_settings.tsx b/ui/litellm-dashboard/src/components/pass_through_settings.tsx index 0fdd9c632bf..63fe0f92961 100644 --- a/ui/litellm-dashboard/src/components/pass_through_settings.tsx +++ b/ui/litellm-dashboard/src/components/pass_through_settings.tsx @@ -263,8 +263,6 @@ const PassThroughSettings: React.FC = ({
} - getRowCanExpand={() => false} isLoading={false} noDataMessage="No pass-through endpoints configured" /> diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index 265e331e6a9..ee08712e56b 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -287,6 +287,7 @@ export default function SpendLogsTable({ accessToken, token, userRole, userID, p row.request_id} onRowClick={handleRowClick} isLoading={isLogsLoading} /> diff --git a/ui/litellm-dashboard/src/components/view_logs/table.test.tsx b/ui/litellm-dashboard/src/components/view_logs/table.test.tsx index 198f1c9d0da..f08da6da693 100644 --- a/ui/litellm-dashboard/src/components/view_logs/table.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/table.test.tsx @@ -74,6 +74,14 @@ describe("DataTable states", () => { expect(screen.getByText("Nothing here")).toBeInTheDocument(); }); + it("falls back to generic loading and empty defaults", () => { + const { rerender } = render(); + expect(screen.getByText("Loading...")).toBeInTheDocument(); + + rerender(); + expect(screen.getByText("No results")).toBeInTheDocument(); + }); + it("renders row data through plain TanStack column defs, including custom cell renderers", () => { const columns: ColumnDef[] = [ { header: "A", accessorKey: "a" }, @@ -129,26 +137,33 @@ describe("DataTable expansion", () => { expect(screen.queryByText("details for r1")).not.toBeInTheDocument(); }); - it("renders child rows as sibling table rows (child-rows path)", async () => { + it("keeps expansion attached to the same row through data reorders when getRowId is injected", async () => { const user = userEvent.setup(); - render( + const { rerender } = render( row.request_id} getRowCanExpand={() => true} - renderChildRows={({ row }) => ( - - child of {row.original.request_id} - - )} + renderSubComponent={({ row }) =>
details for {row.original.request_id}
} />, ); - await user.click(screen.getByRole("button", { name: "expand r2" })); + await user.click(screen.getByRole("button", { name: "expand r1" })); + expect(screen.getByText("details for r1")).toBeInTheDocument(); + + rerender( + row.request_id} + getRowCanExpand={() => true} + renderSubComponent={({ row }) =>
details for {row.original.request_id}
} + />, + ); - const childCell = screen.getByText("child of r2"); - expect(childCell.closest("tr")).not.toBeNull(); - expect(within(screen.getByRole("table")).getByText("child of r2")).toBeInTheDocument(); + expect(screen.getByText("details for r1")).toBeInTheDocument(); + expect(screen.queryByText("details for r2")).not.toBeInTheDocument(); }); it("does not expand rows when getRowCanExpand is missing even if a renderer is provided", () => { diff --git a/ui/litellm-dashboard/src/components/view_logs/table.tsx b/ui/litellm-dashboard/src/components/view_logs/table.tsx index 4510cc9a1f0..3a83dde0b5d 100644 --- a/ui/litellm-dashboard/src/components/view_logs/table.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/table.tsx @@ -10,16 +10,15 @@ import { SortingState, } from "@tanstack/react-table"; -import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react"; +import { Table, TableHeader, TableHead, TableBody, TableRow, TableCell } from "@/components/ui/table"; interface DataTableProps { data: TData[]; columns: ColumnDef[]; + getRowId?: (row: TData, index: number) => string; onRowClick?: (row: TData) => void; - /** Renders inside a single colspan cell (used by audit logs) */ + /** Renders inside a single colspan cell */ renderSubComponent?: (props: { row: Row }) => React.ReactElement; - /** Renders directly in tbody as sibling table rows (used by MCP children) */ - renderChildRows?: (props: { row: Row }) => React.ReactNode; getRowCanExpand?: (row: Row) => boolean; isLoading?: boolean; loadingMessage?: string; @@ -31,16 +30,16 @@ interface DataTableProps { export function DataTable({ data = [], columns, + getRowId, onRowClick, renderSubComponent, - renderChildRows, getRowCanExpand, isLoading = false, - loadingMessage = "🚅 Loading logs...", - noDataMessage = "No logs found", + loadingMessage = "Loading...", + noDataMessage = "No results", enableSorting = false, }: DataTableProps) { - const supportsExpansion = !!(renderSubComponent || renderChildRows) && !!getRowCanExpand; + const supportsExpansion = !!renderSubComponent && !!getRowCanExpand; const hasExplicitColumnSizes = columns.some((column) => column.size !== undefined); const [sorting, setSorting] = useState([]); @@ -55,24 +54,19 @@ export function DataTable({ enableSortingRemoval: false, }), ...(supportsExpansion && { getRowCanExpand }), - getRowId: (row: TData, index: number) => { - const _row: any = row as any; - return _row?.request_id ?? String(index); - }, + ...(getRowId && { getRowId }), getCoreRowModel: getCoreRowModel(), ...(enableSorting && { getSortedRowModel: getSortedRowModel() }), ...(supportsExpansion && { getExpandedRowModel: getExpandedRowModel() }), }); - const tableClassName = hasExplicitColumnSizes - ? "[&_td]:py-0.5 [&_th]:py-1 [&_table]:table-fixed" - : "[&_td]:py-0.5 [&_th]:py-1 table-fixed w-full box-border"; + const tableClassName = hasExplicitColumnSizes ? "table-fixed" : "table-fixed w-full box-border"; const tableStyle = hasExplicitColumnSizes ? { minWidth: table.getCenterTotalSize() } : { minWidth: "400px" }; return ( -
+
- + {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { @@ -80,9 +74,9 @@ export function DataTable({ const isSorted = header.column.getIsSorted(); return ( - @@ -90,23 +84,23 @@ export function DataTable({
{flexRender(header.column.columnDef.header, header.getContext())} {canSort && ( - + {isSorted === "asc" ? "↑" : isSorted === "desc" ? "↓" : "⇅"} )}
)} -
+
); })} ))} - + {isLoading ? ( -
+

{loadingMessage}

@@ -115,7 +109,7 @@ export function DataTable({ table.getRowModel().rows.map((row) => ( onRowClick?.(row.original)} > {row.getVisibleCells().map((cell) => ( @@ -129,11 +123,7 @@ export function DataTable({ ))} - {/* Child rows rendered as real table rows (MCP children) */} - {supportsExpansion && row.getIsExpanded() && renderChildRows && renderChildRows({ row })} - - {/* Legacy sub-component in colspan cell (audit logs) */} - {supportsExpansion && row.getIsExpanded() && renderSubComponent && !renderChildRows && ( + {supportsExpansion && row.getIsExpanded() && renderSubComponent && (
{renderSubComponent({ row })}
@@ -145,7 +135,7 @@ export function DataTable({ ) : ( -
+

{noDataMessage}

From 545cd3b349704a1914a7ccd4fe617bfbc7448c2b Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Tue, 7 Jul 2026 11:56:22 -0700 Subject: [PATCH 3/8] test(ui): assert child rows hidden before expansion in DataTable test --- ui/litellm-dashboard/src/components/view_logs/table.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/litellm-dashboard/src/components/view_logs/table.test.tsx b/ui/litellm-dashboard/src/components/view_logs/table.test.tsx index 198f1c9d0da..7299d280769 100644 --- a/ui/litellm-dashboard/src/components/view_logs/table.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/table.test.tsx @@ -144,6 +144,8 @@ describe("DataTable expansion", () => { />, ); + expect(screen.queryByText("child of r2")).not.toBeInTheDocument(); + await user.click(screen.getByRole("button", { name: "expand r2" })); const childCell = screen.getByText("child of r2"); From d9b899ef0972eabaea90d513b87a2c071dffb623 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Tue, 7 Jul 2026 12:15:06 -0700 Subject: [PATCH 4/8] fix(ui): suppress row hover on DataTable placeholder rows --- .../src/components/view_logs/table.test.tsx | 21 +++++++++++++++++++ .../src/components/view_logs/table.tsx | 6 +++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/ui/litellm-dashboard/src/components/view_logs/table.test.tsx b/ui/litellm-dashboard/src/components/view_logs/table.test.tsx index f08da6da693..ef7d26596ff 100644 --- a/ui/litellm-dashboard/src/components/view_logs/table.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/table.test.tsx @@ -82,6 +82,27 @@ describe("DataTable states", () => { expect(screen.getByText("No results")).toBeInTheDocument(); }); + it("suppresses the primitive's row hover on loading, empty, and expansion placeholder rows", async () => { + const user = userEvent.setup(); + const { rerender } = render(); + expect(screen.getByText("Loading...").closest("tr")).toHaveClass("hover:bg-transparent"); + + rerender(); + expect(screen.getByText("No results").closest("tr")).toHaveClass("hover:bg-transparent"); + + rerender( + true} + renderSubComponent={({ row }) =>
details for {row.original.request_id}
} + />, + ); + await user.click(screen.getByRole("button", { name: "expand r1" })); + expect(screen.getByText("details for r1").closest("tr")).toHaveClass("hover:bg-transparent"); + expect(screen.getByText("alpha").closest("tr")).not.toHaveClass("hover:bg-transparent"); + }); + it("renders row data through plain TanStack column defs, including custom cell renderers", () => { const columns: ColumnDef[] = [ { header: "A", accessorKey: "a" }, diff --git a/ui/litellm-dashboard/src/components/view_logs/table.tsx b/ui/litellm-dashboard/src/components/view_logs/table.tsx index 3a83dde0b5d..bead37000f9 100644 --- a/ui/litellm-dashboard/src/components/view_logs/table.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/table.tsx @@ -98,7 +98,7 @@ export function DataTable({ {isLoading ? ( - +

{loadingMessage}

@@ -124,7 +124,7 @@ export function DataTable({ {supportsExpansion && row.getIsExpanded() && renderSubComponent && ( - +
{renderSubComponent({ row })}
@@ -133,7 +133,7 @@ export function DataTable({ )) ) : ( - +

{noDataMessage}

From f9f67c229aa93f58e566db0e6f83ae3631f1e7fa Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Tue, 7 Jul 2026 12:53:59 -0700 Subject: [PATCH 5/8] feat(ui): polish DataTable with skeleton loading, header band, and numeric column alignment --- .../components/EntityUsage/TopKeyView.tsx | 1 + .../components/EntityUsage/TopModelView.tsx | 4 ++ .../components/mcp_tools/MCPToolsetsTab.tsx | 1 - .../src/components/view_logs/columns.tsx | 6 ++- .../src/components/view_logs/table.test.tsx | 36 +++++++++---- .../src/components/view_logs/table.tsx | 51 ++++++++++++------- 6 files changed, 69 insertions(+), 30 deletions(-) diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.tsx index ddafcde3b1b..40bc41b3e8c 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.tsx @@ -164,6 +164,7 @@ const TopKeyView: React.FC = ({ topKeys, teams, showTags = fals const spendColumn = { header: "Spend (USD)", accessorKey: "spend", + meta: { numeric: true }, cell: (info: any) => { const value = info.getValue(); return value > 0 && value < 0.01 ? "<$0.01" : `$${formatNumberWithCommas(value, 2)}`; diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopModelView.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopModelView.tsx index 1bb8ac1d95a..7562ef06a03 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopModelView.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopModelView.tsx @@ -30,6 +30,7 @@ export default function TopModelView({ topModels, topModelsLimit, setTopModelsLi { header: "Spend (USD)", accessorKey: "spend", + meta: { numeric: true }, cell: (info: any) => { const value = info.getValue(); return `$${formatNumberWithCommas(value, 2)}`; @@ -38,16 +39,19 @@ export default function TopModelView({ topModels, topModelsLimit, setTopModelsLi { header: "Successful", accessorKey: "successful_requests", + meta: { numeric: true }, cell: (info: any) => {info.getValue()?.toLocaleString() || 0}, }, { header: "Failed", accessorKey: "failed_requests", + meta: { numeric: true }, cell: (info: any) => {info.getValue()?.toLocaleString() || 0}, }, { header: "Tokens", accessorKey: "tokens", + meta: { numeric: true }, cell: (info: any) => info.getValue()?.toLocaleString() || 0, }, ]; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx b/ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx index 546df9ebc4b..f60a9858eac 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx @@ -511,7 +511,6 @@ export function MCPToolsetsTab({ accessToken, userRole }: MCPToolsetsTabProps) { columns={columns} isLoading={isLoading} noDataMessage="No toolsets yet. Click 'New Toolset' to create one." - loadingMessage="Loading toolsets..." enableSorting={true} /> diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.tsx index 0508c562df8..ffbc83e5c51 100644 --- a/ui/litellm-dashboard/src/components/view_logs/columns.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/columns.tsx @@ -231,13 +231,14 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef[] : "Cost", accessorKey: "spend", size: 110, + meta: { numeric: true }, cell: (info: any) => { const row = info.row.original; const mcpCount = row.mcp_tool_call_count || 0; const mcpSpend = row.mcp_tool_call_spend || 0; return ( -
+
{getSpendString(info.getValue() || 0)} @@ -263,6 +264,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef[] ) : "Duration (s)", accessorKey: "request_duration_ms", + meta: { numeric: true }, cell: (info: any) => { const ms = info.getValue(); if (ms == null) return -; @@ -287,6 +289,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef[] ) : "TTFT (s)", accessorKey: "completionStartTime", + meta: { numeric: true }, cell: (info: any) => { const row = info.row.original; const completionStartTime = info.getValue(); @@ -395,6 +398,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef[] : "Tokens", accessorKey: "total_tokens", size: 140, + meta: { numeric: true }, cell: (info: any) => { const row = info.row.original; return ( diff --git a/ui/litellm-dashboard/src/components/view_logs/table.test.tsx b/ui/litellm-dashboard/src/components/view_logs/table.test.tsx index ef7d26596ff..1fcdc86c606 100644 --- a/ui/litellm-dashboard/src/components/view_logs/table.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/table.test.tsx @@ -2,7 +2,7 @@ import type { ColumnDef } from "@tanstack/react-table"; import { render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; -import { DataTable } from "./table"; +import { DataTable, SKELETON_ROW_COUNT } from "./table"; type Row = { request_id: string; a: string; b: string }; @@ -61,10 +61,11 @@ describe("DataTable column sizing", () => { }); describe("DataTable states", () => { - it("shows the loading message instead of rows while loading", () => { - render(); + it("renders skeleton rows mirroring the column layout while loading", () => { + const { container } = render(); - expect(screen.getByText("Fetching things")).toBeInTheDocument(); + const skeletons = container.querySelectorAll('[data-slot="skeleton"]'); + expect(skeletons).toHaveLength(SKELETON_ROW_COUNT * unsizedColumns.length); expect(screen.queryByText("alpha")).not.toBeInTheDocument(); }); @@ -74,18 +75,15 @@ describe("DataTable states", () => { expect(screen.getByText("Nothing here")).toBeInTheDocument(); }); - it("falls back to generic loading and empty defaults", () => { - const { rerender } = render(); - expect(screen.getByText("Loading...")).toBeInTheDocument(); - - rerender(); + it("falls back to the generic empty default", () => { + render(); expect(screen.getByText("No results")).toBeInTheDocument(); }); it("suppresses the primitive's row hover on loading, empty, and expansion placeholder rows", async () => { const user = userEvent.setup(); - const { rerender } = render(); - expect(screen.getByText("Loading...").closest("tr")).toHaveClass("hover:bg-transparent"); + const { rerender, container } = render(); + expect(container.querySelector('[data-slot="skeleton"]')?.closest("tr")).toHaveClass("hover:bg-transparent"); rerender(); expect(screen.getByText("No results").closest("tr")).toHaveClass("hover:bg-transparent"); @@ -113,6 +111,22 @@ describe("DataTable states", () => { expect(screen.getByText("alpha")).toBeInTheDocument(); expect(screen.getByText("custom:beta")).toBeInTheDocument(); }); + + it("right-aligns headers and cells with tabular figures for numeric meta columns", () => { + const columns: ColumnDef[] = [ + { header: "A", accessorKey: "a" }, + { header: "B", accessorKey: "b", meta: { numeric: true } }, + ]; + render(); + + const headers = screen.getAllByRole("columnheader"); + expect(headers[1].querySelector("div")).toHaveClass("justify-end"); + expect(headers[0].querySelector("div")).not.toHaveClass("justify-end"); + + const cells = screen.getAllByRole("cell"); + expect(cells[1]).toHaveClass("text-right", "tabular-nums"); + expect(cells[0]).not.toHaveClass("text-right"); + }); }); describe("DataTable row interaction", () => { diff --git a/ui/litellm-dashboard/src/components/view_logs/table.tsx b/ui/litellm-dashboard/src/components/view_logs/table.tsx index bead37000f9..01dc61eda66 100644 --- a/ui/litellm-dashboard/src/components/view_logs/table.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/table.tsx @@ -1,6 +1,7 @@ import { Fragment, useState } from "react"; import { ColumnDef, + RowData, flexRender, getCoreRowModel, getExpandedRowModel, @@ -11,6 +12,15 @@ import { } from "@tanstack/react-table"; import { Table, TableHeader, TableHead, TableBody, TableRow, TableCell } from "@/components/ui/table"; +import { Skeleton } from "@/components/ui/skeleton"; + +declare module "@tanstack/react-table" { + interface ColumnMeta { + numeric?: boolean; + } +} + +export const SKELETON_ROW_COUNT = 8; interface DataTableProps { data: TData[]; @@ -21,7 +31,6 @@ interface DataTableProps { renderSubComponent?: (props: { row: Row }) => React.ReactElement; getRowCanExpand?: (row: Row) => boolean; isLoading?: boolean; - loadingMessage?: string; noDataMessage?: string; /** Enable client-side column sorting (defaults to false to avoid conflicts with server-side sorting) */ enableSorting?: boolean; @@ -35,7 +44,6 @@ export function DataTable({ renderSubComponent, getRowCanExpand, isLoading = false, - loadingMessage = "Loading...", noDataMessage = "No results", enableSorting = false, }: DataTableProps) { @@ -68,20 +76,23 @@ export function DataTable({
{table.getHeaderGroups().map((headerGroup) => ( - + {headerGroup.headers.map((header) => { const canSort = enableSorting && header.column.getCanSort(); const isSorted = header.column.getIsSorted(); + const numeric = header.column.columnDef.meta?.numeric; return ( {header.isPlaceholder ? null : ( -
+
{flexRender(header.column.columnDef.header, header.getContext())} {canSort && ( @@ -98,13 +109,19 @@ export function DataTable({ {isLoading ? ( - - -
-

{loadingMessage}

-
-
-
+ Array.from({ length: SKELETON_ROW_COUNT }).map((_, rowIndex) => ( + + {table.getVisibleLeafColumns().map((column) => ( + + + + ))} + + )) ) : table.getRowModel().rows.length > 0 ? ( table.getRowModel().rows.map((row) => ( @@ -115,7 +132,9 @@ export function DataTable({ {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} @@ -134,10 +153,8 @@ export function DataTable({ )) ) : ( - -
-

{noDataMessage}

-
+ +

{noDataMessage}

)} From ecb95a0b83faacdf912ea95dc8f5999006379a7a Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Tue, 7 Jul 2026 13:14:09 -0700 Subject: [PATCH 6/8] feat(ui): shape DataTable skeletons per column and keep stale rows during refetch --- .../src/components/view_logs/columns.tsx | 5 +++ .../src/components/view_logs/index.tsx | 3 +- .../src/components/view_logs/table.test.tsx | 22 ++++++++++++ .../src/components/view_logs/table.tsx | 34 +++++++++++++++++-- 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.tsx index ffbc83e5c51..e36fb6b91f2 100644 --- a/ui/litellm-dashboard/src/components/view_logs/columns.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/columns.tsx @@ -120,12 +120,14 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef[] : "Time", accessorKey: "startTime", size: 200, + meta: { skeleton: { width: "w-36" } }, cell: (info: any) => , }, { header: "Type", id: "type", size: 90, + meta: { skeleton: { variant: "pill", width: "w-12" } }, cell: (info: any) => { const row = info.row.original; const sessionCount = row.session_total_count || 1; @@ -171,6 +173,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef[] header: "Status", accessorKey: "metadata.status", size: 100, + meta: { skeleton: { variant: "pill" } }, cell: (info: any) => { const status = info.getValue() || "Success"; const isSuccess = status.toLowerCase() !== "failure"; @@ -190,6 +193,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef[] header: "Session ID", accessorKey: "session_id", size: 120, + meta: { skeleton: { variant: "pill", width: "w-20" } }, cell: (info: any) => { const value = String(info.getValue() || ""); const onSessionClick = info.row.original.onSessionClick; @@ -360,6 +364,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef[] : "Model", accessorKey: "model", size: 200, + meta: { skeleton: { variant: "avatar" } }, cell: (info: any) => { const row = info.row.original; const provider = row.custom_llm_provider; diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index ee08712e56b..e51c9147ce7 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -289,7 +289,8 @@ export default function SpendLogsTable({ accessToken, token, userRole, userID, p data={deferredData} getRowId={(row) => row.request_id} onRowClick={handleRowClick} - isLoading={isLogsLoading} + isLoading={logsQuery.isLoading} + isRefetching={isRefiltering} />
diff --git a/ui/litellm-dashboard/src/components/view_logs/table.test.tsx b/ui/litellm-dashboard/src/components/view_logs/table.test.tsx index 1fcdc86c606..8d4eafbdbc7 100644 --- a/ui/litellm-dashboard/src/components/view_logs/table.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/table.test.tsx @@ -69,6 +69,28 @@ describe("DataTable states", () => { expect(screen.queryByText("alpha")).not.toBeInTheDocument(); }); + it("shapes skeleton cells from column meta: pill, numeric, and text variants", () => { + const columns: ColumnDef[] = [ + { header: "A", accessorKey: "a", meta: { skeleton: { variant: "pill" } } }, + { header: "B", accessorKey: "b", meta: { numeric: true } }, + { header: "C", accessorKey: "a" }, + ]; + const { container } = render(); + + const firstRowSkeletons = container.querySelectorAll("tbody tr:first-child [data-slot='skeleton']"); + expect(firstRowSkeletons[0]).toHaveClass("rounded-full"); + expect(firstRowSkeletons[1]).toHaveClass("ml-auto"); + expect(firstRowSkeletons[2]).toHaveClass("w-2/3"); + }); + + it("keeps stale rows visible with a fade instead of skeletons while refetching", () => { + const { container } = render(); + + expect(screen.getByText("alpha")).toBeInTheDocument(); + expect(container.querySelector('[data-slot="skeleton"]')).not.toBeInTheDocument(); + expect(container.querySelector("tbody")).toHaveClass("opacity-60"); + }); + it("shows the no-data message when there are no rows", () => { render(); diff --git a/ui/litellm-dashboard/src/components/view_logs/table.tsx b/ui/litellm-dashboard/src/components/view_logs/table.tsx index 01dc61eda66..df12739c184 100644 --- a/ui/litellm-dashboard/src/components/view_logs/table.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/table.tsx @@ -17,11 +17,38 @@ import { Skeleton } from "@/components/ui/skeleton"; declare module "@tanstack/react-table" { interface ColumnMeta { numeric?: boolean; + skeleton?: SkeletonMeta; } } +type SkeletonMeta = { + variant?: "text" | "pill" | "number" | "avatar"; + width?: string; +}; + export const SKELETON_ROW_COUNT = 8; +function SkeletonCell({ numeric, skeleton }: { numeric?: boolean; skeleton?: SkeletonMeta }) { + const variant = skeleton?.variant ?? (numeric ? "number" : "text"); + const width = skeleton?.width; + + if (variant === "pill") { + return ; + } + if (variant === "number") { + return ; + } + if (variant === "avatar") { + return ( +
+ + +
+ ); + } + return ; +} + interface DataTableProps { data: TData[]; columns: ColumnDef[]; @@ -31,6 +58,8 @@ interface DataTableProps { renderSubComponent?: (props: { row: Row }) => React.ReactElement; getRowCanExpand?: (row: Row) => boolean; isLoading?: boolean; + /** Stale rows stay visible with a subtle fade while fresh data loads (no skeleton wipe) */ + isRefetching?: boolean; noDataMessage?: string; /** Enable client-side column sorting (defaults to false to avoid conflicts with server-side sorting) */ enableSorting?: boolean; @@ -44,6 +73,7 @@ export function DataTable({ renderSubComponent, getRowCanExpand, isLoading = false, + isRefetching = false, noDataMessage = "No results", enableSorting = false, }: DataTableProps) { @@ -107,7 +137,7 @@ export function DataTable({ ))} - + {isLoading ? ( Array.from({ length: SKELETON_ROW_COUNT }).map((_, rowIndex) => ( @@ -117,7 +147,7 @@ export function DataTable({ className="py-0.5 first:pl-4 last:pr-4" style={hasExplicitColumnSizes ? { width: column.getSize() } : undefined} > - + ))} From eaf965168be70799f214964650a774c9c9e63620 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Tue, 7 Jul 2026 13:28:29 -0700 Subject: [PATCH 7/8] revert(ui): drop DataTable skeleton loading, restore text loading row --- .../components/mcp_tools/MCPToolsetsTab.tsx | 1 + .../src/components/view_logs/columns.tsx | 5 -- .../src/components/view_logs/index.tsx | 3 +- .../src/components/view_logs/table.test.tsx | 42 ++++---------- .../src/components/view_logs/table.tsx | 57 ++++--------------- 5 files changed, 23 insertions(+), 85 deletions(-) diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx b/ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx index f60a9858eac..546df9ebc4b 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx @@ -511,6 +511,7 @@ export function MCPToolsetsTab({ accessToken, userRole }: MCPToolsetsTabProps) { columns={columns} isLoading={isLoading} noDataMessage="No toolsets yet. Click 'New Toolset' to create one." + loadingMessage="Loading toolsets..." enableSorting={true} /> diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.tsx index e36fb6b91f2..ffbc83e5c51 100644 --- a/ui/litellm-dashboard/src/components/view_logs/columns.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/columns.tsx @@ -120,14 +120,12 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef[] : "Time", accessorKey: "startTime", size: 200, - meta: { skeleton: { width: "w-36" } }, cell: (info: any) => , }, { header: "Type", id: "type", size: 90, - meta: { skeleton: { variant: "pill", width: "w-12" } }, cell: (info: any) => { const row = info.row.original; const sessionCount = row.session_total_count || 1; @@ -173,7 +171,6 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef[] header: "Status", accessorKey: "metadata.status", size: 100, - meta: { skeleton: { variant: "pill" } }, cell: (info: any) => { const status = info.getValue() || "Success"; const isSuccess = status.toLowerCase() !== "failure"; @@ -193,7 +190,6 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef[] header: "Session ID", accessorKey: "session_id", size: 120, - meta: { skeleton: { variant: "pill", width: "w-20" } }, cell: (info: any) => { const value = String(info.getValue() || ""); const onSessionClick = info.row.original.onSessionClick; @@ -364,7 +360,6 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef[] : "Model", accessorKey: "model", size: 200, - meta: { skeleton: { variant: "avatar" } }, cell: (info: any) => { const row = info.row.original; const provider = row.custom_llm_provider; diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index e51c9147ce7..ee08712e56b 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -289,8 +289,7 @@ export default function SpendLogsTable({ accessToken, token, userRole, userID, p data={deferredData} getRowId={(row) => row.request_id} onRowClick={handleRowClick} - isLoading={logsQuery.isLoading} - isRefetching={isRefiltering} + isLoading={isLogsLoading} />
diff --git a/ui/litellm-dashboard/src/components/view_logs/table.test.tsx b/ui/litellm-dashboard/src/components/view_logs/table.test.tsx index 8d4eafbdbc7..314dd9454e8 100644 --- a/ui/litellm-dashboard/src/components/view_logs/table.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/table.test.tsx @@ -2,7 +2,7 @@ import type { ColumnDef } from "@tanstack/react-table"; import { render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; -import { DataTable, SKELETON_ROW_COUNT } from "./table"; +import { DataTable } from "./table"; type Row = { request_id: string; a: string; b: string }; @@ -61,51 +61,31 @@ describe("DataTable column sizing", () => { }); describe("DataTable states", () => { - it("renders skeleton rows mirroring the column layout while loading", () => { - const { container } = render(); + it("shows the loading message instead of rows while loading", () => { + render(); - const skeletons = container.querySelectorAll('[data-slot="skeleton"]'); - expect(skeletons).toHaveLength(SKELETON_ROW_COUNT * unsizedColumns.length); + expect(screen.getByText("Fetching things")).toBeInTheDocument(); expect(screen.queryByText("alpha")).not.toBeInTheDocument(); }); - it("shapes skeleton cells from column meta: pill, numeric, and text variants", () => { - const columns: ColumnDef[] = [ - { header: "A", accessorKey: "a", meta: { skeleton: { variant: "pill" } } }, - { header: "B", accessorKey: "b", meta: { numeric: true } }, - { header: "C", accessorKey: "a" }, - ]; - const { container } = render(); - - const firstRowSkeletons = container.querySelectorAll("tbody tr:first-child [data-slot='skeleton']"); - expect(firstRowSkeletons[0]).toHaveClass("rounded-full"); - expect(firstRowSkeletons[1]).toHaveClass("ml-auto"); - expect(firstRowSkeletons[2]).toHaveClass("w-2/3"); - }); - - it("keeps stale rows visible with a fade instead of skeletons while refetching", () => { - const { container } = render(); - - expect(screen.getByText("alpha")).toBeInTheDocument(); - expect(container.querySelector('[data-slot="skeleton"]')).not.toBeInTheDocument(); - expect(container.querySelector("tbody")).toHaveClass("opacity-60"); - }); - it("shows the no-data message when there are no rows", () => { render(); expect(screen.getByText("Nothing here")).toBeInTheDocument(); }); - it("falls back to the generic empty default", () => { - render(); + it("falls back to generic loading and empty defaults", () => { + const { rerender } = render(); + expect(screen.getByText("Loading...")).toBeInTheDocument(); + + rerender(); expect(screen.getByText("No results")).toBeInTheDocument(); }); it("suppresses the primitive's row hover on loading, empty, and expansion placeholder rows", async () => { const user = userEvent.setup(); - const { rerender, container } = render(); - expect(container.querySelector('[data-slot="skeleton"]')?.closest("tr")).toHaveClass("hover:bg-transparent"); + const { rerender } = render(); + expect(screen.getByText("Loading...").closest("tr")).toHaveClass("hover:bg-transparent"); rerender(); expect(screen.getByText("No results").closest("tr")).toHaveClass("hover:bg-transparent"); diff --git a/ui/litellm-dashboard/src/components/view_logs/table.tsx b/ui/litellm-dashboard/src/components/view_logs/table.tsx index df12739c184..ee90f0975af 100644 --- a/ui/litellm-dashboard/src/components/view_logs/table.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/table.tsx @@ -12,43 +12,13 @@ import { } from "@tanstack/react-table"; import { Table, TableHeader, TableHead, TableBody, TableRow, TableCell } from "@/components/ui/table"; -import { Skeleton } from "@/components/ui/skeleton"; declare module "@tanstack/react-table" { interface ColumnMeta { numeric?: boolean; - skeleton?: SkeletonMeta; } } -type SkeletonMeta = { - variant?: "text" | "pill" | "number" | "avatar"; - width?: string; -}; - -export const SKELETON_ROW_COUNT = 8; - -function SkeletonCell({ numeric, skeleton }: { numeric?: boolean; skeleton?: SkeletonMeta }) { - const variant = skeleton?.variant ?? (numeric ? "number" : "text"); - const width = skeleton?.width; - - if (variant === "pill") { - return ; - } - if (variant === "number") { - return ; - } - if (variant === "avatar") { - return ( -
- - -
- ); - } - return ; -} - interface DataTableProps { data: TData[]; columns: ColumnDef[]; @@ -58,8 +28,7 @@ interface DataTableProps { renderSubComponent?: (props: { row: Row }) => React.ReactElement; getRowCanExpand?: (row: Row) => boolean; isLoading?: boolean; - /** Stale rows stay visible with a subtle fade while fresh data loads (no skeleton wipe) */ - isRefetching?: boolean; + loadingMessage?: string; noDataMessage?: string; /** Enable client-side column sorting (defaults to false to avoid conflicts with server-side sorting) */ enableSorting?: boolean; @@ -73,7 +42,7 @@ export function DataTable({ renderSubComponent, getRowCanExpand, isLoading = false, - isRefetching = false, + loadingMessage = "Loading...", noDataMessage = "No results", enableSorting = false, }: DataTableProps) { @@ -137,21 +106,15 @@ export function DataTable({
))}
- + {isLoading ? ( - Array.from({ length: SKELETON_ROW_COUNT }).map((_, rowIndex) => ( - - {table.getVisibleLeafColumns().map((column) => ( - - - - ))} - - )) + + +
+

{loadingMessage}

+
+
+
) : table.getRowModel().rows.length > 0 ? ( table.getRowModel().rows.map((row) => ( From 4a36b23e548cc1e05f4d713eb49a817f4e9075d7 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Tue, 7 Jul 2026 14:53:09 -0700 Subject: [PATCH 8/8] fix(ui): clip DataTable to its rounded wrapper and right-align Duration/TTFT values --- ui/litellm-dashboard/src/components/view_logs/columns.tsx | 4 ++-- .../src/components/view_logs/table.test.tsx | 7 +++++++ ui/litellm-dashboard/src/components/view_logs/table.tsx | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.tsx index ffbc83e5c51..7452992ed59 100644 --- a/ui/litellm-dashboard/src/components/view_logs/columns.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/columns.tsx @@ -271,7 +271,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef[] const seconds = (ms / 1000).toFixed(2); return ( - {seconds} + {seconds} ); }, @@ -301,7 +301,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef[] const ttftSeconds = (ttftMs / 1000).toFixed(2); return ( - {ttftSeconds} + {ttftSeconds} ); }, diff --git a/ui/litellm-dashboard/src/components/view_logs/table.test.tsx b/ui/litellm-dashboard/src/components/view_logs/table.test.tsx index 314dd9454e8..9a8469cfeba 100644 --- a/ui/litellm-dashboard/src/components/view_logs/table.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/table.test.tsx @@ -114,6 +114,13 @@ describe("DataTable states", () => { expect(screen.getByText("custom:beta")).toBeInTheDocument(); }); + it("clips the table to the rounded wrapper so the header band cannot bleed past the corners", () => { + const { container } = render(); + + const wrapper = container.firstElementChild; + expect(wrapper).toHaveClass("rounded-lg", "overflow-hidden"); + }); + it("right-aligns headers and cells with tabular figures for numeric meta columns", () => { const columns: ColumnDef[] = [ { header: "A", accessorKey: "a" }, diff --git a/ui/litellm-dashboard/src/components/view_logs/table.tsx b/ui/litellm-dashboard/src/components/view_logs/table.tsx index ee90f0975af..c96f34f9b93 100644 --- a/ui/litellm-dashboard/src/components/view_logs/table.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/table.tsx @@ -71,7 +71,7 @@ export function DataTable({ const tableStyle = hasExplicitColumnSizes ? { minWidth: table.getCenterTotalSize() } : { minWidth: "400px" }; return ( -
+
{table.getHeaderGroups().map((headerGroup) => (