Merge pull request #17108 from BerriAI/litellm_user_table_sort_ui

[Feature] UI - User Table Sort by All
This commit is contained in:
yuneng-jiang
2025-11-25 21:35:27 -08:00
committed by GitHub
3 changed files with 82 additions and 22 deletions
@@ -22,10 +22,12 @@ export const columns = (
handleUserClick: (userId: string, openInEditMode?: boolean) => void,
selectionOptions?: SelectionOptions,
): ColumnDef<UserInfo>[] => {
// Backend sortable columns: user_id, user_email, created_at, spend, user_alias, user_role
const baseColumns: ColumnDef<UserInfo>[] = [
{
header: "User ID",
accessorKey: "user_id",
enableSorting: true,
cell: ({ row }) => (
<Tooltip title={row.original.user_id}>
<span className="text-xs">{row.original.user_id ? `${row.original.user_id.slice(0, 7)}...` : "-"}</span>
@@ -35,16 +37,19 @@ export const columns = (
{
header: "Email",
accessorKey: "user_email",
enableSorting: true,
cell: ({ row }) => <span className="text-xs">{row.original.user_email || "-"}</span>,
},
{
header: "Global Proxy Role",
accessorKey: "user_role",
enableSorting: true,
cell: ({ row }) => <span className="text-xs">{possibleUIRoles?.[row.original.user_role]?.ui_label || "-"}</span>,
},
{
header: "Spend (USD)",
accessorKey: "spend",
enableSorting: true,
cell: ({ row }) => (
<span className="text-xs">{row.original.spend ? formatNumberWithCommas(row.original.spend, 4) : "-"}</span>
),
@@ -52,6 +57,7 @@ export const columns = (
{
header: "Budget (USD)",
accessorKey: "max_budget",
enableSorting: false,
cell: ({ row }) => (
<span className="text-xs">{row.original.max_budget !== null ? row.original.max_budget : "Unlimited"}</span>
),
@@ -66,6 +72,7 @@ export const columns = (
</div>
),
accessorKey: "sso_user_id",
enableSorting: false,
cell: ({ row }) => (
<span className="text-xs">{row.original.sso_user_id !== null ? row.original.sso_user_id : "-"}</span>
),
@@ -73,6 +80,7 @@ export const columns = (
{
header: "API Keys",
accessorKey: "key_count",
enableSorting: false,
cell: ({ row }) => (
<Grid numItems={2}>
{row.original.key_count > 0 ? (
@@ -90,7 +98,7 @@ export const columns = (
{
header: "Created At",
accessorKey: "created_at",
sortingFn: "datetime",
enableSorting: true,
cell: ({ row }) => (
<span className="text-xs">
{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 }) => (
<span className="text-xs">
{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 }) => (
<div className="flex gap-2">
<Tooltip title="Edit user details">
@@ -148,6 +157,7 @@ export const columns = (
return [
{
id: "select",
enableSorting: false,
header: () => (
<Checkbox
indeterminate={isIndeterminate}
@@ -1,6 +1,5 @@
import { render } from "@testing-library/react";
import { act, fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import React from "react";
import { UserDataTable } from "./table";
@@ -21,7 +20,7 @@ describe("UserDataTable", () => {
const updateFilters = vi.fn();
const { getByText } = render(
render(
<UserDataTable
data={[]}
columns={[]}
@@ -41,6 +40,58 @@ describe("UserDataTable", () => {
/>,
);
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(
<UserDataTable
data={[]}
columns={[]}
accessToken={null}
userRole={"Admin"}
possibleUIRoles={possibleUIRoles}
filters={filters}
updateFilters={updateFilters}
initialFilters={filters}
teams={[]}
handleEdit={vi.fn()}
handleDelete={vi.fn()}
handleResetPassword={vi.fn()}
userListResponse={{ users: [], total: 0, page: 1, page_size: 25, total_pages: 1 }}
currentPage={1}
handlePageChange={vi.fn()}
onSortChange={onSortChange}
currentSort={{ sortBy: filters.sort_by, sortOrder: filters.sort_order }}
/>,
);
const emailHeader = screen.getByRole("columnheader", { name: /email/i });
act(() => {
fireEvent.click(emailHeader);
});
expect(onSortChange).toHaveBeenCalledWith("user_email", "desc");
});
});
@@ -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()}
>
<div className="flex items-center justify-between gap-2">
@@ -412,7 +411,7 @@ export function UserDataTable({
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</div>
{header.id !== "actions" && (
{header.id !== "actions" && header.column.getCanSort() && (
<div className="w-4">
{header.column.getIsSorted() ? (
{