From 1aa4cb0d2ac16eaa2b5712c916690094596930e9 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 9 Jul 2026 16:08:18 -0700 Subject: [PATCH] refactor(ui): migrate Team Info virtual keys table onto shared DataTable Second proof-of-concept consumer for the shared DataTable. Replaces the hand-rolled tremor table in the Team Info Virtual Keys tab with DataTable in server-sort and server-pagination mode plus column resizing; the file drops about 150 lines. Sortable headers now use DataTableSortHeader, pagination is a detached DataTablePagination driven by the page state, the id-cell still opens the key drawer, and the body scrolls under a sticky header via maxBodyHeight. Two behavior changes: the pagination control is the standardized bar (row range plus page-size select) rather than the old Previous/Next buttons, and a sort header cycles ascending/descending without a third unsorted state, which also removes a latent case where clearing the sort left the server sorted. Updates the TeamVirtualKeysTable and TeamInfo tests to the new pagination, adds a test that a sort-header click routes to useKeys as a server sort, and lowers the no-large-inline-object-arg metric by one and the file's no-nested-ternary suppression from two to one to match the leaner code. --- ui/litellm-dashboard/eslint-metrics.json | 2 +- ui/litellm-dashboard/eslint-suppressions.json | 2 +- .../src/components/team/TeamInfo.test.tsx | 8 +- .../team/TeamVirtualKeysTable.test.tsx | 32 ++- .../components/team/TeamVirtualKeysTable.tsx | 235 +++--------------- 5 files changed, 74 insertions(+), 205 deletions(-) diff --git a/ui/litellm-dashboard/eslint-metrics.json b/ui/litellm-dashboard/eslint-metrics.json index fcf60934f64..ad60a0d0c89 100644 --- a/ui/litellm-dashboard/eslint-metrics.json +++ b/ui/litellm-dashboard/eslint-metrics.json @@ -1,7 +1,7 @@ { "@typescript-eslint/no-explicit-any": 1980, "complexity": 128, - "local/no-large-inline-object-arg": 519, + "local/no-large-inline-object-arg": 518, "local/no-long-condition-chain": 233, "max-depth": 59, "no-console": 15 diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index fe8f182c106..6d310a97dbf 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -2323,7 +2323,7 @@ }, "src/components/team/TeamVirtualKeysTable.tsx": { "no-nested-ternary": { - "count": 2 + "count": 1 }, "no-restricted-imports": { "count": 1 diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx index 46eac1c5772..4ccfb891417 100644 --- a/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx @@ -536,7 +536,7 @@ describe("TeamInfoView", () => { await user.click(virtualKeysTab); await waitFor(() => { - expect(screen.getByText("Page 1 of 1")).toBeInTheDocument(); + expect(screen.getByTestId("pagination-range")).toHaveTextContent("Showing 1-5 of 5"); }); }); @@ -584,9 +584,9 @@ describe("TeamInfoView", () => { expect(screen.getByRole("button", { name: "Filters" })).toBeInTheDocument(); }); expect(screen.getByRole("button", { name: "Reset Filters" })).toBeInTheDocument(); - expect(screen.getByText("Page 1 of 1")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Previous" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Next" })).toBeInTheDocument(); + expect(screen.getByTestId("pagination-range")).toHaveTextContent("Showing 1-1 of 1"); + expect(screen.getByTestId("pagination-prev")).toBeInTheDocument(); + expect(screen.getByTestId("pagination-next")).toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.test.tsx b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.test.tsx index 954f5ea98c5..9eb0282580b 100644 --- a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.test.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.test.tsx @@ -163,7 +163,7 @@ describe("TeamVirtualKeysTable", () => { expect(screen.getByText("bob_key_team1")).toBeInTheDocument(); }); - it("should show Page X of Y when multiple pages exist", async () => { + it("should show the current range from total_count when multiple pages exist", async () => { mockUseKeys.mockReturnValue({ data: { keys: [createMockKey()], @@ -179,7 +179,7 @@ describe("TeamVirtualKeysTable", () => { renderWithProviders(); await waitFor(() => { - expect(screen.getByText("Page 1 of 3")).toBeInTheDocument(); + expect(screen.getByTestId("pagination-range")).toHaveTextContent("Showing 1-50 of 100"); }); }); @@ -203,17 +203,39 @@ describe("TeamVirtualKeysTable", () => { renderWithProviders(); await waitFor(() => { - expect(screen.getByText("Page 1 of 3")).toBeInTheDocument(); + expect(screen.getByTestId("pagination-range")).toHaveTextContent("Showing 1-50 of 100"); }); - const nextButton = screen.getByRole("button", { name: "Next" }); - await user.click(nextButton); + await user.click(screen.getByTestId("pagination-next")); await waitFor(() => { expect(mockUseKeys).toHaveBeenLastCalledWith(2, 50, expect.objectContaining({ teamID: "team-1" })); }); }); + it("routes a sort-header click to useKeys as a server-side sort", async () => { + const user = userEvent.setup(); + mockUseKeys.mockReturnValue({ + data: { keys: [createMockKey()], total_count: 1, current_page: 1, total_pages: 1 } as KeysResponse, + isPending: false, + isFetching: false, + refetch: vi.fn(), + } as unknown as ReturnType); + + renderWithProviders(); + + await waitFor(() => expect(screen.getByTestId("sort-header-created_at")).toBeInTheDocument()); + await user.click(screen.getByTestId("sort-header-created_at")); + + await waitFor(() => + expect(mockUseKeys).toHaveBeenLastCalledWith( + 1, + 50, + expect.objectContaining({ sortBy: "created_at", sortOrder: "asc" }), + ), + ); + }); + it("should show Loading keys when isPending", async () => { mockUseKeys.mockReturnValue({ data: undefined, diff --git a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx index b7128e642a5..909608d630f 100644 --- a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx @@ -1,20 +1,12 @@ -// TO-DO: Standardize tables eventually - "use client"; import { useKeys } from "@/app/(dashboard)/hooks/keys/useKeys"; import { DateCell, IdCell, MoneyCell } from "@/components/shared/table_cells"; -import { ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, SwitchVerticalIcon } from "@heroicons/react/outline"; -import { - ColumnDef, - flexRender, - getCoreRowModel, - PaginationState, - SortingState, - useReactTable, -} from "@tanstack/react-table"; -import { Badge, Icon, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Text } from "@tremor/react"; +import { DataTable, DataTablePagination, DataTableSortHeader } from "@/components/shared/DataTable"; +import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline"; +import { ColumnDef, PaginationState, SortingState } from "@tanstack/react-table"; +import { Badge, Icon, Text } from "@tremor/react"; import { InfoCircleOutlined } from "@ant-design/icons"; -import { Popover, Skeleton, Tooltip, Typography } from "antd"; +import { Popover, Tooltip, Typography } from "antd"; import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key"; @@ -83,7 +75,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi })); }, [keys?.keys, organization?.organization_id]); - const pageCount = keys?.total_pages ?? 0; + const rowCount = keys?.total_count ?? 0; const [expandedAccordions, setExpandedAccordions] = useState>({}); const currentTeam: Team = useMemo( @@ -200,7 +192,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi { id: "token", accessorKey: "token", - header: "Key ID", + header: ({ column }) => , size: 100, enableSorting: true, cell: (info) => ( @@ -210,7 +202,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi { id: "key_alias", accessorKey: "key_alias", - header: "Key Alias", + header: ({ column }) => , size: 150, enableSorting: true, cell: (info) => { @@ -282,7 +274,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi { id: "created_at", accessorKey: "created_at", - header: "Created At", + header: ({ column }) => , size: 120, enableSorting: true, cell: (info) => , @@ -349,7 +341,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi { id: "updated_at", accessorKey: "updated_at", - header: "Updated At", + header: ({ column }) => , size: 120, enableSorting: true, cell: (info) => , @@ -383,7 +375,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi { id: "spend", accessorKey: "spend", - header: "Spend (USD)", + header: ({ column }) => , size: 100, enableSorting: true, cell: (info) => , @@ -391,7 +383,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi { id: "max_budget", accessorKey: "max_budget", - header: "Budget (USD)", + header: ({ column }) => , size: 110, enableSorting: true, cell: (info) => ( @@ -529,22 +521,6 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi [sorting, handleFilterChange], ); - const table = useReactTable({ - data: displayKeys, - columns, - columnResizeMode: "onChange", - columnResizeDirection: "ltr", - state: { sorting, pagination: tablePagination }, - onSortingChange: handleSortingChange, - onPaginationChange: setTablePagination, - getCoreRowModel: getCoreRowModel(), - // getSortedRowModel not needed — manualSorting: true delegates sorting to the server - enableSorting: true, - manualSorting: true, // Server sorts via useKeys. Avoid redundant client-side sort - manualPagination: true, - pageCount: pageCount, - }); - return (
{selectedKey ? ( @@ -566,165 +542,36 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi />
-
-
- {isLoading || isFetching ? ( - - ) : ( - - Page {pageIndex + 1} of {table.getPageCount()} - - )} - - {isLoading || isFetching ? ( - - ) : ( - - )} - - {isLoading || isFetching ? ( - - ) : ( - - )} -
-
-
-
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - { - const resizer = document.querySelector(`[data-header-id="${header.id}"] .resizer`); - if (resizer) (resizer as HTMLElement).style.opacity = "0.5"; - }} - onMouseLeave={() => { - const resizer = document.querySelector(`[data-header-id="${header.id}"] .resizer`); - if (resizer && !header.column.getIsResizing()) - (resizer as HTMLElement).style.opacity = "0"; - }} - onClick={header.column.getCanSort() ? header.column.getToggleSortingHandler() : undefined} - > -
-
- {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} -
- {header.id !== "actions" && header.column.getCanSort() && ( -
- {header.column.getIsSorted() ? ( - { - asc: , - desc: , - }[header.column.getIsSorted() as string] - ) : ( - - )} -
- )} -
header.column.resetSize()} - onMouseDown={header.getResizeHandler()} - onTouchStart={header.getResizeHandler()} - className={`resizer ${table.options.columnResizeDirection} ${ - header.column.getIsResizing() ? "isResizing" : "" - }`} - style={{ - position: "absolute", - right: 0, - top: 0, - height: "100%", - width: "5px", - background: header.column.getIsResizing() ? "#3b82f6" : "transparent", - cursor: "col-resize", - userSelect: "none", - touchAction: "none", - opacity: header.column.getIsResizing() ? 1 : 0, - }} - /> -
- - ))} - - ))} - - - {isLoading || isFetching ? ( - - -
-

Loading keys...

-
-
-
- ) : displayKeys.length > 0 ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - 3 - ? "px-0" - : "" - }`} - > - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - ) : ( - - -
-

No keys found

-
-
-
- )} -
-
-
-
+
+ setTablePagination((prev) => ({ ...prev, pageIndex: nextPage }))} + onPageSizeChange={(nextSize) => setTablePagination({ pageIndex: 0, pageSize: nextSize })} + isLoading={isLoading || isFetching} + />
+ + null} + enableColumnResizing + columnResizeMode="onChange" + isLoading={isLoading || isFetching} + loadingMessage="Loading keys..." + noDataMessage="No keys found" + maxBodyHeight="75vh" + size="compact" + />
)}