fix(ui): right-align money and count columns across tables (#37889)

* fix(ui): right-align money and count columns across tables

Make numeric the one alignment token for the three shared table
wrappers (DataTable, MemberTable, SimpleTable) via NUMERIC_CELL_CLASS,
and flag every money, cost, and bare-count column that was still
left-aligned. Raw ui/table usages that render spend or budgets get the
same class on their header and cell.

* test(ui): render the organizations alignment test with providers

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(ui): query alignment cells by role instead of DOM traversal

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
ryan-crabbe-berri 2026-09-30 10:59:53 -07:00 • committed by GitHub
parent 7204942756
commit 0736a14326
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 217 additions and 39 deletions

View file

@ -33,6 +33,12 @@ describe("AgentsTable", () => {
}
});
it("right-aligns the Spend (USD) column", () => {
render(<AgentsTable agents={[]} {...baseProps} />);
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();

View file

@ -90,7 +90,7 @@ export const getAgentsTableColumns = ({
{
id: "spend",
accessorKey: "spend",
meta: { title: "Spend (USD)" },
meta: { title: "Spend (USD)", numeric: true },
header: ({ column }) => <DataTableSortHeader column={column} title="Spend (USD)" />,
size: 130,
enableSorting: true,

View file

@ -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", () => {

View file

@ -80,10 +80,11 @@ const ProviderDiscountTable: React.FC<ProviderDiscountTableProps> = ({
},
{
header: "Discount Percentage",
numeric: true,
cell: (row) => {
const { displayName } = getProviderLogoAndName(row.provider);
return (
<div className="flex items-center gap-2">
<div className="flex items-center justify-end gap-2">
{editingProvider === row.provider ? (
<>
<Input

View file

@ -46,6 +46,8 @@ describe("ProviderMarginTable", () => {
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", () => {

View file

@ -123,10 +123,11 @@ const ProviderMarginTable: React.FC<ProviderMarginTableProps> = ({
},
{
header: "Margin",
numeric: true,
cell: (row) => {
const displayName = marginRowDisplayName(row.provider);
return (
<div className="flex items-center gap-2">
<div className="flex items-center justify-end gap-2">
{editingProvider === row.provider ? (
<>
<div className="flex items-center gap-2">

View file

@ -174,6 +174,8 @@ describe("AllModelsTable", () => {
const { rerender } = render(<AllModelsTable {...baseProps} />);
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(<AllModelsTable {...baseProps} data={[makeModel({ input_cost: null, output_cost: null })]} />);
expect(screen.queryByText(/^\$/)).not.toBeInTheDocument();

View file

@ -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 }) => <DataTableSortHeader column={column} title="Costs" />,
enableSorting: true,
size: 130,

View file

@ -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<UsagePageProps> = ({ accessToken, token, userRole, use
<TableHeader>
<TableRow>
<TableHead>Provider</TableHead>
<TableHead>Spend</TableHead>
<TableHead className={NUMERIC_CELL_CLASS}>Spend</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{spendByProvider.map((provider) => (
<TableRow key={provider.provider}>
<TableCell>{provider.provider}</TableCell>
<TableCell>
<TableCell className={NUMERIC_CELL_CLASS}>
<MoneyCell value={provider.spend} decimals={2} />
</TableCell>
</TableRow>
@ -840,8 +848,8 @@ const UsagePage: React.FC<UsagePageProps> = ({ accessToken, token, userRole, use
<TableHeader>
<TableRow>
<TableHead>Customer</TableHead>
<TableHead>Spend</TableHead>
<TableHead>Total Events</TableHead>
<TableHead className={NUMERIC_CELL_CLASS}>Spend</TableHead>
<TableHead className={NUMERIC_CELL_CLASS}>Total Events</TableHead>
</TableRow>
</TableHeader>
@ -849,10 +857,10 @@ const UsagePage: React.FC<UsagePageProps> = ({ accessToken, token, userRole, use
{topUsers?.map((user: any, index: number) => (
<TableRow key={index}>
<TableCell>{user.end_user}</TableCell>
<TableCell>
<TableCell className={NUMERIC_CELL_CLASS}>
<MoneyCell value={user.total_spend} decimals={2} />
</TableCell>
<TableCell>{user.total_count}</TableCell>
<TableCell className={NUMERIC_CELL_CLASS}>{user.total_count}</TableCell>
</TableRow>
))}
</TableBody>

View file

@ -82,6 +82,16 @@ describe("OrganizationsTable", () => {
}
});
it("right-aligns the money and count columns only", () => {
renderWithProviders(<OrganizationsTable {...baseProps} organizations={[]} />);
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();

View file

@ -129,7 +129,7 @@ export const getOrganizationsTableColumns = ({
{
id: "spend",
accessorKey: "spend",
meta: { title: "Spend (USD)" },
meta: { title: "Spend (USD)", numeric: true },
header: ({ column }) => <DataTableSortHeader column={column} title="Spend (USD)" />,
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,

View file

@ -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<BulkEditUserModalProps> = ({
<TableHead className="w-[30%]">User ID</TableHead>
<TableHead className="w-[25%]">Email</TableHead>
<TableHead className="w-[25%]">Current Role</TableHead>
<TableHead className="w-[20%]">Budget</TableHead>
<TableHead className={cn("w-[20%]", NUMERIC_CELL_CLASS)}>Budget</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@ -263,7 +272,7 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
<TableCell className="text-xs text-foreground">
{possibleUIRoles?.[user.user_role]?.ui_label || user.user_role}
</TableCell>
<TableCell>
<TableCell className={NUMERIC_CELL_CLASS}>
<MoneyCell value={user.max_budget} decimals={2} emptyText="Unlimited" showZero />
</TableCell>
</TableRow>

View file

@ -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", () => {

View file

@ -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 }) => <DataTableSortHeader column={column} title="Tokens" />,
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 }) => <DataTableSortHeader column={column} title="Cost/1M" />,
size: 110,
enableSorting: true,

View file

@ -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();

View file

@ -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,

View file

@ -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(<VirtualKeysTable />);
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(<VirtualKeysTable />);
await waitFor(() => {

View file

@ -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 }) => <DataTableMultiSortHeader table={table} fields={SPEND_BUDGET_SORT_FIELDS} />,
size: 180,
enableSorting: true,

View file

@ -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<BulkCreateUsersProps> = ({
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead>Teams</TableHead>
<TableHead>Budget</TableHead>
<TableHead className={NUMERIC_CELL_CLASS}>Budget</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
@ -809,7 +817,7 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
<TableCell className="whitespace-normal break-words">{record.user_email}</TableCell>
<TableCell className="whitespace-normal break-words">{record.user_role}</TableCell>
<TableCell className="whitespace-normal break-words">{record.teams}</TableCell>
<TableCell>{record.max_budget}</TableCell>
<TableCell className={NUMERIC_CELL_CLASS}>{record.max_budget}</TableCell>
<TableCell className="whitespace-normal break-words">{renderStatusCell(record)}</TableCell>
</TableRow>
))}

View file

@ -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<Props> = ({ accessToken, userId, premiumUser }) => {
<TableHeader>
<TableRow className="bg-muted/50">
<TableHead className="text-xs font-semibold uppercase tracking-wide">Key</TableHead>
<TableHead className="text-xs font-semibold uppercase tracking-wide">Spend</TableHead>
<TableHead className={cn("text-xs font-semibold uppercase tracking-wide", NUMERIC_CELL_CLASS)}>
Spend
</TableHead>
<TableHead className="text-xs font-semibold uppercase tracking-wide">Expires</TableHead>
<TableHead className="text-xs font-semibold uppercase tracking-wide">Created</TableHead>
{premiumUser && (
@ -220,7 +231,9 @@ const KeysPanel: React.FC<Props> = ({ accessToken, userId, premiumUser }) => {
<TableHeader>
<TableRow className="bg-muted/50">
<TableHead className="text-xs font-semibold uppercase tracking-wide">Key</TableHead>
<TableHead className="text-xs font-semibold uppercase tracking-wide">Spend</TableHead>
<TableHead className={cn("text-xs font-semibold uppercase tracking-wide", NUMERIC_CELL_CLASS)}>
Spend
</TableHead>
<TableHead className="text-xs font-semibold uppercase tracking-wide">Expires</TableHead>
<TableHead className="text-xs font-semibold uppercase tracking-wide">Created</TableHead>
{premiumUser && (
@ -237,7 +250,7 @@ const KeysPanel: React.FC<Props> = ({ accessToken, userId, premiumUser }) => {
<span className="font-mono text-[13px]">{maskKey(record.key_name)}</span>
{record.key_alias && <div className="text-xs text-muted-foreground">{record.key_alias}</div>}
</TableCell>
<TableCell className="text-[13px]">
<TableCell className={cn("text-[13px]", NUMERIC_CELL_CLASS)}>
${record.spend?.toFixed(2) ?? "0.00"}
{record.max_budget != null && record.max_budget > 0 && (
<span className="text-muted-foreground"> / ${record.max_budget.toFixed(2)}</span>

View file

@ -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: () => <span>$1.50</span> },
{ title: "Joined", key: "joined", render: () => <span>Aug 1</span> },
],
});
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");
});
});

View file

@ -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<Member> => {
header: () => <span className="font-medium">{column.title}</span>,
enableSorting: false,
enableGlobalFilter: false,
meta: { numeric: column.numeric },
cell: ({ row }) => column.render(row.original),
};
}
@ -97,6 +99,7 @@ const extraColumnDef = (column: MemberTableColumn): ColumnDef<Member> => {
sortDescFirst: false,
sortUndefined: "last",
enableGlobalFilter: false,
meta: { numeric: column.numeric },
cell: ({ row }) => column.render(row.original),
};
};

View file

@ -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<Row>[] = [
{ 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(<SimpleTable data={[{ name: "Alice", spend: 42 }]} columns={columns} />);
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");
});
});

View file

@ -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<T> {
header: string;
accessor?: keyof T;
cell?: (row: T) => React.ReactNode;
width?: string;
numeric?: boolean;
}
interface SimpleTableProps<T> {
@ -34,7 +43,11 @@ export function SimpleTable<T>({
<TableHeader>
<TableRow>
{columns.map((column, index) => (
<TableHead key={index} style={{ width: column.width }}>
<TableHead
key={index}
style={{ width: column.width }}
className={column.numeric ? NUMERIC_CELL_CLASS : undefined}
>
{column.header}
</TableHead>
))}
@ -51,7 +64,7 @@ export function SimpleTable<T>({
data.map((row, rowIndex) => (
<TableRow key={getRowKey ? getRowKey(row, rowIndex) : rowIndex}>
{columns.map((column, colIndex) => (
<TableCell key={colIndex}>
<TableCell key={colIndex} className={column.numeric ? NUMERIC_CELL_CLASS : undefined}>
{column.cell ? column.cell(row) : String(row[column.accessor as keyof T] ?? "")}
</TableCell>
))}

View file

@ -134,6 +134,7 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
{
title: "Spend (USD)",
key: "spend",
numeric: true,
sortValue: (record: Member) => orgMemberFor(record)?.spend ?? null,
render: (record: Member) => <MoneyCell value={orgMemberFor(record)?.spend} decimals={4} />,
},

View file

@ -141,8 +141,33 @@ const expansionColumns: ColumnDef<Person, unknown>[] = [
},
];
const numericColumns: ColumnDef<Person, unknown>[] = [
{
accessorKey: "name",
header: "Name",
cell: ({ row }) => <span data-testid="name-cell">{row.original.name}</span>,
},
{
id: "spend",
header: ({ column }) => <DataTableSortHeader column={column} title="Spend" />,
meta: { numeric: true },
cell: () => <span>$1.50</span>,
},
];
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(<DataTable data={[person("a", "Alice")]} columns={numericColumns} sortingMode="client" />);
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();

View file

@ -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<TData>({ 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<TData>({ 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,
)}

View file

@ -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();
});

View file

@ -186,6 +186,7 @@ export default function TeamMemberTab({
</span>
),
key: "spend",
numeric: true,
sortValue: (record: Member) => getUserCurrentCycleSpend(record.user_id),
render: (record: Member) => <MoneyCell value={getUserCurrentCycleSpend(record.user_id)} decimals={2} />,
},
@ -199,6 +200,7 @@ export default function TeamMemberTab({
</span>
),
key: "total_spend",
numeric: true,
sortValue: (record: Member) => getUserTotalSpend(record.user_id),
render: (record: Member) => <MoneyCell value={getUserTotalSpend(record.user_id)} decimals={2} />,
},
@ -212,11 +214,12 @@ export default function TeamMemberTab({
</span>
),
key: "budget",
numeric: true,
sortValue: (record: Member) => getUserBudget(record.user_id),
render: (record: Member) => {
const source = getUserBudgetSource(record.user_id);
return (
<span className="flex items-center gap-2">
<span className="flex items-center justify-end gap-2">
<MoneyCell value={getUserBudget(record.user_id)} decimals={2} emptyText="Unlimited" showZero />
{source !== "none" && (
<Badge variant={source === "custom" ? "outline" : "secondary"} data-testid="member-budget-source">

View file

@ -131,6 +131,14 @@ describe("TeamVirtualKeysTable", () => {
});
});
it("right-aligns the Spend (USD) and Budget (USD) columns", async () => {
renderWithProviders(<TeamVirtualKeysTable {...defaultProps} />);
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: {

View file

@ -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 }) => <DataTableSortHeader column={column} title="Spend (USD)" variant="header-cycle" />,
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 }) => <DataTableSortHeader column={column} title="Budget (USD)" variant="header-cycle" />,
size: 110,
enableSorting: true,

View file

@ -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<HTMLTableElement, React.ComponentPropsWithoutRef<"table">>(
({ className, ...props }, ref) => (
<div data-slot="table-container" className="relative w-full overflow-x-auto">
@ -96,4 +98,4 @@ const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.ComponentPr
);
TableCaption.displayName = "TableCaption";
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
export { NUMERIC_CELL_CLASS, Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };