mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
feat(ui): let Lens runs pick their columns from the table header
Adds an icon-only variant to the shared DataTableViewOptions picker so any InspectorTable can host it in a header cell, and wires it into the runs list with visibility saved per table. Time and Agent stay pinned visible. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f3387221ea
commit
93e6ccff8d
3 changed files with 84 additions and 8 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import { fireEvent, render, screen, within } from "@testing-library/react";
|
import { fireEvent, render, screen, within } from "@testing-library/react";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { renderWithProviders } from "../../../../../tests/test-utils";
|
import { renderWithProviders } from "../../../../../tests/test-utils";
|
||||||
import { Inspector } from "@/components/shared/Inspector";
|
import { Inspector } from "@/components/shared/Inspector";
|
||||||
|
|
@ -108,3 +109,46 @@ describe("AgentTracesTable virtualization", () => {
|
||||||
expect(screen.queryByText("question 0")).not.toBeInTheDocument();
|
expect(screen.queryByText("question 0")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("AgentTracesTable column picker", () => {
|
||||||
|
const runs = (traceList as TracePage).data as TraceSummary[];
|
||||||
|
const renderRuns = () =>
|
||||||
|
renderWithProviders(
|
||||||
|
inList(
|
||||||
|
<AgentTracesTable
|
||||||
|
traces={runs}
|
||||||
|
isLoading={false}
|
||||||
|
error={null}
|
||||||
|
hasMore={false}
|
||||||
|
onLoadMore={vi.fn()}
|
||||||
|
onSetUpTracing={vi.fn()}
|
||||||
|
/>,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import { ArrowDown, ChevronRight } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useInView } from "react-intersection-observer";
|
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 { InspectorTable, useInspectorTable } from "@/components/shared/InspectorTable";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
@ -81,6 +83,7 @@ const RUN_COLUMNS: ColumnDef<TraceSummary>[] = [
|
||||||
{
|
{
|
||||||
id: "time",
|
id: "time",
|
||||||
size: 170,
|
size: 170,
|
||||||
|
enableHiding: false,
|
||||||
header: () => (
|
header: () => (
|
||||||
<span className="inline-flex items-center gap-1 whitespace-nowrap">
|
<span className="inline-flex items-center gap-1 whitespace-nowrap">
|
||||||
Time <ArrowDown className="size-2.5" />
|
Time <ArrowDown className="size-2.5" />
|
||||||
|
|
@ -95,6 +98,7 @@ const RUN_COLUMNS: ColumnDef<TraceSummary>[] = [
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
meta: {
|
meta: {
|
||||||
|
title: "Time",
|
||||||
className: "font-mono tabular-nums text-muted-foreground",
|
className: "font-mono tabular-nums text-muted-foreground",
|
||||||
renderSkeleton: () => <Skeleton className="h-3 w-24" />,
|
renderSkeleton: () => <Skeleton className="h-3 w-24" />,
|
||||||
},
|
},
|
||||||
|
|
@ -102,6 +106,7 @@ const RUN_COLUMNS: ColumnDef<TraceSummary>[] = [
|
||||||
{
|
{
|
||||||
id: "agent",
|
id: "agent",
|
||||||
size: 160,
|
size: 160,
|
||||||
|
enableHiding: false,
|
||||||
header: "Agent",
|
header: "Agent",
|
||||||
cell: ({ row }) => <AgentCell run={row.original} />,
|
cell: ({ row }) => <AgentCell run={row.original} />,
|
||||||
meta: {
|
meta: {
|
||||||
|
|
@ -157,9 +162,10 @@ const RUN_COLUMNS: ColumnDef<TraceSummary>[] = [
|
||||||
{
|
{
|
||||||
id: "open",
|
id: "open",
|
||||||
size: 32,
|
size: 32,
|
||||||
header: "",
|
enableHiding: false,
|
||||||
|
header: ({ table }) => <DataTableViewOptions table={table} label="Columns" iconOnly />,
|
||||||
cell: () => <ChevronRight className="size-3 text-muted-foreground/60" />,
|
cell: () => <ChevronRight className="size-3 text-muted-foreground/60" />,
|
||||||
meta: { className: "px-0", renderSkeleton: () => null },
|
meta: { className: "px-0", headerClassName: "px-1", renderSkeleton: () => null },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -214,11 +220,14 @@ export function AgentTracesTable({
|
||||||
const settled = !isLoading && !error;
|
const settled = !isLoading && !error;
|
||||||
const isEmpty = settled && !hasMore && traces.length === 0;
|
const isEmpty = settled && !hasMore && traces.length === 0;
|
||||||
const autoContinue = settled && hasMore && traces.length > 0;
|
const autoContinue = settled && hasMore && traces.length > 0;
|
||||||
|
const { columnVisibility, onColumnVisibilityChange } = usePersistedColumnVisibility("lens-traces");
|
||||||
const tableOptions: TableOptions<TraceSummary> = {
|
const tableOptions: TableOptions<TraceSummary> = {
|
||||||
data: traces,
|
data: traces,
|
||||||
columns: RUN_COLUMNS,
|
columns: RUN_COLUMNS,
|
||||||
getRowId: runKey,
|
getRowId: runKey,
|
||||||
autoResetAll: false,
|
autoResetAll: false,
|
||||||
|
state: { columnVisibility },
|
||||||
|
onColumnVisibilityChange,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
};
|
};
|
||||||
const table = useReactTable(tableOptions);
|
const table = useReactTable(tableOptions);
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,21 @@ import type { Table } from "@tanstack/react-table";
|
||||||
import { Check, Columns3 } from "lucide-react";
|
import { Check, Columns3 } from "lucide-react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/lib/cva.config";
|
||||||
|
|
||||||
interface DataTableViewOptionsProps<TData> {
|
interface DataTableViewOptionsProps<TData> {
|
||||||
table: Table<TData>;
|
table: Table<TData>;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
iconOnly?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTableViewOptions<TData>({ table, label = "View", className }: DataTableViewOptionsProps<TData>) {
|
export function DataTableViewOptions<TData>({
|
||||||
|
table,
|
||||||
|
label = "View",
|
||||||
|
iconOnly = false,
|
||||||
|
className,
|
||||||
|
}: DataTableViewOptionsProps<TData>) {
|
||||||
const hideableColumns = table.getAllLeafColumns().filter((column) => column.getCanHide());
|
const hideableColumns = table.getAllLeafColumns().filter((column) => column.getCanHide());
|
||||||
|
|
||||||
if (hideableColumns.length === 0) {
|
if (hideableColumns.length === 0) {
|
||||||
|
|
@ -23,10 +30,26 @@ export function DataTableViewOptions<TData>({ table, label = "View", className }
|
||||||
<Menu.Root>
|
<Menu.Root>
|
||||||
<Menu.Trigger
|
<Menu.Trigger
|
||||||
render={
|
render={
|
||||||
<Button variant="outline" size="sm" className={className} data-testid="view-options-trigger">
|
iconOnly ? (
|
||||||
<Columns3 />
|
<Button
|
||||||
{label}
|
variant="ghost"
|
||||||
</Button>
|
size="icon-xs"
|
||||||
|
aria-label={label}
|
||||||
|
title={label}
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground/60 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-testid="view-options-trigger"
|
||||||
|
>
|
||||||
|
<Columns3 />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button variant="outline" size="sm" className={className} data-testid="view-options-trigger">
|
||||||
|
<Columns3 />
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Menu.Portal>
|
<Menu.Portal>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue