mirror of
https://github.com/tiennm99/litellm.git
synced 2026-06-18 00:48:01 +00:00
Merge pull request #17108 from BerriAI/litellm_user_table_sort_ui
[Feature] UI - User Table Sort by All
This commit is contained in:
@@ -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() ? (
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user