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.
This commit is contained in:
Yuneng Jiang 2026-07-09 16:08:18 -07:00
parent b0ff698add
commit 1aa4cb0d2a
No known key found for this signature in database
5 changed files with 74 additions and 205 deletions

View file

@ -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

View file

@ -2323,7 +2323,7 @@
},
"src/components/team/TeamVirtualKeysTable.tsx": {
"no-nested-ternary": {
"count": 2
"count": 1
},
"no-restricted-imports": {
"count": 1

View file

@ -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();
});
});

View file

@ -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(<TeamVirtualKeysTable {...defaultProps} />);
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(<TeamVirtualKeysTable {...defaultProps} />);
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<typeof useKeys>);
renderWithProviders(<TeamVirtualKeysTable {...defaultProps} />);
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,

View file

@ -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<Record<string, boolean>>({});
const currentTeam: Team = useMemo(
@ -200,7 +192,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
{
id: "token",
accessorKey: "token",
header: "Key ID",
header: ({ column }) => <DataTableSortHeader column={column} title="Key ID" variant="header-cycle" />,
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 }) => <DataTableSortHeader column={column} title="Key Alias" variant="header-cycle" />,
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 }) => <DataTableSortHeader column={column} title="Created At" variant="header-cycle" />,
size: 120,
enableSorting: true,
cell: (info) => <DateCell value={info.getValue() as string | null} precision="date" />,
@ -349,7 +341,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
{
id: "updated_at",
accessorKey: "updated_at",
header: "Updated At",
header: ({ column }) => <DataTableSortHeader column={column} title="Updated At" variant="header-cycle" />,
size: 120,
enableSorting: true,
cell: (info) => <DateCell value={info.getValue() as string | null} precision="date" fallback="Never" />,
@ -383,7 +375,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
{
id: "spend",
accessorKey: "spend",
header: "Spend (USD)",
header: ({ column }) => <DataTableSortHeader column={column} title="Spend (USD)" variant="header-cycle" />,
size: 100,
enableSorting: true,
cell: (info) => <MoneyCell value={info.getValue() as number | null} decimals={4} />,
@ -391,7 +383,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
{
id: "max_budget",
accessorKey: "max_budget",
header: "Budget (USD)",
header: ({ column }) => <DataTableSortHeader column={column} title="Budget (USD)" variant="header-cycle" />,
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 (
<div className="w-full h-full overflow-hidden">
{selectedKey ? (
@ -566,165 +542,36 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
/>
</div>
<div className="flex items-center justify-end w-full mb-4">
<div className="inline-flex items-center gap-2">
{isLoading || isFetching ? (
<Skeleton.Node active style={{ width: 74, height: 20 }} />
) : (
<span className="text-sm text-gray-700">
Page {pageIndex + 1} of {table.getPageCount()}
</span>
)}
{isLoading || isFetching ? (
<Skeleton.Button active size="small" style={{ width: 84, height: 30 }} />
) : (
<button
onClick={() => table.previousPage()}
disabled={isLoading || isFetching || !table.getCanPreviousPage()}
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
)}
{isLoading || isFetching ? (
<Skeleton.Button active size="small" style={{ width: 58, height: 30 }} />
) : (
<button
onClick={() => table.nextPage()}
disabled={isLoading || isFetching || !table.getCanNextPage()}
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
)}
</div>
</div>
<div className="h-[75vh] overflow-auto">
<div className="rounded-lg custom-border relative">
<div className="overflow-x-auto">
<Table className="[&_td]:py-0.5 [&_th]:py-1" style={{ width: table.getCenterTotalSize() }}>
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHeaderCell
key={header.id}
data-header-id={header.id}
className={`py-1 h-8 relative hover:bg-gray-50 ${
header.id === "actions"
? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]"
: ""
}`}
style={{
width: header.getSize(),
position: "relative",
cursor: header.column.getCanSort() ? "pointer" : "default",
}}
onMouseEnter={() => {
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}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</div>
{header.id !== "actions" && header.column.getCanSort() && (
<div className="w-4">
{header.column.getIsSorted() ? (
{
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />,
}[header.column.getIsSorted() as string]
) : (
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
)}
</div>
)}
<div
onDoubleClick={() => 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,
}}
/>
</div>
</TableHeaderCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{isLoading || isFetching ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>Loading keys...</p>
</div>
</TableCell>
</TableRow>
) : displayKeys.length > 0 ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="h-8">
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
style={{
width: cell.column.getSize(),
maxWidth: "8-x",
whiteSpace: "pre-wrap",
overflow: "hidden",
}}
className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${
cell.column.id === "models" &&
Array.isArray(cell.getValue()) &&
(cell.getValue() as string[]).length > 3
? "px-0"
: ""
}`}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>No keys found</p>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
<div className="w-full mb-4">
<DataTablePagination
page={pageIndex}
pageSize={pageSize}
rowCount={rowCount}
onPageChange={(nextPage) => setTablePagination((prev) => ({ ...prev, pageIndex: nextPage }))}
onPageSizeChange={(nextSize) => setTablePagination({ pageIndex: 0, pageSize: nextSize })}
isLoading={isLoading || isFetching}
/>
</div>
<DataTable
data={displayKeys}
columns={columns}
sortingMode="server"
sorting={sorting}
onSortingChange={handleSortingChange}
paginationMode="server"
pagination={tablePagination}
onPaginationChange={setTablePagination}
rowCount={rowCount}
paginationSlot={() => null}
enableColumnResizing
columnResizeMode="onChange"
isLoading={isLoading || isFetching}
loadingMessage="Loading keys..."
noDataMessage="No keys found"
maxBodyHeight="75vh"
size="compact"
/>
</div>
)}
</div>