diff --git a/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsTable.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsTable.test.tsx
index 839eb406200..724a72a77f3 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsTable.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsTable.test.tsx
@@ -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(, {
+ 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(, {
+ 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();
+ 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();
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(, {
- searchParams: "?sort_by=members&sort_order=asc",
+ searchParams: "?sort_by=actions&sort_order=asc",
});
expect(bodyRowAliases()).toEqual(["Zed", "Mid", "Ace"]);
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsTable.tsx
index a9ac0b7e699..df1c475ebb1 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsTable.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsTable.tsx
@@ -62,6 +62,7 @@ const OrganizationsTable: React.FC = ({
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}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsTableColumns.tsx b/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsTableColumns.tsx
index 5f170a32941..dcbf141314b 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsTableColumns.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsTableColumns.tsx
@@ -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 }) => ,
size: 120,
- enableSorting: false,
+ enableSorting: true,
cell: ({ row }) => (
),
},
{
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 }) => ,
size: 260,
- enableSorting: false,
+ enableSorting: true,
cell: ({ row }) => ,
},
{
id: "limits",
meta: { title: "TPM / RPM Limits" },
- header: "TPM / RPM Limits",
+ header: ({ table }) => (
+
+ ),
size: 150,
enableSorting: false,
cell: ({ row }) => ,
},
{
id: "members",
+ accessorFn: (organization) => organization.members?.length ?? 0,
+ sortUndefined: "last",
+ sortDescFirst: true,
meta: { title: "Members", numeric: true },
- header: "Members",
+ header: ({ column }) => ,
size: 100,
- enableSorting: false,
+ enableSorting: true,
cell: ({ row }) => {row.original.members?.length ?? 0} Members,
},
+ ...(["tpm_limit", "rpm_limit"] as const).map(
+ (field): ColumnDef => ({
+ 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" },
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/useOrganizationsTableState.ts b/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/useOrganizationsTableState.ts
index 20a54a25ba1..ffe67c0aa14 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/useOrganizationsTableState.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/useOrganizationsTableState.ts
@@ -4,7 +4,17 @@ const FILTER_COLUMNS = ["org_id"] as const;
type FilterColumn = (typeof FILTER_COLUMNS)[number];
const TABLE_STATE_OPTIONS: UrlTableStateOptions = {
- 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,