This commit is contained in:
ryan-crabbe-berri 2026-08-26 08:09:03 -04:00 • committed by GitHub
commit 9e20545a8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 249 additions and 41 deletions

View file

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

@ -173,6 +173,8 @@ describe("AllModelsTable", () => {
const { rerender } = render(<AllModelsTable {...baseProps} />);
expect(screen.getByText("$30")).toBeInTheDocument();
expect(screen.getByText("$60")).toBeInTheDocument();
expect(screen.getByText("$30").closest("td")).toHaveClass("text-right");
expect(screen.getByRole("columnheader", { name: /costs/i })).toHaveClass("text-right");
rerender(
<AllModelsTable

View file

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

@ -52,6 +52,16 @@ describe("OrganizationsTable", () => {
}
});
it("right-aligns the money and count columns only", () => {
render(<OrganizationsTable {...baseProps} organizations={[]} />);
for (const header of ["Spend (USD)", "Budget (USD)", "Members"]) {
expect(screen.getByText(header).closest("th")).toHaveClass("text-right");
}
for (const header of ["Organization Name", "TPM / RPM Limits"]) {
expect(screen.getByText(header).closest("th")).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.getByText("128.0K / 16.4K").closest("td")).toHaveClass("text-right");
expect(screen.getByText("$2.50").closest("td")).toHaveClass("text-right");
expect(screen.getByText("gpt-4o").closest("td")).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

@ -149,6 +149,12 @@ describe("sort contract – only backend-sortable columns are sortable", () => {
});
});
it("right-aligns Spend / Budget but not Team", () => {
renderTable();
expect(screen.getByText("Spend / Budget").closest("th")).toHaveClass("text-right");
expect(screen.getByText("Team").closest("th")).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

@ -209,7 +209,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,
@ -233,7 +233,7 @@ export const getTeamTableColumns = ({
},
{
id: "members",
meta: { title: "Members" },
meta: { title: "Members", numeric: true },
header: "Members",
size: 110,
enableSorting: false,
@ -241,7 +241,7 @@ export const getTeamTableColumns = ({
},
{
id: "models",
meta: { title: "Models" },
meta: { title: "Models", numeric: true },
header: "Models",
size: 100,
enableSorting: false,

View file

@ -196,6 +196,13 @@ it("should render VirtualKeysTable component", () => {
expect(screen.getByText("Test Key Alias")).toBeInTheDocument();
});
it("right-aligns the Spend / Budget column", async () => {
renderWithProviders(<VirtualKeysTable />);
const spendHeader = await screen.findByText("Spend", { selector: "[data-sort-field='spend']" });
expect(spendHeader.closest("th")).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

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

@ -0,0 +1,31 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import type { Member } from "@/components/networking";
import MemberTable, { type MemberTableColumn } from "./MemberTable";
const member: Member = { role: "user", user_id: "u1", user_email: "u1@x.io" };
const extraColumns: MemberTableColumn[] = [
{ title: "Spend (USD)", key: "spend", numeric: true, render: () => <span>$1.50</span> },
{ title: "Joined", key: "joined", render: () => <span>Aug 1</span> },
];
describe("MemberTable numeric columns", () => {
it("right-aligns the header and cells of a numeric extra column only", () => {
render(
<MemberTable
members={[member]}
canEdit={false}
onEdit={vi.fn()}
onDelete={vi.fn()}
extraColumns={extraColumns}
/>,
);
expect(screen.getByText("Spend (USD)").closest("th")).toHaveClass("text-right", "tabular-nums");
expect(screen.getByText("$1.50").closest("td")).toHaveClass("text-right", "tabular-nums");
expect(screen.getByText("Joined").closest("th")).not.toHaveClass("text-right");
expect(screen.getByText("Aug 1").closest("td")).not.toHaveClass("text-right");
});
});

View file

@ -2,7 +2,15 @@ import { SimpleTooltip } from "@/components/ui/tooltip";
import { Member } from "@/components/networking";
import { StatusBadge } from "@/components/shared/table_cells";
import { Button } from "@/components/ui/button";
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 { Crown, Info, User, UserPlus } from "lucide-react";
import React from "react";
import TableIconActionButton from "./IconActionButton/TableIconActionButtons/TableIconActionButton";
@ -12,6 +20,7 @@ export interface MemberTableColumn {
key: React.Key;
dataIndex?: keyof Member;
render?: (value: Member[keyof Member], member: Member, index: number) => React.ReactNode;
numeric?: boolean;
}
export interface MemberTableProps {
@ -34,6 +43,9 @@ const extraColumnCell = (column: MemberTableColumn, member: Member, index: numbe
const STICKY_ACTIONS_CLASS = "sticky right-0 w-[120px] bg-background";
const extraColumnClass = (column: MemberTableColumn): string | undefined =>
column.numeric ? NUMERIC_CELL_CLASS : undefined;
export default function MemberTable({
members,
canEdit,
@ -69,7 +81,9 @@ export default function MemberTable({
)}
</TableHead>
{extraColumns.map((column) => (
<TableHead key={column.key}>{column.title}</TableHead>
<TableHead key={column.key} className={extraColumnClass(column)}>
{column.title}
</TableHead>
))}
<TableHead className={STICKY_ACTIONS_CLASS}>Actions</TableHead>
</TableRow>
@ -103,7 +117,9 @@ export default function MemberTable({
</span>
</TableCell>
{extraColumns.map((column) => (
<TableCell key={column.key}>{extraColumnCell(column, member, memberIndex)}</TableCell>
<TableCell key={column.key} className={extraColumnClass(column)}>
{extraColumnCell(column, member, memberIndex)}
</TableCell>
))}
<TableCell className={STICKY_ACTIONS_CLASS}>
{canEdit ? (

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.getByText("42").closest("td")).toHaveClass("text-right", "tabular-nums");
expect(screen.getByRole("columnheader", { name: "Name" })).not.toHaveClass("text-right");
expect(screen.getByText("Alice").closest("td")).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

@ -125,6 +125,7 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
{
title: "Spend (USD)",
key: "spend",
numeric: true,
render: (_: unknown, record: Member) => {
const orgMember =
record.user_id != null ? (orgData.members || []).find((m) => m.user_id === record.user_id) : undefined;

View file

@ -134,8 +134,35 @@ 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" />);
const spendHeader = screen.getByText("Spend").closest("th");
expect(spendHeader).toHaveClass("text-right", "tabular-nums");
expect(spendHeader?.querySelector("div")).toHaveClass("justify-end");
expect(screen.getByText("$1.50").closest("td")).toHaveClass("text-right", "tabular-nums");
expect(screen.getByText("Name").closest("th")).not.toHaveClass("text-right");
expect(screen.getByTestId("name-cell").closest("td")).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

@ -30,6 +30,7 @@ import { Fragment, useState } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import {
NUMERIC_CELL_CLASS,
Table as TableRoot,
TableBody,
TableCell,
@ -179,7 +180,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,
@ -225,7 +226,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

@ -249,6 +249,9 @@ describe("TeamMembersComponent", () => {
expect(screen.getByText("$100.50")).toBeInTheDocument();
expect(screen.getByText("$1,538.26")).toBeInTheDocument();
expect(screen.getByText("$100.50").closest("td")).toHaveClass("text-right");
expect(screen.getByText("Team Member Budget (USD)").closest("th")).toHaveClass("text-right");
expect(screen.getByText("User Email").closest("th")).not.toHaveClass("text-right");
expect(screen.getByText(/100 RPM/)).toBeInTheDocument();
expect(screen.getByText(/10000 TPM/)).toBeInTheDocument();
});

View file

@ -141,6 +141,7 @@ export default function TeamMemberTab({
</span>
),
key: "spend",
numeric: true,
render: (_: unknown, record: Member) => (
<MoneyCell value={getUserCurrentCycleSpend(record.user_id)} decimals={2} />
),
@ -155,11 +156,13 @@ export default function TeamMemberTab({
</span>
),
key: "total_spend",
numeric: true,
render: (_: unknown, record: Member) => <MoneyCell value={getUserTotalSpend(record.user_id)} decimals={2} />,
},
{
title: "Team Member Budget (USD)",
key: "budget",
numeric: true,
render: (_: unknown, record: Member) => (
<MoneyCell value={getUserBudget(record.user_id)} decimals={2} emptyText="Unlimited" showZero />
),

View file

@ -127,6 +127,14 @@ describe("TeamVirtualKeysTable", () => {
});
});
it("right-aligns the Spend (USD) and Budget (USD) columns", async () => {
renderWithProviders(<TeamVirtualKeysTable {...defaultProps} />);
expect((await screen.findByText("Spend (USD)")).closest("th")).toHaveClass("text-right");
expect(screen.getByText("Budget (USD)").closest("th")).toHaveClass("text-right");
expect(screen.getByText("Key ID").closest("th")).not.toHaveClass("text-right");
});
it("should display keys in table when data is loaded", async () => {
mockUseKeys.mockReturnValue({
data: {

View file

@ -301,7 +301,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,
@ -310,7 +310,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 };