diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTable.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTable.test.tsx index bef938cd31c..17bb8bbfec4 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTable.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTable.test.tsx @@ -33,6 +33,12 @@ describe("AgentsTable", () => { } }); + it("right-aligns the Spend (USD) column", () => { + render(); + expect(screen.getByRole("columnheader", { name: "Spend (USD)" })).toHaveClass("text-right"); + expect(screen.getByRole("columnheader", { name: "Agent Name" })).not.toHaveClass("text-right"); + }); + it("renders the agent's model and opens the detail view when the ID cell is clicked", async () => { const user = userEvent.setup(); const onAgentClick = vi.fn(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTableColumns.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTableColumns.tsx index a8fe3973a42..9ec1eb097d2 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTableColumns.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTableColumns.tsx @@ -90,7 +90,7 @@ export const getAgentsTableColumns = ({ { id: "spend", accessorKey: "spend", - meta: { title: "Spend (USD)" }, + meta: { title: "Spend (USD)", numeric: true }, header: ({ column }) => , size: 130, enableSorting: true, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_discount_table.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_discount_table.test.tsx index 6606a4e6aaf..8d1ee100a2a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_discount_table.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_discount_table.test.tsx @@ -50,6 +50,8 @@ describe("ProviderDiscountTable", () => { expect(screen.getByRole("columnheader", { name: "Provider" })).toBeInTheDocument(); expect(screen.getByRole("columnheader", { name: "Discount Percentage" })).toBeInTheDocument(); expect(screen.getByRole("columnheader", { name: "Actions" })).toBeInTheDocument(); + expect(screen.getByRole("columnheader", { name: "Discount Percentage" })).toHaveClass("text-right"); + expect(screen.getByRole("columnheader", { name: "Provider" })).not.toHaveClass("text-right"); }); it("should display provider display names in the table", () => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_discount_table.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_discount_table.tsx index fcc4c2af935..3d8be33fc4a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_discount_table.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_discount_table.tsx @@ -80,10 +80,11 @@ const ProviderDiscountTable: React.FC = ({ }, { header: "Discount Percentage", + numeric: true, cell: (row) => { const { displayName } = getProviderLogoAndName(row.provider); return ( -
+
{editingProvider === row.provider ? ( <> { expect(screen.getByRole("columnheader", { name: "Provider" })).toBeInTheDocument(); expect(screen.getByRole("columnheader", { name: "Margin" })).toBeInTheDocument(); expect(screen.getByRole("columnheader", { name: "Actions" })).toBeInTheDocument(); + expect(screen.getByRole("columnheader", { name: "Margin" })).toHaveClass("text-right"); + expect(screen.getByRole("columnheader", { name: "Provider" })).not.toHaveClass("text-right"); }); it("should display the provider display name", () => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_margin_table.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_margin_table.tsx index 04823ac4aa0..5352695ef0a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_margin_table.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_margin_table.tsx @@ -123,10 +123,11 @@ const ProviderMarginTable: React.FC = ({ }, { header: "Margin", + numeric: true, cell: (row) => { const displayName = marginRowDisplayName(row.provider); return ( -
+
{editingProvider === row.provider ? ( <>
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTable.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTable.test.tsx index cc0169d745c..be6130b0288 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTable.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTable.test.tsx @@ -174,6 +174,8 @@ describe("AllModelsTable", () => { const { rerender } = render(); expect(screen.getByText("$30")).toBeInTheDocument(); expect(screen.getByText("$60")).toBeInTheDocument(); + expect(screen.getByRole("cell", { name: /\$30/ })).toHaveClass("text-right"); + expect(screen.getByRole("columnheader", { name: /costs/i })).toHaveClass("text-right"); rerender(); expect(screen.queryByText(/^\$/)).not.toBeInTheDocument(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/ModelsTableColumns.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/ModelsTableColumns.tsx index cbc31747688..9581d3db198 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/ModelsTableColumns.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/ModelsTableColumns.tsx @@ -437,7 +437,7 @@ export const getModelsTableColumns = ({ { id: COSTS_COLUMN_ID, accessorFn: (row) => row.input_cost, - meta: { title: "Costs" }, + meta: { title: "Costs", numeric: true }, header: ({ column }) => , enableSorting: true, size: 130, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/old-usage/_components/usage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/old-usage/_components/usage.tsx index 889a17bc88d..15b2a30b50c 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/old-usage/_components/usage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/old-usage/_components/usage.tsx @@ -19,7 +19,15 @@ import { } from "@/components/ui/combobox"; import { Meter, MeterIndicator, MeterTrack } from "@/components/shared/Meter"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + NUMERIC_CELL_CLASS, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { AreaChart, BarChart, DonutChart } from "@/components/shared/charts"; @@ -651,14 +659,14 @@ const UsagePage: React.FC = ({ accessToken, token, userRole, use Provider - Spend + Spend {spendByProvider.map((provider) => ( {provider.provider} - + @@ -840,8 +848,8 @@ const UsagePage: React.FC = ({ accessToken, token, userRole, use Customer - Spend - Total Events + Spend + Total Events @@ -849,10 +857,10 @@ const UsagePage: React.FC = ({ accessToken, token, userRole, use {topUsers?.map((user: any, index: number) => ( {user.end_user} - + - {user.total_count} + {user.total_count} ))} 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 9d163fe2c08..839eb406200 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 @@ -82,6 +82,16 @@ describe("OrganizationsTable", () => { } }); + it("right-aligns the money and count columns only", () => { + renderWithProviders(); + 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"]) { + expect(screen.getByRole("columnheader", { name: header })).not.toHaveClass("text-right"); + } + }); + it("opens the detail view when the organization ID cell is clicked", async () => { const user = userEvent.setup(); const onOrganizationClick = vi.fn(); 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 0fea6c6606e..5f170a32941 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsTableColumns.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/organizations/_components/OrganizationsTableColumns.tsx @@ -129,7 +129,7 @@ export const getOrganizationsTableColumns = ({ { id: "spend", accessorKey: "spend", - meta: { title: "Spend (USD)" }, + meta: { title: "Spend (USD)", numeric: true }, header: ({ column }) => , size: 120, enableSorting: true, @@ -137,7 +137,7 @@ export const getOrganizationsTableColumns = ({ }, { id: "max_budget", - meta: { title: "Budget (USD)" }, + meta: { title: "Budget (USD)", numeric: true }, header: "Budget (USD)", size: 120, enableSorting: false, @@ -163,7 +163,7 @@ export const getOrganizationsTableColumns = ({ }, { id: "members", - meta: { title: "Members" }, + meta: { title: "Members", numeric: true }, header: "Members", size: 100, enableSorting: false, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/BulkEditUsers.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/BulkEditUsers.tsx index 459e3fd8c92..67a648dacd1 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/BulkEditUsers.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/BulkEditUsers.tsx @@ -9,7 +9,16 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Separator } from "@/components/ui/separator"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + NUMERIC_CELL_CLASS, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { cn } from "@/lib/cva.config"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; interface BulkEditUserModalProps { @@ -250,7 +259,7 @@ const BulkEditUserModal: React.FC = ({ User ID Email Current Role - Budget + Budget @@ -263,7 +272,7 @@ const BulkEditUserModal: React.FC = ({ {possibleUIRoles?.[user.user_role]?.ui_label || user.user_role} - + diff --git a/ui/litellm-dashboard/src/components/AIHub/ModelHubTableColumns.test.tsx b/ui/litellm-dashboard/src/components/AIHub/ModelHubTableColumns.test.tsx index 3a2fea66b0c..678b811921c 100644 --- a/ui/litellm-dashboard/src/components/AIHub/ModelHubTableColumns.test.tsx +++ b/ui/litellm-dashboard/src/components/AIHub/ModelHubTableColumns.test.tsx @@ -50,6 +50,9 @@ describe("getModelHubTableColumns", () => { expect(screen.getByText("128.0K / 16.4K")).toBeInTheDocument(); expect(screen.getByText("$2.50")).toBeInTheDocument(); expect(screen.getByText("$10.00")).toBeInTheDocument(); + expect(screen.getByRole("cell", { name: "128.0K / 16.4K" })).toHaveClass("text-right"); + expect(screen.getByRole("cell", { name: /\$2\.50/ })).toHaveClass("text-right"); + expect(screen.getByRole("cell", { name: "gpt-4o" })).not.toHaveClass("text-right"); }); it("shows capability badges only for supported features", () => { diff --git a/ui/litellm-dashboard/src/components/AIHub/ModelHubTableColumns.tsx b/ui/litellm-dashboard/src/components/AIHub/ModelHubTableColumns.tsx index 9f74771f3b1..fc71a9340ac 100644 --- a/ui/litellm-dashboard/src/components/AIHub/ModelHubTableColumns.tsx +++ b/ui/litellm-dashboard/src/components/AIHub/ModelHubTableColumns.tsx @@ -143,7 +143,7 @@ export const getModelHubTableColumns = ({ onModelClick }: ModelHubTableColumnsDe { id: "max_input_tokens", accessorKey: "max_input_tokens", - meta: { title: "Tokens", className: "hidden lg:table-cell" }, + meta: { title: "Tokens", className: "hidden lg:table-cell", numeric: true }, header: ({ column }) => , size: 110, enableSorting: true, @@ -165,7 +165,7 @@ export const getModelHubTableColumns = ({ onModelClick }: ModelHubTableColumnsDe { id: "input_cost_per_token", accessorKey: "input_cost_per_token", - meta: { title: "Cost/1M", skeleton: "twoLine" }, + meta: { title: "Cost/1M", skeleton: "twoLine", numeric: true }, header: ({ column }) => , size: 110, enableSorting: true, diff --git a/ui/litellm-dashboard/src/components/TeamsPage/TeamsTable.test.tsx b/ui/litellm-dashboard/src/components/TeamsPage/TeamsTable.test.tsx index 9699ea2b7d1..ad89b62d611 100644 --- a/ui/litellm-dashboard/src/components/TeamsPage/TeamsTable.test.tsx +++ b/ui/litellm-dashboard/src/components/TeamsPage/TeamsTable.test.tsx @@ -161,6 +161,12 @@ describe("sort contract – only backend-sortable columns are sortable", () => { }); }); + it("right-aligns Spend / Budget but not Team", () => { + renderTable(); + expect(screen.getByRole("columnheader", { name: "Spend / Budget" })).toHaveClass("text-right"); + expect(screen.getByRole("columnheader", { name: "Team" })).not.toHaveClass("text-right"); + }); + it("does not make Spend / Budget sortable (the backend rejects sort_by=spend)", () => { renderTable(); expect(screen.queryByText("Spend / Budget").closest("button")).toBeNull(); diff --git a/ui/litellm-dashboard/src/components/TeamsPage/teamTableColumns.tsx b/ui/litellm-dashboard/src/components/TeamsPage/teamTableColumns.tsx index 84369a58307..05bfccf6417 100644 --- a/ui/litellm-dashboard/src/components/TeamsPage/teamTableColumns.tsx +++ b/ui/litellm-dashboard/src/components/TeamsPage/teamTableColumns.tsx @@ -210,7 +210,7 @@ export const getTeamTableColumns = ({ { id: "spend", accessorKey: "spend", - meta: { title: "Spend / Budget", skeleton: "meter" }, + meta: { title: "Spend / Budget", skeleton: "meter", numeric: true }, header: "Spend / Budget", size: 200, enableSorting: false, @@ -234,7 +234,7 @@ export const getTeamTableColumns = ({ }, { id: "members", - meta: { title: "Members" }, + meta: { title: "Members", numeric: true }, header: "Members", size: 110, enableSorting: false, @@ -242,7 +242,7 @@ export const getTeamTableColumns = ({ }, { id: "models", - meta: { title: "Models" }, + meta: { title: "Models", numeric: true }, header: "Models", size: 100, enableSorting: false, diff --git a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx index 7ef7f1fcb09..423723a5938 100644 --- a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx +++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx @@ -207,6 +207,12 @@ it("should render VirtualKeysTable component", () => { expect(screen.getByText("Test Key Alias")).toBeInTheDocument(); }); +it("right-aligns the Spend / Budget column", async () => { + renderWithProviders(); + expect(await screen.findByRole("columnheader", { name: /^Spend/ })).toHaveClass("text-right"); + expect(screen.getByRole("columnheader", { name: /^Key$/ })).not.toHaveClass("text-right"); +}); + it("shows the Budget Reset column by default", async () => { renderWithProviders(); await waitFor(() => { diff --git a/ui/litellm-dashboard/src/components/VirtualKeysPage/keyTableColumns.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/keyTableColumns.tsx index 477d7b0ecb4..d69e90b1882 100644 --- a/ui/litellm-dashboard/src/components/VirtualKeysPage/keyTableColumns.tsx +++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/keyTableColumns.tsx @@ -267,7 +267,7 @@ export const getKeyTableColumns = ({ { id: "spend", accessorKey: "spend", - meta: { title: "Spend / Budget", skeleton: "meter" }, + meta: { title: "Spend / Budget", skeleton: "meter", numeric: true }, header: ({ table }) => , size: 180, enableSorting: true, diff --git a/ui/litellm-dashboard/src/components/bulk_create_users_button.tsx b/ui/litellm-dashboard/src/components/bulk_create_users_button.tsx index 8669f067d6d..b5029e98f9c 100644 --- a/ui/litellm-dashboard/src/components/bulk_create_users_button.tsx +++ b/ui/litellm-dashboard/src/components/bulk_create_users_button.tsx @@ -1,7 +1,15 @@ import React, { useState, useEffect } from "react"; import { Button, buttonVariants } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + NUMERIC_CELL_CLASS, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import { Download, FileText, FileWarning, Trash2, TriangleAlert, Upload } from "lucide-react"; import { userCreateCall, invitationCreateCall, getProxyUISettings } from "./networking"; import Papa from "papaparse"; @@ -798,7 +806,7 @@ const BulkCreateUsersButton: React.FC = ({ Email Role Teams - Budget + Budget Status @@ -809,7 +817,7 @@ const BulkCreateUsersButton: React.FC = ({ {record.user_email} {record.user_role} {record.teams} - {record.max_budget} + {record.max_budget} {renderStatusCell(record)} ))} diff --git a/ui/litellm-dashboard/src/components/chat/KeysPanel.tsx b/ui/litellm-dashboard/src/components/chat/KeysPanel.tsx index 8b8bd6c8189..fe37102eaba 100644 --- a/ui/litellm-dashboard/src/components/chat/KeysPanel.tsx +++ b/ui/litellm-dashboard/src/components/chat/KeysPanel.tsx @@ -10,7 +10,16 @@ import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + NUMERIC_CELL_CLASS, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { cn } from "@/lib/cva.config"; import { toast } from "@/lib/toast"; import { keyListCall, regenerateKeyCall } from "../networking"; import { KeyResponse } from "../key_team_helpers/key_list"; @@ -176,7 +185,9 @@ const KeysPanel: React.FC = ({ accessToken, userId, premiumUser }) => { Key - Spend + + Spend + Expires Created {premiumUser && ( @@ -220,7 +231,9 @@ const KeysPanel: React.FC = ({ accessToken, userId, premiumUser }) => { Key - Spend + + Spend + Expires Created {premiumUser && ( @@ -237,7 +250,7 @@ const KeysPanel: React.FC = ({ accessToken, userId, premiumUser }) => { {maskKey(record.key_name)} {record.key_alias &&
{record.key_alias}
}
- + ${record.spend?.toFixed(2) ?? "0.00"} {record.max_budget != null && record.max_budget > 0 && ( / ${record.max_budget.toFixed(2)} diff --git a/ui/litellm-dashboard/src/components/common_components/MemberTable.test.tsx b/ui/litellm-dashboard/src/components/common_components/MemberTable.test.tsx index a16f11eab60..418f27c8188 100644 --- a/ui/litellm-dashboard/src/components/common_components/MemberTable.test.tsx +++ b/ui/litellm-dashboard/src/components/common_components/MemberTable.test.tsx @@ -213,3 +213,20 @@ describe("MemberTable actions", () => { expect(screen.getByText("No members found")).toBeInTheDocument(); }); }); + +describe("MemberTable numeric columns", () => { + it("right-aligns the header and cells of a numeric extra column only", () => { + renderTable({ + members: [MEMBERS[0]], + extraColumns: [ + { title: "Spend (USD)", key: "spend", numeric: true, render: () => $1.50 }, + { title: "Joined", key: "joined", render: () => Aug 1 }, + ], + }); + + expect(screen.getByRole("columnheader", { name: "Spend (USD)" })).toHaveClass("text-right", "tabular-nums"); + expect(screen.getByRole("cell", { name: "$1.50" })).toHaveClass("text-right", "tabular-nums"); + expect(screen.getByRole("columnheader", { name: "Joined" })).not.toHaveClass("text-right"); + expect(screen.getByRole("cell", { name: "Aug 1" })).not.toHaveClass("text-right"); + }); +}); diff --git a/ui/litellm-dashboard/src/components/common_components/MemberTable.tsx b/ui/litellm-dashboard/src/components/common_components/MemberTable.tsx index 6efc82e2d35..3cbe59bd839 100644 --- a/ui/litellm-dashboard/src/components/common_components/MemberTable.tsx +++ b/ui/litellm-dashboard/src/components/common_components/MemberTable.tsx @@ -24,6 +24,7 @@ export interface MemberTableColumn { key: string; render: (member: Member) => React.ReactNode; sortValue?: (member: Member) => MemberTableSortValue; + numeric?: boolean; } export interface MemberTableProps { @@ -87,6 +88,7 @@ const extraColumnDef = (column: MemberTableColumn): ColumnDef => { header: () => {column.title}, enableSorting: false, enableGlobalFilter: false, + meta: { numeric: column.numeric }, cell: ({ row }) => column.render(row.original), }; } @@ -97,6 +99,7 @@ const extraColumnDef = (column: MemberTableColumn): ColumnDef => { sortDescFirst: false, sortUndefined: "last", enableGlobalFilter: false, + meta: { numeric: column.numeric }, cell: ({ row }) => column.render(row.original), }; }; diff --git a/ui/litellm-dashboard/src/components/common_components/simple_table.test.tsx b/ui/litellm-dashboard/src/components/common_components/simple_table.test.tsx new file mode 100644 index 00000000000..885d0ed51ac --- /dev/null +++ b/ui/litellm-dashboard/src/components/common_components/simple_table.test.tsx @@ -0,0 +1,25 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { SimpleTable, type SimpleTableColumn } from "./simple_table"; + +interface Row { + name: string; + spend: number; +} + +const columns: SimpleTableColumn[] = [ + { header: "Name", accessor: "name" }, + { header: "Spend", accessor: "spend", numeric: true }, +]; + +describe("SimpleTable numeric columns", () => { + it("right-aligns the header and cells of a numeric column only", () => { + render(); + + expect(screen.getByRole("columnheader", { name: "Spend" })).toHaveClass("text-right", "tabular-nums"); + expect(screen.getByRole("cell", { name: "42" })).toHaveClass("text-right", "tabular-nums"); + expect(screen.getByRole("columnheader", { name: "Name" })).not.toHaveClass("text-right"); + expect(screen.getByRole("cell", { name: "Alice" })).not.toHaveClass("text-right"); + }); +}); diff --git a/ui/litellm-dashboard/src/components/common_components/simple_table.tsx b/ui/litellm-dashboard/src/components/common_components/simple_table.tsx index 6a30a2e0273..a4b84d28801 100644 --- a/ui/litellm-dashboard/src/components/common_components/simple_table.tsx +++ b/ui/litellm-dashboard/src/components/common_components/simple_table.tsx @@ -1,11 +1,20 @@ import React from "react"; -import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table"; +import { + NUMERIC_CELL_CLASS, + Table, + TableHeader, + TableRow, + TableHead, + TableBody, + TableCell, +} from "@/components/ui/table"; export interface SimpleTableColumn { header: string; accessor?: keyof T; cell?: (row: T) => React.ReactNode; width?: string; + numeric?: boolean; } interface SimpleTableProps { @@ -34,7 +43,11 @@ export function SimpleTable({ {columns.map((column, index) => ( - + {column.header} ))} @@ -51,7 +64,7 @@ export function SimpleTable({ data.map((row, rowIndex) => ( {columns.map((column, colIndex) => ( - + {column.cell ? column.cell(row) : String(row[column.accessor as keyof T] ?? "")} ))} diff --git a/ui/litellm-dashboard/src/components/organization/organization_view.tsx b/ui/litellm-dashboard/src/components/organization/organization_view.tsx index c800d12ad62..f325e92d2b5 100644 --- a/ui/litellm-dashboard/src/components/organization/organization_view.tsx +++ b/ui/litellm-dashboard/src/components/organization/organization_view.tsx @@ -134,6 +134,7 @@ const OrganizationInfoView: React.FC = ({ { title: "Spend (USD)", key: "spend", + numeric: true, sortValue: (record: Member) => orgMemberFor(record)?.spend ?? null, render: (record: Member) => , }, diff --git a/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.test.tsx b/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.test.tsx index a7e4befa6ac..df88287b369 100644 --- a/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.test.tsx +++ b/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.test.tsx @@ -141,8 +141,33 @@ const expansionColumns: ColumnDef[] = [ }, ]; +const numericColumns: ColumnDef[] = [ + { + accessorKey: "name", + header: "Name", + cell: ({ row }) => {row.original.name}, + }, + { + id: "spend", + header: ({ column }) => , + meta: { numeric: true }, + cell: () => $1.50, + }, +]; + const CHARLIE_ALICE_BOB: Person[] = [person("c", "Charlie"), person("a", "Alice"), person("b", "Bob")]; +describe("DataTable numeric columns", () => { + it("right-aligns the header and cells of a numeric column only", () => { + render(); + + expect(screen.getByRole("columnheader", { name: "Spend" })).toHaveClass("text-right", "tabular-nums"); + expect(screen.getByRole("cell", { name: "$1.50" })).toHaveClass("text-right", "tabular-nums"); + expect(screen.getByRole("columnheader", { name: "Name" })).not.toHaveClass("text-right"); + expect(screen.getByRole("cell", { name: "Alice" })).not.toHaveClass("text-right"); + }); +}); + describe("DataTable sorting", () => { it("client mode reorders rows when the sort header is clicked", async () => { const user = userEvent.setup(); diff --git a/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.tsx b/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.tsx index 340f8d4f44f..feb1615b4e1 100644 --- a/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.tsx +++ b/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.tsx @@ -31,6 +31,7 @@ import { Fragment, useEffect, useState } from "react"; import { Skeleton } from "@/components/ui/skeleton"; import { + NUMERIC_CELL_CLASS, Table as TableRoot, TableBody, TableCell, @@ -193,7 +194,7 @@ function DataTableHeadCell({ header, size, stickyHeader, enableColumnResi className={cn( "relative text-muted-foreground", size === "compact" ? "h-8 px-2 py-1 text-xs" : "", - meta?.numeric ? "text-right" : "", + meta?.numeric ? NUMERIC_CELL_CLASS : "", meta?.className, meta?.headerClassName, sticky.className, @@ -238,7 +239,7 @@ function DataTableBodyCell({ cell, size, stickyHeader, enableColumnResizi className={cn( "overflow-hidden text-ellipsis", size === "compact" ? "px-2 py-1 text-xs" : "", - meta?.numeric ? "text-right tabular-nums" : "", + meta?.numeric ? NUMERIC_CELL_CLASS : "", meta?.className, sticky.className, )} diff --git a/ui/litellm-dashboard/src/components/team/TeamMemberTab.test.tsx b/ui/litellm-dashboard/src/components/team/TeamMemberTab.test.tsx index bd7398bc6c8..1d50a9a4670 100644 --- a/ui/litellm-dashboard/src/components/team/TeamMemberTab.test.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamMemberTab.test.tsx @@ -292,6 +292,9 @@ describe("TeamMembersComponent", () => { expect(screen.getByText("$100.50")).toBeInTheDocument(); expect(screen.getByText("$1,538.26")).toBeInTheDocument(); + expect(screen.getByRole("cell", { name: "$100.50" })).toHaveClass("text-right"); + expect(screen.getByRole("columnheader", { name: /^Team Member Budget \(USD\)/ })).toHaveClass("text-right"); + expect(screen.getByRole("columnheader", { name: "User Email" })).not.toHaveClass("text-right"); expect(screen.getByText(/100 RPM/)).toBeInTheDocument(); expect(screen.getByText(/10000 TPM/)).toBeInTheDocument(); }); diff --git a/ui/litellm-dashboard/src/components/team/TeamMemberTab.tsx b/ui/litellm-dashboard/src/components/team/TeamMemberTab.tsx index e8576ce6dfe..a24d1b1e7cd 100644 --- a/ui/litellm-dashboard/src/components/team/TeamMemberTab.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamMemberTab.tsx @@ -186,6 +186,7 @@ export default function TeamMemberTab({ ), key: "spend", + numeric: true, sortValue: (record: Member) => getUserCurrentCycleSpend(record.user_id), render: (record: Member) => , }, @@ -199,6 +200,7 @@ export default function TeamMemberTab({ ), key: "total_spend", + numeric: true, sortValue: (record: Member) => getUserTotalSpend(record.user_id), render: (record: Member) => , }, @@ -212,11 +214,12 @@ export default function TeamMemberTab({ ), key: "budget", + numeric: true, sortValue: (record: Member) => getUserBudget(record.user_id), render: (record: Member) => { const source = getUserBudgetSource(record.user_id); return ( - + {source !== "none" && ( diff --git a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.test.tsx b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.test.tsx index 08408fb4ff4..7561264ea65 100644 --- a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.test.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.test.tsx @@ -131,6 +131,14 @@ describe("TeamVirtualKeysTable", () => { }); }); + it("right-aligns the Spend (USD) and Budget (USD) columns", async () => { + renderWithProviders(); + + expect(await screen.findByRole("columnheader", { name: "Spend (USD)" })).toHaveClass("text-right"); + expect(screen.getByRole("columnheader", { name: "Budget (USD)" })).toHaveClass("text-right"); + expect(screen.getByRole("columnheader", { name: "Key ID" })).not.toHaveClass("text-right"); + }); + it("should display keys in table when data is loaded", async () => { mockUseKeys.mockReturnValue({ data: { diff --git a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx index 5b1b71e060e..4e4fa3f4bb2 100644 --- a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx @@ -285,7 +285,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi { id: "spend", accessorKey: "spend", - meta: { title: "Spend (USD)" }, + meta: { title: "Spend (USD)", numeric: true }, header: ({ column }) => , size: 100, enableSorting: true, @@ -294,7 +294,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi { id: "max_budget", accessorKey: "max_budget", - meta: { title: "Budget (USD)" }, + meta: { title: "Budget (USD)", numeric: true }, header: ({ column }) => , size: 110, enableSorting: true, diff --git a/ui/litellm-dashboard/src/components/ui/table.tsx b/ui/litellm-dashboard/src/components/ui/table.tsx index 6271a9e89ac..1c4c1a981de 100644 --- a/ui/litellm-dashboard/src/components/ui/table.tsx +++ b/ui/litellm-dashboard/src/components/ui/table.tsx @@ -4,6 +4,8 @@ import * as React from "react"; import { cn } from "@/lib/cva.config"; +const NUMERIC_CELL_CLASS = "text-right tabular-nums"; + const Table = React.forwardRef>( ({ className, ...props }, ref) => (
@@ -96,4 +98,4 @@ const TableCaption = React.forwardRef