diff --git a/ui/litellm-dashboard/src/components/view_users/columns.tsx b/ui/litellm-dashboard/src/components/view_users/columns.tsx index 32bfa0ed6d..20df4fc246 100644 --- a/ui/litellm-dashboard/src/components/view_users/columns.tsx +++ b/ui/litellm-dashboard/src/components/view_users/columns.tsx @@ -22,10 +22,12 @@ export const columns = ( handleUserClick: (userId: string, openInEditMode?: boolean) => void, selectionOptions?: SelectionOptions, ): ColumnDef[] => { + // Backend sortable columns: user_id, user_email, created_at, spend, user_alias, user_role const baseColumns: ColumnDef[] = [ { header: "User ID", accessorKey: "user_id", + enableSorting: true, cell: ({ row }) => ( {row.original.user_id ? `${row.original.user_id.slice(0, 7)}...` : "-"} @@ -35,16 +37,19 @@ export const columns = ( { header: "Email", accessorKey: "user_email", + enableSorting: true, cell: ({ row }) => {row.original.user_email || "-"}, }, { header: "Global Proxy Role", accessorKey: "user_role", + enableSorting: true, cell: ({ row }) => {possibleUIRoles?.[row.original.user_role]?.ui_label || "-"}, }, { header: "Spend (USD)", accessorKey: "spend", + enableSorting: true, cell: ({ row }) => ( {row.original.spend ? formatNumberWithCommas(row.original.spend, 4) : "-"} ), @@ -52,6 +57,7 @@ export const columns = ( { header: "Budget (USD)", accessorKey: "max_budget", + enableSorting: false, cell: ({ row }) => ( {row.original.max_budget !== null ? row.original.max_budget : "Unlimited"} ), @@ -66,6 +72,7 @@ export const columns = ( ), accessorKey: "sso_user_id", + enableSorting: false, cell: ({ row }) => ( {row.original.sso_user_id !== null ? row.original.sso_user_id : "-"} ), @@ -73,6 +80,7 @@ export const columns = ( { header: "API Keys", accessorKey: "key_count", + enableSorting: false, cell: ({ row }) => ( {row.original.key_count > 0 ? ( @@ -90,7 +98,7 @@ export const columns = ( { header: "Created At", accessorKey: "created_at", - sortingFn: "datetime", + enableSorting: true, cell: ({ row }) => ( {row.original.created_at ? new Date(row.original.created_at).toLocaleDateString() : "-"} @@ -100,7 +108,7 @@ export const columns = ( { header: "Updated At", accessorKey: "updated_at", - sortingFn: "datetime", + enableSorting: false, cell: ({ row }) => ( {row.original.updated_at ? new Date(row.original.updated_at).toLocaleDateString() : "-"} @@ -110,6 +118,7 @@ export const columns = ( { id: "actions", header: "Actions", + enableSorting: false, cell: ({ row }) => (
@@ -148,6 +157,7 @@ export const columns = ( return [ { id: "select", + enableSorting: false, header: () => ( { const updateFilters = vi.fn(); - const { getByText } = render( + render( { />, ); - expect(getByText("Filters")).toBeInTheDocument(); + expect(screen.getByText("Filters")).toBeInTheDocument(); + }); + + it("should call onSortChange when clicking a sortable header", () => { + const filters = { + email: "", + user_id: "", + user_role: "", + sso_user_id: "", + team: "", + model: "", + min_spend: null, + max_spend: null, + sort_by: "created_at", + sort_order: "desc" as const, + }; + + const updateFilters = vi.fn(); + const onSortChange = vi.fn(); + + const possibleUIRoles = { + admin: { ui_label: "Admin" }, + user: { ui_label: "User" }, + }; + + render( + , + ); + + const emailHeader = screen.getByRole("columnheader", { name: /email/i }); + act(() => { + fireEvent.click(emailHeader); + }); + + expect(onSortChange).toHaveBeenCalledWith("user_email", "desc"); }); }); diff --git a/ui/litellm-dashboard/src/components/view_users/table.tsx b/ui/litellm-dashboard/src/components/view_users/table.tsx index 7ce8f4ce65..f20eb2a6d1 100644 --- a/ui/litellm-dashboard/src/components/view_users/table.tsx +++ b/ui/litellm-dashboard/src/components/view_users/table.tsx @@ -1,11 +1,4 @@ -import { - ColumnDef, - flexRender, - getCoreRowModel, - getSortedRowModel, - SortingState, - useReactTable, -} from "@tanstack/react-table"; +import { ColumnDef, flexRender, getCoreRowModel, SortingState, useReactTable } from "@tanstack/react-table"; import React from "react"; import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell, Select, SelectItem } from "@tremor/react"; import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline"; @@ -167,17 +160,23 @@ export function UserDataTable({ state: { sorting, }, - onSortingChange: (newSorting: any) => { + onSortingChange: (updaterOrValue: any) => { + const newSorting = typeof updaterOrValue === "function" ? updaterOrValue(sorting) : updaterOrValue; setSorting(newSorting); - if (newSorting.length > 0) { + if (newSorting && Array.isArray(newSorting) && newSorting.length > 0 && newSorting[0]) { const sortState = newSorting[0]; - const sortBy = sortState.id; - const sortOrder = sortState.desc ? "desc" : "asc"; - onSortChange?.(sortBy, sortOrder); + if (sortState.id) { + const sortBy = sortState.id; + const sortOrder = sortState.desc ? "desc" : "asc"; + onSortChange?.(sortBy, sortOrder); + } + } else { + // Reset to default sort when no sorting is selected + onSortChange?.("created_at", "desc"); } }, getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), + manualSorting: true, enableSorting: true, }); @@ -403,7 +402,7 @@ export function UserDataTable({ header.id === "actions" ? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]" : "" - }`} + } ${header.column.getCanSort() ? "cursor-pointer hover:bg-gray-50" : ""}`} onClick={header.column.getToggleSortingHandler()} >
@@ -412,7 +411,7 @@ export function UserDataTable({ ? null : flexRender(header.column.columnDef.header, header.getContext())}
- {header.id !== "actions" && ( + {header.id !== "actions" && header.column.getCanSort() && (
{header.column.getIsSorted() ? ( {