feat(ui): sort every organizations table data column (#48106)

* feat(ui): sort every organizations table data column

* fix(ui): normalize organization sorting and unlimited limits
This commit is contained in:
moe-berri 2026-10-23 15:45:32 -07:00 • committed by GitHub
parent 1aa531767c
commit ffe3b5e07e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 130 additions and 12 deletions

View file

@ -66,6 +66,85 @@ const baseProps = {
};
describe("OrganizationsTable", () => {
it.each(["max_budget", "tpm_limit", "rpm_limit"])(
"sorts %s numerically with Unlimited as infinity",
async (field) => {
const user = userEvent.setup();
const organizations = [20, 3, null, 100, 0].map((value, index) =>
makeOrganization({
organization_id: `org-${index}`,
organization_alias: `Value ${value}`,
litellm_budget_table: { [field]: value },
}),
);
renderWithProviders(<OrganizationsTable {...baseProps} organizations={organizations} />, {
searchParams: { sort_by: field, sort_order: "asc", page_size: "25" },
});
const names = () =>
screen
.getAllByRole("row")
.slice(1)
.map((row) => within(row).getByText(/^Value /).textContent);
expect(names()).toEqual(["Value 0", "Value 3", "Value 20", "Value 100", "Value null"]);
if (field === "max_budget") {
await user.click(screen.getByTestId("sort-header-max_budget"));
} else {
await user.click(screen.getByRole("button", { name: "Sort options for TPM or RPM" }));
await user.click(
await screen.findByRole("menuitem", { name: `${field === "tpm_limit" ? "TPM" : "RPM"} descending` }),
);
}
await waitFor(() => expect(names()).toEqual(["Value null", "Value 100", "Value 20", "Value 3", "Value 0"]));
},
);
it.each(["asc", "desc"])("groups both All Proxy Models representations when sorting %s", (direction) => {
const organizations = [
makeOrganization({ organization_id: "org-empty", organization_alias: "Name Empty", models: [] }),
makeOrganization({ organization_id: "org-z", organization_alias: "Name Z", models: ["z"] }),
makeOrganization({
organization_id: "org-wildcard",
organization_alias: "Name Wildcard",
models: ["all-proxy-models"],
}),
makeOrganization({ organization_id: "org-a", organization_alias: "Name A", models: ["a"] }),
];
renderWithProviders(<OrganizationsTable {...baseProps} organizations={organizations} />, {
searchParams: { sort_by: "models", sort_order: direction },
});
const names = screen
.getAllByRole("row")
.slice(1)
.map((row) => within(row).getByText(/^Name /).textContent);
const models = direction === "asc" ? ["Name A", "Name Z"] : ["Name Z", "Name A"];
expect(names).toEqual([...models, "Name Empty", "Name Wildcard"]);
expect(screen.getAllByText("All Proxy Models")).toHaveLength(2);
});
it.each(["models", "members"])("sorts %s by its displayed data", async (field) => {
const user = userEvent.setup();
const organizations = [
{ organization_id: "org-zed", organization_alias: "Zed", models: ["z"], members: [] },
{
organization_id: "org-ace",
organization_alias: "Ace",
models: ["a"],
members: [{ user_id: "1" }, { user_id: "2" }],
},
{
organization_id: "org-mid",
organization_alias: "Mid",
models: ["m"],
members: [{ user_id: "3" }],
},
].map(makeOrganization);
renderWithProviders(<OrganizationsTable {...baseProps} organizations={organizations} />);
await user.click(screen.getByTestId(`sort-header-${field}`));
await waitFor(() => expect(bodyRowAliases()).toEqual(["Ace", "Mid", "Zed"]));
await user.click(screen.getByTestId(`sort-header-${field}`));
await waitFor(() => expect(bodyRowAliases()).toEqual(["Zed", "Mid", "Ace"]));
});
it("renders every column header", () => {
renderWithProviders(<OrganizationsTable {...baseProps} organizations={[]} />);
for (const header of [
@ -75,7 +154,6 @@ describe("OrganizationsTable", () => {
"Spend (USD)",
"Budget (USD)",
"Models",
"TPM / RPM Limits",
"Members",
]) {
expect(screen.getByText(header)).toBeInTheDocument();
@ -87,7 +165,7 @@ describe("OrganizationsTable", () => {
for (const header of ["Spend (USD)", "Budget (USD)", "Members"]) {
expect(screen.getByRole("columnheader", { name: header })).toHaveClass("text-right");
}
for (const header of ["Organization Name", "TPM / RPM Limits"]) {
for (const header of ["Organization Name"]) {
expect(screen.getByRole("columnheader", { name: header })).not.toHaveClass("text-right");
}
});
@ -270,7 +348,7 @@ describe("OrganizationsTable URL state", () => {
it("falls back to sorting by creation date for a ?sort_by= column that cannot be sorted", () => {
renderWithProviders(<OrganizationsTable {...baseProps} organizations={sortableOrganizations} />, {
searchParams: "?sort_by=members&sort_order=asc",
searchParams: "?sort_by=actions&sort_order=asc",
});
expect(bodyRowAliases()).toEqual(["Zed", "Mid", "Ace"]);

View file

@ -62,6 +62,7 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
onPaginationChange={onPaginationChange}
columns={columns}
getRowId={(organization, index) => organization.organization_id || String(index)}
defaultColumnVisibility={{ tpm_limit: false, rpm_limit: false }}
sortingMode="client"
sorting={sorting}
onSortingChange={onSortingChange}

View file

@ -3,7 +3,7 @@
import { ColumnDef } from "@tanstack/react-table";
import { MoreHorizontal, Pencil, Trash2 } from "lucide-react";
import { DataTableSortHeader } from "@/components/shared/DataTable";
import { DataTableMultiSortHeader, DataTableSortHeader } from "@/components/shared/DataTable";
import { DateCell, IdentityCell, ModelsCell, MoneyCell } from "@/components/shared/table_cells";
import { Organization } from "@/components/networking";
import { buttonVariants } from "@/components/ui/button";
@ -137,38 +137,67 @@ export const getOrganizationsTableColumns = ({
},
{
id: "max_budget",
accessorFn: (organization) => getOrganizationBudget(organization).max_budget ?? Number.POSITIVE_INFINITY,
sortUndefined: "last",
sortDescFirst: true,
meta: { title: "Budget (USD)", numeric: true },
header: "Budget (USD)",
header: ({ column }) => <DataTableSortHeader column={column} title="Budget (USD)" />,
size: 120,
enableSorting: false,
enableSorting: true,
cell: ({ row }) => (
<MoneyCell value={getOrganizationBudget(row.original).max_budget} decimals={2} emptyText="Unlimited" showZero />
),
},
{
id: "models",
accessorFn: (organization) =>
organization.models?.length && !organization.models.includes("all-proxy-models")
? [...organization.models].sort().join(", ")
: undefined,
sortUndefined: "last",
sortDescFirst: false,
meta: { title: "Models", skeleton: "chips" },
header: "Models",
header: ({ column }) => <DataTableSortHeader column={column} title="Models" />,
size: 260,
enableSorting: false,
enableSorting: true,
cell: ({ row }) => <ModelsCell models={row.original.models} />,
},
{
id: "limits",
meta: { title: "TPM / RPM Limits" },
header: "TPM / RPM Limits",
header: ({ table }) => (
<DataTableMultiSortHeader
table={table}
fields={[
{ id: "tpm_limit", label: "TPM" },
{ id: "rpm_limit", label: "RPM" },
]}
/>
),
size: 150,
enableSorting: false,
cell: ({ row }) => <OrganizationLimitsCell organization={row.original} />,
},
{
id: "members",
accessorFn: (organization) => organization.members?.length ?? 0,
sortUndefined: "last",
sortDescFirst: true,
meta: { title: "Members", numeric: true },
header: "Members",
header: ({ column }) => <DataTableSortHeader column={column} title="Members" />,
size: 100,
enableSorting: false,
enableSorting: true,
cell: ({ row }) => <span className="text-sm">{row.original.members?.length ?? 0} Members</span>,
},
...(["tpm_limit", "rpm_limit"] as const).map(
(field): ColumnDef<Organization> => ({
id: field,
accessorFn: (organization) => getOrganizationBudget(organization)[field] ?? Number.POSITIVE_INFINITY,
sortUndefined: "last",
sortDescFirst: true,
enableHiding: false,
}),
),
{
id: "actions",
meta: { className: "text-right", headerClassName: "text-right" },

View file

@ -4,7 +4,17 @@ const FILTER_COLUMNS = ["org_id"] as const;
type FilterColumn = (typeof FILTER_COLUMNS)[number];
const TABLE_STATE_OPTIONS: UrlTableStateOptions<FilterColumn> = {
sortFields: ["organization_id", "organization_alias", "created_at", "spend"],
sortFields: [
"organization_id",
"organization_alias",
"created_at",
"spend",
"max_budget",
"models",
"members",
"tpm_limit",
"rpm_limit",
],
defaultSort: { id: "created_at", desc: true },
defaultPageSize: 25,
filterColumns: FILTER_COLUMNS,