diff --git a/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.test.tsx b/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.test.tsx
index dff968334f7..a99c44a98a6 100644
--- a/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.test.tsx
+++ b/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.test.tsx
@@ -1,5 +1,6 @@
-import { fireEvent, render, screen } from "@testing-library/react";
-import { describe, expect, it, vi } from "vitest";
+import { fireEvent, render, screen, within } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../../../../../tests/test-utils";
import { Inspector } from "@/components/shared/Inspector";
@@ -51,6 +52,30 @@ describe("AgentTracesTable empty state", () => {
});
});
+describe("AgentTracesTable loading state", () => {
+ it("fills the first page load with skeleton rows instead of an empty table", () => {
+ render(
+ inList(
+ ,
+ ),
+ );
+ expect(screen.getByRole("status")).toHaveTextContent("Loading runs…");
+ const placeholders = screen.getAllByTestId("runs-placeholder");
+ expect(placeholders.length).toBeGreaterThanOrEqual(8);
+ const columnCount = screen.getAllByRole("columnheader").length;
+ expect(within(placeholders[0]).getAllByRole("cell", { hidden: true })).toHaveLength(columnCount);
+ expect(screen.queryByText(/No runs/)).not.toBeInTheDocument();
+ });
+});
+
describe("AgentTracesTable virtualization", () => {
const template = (traceList as TracePage).data[0] as TraceSummary;
const manyRuns: TraceSummary[] = Array.from({ length: 500 }, (_, i) => ({
@@ -84,3 +109,71 @@ describe("AgentTracesTable virtualization", () => {
expect(screen.queryByText("question 0")).not.toBeInTheDocument();
});
});
+
+describe("AgentTracesTable column picker", () => {
+ const runs = (traceList as TracePage).data as TraceSummary[];
+ const renderRuns = () =>
+ renderWithProviders(
+ inList(
+ ,
+ ),
+ );
+ const headers = () => screen.getAllByRole("columnheader").map((header) => header.textContent);
+
+ beforeEach(() => localStorage.clear());
+
+ it("hides a column from the header picker and keeps it hidden after a remount", async () => {
+ const user = userEvent.setup();
+ const { unmount } = renderRuns();
+ const before = headers().length;
+ expect(headers()).toContain("Cost");
+
+ await user.click(screen.getByRole("button", { name: "Columns" }));
+ expect(screen.queryByTestId("view-option-time")).not.toBeInTheDocument();
+ expect(screen.queryByTestId("view-option-agent")).not.toBeInTheDocument();
+ expect(screen.queryByTestId("view-option-open")).not.toBeInTheDocument();
+ await user.click(await screen.findByTestId("view-option-cost"));
+
+ expect(headers()).not.toContain("Cost");
+ expect(headers()).toHaveLength(before - 1);
+ const [firstRow] = screen.getAllByTestId("agent-trace-row");
+ expect(within(firstRow).getAllByRole("cell")).toHaveLength(before - 1);
+
+ unmount();
+ renderRuns();
+ expect(headers()).not.toContain("Cost");
+ expect(headers()).toHaveLength(before - 1);
+ });
+
+ it("shapes loading skeletons to the columns still visible", async () => {
+ const user = userEvent.setup();
+ const { unmount } = renderRuns();
+ await user.click(screen.getByRole("button", { name: "Columns" }));
+ await user.click(await screen.findByTestId("view-option-cost"));
+ unmount();
+
+ render(
+ inList(
+ ,
+ ),
+ );
+ const columnCount = screen.getAllByRole("columnheader").length;
+ const [placeholder] = screen.getAllByTestId("runs-placeholder");
+ expect(within(placeholder).getAllByRole("cell", { hidden: true })).toHaveLength(columnCount);
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.tsx b/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.tsx
index 7d860837bab..1d1334b7881 100644
--- a/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.tsx
+++ b/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesTable.tsx
@@ -5,8 +5,11 @@ import { ArrowDown, ChevronRight } from "lucide-react";
import { useEffect } from "react";
import { useInView } from "react-intersection-observer";
+import { DataTableViewOptions } from "@/components/shared/DataTable/DataTableViewOptions";
+import { usePersistedColumnVisibility } from "@/components/shared/DataTable/usePersistedColumnVisibility";
import { InspectorTable, useInspectorTable } from "@/components/shared/InspectorTable";
import { Button } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
import { formatActivityTimestamp, formatRunTimestamp, localTimeZoneAbbreviation } from "@/utils/activityTimestamp";
import { SpanIcon } from "../ui/SpanIcon";
@@ -39,6 +42,7 @@ const runKey = (run: TraceSummary): string => run.trace_ref || run.trace_id;
const PREFETCH_MARGIN = "0px 0px 480px 0px";
const PLACEHOLDER_ROWS = [0, 1, 2];
+const SKELETON_ROWS = Array.from({ length: 12 }, (_, i) => i);
const ROW_HEIGHT = 36;
const MUTED_NUM = "font-mono text-muted-foreground";
const NUM = "font-mono text-foreground";
@@ -79,6 +83,7 @@ const RUN_COLUMNS: ColumnDef[] = [
{
id: "time",
size: 170,
+ enableHiding: false,
header: () => (
Time
@@ -92,9 +97,27 @@ const RUN_COLUMNS: ColumnDef[] = [
{formatRunTimestamp(row.original.start_time)}
),
- meta: { className: "font-mono tabular-nums text-muted-foreground" },
+ meta: {
+ title: "Time",
+ className: "font-mono tabular-nums text-muted-foreground",
+ renderSkeleton: () => ,
+ },
+ },
+ {
+ id: "agent",
+ size: 160,
+ enableHiding: false,
+ header: "Agent",
+ cell: ({ row }) => ,
+ meta: {
+ renderSkeleton: () => (
+
+
+
+
+ ),
+ },
},
- { id: "agent", size: 160, header: "Agent", cell: ({ row }) => },
{ id: "input", header: "Input", cell: ({ row }) => },
{
id: "agents",
@@ -139,26 +162,15 @@ const RUN_COLUMNS: ColumnDef[] = [
{
id: "open",
size: 32,
- header: "",
+ enableHiding: false,
+ header: ({ table }) => ,
cell: () => ,
- meta: { className: "px-0" },
+ meta: { className: "px-0", headerClassName: "px-1", renderSkeleton: () => null },
},
];
-function PlaceholderRow({ rowRef }: { rowRef?: (node: Element | null) => void }) {
- return (
-
- |
-
- |
-
-
- |
-
-
- |
-
- );
+function PlaceholderRow({ index, rowRef }: { index: number; rowRef?: (node: Element | null) => void }) {
+ return ;
}
function LoadMoreRows({ isFetching, onLoadMore }: { isFetching: boolean; onLoadMore: () => void }) {
@@ -167,7 +179,9 @@ function LoadMoreRows({ isFetching, onLoadMore }: { isFetching: boolean; onLoadM
useEffect(() => {
if (nearTail && !isFetching) onLoadMore();
}, [nearTail, isFetching, onLoadMore]);
- return PLACEHOLDER_ROWS.map((row) => );
+ return PLACEHOLDER_ROWS.map((row) => (
+
+ ));
}
function EmptyRuns({ rangeEmpty, onSetUpTracing }: { rangeEmpty: boolean; onSetUpTracing: () => void }) {
@@ -207,11 +221,14 @@ export function AgentTracesTable({
const isEmpty = settled && !hasMore && traces.length === 0;
const canContinue = settled && hasMore;
const autoContinue = canContinue && traces.length > 0;
+ const { columnVisibility, onColumnVisibilityChange } = usePersistedColumnVisibility("lens-traces");
const tableOptions: TableOptions = {
data: traces,
columns: RUN_COLUMNS,
getRowId: runKey,
autoResetAll: false,
+ state: { columnVisibility },
+ onColumnVisibilityChange,
getCoreRowModel: getCoreRowModel(),
};
const table = useReactTable(tableOptions);
@@ -221,7 +238,12 @@ export function AgentTracesTable({
rowHeight={() => ROW_HEIGHT}
- after={autoContinue && }
+ after={
+ <>
+ {isLoading && SKELETON_ROWS.map((row) => )}
+ {autoContinue && }
+ >
+ }
>
{(row) => (
- {isLoading && Loading runs…
}
+ {isLoading && (
+
+ Loading runs…
+
+ )}
{error && (
diff --git a/ui/litellm-dashboard/src/components/shared/DataTable/DataTableViewOptions.tsx b/ui/litellm-dashboard/src/components/shared/DataTable/DataTableViewOptions.tsx
index ca60e42d9aa..86d99fd72a6 100644
--- a/ui/litellm-dashboard/src/components/shared/DataTable/DataTableViewOptions.tsx
+++ b/ui/litellm-dashboard/src/components/shared/DataTable/DataTableViewOptions.tsx
@@ -5,14 +5,21 @@ import type { Table } from "@tanstack/react-table";
import { Check, Columns3 } from "lucide-react";
import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/cva.config";
interface DataTableViewOptionsProps {
table: Table;
label?: string;
+ iconOnly?: boolean;
className?: string;
}
-export function DataTableViewOptions({ table, label = "View", className }: DataTableViewOptionsProps) {
+export function DataTableViewOptions({
+ table,
+ label = "View",
+ iconOnly = false,
+ className,
+}: DataTableViewOptionsProps) {
const hideableColumns = table.getAllLeafColumns().filter((column) => column.getCanHide());
if (hideableColumns.length === 0) {
@@ -23,10 +30,26 @@ export function DataTableViewOptions({ table, label = "View", className }
-
- {label}
-
+ iconOnly ? (
+
+ ) : (
+
+ )
}
/>
diff --git a/ui/litellm-dashboard/src/components/shared/InspectorTable.test.tsx b/ui/litellm-dashboard/src/components/shared/InspectorTable.test.tsx
index 416d31bc940..8201248c078 100644
--- a/ui/litellm-dashboard/src/components/shared/InspectorTable.test.tsx
+++ b/ui/litellm-dashboard/src/components/shared/InspectorTable.test.tsx
@@ -104,3 +104,57 @@ describe("InspectorTable tree", () => {
expect(screen.getByText("Open: b1")).toBeInTheDocument();
});
});
+
+const SKELETON_COLUMNS: ColumnDef[] = [
+ { id: "name", header: "Name", meta: { renderSkeleton: () => } },
+ { id: "size", header: "Size", meta: { numeric: true } },
+];
+
+function SkeletonTree({ visibility }: { visibility: Record }) {
+ const tableOptions: TableOptions = {
+ data: TREE,
+ columns: SKELETON_COLUMNS,
+ getRowId: (node) => node.id,
+ state: { columnVisibility: visibility },
+ getCoreRowModel: getCoreRowModel(),
+ };
+ const table = useReactTable(tableOptions);
+ return (
+ node.id}
+ selected={null}
+ onSelectedChange={() => {}}
+ noun="node"
+ storageKey="inspector-skeleton-test"
+ >
+
+
+
+
+ rowHeight={() => 36}
+ after={}
+ >
+ {(row) => }
+
+
+
+
+ );
+}
+
+describe("InspectorTable.SkeletonRow", () => {
+ it("renders one hidden cell per visible leaf column and honors renderSkeleton", () => {
+ render();
+ const [row] = screen.getAllByTestId("skeleton-row");
+ expect(row).toHaveAttribute("aria-hidden", "true");
+ expect(within(row).getAllByRole("cell", { hidden: true })).toHaveLength(2);
+ expect(screen.getAllByTestId("custom-skeleton")).toHaveLength(1);
+ });
+
+ it("follows column visibility", () => {
+ render();
+ const [row] = screen.getAllByTestId("skeleton-row");
+ expect(within(row).getAllByRole("cell", { hidden: true })).toHaveLength(1);
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/shared/InspectorTable.tsx b/ui/litellm-dashboard/src/components/shared/InspectorTable.tsx
index d5443fe9ac1..059ad1673ee 100644
--- a/ui/litellm-dashboard/src/components/shared/InspectorTable.tsx
+++ b/ui/litellm-dashboard/src/components/shared/InspectorTable.tsx
@@ -8,6 +8,7 @@ import { ChevronRight } from "lucide-react";
import { createContext, Fragment, useContext, useState, type ComponentProps, type ReactNode } from "react";
import { Inspector } from "@/components/shared/Inspector";
+import { Skeleton } from "@/components/ui/skeleton";
import { TableBody, TableCell, TableHead, TableHeader } from "@/components/ui/table";
import { cn } from "@/lib/cva.config";
@@ -137,6 +138,37 @@ function Row({ row, item, className, ...props }: RowProps) {
);
}
+const SKELETON_WIDTHS = ["w-[58%]", "w-[44%]", "w-[70%]", "w-[50%]", "w-[64%]", "w-[48%]"] as const;
+
+type SkeletonRowProps = ComponentProps<"tr"> & { readonly index: number };
+
+/** One placeholder row shaped by the visible columns: `meta.renderSkeleton` wins, numeric cells right-align. */
+function SkeletonRow({ index, className, ...props }: SkeletonRowProps) {
+ const { table } = useInspectorTable();
+ return (
+
+ {table.getVisibleLeafColumns().map((column, position) => {
+ const meta = column.columnDef.meta;
+ return (
+
+ {meta?.renderSkeleton ? (
+ meta.renderSkeleton()
+ ) : (
+
+ )}
+
+ );
+ })}
+
+ );
+}
+
interface IndentProps {
readonly row: TanStackRow;
readonly toggleLabel?: (expanded: boolean) => string;
@@ -176,4 +208,4 @@ function Indent({ row, toggleLabel = (expanded) => (expanded ? "Collapse" : "
);
}
-export const InspectorTable = { Root, Grid, Header, Body, Row, Indent } as const;
+export const InspectorTable = { Root, Grid, Header, Body, Row, SkeletonRow, Indent } as const;