refactor(ui): consolidate table cells onto a shared table_cells kit (#32393)

* feat(ui): add shared table_cells kit and convert logs columns

DateCell, MoneyCell, IdCell and StatusBadge consolidate the duplicated
per-table cell implementations behind one component each. The logs page
columns are the reference conversion; the dead auditLogColumns export
(superseded by audit_logs.tsx) is removed with it

* refactor(ui): consolidate table cells onto the shared table_cells kit

106 cell sites across 44 table files converge onto DateCell, MoneyCell,
IdCell and StatusBadge, replacing 8 date formats, 6 spend formats, 7 id
truncation strategies and 6 status badge styles with one implementation
each. Badge now forwards refs so Base UI tooltip triggers composed over
it can attach (they previously never opened under React 18). TimeCell
is deleted; its two consumers now render DateCell

* fix(ui): suppress cost tooltip for zero spend and drop dead getStatusBadge param

The logs Cost tooltip showed the raw $0 over a "-" cell for zero or
null spend (pre-existing, surfaced by review); the tooltip now only
renders when there is a real amount. healthCheckColumns no longer takes
the unused getStatusBadge callback and its dead definition is removed

* fix(ui): restyle StatusBadge as tinted pill matching the prior antd Tag look

* fix(ui): keep StatusBadge fully rounded like the other kit pills
This commit is contained in:
ryan-crabbe-berri 2026-07-08 15:28:28 -07:00 • committed by GitHub
parent 5b8bf5357a
commit bd23c44cb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
75 changed files with 918 additions and 1139 deletions

View file

@ -1,7 +1,7 @@
{
"@typescript-eslint/no-explicit-any": 1988,
"@typescript-eslint/no-explicit-any": 1982,
"complexity": 128,
"local/no-large-inline-object-arg": 513,
"local/no-large-inline-object-arg": 512,
"local/no-long-condition-chain": 233,
"max-depth": 59,
"no-console": 15

View file

@ -1650,7 +1650,7 @@
"count": 1
},
"no-restricted-imports": {
"count": 2
"count": 1
}
},
"src/components/guardrails/tool_permission/ToolPermissionRulesEditor.tsx": {
@ -2491,11 +2491,6 @@
"count": 4
}
},
"src/components/view_logs/columns.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/view_logs/index.tsx": {
"no-restricted-imports": {
"count": 1

View file

@ -19,6 +19,7 @@ import {
SortState,
TableHeaderSortDropdown,
} from "@/components/common_components/TableHeaderSortDropdown/TableHeaderSortDropdown";
import { DateCell, IdCell } from "@/components/shared/table_cells";
import { AccessGroupDetail } from "./AccessGroupsDetailsPage";
import { AccessGroupCreateModal } from "./AccessGroupsModal/AccessGroupCreateModal";
import { AccessGroup } from "./types";
@ -143,21 +144,7 @@ export function AccessGroupsPage() {
header: () => <span>ID</span>,
enableSorting: false,
size: 170,
cell: ({ row }) => {
const record = row.original;
return (
<Tooltip title={record.id}>
<Text
ellipsis
className="text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs cursor-pointer"
style={{ fontSize: 14, padding: "1px 8px" }}
onClick={() => setSelectedGroupId(record.id)}
>
{record.id}
</Text>
</Tooltip>
);
},
cell: ({ row }) => <IdCell value={row.original.id} onClick={setSelectedGroupId} />,
},
{
id: "name",
@ -211,7 +198,7 @@ export function AccessGroupsPage() {
header: () => <span>Created</span>,
enableSorting: true,
sortingFn: "datetime",
cell: ({ getValue }) => new Date(getValue() as string).toLocaleDateString(),
cell: ({ getValue }) => <DateCell value={getValue() as string} precision="date" />,
meta: { responsive: ["lg"] },
},
{
@ -219,7 +206,7 @@ export function AccessGroupsPage() {
accessorKey: "updatedAt",
header: () => <span>Updated</span>,
enableSorting: false,
cell: ({ getValue }) => new Date(getValue() as string).toLocaleDateString(),
cell: ({ getValue }) => <DateCell value={getValue() as string} precision="date" />,
meta: { responsive: ["xl"] },
},
...(canModify

View file

@ -25,6 +25,7 @@ import DeleteResourceModal from "@/components/common_components/DeleteResourceMo
import TableIconActionButton from "@/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
import NotificationsManager from "@/components/molecules/notifications_manager";
import { useBudgets, useDeleteBudget, budgetItem } from "@/app/(dashboard)/hooks/budgets/useBudgets";
import { MoneyCell } from "@/components/shared/table_cells";
import BudgetModal from "./budget_modal";
import EditBudgetModal from "./edit_budget_modal";
import { CREATE_END_USER_CURL_COMMAND, CHAT_COMPLETIONS_CURL_COMMAND, OPENAI_SDK_PYTHON_CODE } from "./constants";
@ -127,7 +128,9 @@ const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
.map((value: budgetItem) => (
<TableRow key={value.budget_id}>
<TableCell>{value.budget_id}</TableCell>
<TableCell>{value.max_budget ? value.max_budget : "n/a"}</TableCell>
<TableCell>
<MoneyCell value={value.max_budget} decimals={2} showZero emptyText="Unlimited" />
</TableCell>
<TableCell>{value.tpm_limit ? value.tpm_limit : "n/a"}</TableCell>
<TableCell>{value.rpm_limit ? value.rpm_limit : "n/a"}</TableCell>
{canModify && (

View file

@ -2,7 +2,7 @@
import React, { useMemo, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Button, Card, Drawer, Empty, Input, Space, Table, Tooltip, Typography, message } from "antd";
import { Button, Card, Drawer, Empty, Input, Space, Table, Typography, message } from "antd";
import type { ColumnsType } from "antd/es/table";
import {
DeleteOutlined,
@ -13,6 +13,7 @@ import {
SearchOutlined,
} from "@ant-design/icons";
import { MemoryRow, createMemory, deleteMemory, fetchMemoryList, updateMemory } from "@/components/networking";
import { DateCell, IdCell } from "@/components/shared/table_cells";
import { MemoryEditModal } from "./MemoryEditModal";
import DeleteResourceModal from "@/components/common_components/DeleteResourceModal";
@ -191,34 +192,13 @@ export const MemoryView: React.FC<MemoryViewProps> = ({ accessToken }) => {
}
};
const renderIdPill = (id: string | null | undefined, onClick?: () => void) => {
if (!id) return <Text type="secondary">-</Text>;
const short = id.length > 10 ? `${id.slice(0, 7)}...` : id;
const pillClass =
"font-mono text-blue-600 bg-blue-50 text-xs font-medium px-2 py-0.5 rounded-md border border-blue-200 inline-block max-w-[15ch] truncate whitespace-nowrap";
return (
<Tooltip title={id}>
{onClick ? (
<button
onClick={onClick}
className={`${pillClass} hover:bg-blue-100 cursor-pointer transition-colors text-left`}
>
{short}
</button>
) : (
<span className={pillClass}>{short}</span>
)}
</Tooltip>
);
};
const columns: ColumnsType<MemoryRow> = [
{
title: "ID",
dataIndex: "memory_id",
key: "memory_id",
width: 140,
render: (_: unknown, r: MemoryRow) => renderIdPill(r.memory_id, () => setDetailRow(r)),
render: (_: unknown, r: MemoryRow) => <IdCell value={r.memory_id} onClick={() => setDetailRow(r)} />,
},
{
title: "Name",
@ -246,21 +226,21 @@ export const MemoryView: React.FC<MemoryViewProps> = ({ accessToken }) => {
dataIndex: "user_id",
key: "user_id",
width: 160,
render: (uid?: string | null) => renderIdPill(uid),
render: (uid?: string | null) => <IdCell value={uid} />,
},
{
title: "Team ID",
dataIndex: "team_id",
key: "team_id",
width: 160,
render: (tid?: string | null) => renderIdPill(tid),
render: (tid?: string | null) => <IdCell value={tid} />,
},
{
title: "Updated",
dataIndex: "updated_at",
key: "updated_at",
width: 180,
render: (ts?: string) => <Text type="secondary">{formatTimestamp(ts)}</Text>,
render: (ts?: string) => <DateCell value={ts} />,
// No sorter — backend already returns rows in `updated_at DESC` order,
// and a client-side sorter on a paginated view would only affect the
// current page.

View file

@ -3,6 +3,7 @@ import { Empty, Table, Tooltip } from "antd";
import type { ColumnsType } from "antd/es/table";
import type { SpinProps } from "antd";
import DefaultProxyAdminTag from "@/components/common_components/DefaultProxyAdminTag";
import { DateCell } from "@/components/shared/table_cells";
interface ProjectKeysTableProps {
keys: KeyResponse[];
@ -33,13 +34,13 @@ const columns: ColumnsType<KeyResponse> = [
title: "Created",
dataIndex: "created_at",
key: "created_at",
render: (date: string) => (date ? new Date(date).toLocaleDateString() : "—"),
render: (date: string) => <DateCell value={date} precision="date" />,
},
{
title: "Last Active",
dataIndex: "last_active",
key: "last_active",
render: (date: string | null) => (date ? new Date(date).toLocaleDateString() : "Never"),
render: (date: string | null) => <DateCell value={date} precision="date" fallback="Never" />,
},
];

View file

@ -1,5 +1,6 @@
import { useProjects, ProjectResponse } from "@/app/(dashboard)/hooks/projects/useProjects";
import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
import { DateCell, IdCell } from "@/components/shared/table_cells";
import { LoadingOutlined, PlusOutlined } from "@ant-design/icons";
import {
Button,
@ -72,18 +73,7 @@ export function ProjectsPage() {
dataIndex: "project_id",
key: "project_id",
width: 170,
render: (id: string) => (
<Tooltip title={id}>
<Text
ellipsis
className="text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs cursor-pointer"
style={{ fontSize: 14, padding: "1px 8px" }}
onClick={() => setSelectedProjectId(id)}
>
{id}
</Text>
</Tooltip>
),
render: (id: string) => <IdCell value={id} onClick={setSelectedProjectId} />,
},
{
title: "Name",
@ -137,14 +127,14 @@ export function ProjectsPage() {
key: "created_at",
sorter: (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime(),
responsive: ["lg"],
render: (date: string) => new Date(date).toLocaleDateString(),
render: (date: string) => <DateCell value={date} precision="date" />,
},
{
title: "Updated",
dataIndex: "updated_at",
key: "updated_at",
responsive: ["xl"],
render: (date: string) => new Date(date).toLocaleDateString(),
render: (date: string) => <DateCell value={date} precision="date" />,
},
];

View file

@ -2,8 +2,8 @@ import React, { useState, useEffect } from "react";
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Button } from "@tremor/react";
import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon, TrashIcon } from "@heroicons/react/outline";
import { Tooltip } from "antd";
import { CopyOutlined } from "@ant-design/icons";
import { PromptSpec, modelHubCall } from "@/components/networking";
import { DateCell, IdCell } from "@/components/shared/table_cells";
import {
ColumnDef,
flexRender,
@ -62,48 +62,11 @@ const PromptTable: React.FC<PromptTableProps> = ({
fetchModelHubData();
}, [accessToken]);
// Format date helper function
const formatDate = (dateString?: string) => {
if (!dateString) return "-";
const date = new Date(dateString);
return date.toLocaleString();
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};
const columns: ColumnDef<PromptSpec>[] = [
{
header: "Prompt ID",
accessorKey: "prompt_id",
cell: (info: any) => {
const fullId = String(info.getValue() || "");
const displayId = fullId.length > 25 ? `${fullId.slice(0, 25)}...` : fullId;
return (
<div className="flex items-center gap-2">
<Tooltip title={fullId}>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate min-w-[220px] justify-start"
onClick={() => info.getValue() && onPromptClick?.(info.getValue())}
>
{displayId}
</Button>
</Tooltip>
<Tooltip title="Copy prompt ID">
<CopyOutlined
onClick={(e) => {
e.stopPropagation();
copyToClipboard(fullId);
}}
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs"
/>
</Tooltip>
</div>
);
},
cell: (info: any) => <IdCell value={info.getValue()} onClick={onPromptClick} copyable />,
},
{
header: "Model",
@ -162,26 +125,12 @@ const PromptTable: React.FC<PromptTableProps> = ({
{
header: "Created At",
accessorKey: "created_at",
cell: ({ row }) => {
const prompt = row.original;
return (
<Tooltip title={prompt.created_at}>
<span className="text-xs">{formatDate(prompt.created_at)}</span>
</Tooltip>
);
},
cell: ({ row }) => <DateCell value={row.original.created_at} />,
},
{
header: "Updated At",
accessorKey: "updated_at",
cell: ({ row }) => {
const prompt = row.original;
return (
<Tooltip title={prompt.updated_at}>
<span className="text-xs">{formatDate(prompt.updated_at)}</span>
</Tooltip>
);
},
cell: ({ row }) => <DateCell value={row.original.updated_at} />,
},
{
header: "Environment",

View file

@ -1,6 +1,7 @@
import { Tag } from "antd";
import { ColumnsType } from "antd/es/table";
import TableIconActionButton from "@/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
import { DateCell, IdCell } from "@/components/shared/table_cells";
import { SearchTool } from "./types";
export const searchToolColumns = (
@ -20,14 +21,7 @@ export const searchToolColumns = (
return <span className="text-xs">-</span>;
}
return (
<button
onClick={() => onView(tool.search_tool_id!)}
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left cursor-pointer max-w-40"
>
<span className="truncate block">{tool.search_tool_id}</span>
</button>
);
return <IdCell value={tool.search_tool_id} onClick={onView} />;
},
},
{
@ -52,7 +46,7 @@ export const searchToolColumns = (
dataIndex: "created_at",
key: "created_at",
render: (_, tool) => {
return <span className="text-xs">{tool.created_at ? new Date(tool.created_at).toLocaleDateString() : "-"}</span>;
return <DateCell value={tool.created_at} precision="date" />;
},
},
{
@ -60,7 +54,7 @@ export const searchToolColumns = (
dataIndex: "updated_at",
key: "updated_at",
render: (_, tool) => {
return <span className="text-xs">{tool.updated_at ? new Date(tool.updated_at).toLocaleDateString() : "-"}</span>;
return <DateCell value={tool.updated_at} precision="date" />;
},
},
{

View file

@ -91,7 +91,7 @@ describe("BulkEditUserModal", () => {
it("should display budget information in table", () => {
renderWithProviders(<BulkEditUserModal {...defaultProps} />);
expect(screen.getByText("$50")).toBeInTheDocument();
expect(screen.getByText("$50.00")).toBeInTheDocument();
expect(screen.getByText("Unlimited")).toBeInTheDocument();
});

View file

@ -4,6 +4,7 @@ import { userBulkUpdateUserCall, teamBulkMemberAddCall, Member } from "@/compone
import { UserEditView } from "./user_edit_view";
import NotificationsManager from "@/components/molecules/notifications_manager";
import MessageManager from "@/components/molecules/message_manager";
import { MoneyCell } from "@/components/shared/table_cells";
const { Text, Title } = Typography;
@ -270,7 +271,7 @@ const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
key: "max_budget",
width: "20%",
render: (budget: number | null) => (
<Text style={{ fontSize: "12px" }}>{budget !== null ? `$${budget}` : "Unlimited"}</Text>
<MoneyCell value={budget} decimals={2} emptyText="Unlimited" showZero />
),
},
]}

View file

@ -158,7 +158,8 @@ describe("ViewUserDashboard", () => {
expect(
screen.getByText("Are you sure you want to delete this user? This action cannot be undone."),
).toBeInTheDocument();
expect(screen.getByText("user-1")).toBeInTheDocument();
const userIdInstances = screen.getAllByText("user-1");
expect(userIdInstances.length).toBeGreaterThan(0);
const emailInstances = screen.getAllByText("test@example.com");
expect(emailInstances.length).toBeGreaterThan(0);
});

View file

@ -3,8 +3,7 @@ import { Badge, Grid, Icon } from "@tremor/react";
import { Tooltip, Checkbox, Tag } from "antd";
import { UserInfo } from "@/components/networking";
import { PencilAltIcon, TrashIcon, InformationCircleIcon, RefreshIcon } from "@heroicons/react/outline";
import { CopyOutlined } from "@ant-design/icons";
import { formatNumberWithCommas, copyToClipboard } from "@/utils/dataUtils";
import { DateCell, IdCell, MoneyCell } from "@/components/shared/table_cells";
interface SelectionOptions {
selectedUsers: UserInfo[];
@ -29,24 +28,7 @@ export const columns = (
header: "User ID",
accessorKey: "user_id",
enableSorting: true,
cell: ({ row }) => (
<div className="flex items-center space-x-2">
<Tooltip title={row.original.user_id}>
<span className="text-xs">{row.original.user_id ? `${row.original.user_id.slice(0, 7)}...` : "-"}</span>
</Tooltip>
{row.original.user_id && (
<Tooltip title="Copy User ID">
<CopyOutlined
onClick={(e) => {
e.stopPropagation();
copyToClipboard(row.original.user_id, "User ID copied to clipboard");
}}
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs"
/>
</Tooltip>
)}
</div>
),
cell: ({ row }) => <IdCell value={row.original.user_id} variant="plain" copyable />,
},
{
header: "Email",
@ -93,17 +75,13 @@ export const columns = (
header: "Spend (USD)",
accessorKey: "spend",
enableSorting: true,
cell: ({ row }) => (
<span className="text-xs">{row.original.spend ? formatNumberWithCommas(row.original.spend, 4) : "-"}</span>
),
cell: ({ row }) => <MoneyCell value={row.original.spend} decimals={4} />,
},
{
header: "Budget (USD)",
accessorKey: "max_budget",
enableSorting: false,
cell: ({ row }) => (
<span className="text-xs">{row.original.max_budget !== null ? row.original.max_budget : "Unlimited"}</span>
),
cell: ({ row }) => <MoneyCell value={row.original.max_budget} decimals={2} emptyText="Unlimited" showZero />,
},
{
header: () => (
@ -142,21 +120,13 @@ export const columns = (
header: "Created At",
accessorKey: "created_at",
enableSorting: true,
cell: ({ row }) => (
<span className="text-xs">
{row.original.created_at ? new Date(row.original.created_at).toLocaleDateString() : "-"}
</span>
),
cell: ({ row }) => <DateCell value={row.original.created_at} precision="date" />,
},
{
header: "Updated At",
accessorKey: "updated_at",
enableSorting: false,
cell: ({ row }) => (
<span className="text-xs">
{row.original.updated_at ? new Date(row.original.updated_at).toLocaleDateString() : "-"}
</span>
),
cell: ({ row }) => <DateCell value={row.original.updated_at} precision="date" />,
},
{
id: "actions",

View file

@ -2,6 +2,7 @@ import { ColumnDef } from "@tanstack/react-table";
import { Button, Badge, Text } from "@tremor/react";
import { Tooltip, Tag } from "antd";
import { CopyOutlined, InfoCircleOutlined } from "@ant-design/icons";
import { StatusBadge } from "@/components/shared/table_cells";
export interface AgentHubData {
agent_id?: string;
@ -194,17 +195,9 @@ export const getAgentHubTableColumns = (
return publicA - publicB;
},
cell: ({ row }) => {
const agent = row.original;
const isPublic = row.original.is_public === true;
return agent.is_public === true ? (
<Badge color="green" size="xs">
Yes
</Badge>
) : (
<Badge color="gray" size="xs">
No
</Badge>
);
return <StatusBadge tone={isPublic ? "success" : "neutral"} label={isPublic ? "Yes" : "No"} />;
},
meta: {
className: "hidden md:table-cell",

View file

@ -1,5 +1,5 @@
"use client";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { DateCell, IdCell, MoneyCell } from "@/components/shared/table_cells";
import { ChevronDownIcon, ChevronUpIcon, SwitchVerticalIcon } from "@heroicons/react/outline";
import {
ColumnDef,
@ -59,14 +59,7 @@ export function DeletedKeysTable({
header: "Key ID",
size: 150,
maxSize: 250,
cell: (info) => {
const value = info.getValue() as string;
return (
<Tooltip title={value}>
<span className="font-mono text-blue-500 text-xs truncate block max-w-[250px]">{value || "-"}</span>
</Tooltip>
);
},
cell: (info) => <IdCell value={info.getValue() as string} variant="plain" />,
},
{
id: "key_alias",
@ -100,9 +93,7 @@ export function DeletedKeysTable({
header: "Spend (USD)",
size: 100,
maxSize: 140,
cell: (info) => (
<span className="block max-w-[140px]">{formatNumberWithCommas(info.getValue() as number, 4)}</span>
),
cell: (info) => <MoneyCell value={info.getValue() as number} decimals={4} />,
},
{
id: "max_budget",
@ -110,14 +101,9 @@ export function DeletedKeysTable({
header: "Budget (USD)",
size: 110,
maxSize: 150,
cell: (info) => {
const maxBudget = info.getValue() as number | null;
return (
<span className="block max-w-[150px]">
{maxBudget === null ? "Unlimited" : `$${formatNumberWithCommas(maxBudget)}`}
</span>
);
},
cell: (info) => (
<MoneyCell value={info.getValue() as number | null} decimals={0} emptyText="Unlimited" showZero />
),
},
{
id: "user_email",
@ -140,14 +126,7 @@ export function DeletedKeysTable({
header: "User ID",
size: 120,
maxSize: 200,
cell: (info) => {
const userId = info.getValue() as string | null;
return (
<Tooltip title={userId || undefined}>
<span className="truncate block max-w-[200px]">{userId || "-"}</span>
</Tooltip>
);
},
cell: (info) => <IdCell value={info.getValue() as string | null} variant="plain" />,
},
{
id: "created_at",
@ -155,12 +134,7 @@ export function DeletedKeysTable({
header: "Created At",
size: 120,
maxSize: 140,
cell: (info) => {
const value = info.getValue();
return (
<span className="block max-w-[140px]">{value ? new Date(value as string).toLocaleDateString() : "-"}</span>
);
},
cell: (info) => <DateCell value={info.getValue() as string | null | undefined} precision="date" />,
},
{
id: "created_by",
@ -183,10 +157,9 @@ export function DeletedKeysTable({
header: "Deleted At",
size: 120,
maxSize: 140,
cell: (info) => {
const value = (info.row.original as any).deleted_at as string | null | undefined;
return <span className="block max-w-[140px]">{value ? new Date(value).toLocaleDateString() : "-"}</span>;
},
cell: (info) => (
<DateCell value={(info.row.original as any).deleted_at as string | null | undefined} precision="date" />
),
},
{
id: "deleted_by",

View file

@ -1,5 +1,5 @@
"use client";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { DateCell, IdCell, MoneyCell } from "@/components/shared/table_cells";
import { ChevronDownIcon, ChevronUpIcon, SwitchVerticalIcon } from "@heroicons/react/outline";
import {
ColumnDef,
@ -51,14 +51,7 @@ export function DeletedTeamsTable({ teams, isLoading, isFetching }: DeletedTeams
header: "Team ID",
size: 150,
maxSize: 250,
cell: (info) => {
const value = info.getValue() as string;
return (
<Tooltip title={value}>
<span className="font-mono text-blue-500 text-xs truncate block max-w-[250px]">{value || "-"}</span>
</Tooltip>
);
},
cell: (info) => <IdCell value={info.getValue() as string} />,
},
{
id: "created_at",
@ -66,12 +59,7 @@ export function DeletedTeamsTable({ teams, isLoading, isFetching }: DeletedTeams
header: "Created",
size: 120,
maxSize: 140,
cell: (info) => {
const value = info.getValue();
return (
<span className="block max-w-[140px]">{value ? new Date(value as string).toLocaleDateString() : "-"}</span>
);
},
cell: (info) => <DateCell value={info.getValue() as string | null | undefined} precision="date" />,
},
{
id: "spend",
@ -79,12 +67,7 @@ export function DeletedTeamsTable({ teams, isLoading, isFetching }: DeletedTeams
header: "Spend (USD)",
size: 100,
maxSize: 140,
cell: (info) => {
const spend = (info.row.original as any).spend as number | undefined;
return (
<span className="block max-w-[140px]">{spend !== undefined ? formatNumberWithCommas(spend, 4) : "-"}</span>
);
},
cell: (info) => <MoneyCell value={info.getValue() as number | undefined} decimals={4} />,
},
{
id: "max_budget",
@ -92,14 +75,9 @@ export function DeletedTeamsTable({ teams, isLoading, isFetching }: DeletedTeams
header: "Budget (USD)",
size: 110,
maxSize: 150,
cell: (info) => {
const maxBudget = info.getValue() as number | null;
return (
<span className="block max-w-[150px]">
{maxBudget === null || maxBudget === undefined ? "No limit" : `$${formatNumberWithCommas(maxBudget)}`}
</span>
);
},
cell: (info) => (
<MoneyCell value={info.getValue() as number | null} decimals={0} emptyText="Unlimited" showZero />
),
},
{
id: "models",
@ -148,14 +126,7 @@ export function DeletedTeamsTable({ teams, isLoading, isFetching }: DeletedTeams
header: "Organization",
size: 150,
maxSize: 200,
cell: (info) => {
const value = info.getValue() as string;
return (
<Tooltip title={value || undefined}>
<span className="truncate block max-w-[200px]">{value || "-"}</span>
</Tooltip>
);
},
cell: (info) => <IdCell value={info.getValue() as string} variant="plain" />,
},
{
id: "deleted_at",
@ -163,10 +134,7 @@ export function DeletedTeamsTable({ teams, isLoading, isFetching }: DeletedTeams
header: "Deleted At",
size: 120,
maxSize: 140,
cell: (info) => {
const value = (info.row.original as any).deleted_at as string | null | undefined;
return <span className="block max-w-[140px]">{value ? new Date(value).toLocaleDateString() : "-"}</span>;
},
cell: (info) => <DateCell value={info.getValue() as string | null | undefined} precision="date" />,
},
{
id: "deleted_by",

View file

@ -31,6 +31,7 @@ import type { SorterResult } from "antd/es/table/interface";
import { KeyIcon, LayersIcon, SearchIcon, UsersIcon } from "lucide-react";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { AntDLoadingSpinner } from "@/components/ui/AntDLoadingSpinner";
import { DateCell, IdCell } from "@/components/shared/table_cells";
import OrganizationDropdown from "./common_components/OrganizationDropdown";
import TableIconActionButton from "./common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
import { teamListCall as v2TeamListCall, type TeamsResponse } from "@/app/(dashboard)/hooks/teams/useTeams";
@ -670,18 +671,8 @@ const Teams: React.FC<TeamProps> = ({ accessToken, userID, userRole, premiumUser
key: "team_id",
width: 170,
ellipsis: true,
render: (id: string, record: Team) => (
<Tooltip title={id}>
<Text
ellipsis
className="text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs cursor-pointer"
style={{ fontSize: 14, padding: "1px 8px" }}
onClick={() => setSelectedTeamId(record.team_id)}
data-testid="team-id-cell"
>
{id}
</Text>
</Tooltip>
render: (id: string) => (
<IdCell value={id} onClick={(teamId) => setSelectedTeamId(teamId)} dataTestId="team-id-cell" />
),
},
{
@ -797,13 +788,7 @@ const Teams: React.FC<TeamProps> = ({ accessToken, userID, userRole, premiumUser
width: 130,
ellipsis: true,
sorter: true,
render: (date: string | undefined) => (
<Text type="secondary" style={{ fontSize: 13 }}>
{date
? new Date(date).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" })
: "—"}
</Text>
),
render: (date: string | undefined) => <DateCell value={date} precision="date" />,
},
{
title: "Actions",

View file

@ -3,6 +3,7 @@ import type { TableProps } from "antd";
import { Table } from "antd";
import Title from "antd/es/typography/Title";
import React from "react";
import { StatusBadge, type StatusTone } from "@/components/shared/table_cells";
import TableIconActionButton from "../../../common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
import { AlertingObject } from "./types";
@ -61,17 +62,8 @@ export const LoggingCallbacksTable: React.FC<LoggingCallbacksProps> = ({
// and server-fetched rows both render correctly.
const mode = record.type || record.mode || "success";
const label = CALLBACK_MODES.find((m) => m.value === mode)?.label || mode;
const badgeClass =
mode === "success"
? "bg-green-100 text-green-800"
: mode === "failure"
? "bg-red-100 text-red-800"
: "bg-blue-100 text-blue-800";
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${badgeClass}`}>
{label}
</span>
);
const tone: StatusTone = mode === "success" ? "success" : mode === "failure" ? "error" : "info";
return <StatusBadge tone={tone} label={label} />;
},
width: 240,
},

View file

@ -3,7 +3,7 @@
import React, { useCallback, useDeferredValue, useEffect, useMemo, useState } from "react";
import { Button, Switch, Tooltip } from "antd";
import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react";
import { TimeCell } from "./view_logs/time_cell";
import { DateCell, IdCell } from "@/components/shared/table_cells";
import type { SortState } from "./common_components/TableHeaderSortDropdown/TableHeaderSortDropdown";
import { TableHeaderSortDropdown } from "./common_components/TableHeaderSortDropdown/TableHeaderSortDropdown";
import FilterComponent, { FilterOption } from "./molecules/filter";
@ -461,7 +461,7 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
paginated.map((tool) => (
<TableRow key={tool.tool_id} id={`tool-row-${tool.tool_id}`} className="h-8 hover:bg-gray-50">
<TableCell className="py-0.5 max-h-8 overflow-hidden whitespace-nowrap">
<TimeCell utcTime={tool.created_at ?? ""} />
<DateCell value={tool.created_at} />
</TableCell>
<TableCell className="py-0.5 max-h-8 overflow-hidden">
<button
@ -498,16 +498,10 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
</div>
</TableCell>
<TableCell className="py-0.5 max-h-8 overflow-hidden whitespace-nowrap">
<Tooltip title={tool.team_id ?? "-"}>
<span className="max-w-[15ch] truncate block">{tool.team_id ?? "-"}</span>
</Tooltip>
<IdCell value={tool.team_id} variant="plain" />
</TableCell>
<TableCell className="py-0.5 max-h-8 overflow-hidden whitespace-nowrap">
<Tooltip title={tool.key_hash ?? "-"}>
<span className="font-mono max-w-[15ch] truncate block text-blue-600">
{tool.key_hash ?? "-"}
</span>
</Tooltip>
<IdCell value={tool.key_hash} />
</TableCell>
<TableCell className="py-0.5 max-h-8 overflow-hidden whitespace-nowrap">
<Tooltip title={tool.key_alias ?? "-"}>

View file

@ -31,12 +31,6 @@ vi.mock("antd", async () => {
return { Table, Progress };
});
vi.mock("@/utils/dataUtils", () => ({
formatNumberWithCommas: (value: number, decimals?: number) => {
return value.toFixed(decimals || 0);
},
}));
describe("EndpointUsageTable", () => {
it("should render", () => {
const mockEndpointData = {

View file

@ -1,7 +1,7 @@
import React from "react";
import { Table, Progress } from "antd";
import type { ColumnsType } from "antd/es/table";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { MoneyCell } from "@/components/shared/table_cells";
import { MetricWithMetadata } from "../../../types";
interface EndpointUsageTableProps {
@ -112,7 +112,7 @@ const EndpointUsageTable: React.FC<EndpointUsageTableProps> = ({ endpointData })
title: "Spend",
dataIndex: "spend",
key: "spend",
render: (value: number) => `$${formatNumberWithCommas(value, 2)}`,
render: (value: number) => <MoneyCell value={value} decimals={2} />,
},
];

View file

@ -1,4 +1,5 @@
import useTeams from "@/app/(dashboard)/hooks/useTeams";
import { MoneyCell } from "@/components/shared/table_cells";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import {
BarChart,
@ -665,7 +666,9 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
.map((entity) => (
<TableRow key={entity.metadata.id}>
<TableCell>{entity.metadata.alias}</TableCell>
<TableCell>${formatNumberWithCommas(entity.metrics.spend, 4)}</TableCell>
<TableCell>
<MoneyCell value={entity.metrics.spend} decimals={4} />
</TableCell>
<TableCell className="text-green-600">
{entity.metrics.successful_requests.toLocaleString()}
</TableCell>
@ -777,7 +780,9 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
<span>{provider.provider}</span>
</div>
</TableCell>
<TableCell>${formatNumberWithCommas(provider.spend, 2)}</TableCell>
<TableCell>
<MoneyCell value={provider.spend} decimals={2} />
</TableCell>
<TableCell className="text-green-600">
{provider.successful_requests.toLocaleString()}
</TableCell>

View file

@ -1,4 +1,4 @@
import { render, screen } from "@testing-library/react";
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi, beforeEach } from "vitest";
import SpendByProvider from "./SpendByProvider";
@ -200,6 +200,14 @@ describe("SpendByProvider", () => {
expect(screen.getByText("1,234,567")).toBeInTheDocument();
});
it("should render zero spend as a dash when Show Zero Spend is on", () => {
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
fireEvent.click(screen.getAllByRole("switch")[0]);
expect(screen.getAllByText("google").length).toBeGreaterThan(0);
expect(screen.getByText("-")).toBeInTheDocument();
expect(screen.queryByText("$0.00")).not.toBeInTheDocument();
});
it("should filter data correctly when both toggles are off", () => {
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
expect(screen.getAllByText("openai").length).toBeGreaterThan(0);

View file

@ -1,3 +1,4 @@
import { MoneyCell } from "@/components/shared/table_cells";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { InfoCircleOutlined } from "@ant-design/icons";
import {
@ -109,7 +110,9 @@ const SpendByProvider: React.FC<SpendByProviderProps> = ({ loading, isDateChangi
<span>{provider.provider}</span>
</div>
</TableCell>
<TableCell>${formatNumberWithCommas(provider.spend, 2)}</TableCell>
<TableCell>
<MoneyCell value={provider.spend} decimals={2} />
</TableCell>
<TableCell className="text-green-600">{provider.successful_requests.toLocaleString()}</TableCell>
<TableCell className="text-red-600">{provider.failed_requests.toLocaleString()}</TableCell>
<TableCell>{provider.tokens.toLocaleString()}</TableCell>

View file

@ -171,7 +171,9 @@ describe("TopKeyView", () => {
]}
/>,
);
expect(screen.getByText(/sk-1234\.\.\./)).toBeInTheDocument();
const keyId = screen.getByText("sk-1234567890abcdef");
expect(keyId).toBeInTheDocument();
expect(keyId).toHaveClass("truncate");
});
it("should display dash for missing key alias", () => {
@ -206,7 +208,7 @@ describe("TopKeyView", () => {
expect(screen.getByText("$123.46")).toBeInTheDocument();
});
it("should display less than 0.01 spend as <$0.01", () => {
it("should display sub-cent spend as < $0.01", () => {
render(
<TopKeyView
{...baseProps}
@ -214,15 +216,15 @@ describe("TopKeyView", () => {
{
api_key: "key-123",
key_alias: "Test Key",
spend: 0.005,
spend: 0.004,
},
]}
/>,
);
expect(screen.getByText("<$0.01")).toBeInTheDocument();
expect(screen.getByText("< $0.01")).toBeInTheDocument();
});
it("should display zero spend correctly", () => {
it("should display zero spend as a dash", () => {
render(
<TopKeyView
{...baseProps}
@ -235,7 +237,8 @@ describe("TopKeyView", () => {
]}
/>,
);
expect(screen.getByText("$0.00")).toBeInTheDocument();
expect(screen.getByText("-")).toBeInTheDocument();
expect(screen.queryByText("$0.00")).not.toBeInTheDocument();
});
it("should display dash for empty tags", () => {
@ -376,7 +379,7 @@ describe("TopKeyView", () => {
/>,
);
const keyIdButton = screen.getByText(/key-123\.\.\./).closest("button");
const keyIdButton = screen.getByText("key-123").closest("button");
if (keyIdButton) {
await user.click(keyIdButton);
}
@ -410,7 +413,7 @@ describe("TopKeyView", () => {
/>,
);
const keyIdButton = screen.getByText(/key-123\.\.\./).closest("button");
const keyIdButton = screen.getByText("key-123").closest("button");
if (keyIdButton) {
await user.click(keyIdButton);
}
@ -447,7 +450,7 @@ describe("TopKeyView", () => {
/>,
);
const keyIdButton = screen.getByText(/key-123\.\.\./).closest("button");
const keyIdButton = screen.getByText("key-123").closest("button");
if (keyIdButton) {
await user.click(keyIdButton);
}
@ -483,7 +486,7 @@ describe("TopKeyView", () => {
/>,
);
const keyIdButton = screen.getByText(/key-123\.\.\./).closest("button");
const keyIdButton = screen.getByText("key-123").closest("button");
if (keyIdButton) {
await user.click(keyIdButton);
}
@ -522,7 +525,7 @@ describe("TopKeyView", () => {
/>,
);
const keyIdButton = screen.getByText(/key-123\.\.\./).closest("button");
const keyIdButton = screen.getByText("key-123").closest("button");
if (keyIdButton) {
await user.click(keyIdButton);
}
@ -552,7 +555,7 @@ describe("TopKeyView", () => {
/>,
);
const keyIdButton = screen.getByText(/key-123\.\.\./).closest("button");
const keyIdButton = screen.getByText("key-123").closest("button");
if (keyIdButton) {
await user.click(keyIdButton);
}

View file

@ -1,6 +1,7 @@
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { IdCell, MoneyCell } from "@/components/shared/table_cells";
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/outline";
import { BarChart, Button } from "@tremor/react";
import { BarChart } from "@tremor/react";
import { Segmented, Tooltip } from "antd";
import React, { useState } from "react";
import { formatNumberWithCommas } from "../../../../utils/dataUtils";
@ -83,20 +84,7 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, teams, showTags = fals
{
header: "Key ID",
accessorKey: "api_key",
cell: (info: any) => (
<div className="overflow-hidden">
<Tooltip title={info.getValue() as string}>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
onClick={() => handleKeyClick(info.row.original)}
>
{info.getValue() ? `${(info.getValue() as string).slice(0, 7)}...` : "-"}
</Button>
</Tooltip>
</div>
),
cell: (info: any) => <IdCell value={info.getValue()} onClick={() => handleKeyClick(info.row.original)} />,
},
{
header: "Key Alias",
@ -165,10 +153,7 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, teams, showTags = fals
header: "Spend (USD)",
accessorKey: "spend",
meta: { numeric: true },
cell: (info: any) => {
const value = info.getValue();
return value > 0 && value < 0.01 ? "<$0.01" : `$${formatNumberWithCommas(value, 2)}`;
},
cell: (info: any) => <MoneyCell value={info.getValue()} decimals={2} />,
};
const columns = showTags ? [...baseColumns, tagsColumn, spendColumn] : [...baseColumns, spendColumn];

View file

@ -175,7 +175,7 @@ describe("TopModelView", () => {
setTopModelsLimit={mockSetTopModelsLimit}
/>,
);
expect(screen.getByText("$0.00")).toBeInTheDocument();
expect(screen.getByText("-")).toBeInTheDocument();
expect(screen.getAllByText("0").length).toBeGreaterThan(0);
});

View file

@ -1,6 +1,7 @@
import { BarChart } from "@tremor/react";
import { Segmented } from "antd";
import { useState } from "react";
import { MoneyCell } from "@/components/shared/table_cells";
import { formatNumberWithCommas } from "../../../../utils/dataUtils";
import { DataTable } from "../../../view_logs/table";
@ -31,10 +32,7 @@ export default function TopModelView({ topModels, topModelsLimit, setTopModelsLi
header: "Spend (USD)",
accessorKey: "spend",
meta: { numeric: true },
cell: (info: any) => {
const value = info.getValue();
return `$${formatNumberWithCommas(value, 2)}`;
},
cell: (info: any) => <MoneyCell value={info.getValue()} decimals={2} />,
},
{
header: "Successful",

View file

@ -158,7 +158,7 @@ describe("KeyModelUsageView", () => {
},
];
render(<KeyModelUsageView topModels={modelsWithZeros} />);
expect(screen.getByText("$0.00")).toBeInTheDocument();
expect(screen.getByText("-")).toBeInTheDocument();
expect(screen.getAllByText("0").length).toBeGreaterThan(0);
});

View file

@ -1,3 +1,4 @@
import { MoneyCell } from "@/components/shared/table_cells";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { BarChart, Card, Title } from "@tremor/react";
import { Table } from "antd";
@ -24,7 +25,7 @@ const columns: ColumnsType<TopModelData> = [
title: "Spend (USD)",
dataIndex: "spend",
key: "spend",
render: (value) => `$${formatNumberWithCommas(value, 2)}`,
render: (value) => <MoneyCell value={value} decimals={2} />,
},
{
title: "Successful",

View file

@ -1,4 +1,5 @@
import { act, screen, waitFor, within, fireEvent } from "@testing-library/react";
import { screen, waitFor, within, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi, it, expect, beforeEach, describe, MockedFunction } from "vitest";
import { renderWithProviders } from "../../../tests/test-utils";
import { VirtualKeysTable } from "./VirtualKeysTable";
@ -175,7 +176,7 @@ it("should display key information correctly", async () => {
await waitFor(() => {
expect(screen.getByText("Test Key Alias")).toBeInTheDocument();
expect(screen.getByText("Test Team")).toBeInTheDocument();
expect(screen.getByText("5.5000")).toBeInTheDocument();
expect(screen.getByText("$5.5000")).toBeInTheDocument();
});
});
@ -477,9 +478,8 @@ describe("Status column reflects key.blocked / scim_blocked metadata", () => {
const tag = await screen.findByTestId(`key-status-${mockKey.token_id}`);
expect(tag).toHaveTextContent("Blocked");
act(() => {
fireEvent.mouseEnter(tag);
});
const user = userEvent.setup();
await user.hover(tag);
await waitFor(() => {
expect(screen.getByText(/Blocked by SCIM/i)).toBeInTheDocument();
});

View file

@ -13,20 +13,10 @@ import {
SortingState,
useReactTable,
} from "@tanstack/react-table";
import {
Badge,
Button,
Icon,
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
Text,
} from "@tremor/react";
import { Badge, Icon, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Text } from "@tremor/react";
import { InfoCircleOutlined, SyncOutlined } from "@ant-design/icons";
import { Button as AntButton, Popover, Skeleton, Tag, Tooltip, Typography } from "antd";
import { Button as AntButton, Popover, Skeleton, Typography } from "antd";
import { DateCell, IdCell, MoneyCell, StatusBadge } from "@/components/shared/table_cells";
import React, { useDeferredValue, useMemo, useState } from "react";
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
import { PaginatedKeyAliasSelect } from "../KeyAliasSelect/PaginatedKeyAliasSelect/PaginatedKeyAliasSelect";
@ -145,23 +135,7 @@ export function VirtualKeysTable() {
header: "Key ID",
size: 100,
enableSorting: true,
cell: (info) => {
const value = info.getValue() as string;
const width = info.cell.column.getSize();
return (
<Tooltip title={value}>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate block"
style={{ maxWidth: width, overflow: "hidden" }}
onClick={() => setSelectedKey(info.row.original)}
>
{value ?? "-"}
</Button>
</Tooltip>
);
},
cell: (info) => <IdCell value={info.getValue() as string} onClick={() => setSelectedKey(info.row.original)} />,
},
{
id: "key_alias",
@ -187,22 +161,14 @@ export function VirtualKeysTable() {
cell: ({ row }) => {
const key = row.original;
if (key.blocked !== true) {
return (
<Tag color="green" data-testid={`key-status-${key.token_id}`}>
Active
</Tag>
);
return <StatusBadge tone="success" label="Active" dataTestId={`key-status-${key.token_id}`} />;
}
const isScimBlocked = (key.metadata as Record<string, unknown> | null | undefined)?.scim_blocked === true;
const reason = isScimBlocked
? "Blocked by SCIM (external identity provider deactivated or deleted the owning user)."
: "Blocked. Requests using this key will be rejected with 401.";
return (
<Tooltip title={reason}>
<Tag color="red" data-testid={`key-status-${key.token_id}`}>
Blocked
</Tag>
</Tooltip>
<StatusBadge tone="error" label="Blocked" tooltip={reason} dataTestId={`key-status-${key.token_id}`} />
);
},
},
@ -323,10 +289,7 @@ export function VirtualKeysTable() {
header: "Created At",
size: 120,
enableSorting: true,
cell: (info) => {
const value = info.getValue();
return value ? new Date(value as string).toLocaleDateString() : "-";
},
cell: (info) => <DateCell value={info.getValue() as string | null} precision="date" />,
},
{
id: "created_by",
@ -394,10 +357,7 @@ export function VirtualKeysTable() {
header: "Updated At",
size: 120,
enableSorting: true,
cell: (info) => {
const value = info.getValue();
return value ? new Date(value as string).toLocaleDateString() : "Never";
},
cell: (info) => <DateCell value={info.getValue() as string | null} precision="date" fallback="Never" />,
},
{
id: "last_active",
@ -415,16 +375,7 @@ export function VirtualKeysTable() {
),
size: 130,
enableSorting: false,
cell: (info) => {
const value = info.getValue();
if (!value) return "Unknown";
const date = new Date(value as string);
return (
<Tooltip title={date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "long" })}>
<span>{date.toLocaleDateString()}</span>
</Tooltip>
);
},
cell: (info) => <DateCell value={info.getValue() as string | null} precision="date" fallback="Unknown" />,
},
{
id: "expires",
@ -432,10 +383,7 @@ export function VirtualKeysTable() {
header: "Expires",
size: 120,
enableSorting: false,
cell: (info) => {
const value = info.getValue();
return value ? new Date(value as string).toLocaleDateString() : "Never";
},
cell: (info) => <DateCell value={info.getValue() as string | null} precision="date" fallback="Never" />,
},
{
id: "spend",
@ -443,7 +391,7 @@ export function VirtualKeysTable() {
header: "Spend (USD)",
size: 100,
enableSorting: true,
cell: (info) => formatNumberWithCommas(info.getValue() as number, 4),
cell: (info) => <MoneyCell value={info.getValue() as number} decimals={4} />,
},
{
id: "max_budget",
@ -470,10 +418,7 @@ export function VirtualKeysTable() {
header: "Budget Reset",
size: 130,
enableSorting: false,
cell: (info) => {
const value = info.getValue();
return value ? new Date(value as string).toLocaleString() : "Never";
},
cell: (info) => <DateCell value={info.getValue() as string | null} fallback="Never" />,
},
{
id: "models",

View file

@ -20,7 +20,7 @@ import AgentInfoView from "./agents/agent_info";
import NotificationsManager from "./molecules/notifications_manager";
import { Agent } from "./agents/types";
import { Team } from "./key_team_helpers/key_list";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { DateCell, IdCell, MoneyCell, StatusBadge } from "@/components/shared/table_cells";
import TableIconActionButton from "./common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
interface AgentsPanelProps {
@ -193,19 +193,10 @@ const AgentsPanel: React.FC<AgentsPanelProps> = ({ accessToken, userRole, teams
<Text>{agent.agent_name}</Text>
</TableCell>
<TableCell>
<Tooltip title={agent.agent_id}>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
onClick={() => setSelectedAgentId(agent.agent_id)}
>
{agent.agent_id.slice(0, 7)}...
</Button>
</Tooltip>
<IdCell value={agent.agent_id} onClick={(id) => setSelectedAgentId(id)} />
</TableCell>
<TableCell>
<Text>{formatNumberWithCommas(agent.spend, 4)}</Text>
<MoneyCell value={agent.spend} decimals={4} />
</TableCell>
<TableCell>
<Badge size="xs" color="blue">
@ -213,13 +204,13 @@ const AgentsPanel: React.FC<AgentsPanelProps> = ({ accessToken, userRole, teams
</Badge>
</TableCell>
<TableCell>
<Text>{agent.created_at ? new Date(agent.created_at).toLocaleDateString() : "N/A"}</Text>
<DateCell value={agent.created_at} precision="date" />
</TableCell>
<TableCell>
{(agent.keys?.length ?? 0) > 0 ? (
<Badge color="green">Active</Badge>
<StatusBadge tone="success" label="Active" />
) : (
<Badge color="yellow">Needs Setup</Badge>
<StatusBadge tone="warning" label="Needs Setup" />
)}
</TableCell>
{isAdmin && (

View file

@ -11,6 +11,7 @@ import {
import { Badge, Button, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "@tremor/react";
import { Tooltip } from "antd";
import React, { useState } from "react";
import { DateCell, IdCell, StatusBadge } from "@/components/shared/table_cells";
import NotificationsManager from "../molecules/notifications_manager";
import { getCategoryBadgeColor } from "./helpers";
import { Plugin } from "./types";
@ -34,12 +35,6 @@ const PluginTable: React.FC<PluginTableProps> = ({
}) => {
const [sorting, setSorting] = useState<SortingState>([{ id: "created_at", desc: true }]);
const formatDate = (dateString?: string) => {
if (!dateString) return "-";
const date = new Date(dateString);
return date.toLocaleString();
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
NotificationsManager.success("Copied to clipboard!");
@ -51,19 +46,9 @@ const PluginTable: React.FC<PluginTableProps> = ({
accessorKey: "name",
cell: ({ row }) => {
const plugin = row.original;
const name = plugin.name || "";
return (
<div className="flex items-center gap-2">
<Tooltip title={name}>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate min-w-[150px] justify-start"
onClick={() => onPluginClick(plugin.id)}
>
{name}
</Button>
</Tooltip>
<IdCell value={plugin.name} onClick={() => onPluginClick(plugin.id)} />
<Tooltip title="Copy Plugin ID">
<CopyOutlined
onClick={(e) => {
@ -122,24 +107,13 @@ const PluginTable: React.FC<PluginTableProps> = ({
accessorKey: "enabled",
cell: ({ row }) => {
const plugin = row.original;
return (
<Badge color={plugin.enabled ? "green" : "gray"} className="text-xs font-normal" size="xs">
{plugin.enabled ? "Yes" : "No"}
</Badge>
);
return <StatusBadge tone={plugin.enabled ? "success" : "neutral"} label={plugin.enabled ? "Yes" : "No"} />;
},
},
{
header: "Created At",
accessorKey: "created_at",
cell: ({ row }) => {
const plugin = row.original;
return (
<Tooltip title={plugin.created_at}>
<span className="text-xs">{formatDate(plugin.created_at)}</span>
</Tooltip>
);
},
cell: ({ row }) => <DateCell value={row.original.created_at} />,
},
...(isAdmin
? [

View file

@ -4,7 +4,6 @@ import {
Table,
TableHead,
TableRow,
Badge,
TableHeaderCell,
TableCell,
TableBody,
@ -16,7 +15,8 @@ import {
import { TabPanel, TabPanels, TabGroup, TabList, Tab } from "@tremor/react";
import { getGeneralSettingsCall, updateConfigFieldSetting, deleteConfigFieldSetting } from "./networking";
import { InputNumber } from "antd";
import { TrashIcon, CheckCircleIcon } from "@heroicons/react/outline";
import { TrashIcon } from "@heroicons/react/outline";
import { StatusBadge } from "@/components/shared/table_cells";
import RouterSettings from "./router_settings";
import Fallbacks from "./Settings/RouterSettings/Fallbacks/Fallbacks";
@ -173,13 +173,11 @@ const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, user
</TableCell>
<TableCell>
{value.stored_in_db == true ? (
<Badge icon={CheckCircleIcon} className="text-white">
In DB
</Badge>
<StatusBadge tone="success" label="In DB" />
) : value.stored_in_db == false ? (
<Badge className="text-gray bg-white outline-solid">In Config</Badge>
<StatusBadge tone="neutral" label="In Config" />
) : (
<Badge className="text-gray bg-white outline-solid">Not Set</Badge>
<StatusBadge tone="neutral" label="Not Set" />
)}
</TableCell>
<TableCell>

View file

@ -1,8 +1,8 @@
import React, { useState } from "react";
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Icon, Button } from "@tremor/react";
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Icon } from "@tremor/react";
import { TrashIcon, SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline";
import { Tooltip } from "antd";
import { Badge } from "@tremor/react";
import { DateCell, IdCell, StatusBadge } from "@/components/shared/table_cells";
import {
ColumnDef,
flexRender,
@ -43,13 +43,6 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
const [editModalVisible, setEditModalVisible] = useState(false);
const [selectedGuardrail, setSelectedGuardrail] = useState<Guardrail | null>(null);
// Format date helper function
const formatDate = (dateString?: string) => {
if (!dateString) return "-";
const date = new Date(dateString);
return date.toLocaleString();
};
const handleEditClick = (guardrail: Guardrail) => {
setSelectedGuardrail(guardrail);
setEditModalVisible(true);
@ -65,18 +58,7 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
{
header: "Guardrail ID",
accessorKey: "guardrail_id",
cell: (info: any) => (
<Tooltip title={String(info.getValue() || "")}>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
onClick={() => info.getValue() && onGuardrailClick(info.getValue())}
>
{info.getValue() ? `${String(info.getValue()).slice(0, 7)}...` : ""}
</Button>
</Tooltip>
),
cell: (info: any) => <IdCell value={info.getValue()} onClick={onGuardrailClick} />,
},
{
header: "Name",
@ -126,41 +108,21 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
header: "Default On",
accessorKey: "litellm_params.default_on",
cell: ({ row }) => {
const guardrail = row.original;
const isDefaultOn = !!row.original.litellm_params?.default_on;
return (
<Badge
color={guardrail.litellm_params?.default_on ? "green" : "gray"}
className="text-xs font-normal"
size="xs"
>
{guardrail.litellm_params?.default_on ? "Default On" : "Default Off"}
</Badge>
<StatusBadge tone={isDefaultOn ? "success" : "neutral"} label={isDefaultOn ? "Default On" : "Default Off"} />
);
},
},
{
header: "Created At",
accessorKey: "created_at",
cell: ({ row }) => {
const guardrail = row.original;
return (
<Tooltip title={guardrail.created_at}>
<span className="text-xs">{formatDate(guardrail.created_at)}</span>
</Tooltip>
);
},
cell: ({ row }) => <DateCell value={row.original.created_at} />,
},
{
header: "Updated At",
accessorKey: "updated_at",
cell: ({ row }) => {
const guardrail = row.original;
return (
<Tooltip title={guardrail.updated_at}>
<span className="text-xs">{formatDate(guardrail.updated_at)}</span>
</Tooltip>
);
},
cell: ({ row }) => <DateCell value={row.original.updated_at} />,
},
{
id: "actions",

View file

@ -2,6 +2,7 @@ import { ColumnDef } from "@tanstack/react-table";
import { Button, Badge, Text } from "@tremor/react";
import { Tooltip, Tag } from "antd";
import { CopyOutlined, InfoCircleOutlined } from "@ant-design/icons";
import { StatusBadge, type StatusTone } from "@/components/shared/table_cells";
export interface MCPServerData {
server_id: string;
@ -124,21 +125,17 @@ export const mcpHubColumns = (
cell: ({ row }) => {
const server = row.original;
const statusColors: Record<string, string> = {
active: "green",
inactive: "red",
unknown: "gray",
healthy: "green",
unhealthy: "red",
const statusTones: Record<string, StatusTone> = {
active: "success",
inactive: "error",
unknown: "neutral",
healthy: "success",
unhealthy: "error",
};
const color = statusColors[server.status] || "gray";
const tone = statusTones[server.status] || "neutral";
return (
<Badge color={color} size="sm">
{server.status || "unknown"}
</Badge>
);
return <StatusBadge tone={tone} label={server.status || "unknown"} />;
},
},
{

View file

@ -6,6 +6,7 @@ import { ColumnDef } from "@tanstack/react-table";
import { useMCPToolsets } from "@/app/(dashboard)/hooks/mcpServers/useMCPToolsets";
import { useMCPServers } from "@/app/(dashboard)/hooks/mcpServers/useMCPServers";
import { useQueryClient } from "@tanstack/react-query";
import { DateCell, IdCell } from "@/components/shared/table_cells";
import { DataTable } from "../view_logs/table";
import { createMCPToolset, updateMCPToolset, deleteMCPToolset, listMCPTools, getProxyBaseUrl } from "../networking";
import { MCPToolset, MCPToolsetTool } from "./types";
@ -302,11 +303,7 @@ function toolsetColumns(
{
header: "Toolset ID",
accessorKey: "toolset_id",
cell: ({ row }) => (
<span className="font-mono text-xs bg-gray-100 px-2 py-0.5 rounded-sm text-gray-600">
{row.original.toolset_id.slice(0, 8)}…
</span>
),
cell: ({ row }) => <IdCell value={row.original.toolset_id} />,
},
{
header: "Name",
@ -359,11 +356,7 @@ function toolsetColumns(
{
header: "Created",
accessorKey: "created_at",
cell: ({ row }) => (
<span className="text-xs text-gray-500">
{row.original.created_at ? new Date(row.original.created_at).toLocaleDateString() : "—"}
</span>
),
cell: ({ row }) => <DateCell value={row.original.created_at} precision="date" />,
},
...(isAdmin
? [

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from "react";
import { Title, Text, Button, Badge } from "@tremor/react";
import { Title, Text, Button } from "@tremor/react";
import { Modal } from "antd";
import { Button as AntdButton } from "antd";
import { ModelDataTable } from "./table";
@ -468,21 +468,6 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
onPageChange?.(page);
};
const getStatusBadge = (status: string) => {
switch (status) {
case "healthy":
return <Badge color="emerald">healthy</Badge>;
case "unhealthy":
return <Badge color="red">unhealthy</Badge>;
case "checking":
return <Badge color="blue">checking</Badge>;
case "none":
return <Badge color="gray">none</Badge>;
default:
return <Badge color="gray">unknown</Badge>;
}
};
const showErrorModal = (modelName: string, cleanedError: string, fullError: string) => {
setSelectedErrorDetails({
modelName,
@ -612,7 +597,6 @@ const HealthCheckComponent: React.FC<HealthCheckComponentProps> = ({
handleModelSelection,
handleSelectAll,
runIndividualHealthCheck,
getStatusBadge,
getDisplayModelName,
showErrorModal,
showSuccessModal,

View file

@ -3,6 +3,7 @@ import { Tooltip, Checkbox } from "antd";
import { Text } from "@tremor/react";
import { InformationCircleIcon, PlayIcon, RefreshIcon } from "@heroicons/react/outline";
import { Team } from "@/components/key_team_helpers/key_list";
import { IdCell, StatusBadge, type StatusTone } from "@/components/shared/table_cells";
interface HealthCheckData {
model_name: string;
@ -21,6 +22,18 @@ interface HealthCheckData {
health_full_error?: string;
}
const HEALTH_STATUS_TONES: Record<string, StatusTone> = {
healthy: "success",
unhealthy: "error",
checking: "info",
none: "neutral",
};
const healthStatusBadge = (status: string): JSX.Element => {
const tone = HEALTH_STATUS_TONES[status];
return tone ? <StatusBadge tone={tone} label={status} /> : <StatusBadge tone="neutral" label="unknown" />;
};
interface HealthStatus {
status: string;
lastCheck: string;
@ -38,7 +51,6 @@ export const healthCheckColumns = (
handleModelSelection: (modelId: string, checked: boolean) => void,
handleSelectAll: (checked: boolean) => void,
runIndividualHealthCheck: (modelId: string) => void,
getStatusBadge: (status: string) => JSX.Element,
getDisplayModelName: (model: any) => string,
showErrorModal?: (modelName: string, cleanedError: string, fullError: string) => void,
showSuccessModal?: (modelName: string, response: any) => void,
@ -72,14 +84,7 @@ export const healthCheckColumns = (
onChange={(e) => handleModelSelection(modelId, e.target.checked)}
onClick={(e) => e.stopPropagation()}
/>
<Tooltip title={model.model_info.id}>
<div
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left w-full truncate whitespace-nowrap cursor-pointer max-w-[15ch]"
onClick={() => setSelectedModelId && setSelectedModelId(model.model_info.id)}
>
{model.model_info.id}
</div>
</Tooltip>
<IdCell value={model.model_info.id} onClick={setSelectedModelId} />
</div>
);
},
@ -175,7 +180,7 @@ export const healthCheckColumns = (
return (
<div className="flex items-center space-x-2">
{getStatusBadge(healthStatus.status)}
{healthStatusBadge(healthStatus.status)}
{hasSuccessResponse && showSuccessModal && (
<Tooltip title="View response details" placement="top">
<button

View file

@ -2,6 +2,7 @@ import { ColumnDef } from "@tanstack/react-table";
import { Button, Badge, Text } from "@tremor/react";
import { Tooltip, Tag } from "antd";
import { CopyOutlined, InfoCircleOutlined } from "@ant-design/icons";
import { StatusBadge } from "@/components/shared/table_cells";
interface ModelHubData {
model_group: string;
@ -212,13 +213,9 @@ export const modelHubColumns = (
const model = row.original;
return model.is_public_model_group === true ? (
<Badge color="green" size="xs">
Yes
</Badge>
<StatusBadge tone="success" label="Yes" />
) : (
<Badge color="gray" size="xs">
No
</Badge>
<StatusBadge tone="neutral" label="No" />
);
},
meta: {

View file

@ -451,6 +451,64 @@ describe("columns", () => {
expect(costCells.length).toBeGreaterThan(0);
});
it("should call setSelectedModelId without triggering the row click when the model ID pill is clicked", async () => {
const user = userEvent.setup();
const setSelectedModelId = vi.fn();
const cols = columns(
defaultProps.userRole,
defaultProps.userID,
defaultProps.premiumUser,
setSelectedModelId,
defaultProps.setSelectedTeamId,
defaultProps.getDisplayModelName,
defaultProps.handleEditClick,
defaultProps.handleRefreshClick,
defaultProps.expandedRows,
defaultProps.setExpandedRows,
);
const model = createMockModel();
const rowClick = vi.fn();
render(
<div onClick={rowClick}>
<TestTable data={[model]} columns={cols} />
</div>,
);
await user.click(screen.getByText("test-model-id"));
expect(setSelectedModelId).toHaveBeenCalledWith("test-model-id");
expect(rowClick).not.toHaveBeenCalled();
});
it("should call setSelectedTeamId without triggering the row click when the team ID pill is clicked", async () => {
const user = userEvent.setup();
const setSelectedTeamId = vi.fn();
const cols = columns(
defaultProps.userRole,
defaultProps.userID,
defaultProps.premiumUser,
defaultProps.setSelectedModelId,
setSelectedTeamId,
defaultProps.getDisplayModelName,
defaultProps.handleEditClick,
defaultProps.handleRefreshClick,
defaultProps.expandedRows,
defaultProps.setExpandedRows,
);
const model = createMockModel();
const rowClick = vi.fn();
render(
<div onClick={rowClick}>
<TestTable data={[model]} columns={cols} />
</div>,
);
await user.click(screen.getByText("test-team-id"));
expect(setSelectedTeamId).toHaveBeenCalledWith("test-team-id");
expect(rowClick).not.toHaveBeenCalled();
});
it("should display '-' when team ID is missing", () => {
const cols = columns(
defaultProps.userRole,

View file

@ -1,8 +1,9 @@
import { EditOutlined, InfoCircleOutlined, SyncOutlined } from "@ant-design/icons";
import { TrashIcon } from "@heroicons/react/outline";
import { ColumnDef } from "@tanstack/react-table";
import { Badge, Button, Icon } from "@tremor/react";
import { Badge, Icon } from "@tremor/react";
import { Divider, Flex, Popover, Space, Switch, Tooltip, Typography } from "antd";
import { DateCell, IdCell, StatusBadge } from "@/components/shared/table_cells";
import { ModelData } from "../../model_dashboard/types";
import { ProviderLogo } from "./ProviderLogo";
@ -65,19 +66,9 @@ export const columns = (
cell: ({ row }) => {
const model = row.original;
return (
<Tooltip title={model.model_info.id}>
<Text
ellipsis
className="text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs cursor-pointer w-full block"
style={{ fontSize: 14, padding: "1px 8px" }}
onClick={(e) => {
e.stopPropagation();
setSelectedModelId(model.model_info.id);
}}
>
{model.model_info.id}
</Text>
</Tooltip>
<div onClick={(e) => e.stopPropagation()}>
<IdCell value={model.model_info.id} onClick={setSelectedModelId} />
</div>
);
},
},
@ -235,11 +226,7 @@ export const columns = (
minSize: 80,
cell: ({ row }) => {
const model = row.original;
return (
<span className="text-xs">
{model.model_info.updated_at ? new Date(model.model_info.updated_at).toLocaleDateString() : "-"}
</span>
);
return <DateCell value={model.model_info.updated_at} precision="date" />;
},
},
{
@ -282,20 +269,8 @@ export const columns = (
cell: ({ row }) => {
const model = row.original;
return model.model_info.team_id ? (
<div className="overflow-hidden w-full">
<Tooltip title={model.model_info.team_id}>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate w-full"
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
setSelectedTeamId(model.model_info.team_id);
}}
>
{model.model_info.team_id.slice(0, 7)}...
</Button>
</Tooltip>
<div onClick={(e) => e.stopPropagation()}>
<IdCell value={model.model_info.team_id} onClick={setSelectedTeamId} />
</div>
) : (
"-"
@ -370,15 +345,10 @@ export const columns = (
minSize: 80,
cell: ({ row }) => {
const model = row.original;
return (
<div
className={`
inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
${model.model_info.db_model ? "bg-blue-50 text-blue-600" : "bg-gray-100 text-gray-600"}
`}
>
{model.model_info.db_model ? "DB Model" : "Config Model"}
</div>
return model.model_info.db_model ? (
<StatusBadge tone="info" label="DB Model" />
) : (
<StatusBadge tone="neutral" label="Config Model" />
);
},
},

View file

@ -1,6 +1,7 @@
import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
import { organizationKeys, useOrganization } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
import { useQueryClient } from "@tanstack/react-query";
import { MoneyCell } from "@/components/shared/table_cells";
import { formatNumberWithCommas, copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
import { createTeamAliasMap } from "@/utils/teamUtils";
import { ArrowLeftIcon } from "@heroicons/react/outline";
@ -196,7 +197,7 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
render: (_: unknown, record: Member) => {
const orgMember =
record.user_id != null ? (orgData.members || []).find((m) => m.user_id === record.user_id) : undefined;
return <Typography.Text>${formatNumberWithCommas(orgMember?.spend ?? 0, 4)}</Typography.Text>;
return <MoneyCell value={orgMember?.spend} decimals={4} />;
},
},
{

View file

@ -27,7 +27,7 @@ import {
import { Form, Input, Modal, Select as Select2, Tooltip } from "antd";
import { useQueryClient } from "@tanstack/react-query";
import React, { useState } from "react";
import { formatNumberWithCommas } from "../utils/dataUtils";
import { DateCell, IdCell, MoneyCell } from "@/components/shared/table_cells";
import DeleteResourceModal from "./common_components/DeleteResourceModal";
import TableIconActionButton from "./common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
@ -263,30 +263,22 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
.map((org: Organization) => (
<TableRow key={org.organization_id}>
<TableCell>
<div className="overflow-hidden">
<Tooltip title={org.organization_id}>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
onClick={() => setSelectedOrgId(org.organization_id)}
>
{org.organization_id?.slice(0, 7)}
...
</Button>
</Tooltip>
</div>
<IdCell value={org.organization_id} onClick={setSelectedOrgId} />
</TableCell>
<TableCell>{org.organization_alias}</TableCell>
<TableCell>
{org.created_at ? new Date(org.created_at).toLocaleDateString() : "N/A"}
<DateCell value={org.created_at} precision="date" />
</TableCell>
<TableCell>{formatNumberWithCommas(org.spend, 4)}</TableCell>
<TableCell>
{org.litellm_budget_table?.max_budget !== null &&
org.litellm_budget_table?.max_budget !== undefined
? org.litellm_budget_table?.max_budget
: "No limit"}
<MoneyCell value={org.spend} decimals={4} />
</TableCell>
<TableCell>
<MoneyCell
value={org.litellm_budget_table?.max_budget}
decimals={2}
emptyText="Unlimited"
showZero
/>
</TableCell>
<TableCell
style={{

View file

@ -7,6 +7,7 @@ import AddPassThroughEndpoint from "./add_pass_through";
import PassThroughInfoView from "./pass_through_info";
import { DataTable } from "./view_logs/table";
import { ColumnDef } from "@tanstack/react-table";
import { IdCell, StatusBadge } from "@/components/shared/table_cells";
import { Eye, EyeOff } from "lucide-react";
import NotificationsManager from "./molecules/notifications_manager";
@ -136,16 +137,7 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
{
header: "ID",
accessorKey: "id",
cell: (info: any) => (
<Tooltip title={info.row.original.id}>
<div
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left w-full truncate whitespace-nowrap cursor-pointer max-w-[15ch]"
onClick={() => info.row.original.id && setSelectedEndpointId(info.row.original.id)}
>
{info.row.original.id}
</div>
</Tooltip>
),
cell: (info: any) => <IdCell value={info.row.original.id} onClick={setSelectedEndpointId} />,
},
{
header: "Path",
@ -192,7 +184,9 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
</div>
),
accessorKey: "auth",
cell: (info: any) => <Badge color={info.getValue() ? "green" : "gray"}>{info.getValue() ? "Yes" : "No"}</Badge>,
cell: (info: any) => (
<StatusBadge tone={info.getValue() ? "success" : "neutral"} label={info.getValue() ? "Yes" : "No"} />
),
},
{
header: "Headers",

View file

@ -140,10 +140,13 @@ describe("AttachmentTable", () => {
expect(screen.queryByRole("button", { name: /TrashIcon/i })).not.toBeInTheDocument();
});
it("should show a truncated attachment ID in the table", () => {
it("should show the attachment ID as truncated plain mono text", () => {
const attachment = makeAttachment({ attachment_id: "att-abcdef1234567" });
renderWithProviders(<AttachmentTable {...defaultProps} attachments={[attachment]} />);
expect(screen.getByText("att-abc...")).toBeInTheDocument();
const idElement = screen.getByText("att-abcdef1234567");
expect(idElement.className).toContain("font-mono");
expect(idElement.className).toContain("truncate");
expect(idElement.className).not.toContain("bg-blue-50");
});
it("should render model tags when the attachment has models", () => {

View file

@ -10,6 +10,7 @@ import {
SortingState,
useReactTable,
} from "@tanstack/react-table";
import { DateCell, IdCell } from "@/components/shared/table_cells";
import { PolicyAttachment } from "./types";
import ImpactPopover from "./impact_popover";
@ -30,24 +31,11 @@ const AttachmentTable: React.FC<AttachmentTableProps> = ({
}) => {
const [sorting, setSorting] = useState<SortingState>([{ id: "created_at", desc: true }]);
// Format date helper function
const formatDate = (dateString?: string) => {
if (!dateString) return "-";
const date = new Date(dateString);
return date.toLocaleString();
};
const columns: ColumnDef<PolicyAttachment>[] = [
{
header: "Attachment ID",
accessorKey: "attachment_id",
cell: (info: any) => (
<Tooltip title={String(info.getValue() || "")}>
<span className="font-mono text-xs text-gray-600">
{info.getValue() ? `${String(info.getValue()).slice(0, 7)}...` : ""}
</span>
</Tooltip>
),
cell: (info: any) => <IdCell value={info.getValue()} variant="plain" />,
},
{
header: "Policy",
@ -183,14 +171,7 @@ const AttachmentTable: React.FC<AttachmentTableProps> = ({
{
header: "Created At",
accessorKey: "created_at",
cell: ({ row }) => {
const attachment = row.original;
return (
<Tooltip title={attachment.created_at}>
<span className="text-xs">{formatDate(attachment.created_at)}</span>
</Tooltip>
);
},
cell: ({ row }) => <DateCell value={row.original.created_at} />,
},
{
id: "actions",

View file

@ -10,6 +10,7 @@ import {
SortingState,
useReactTable,
} from "@tanstack/react-table";
import { DateCell } from "@/components/shared/table_cells";
import { Policy } from "./types";
/** One row per policy name; primaryPolicy is used for display and for Edit (FlowBuilder loads all versions) */
@ -59,12 +60,6 @@ const PolicyTable: React.FC<PolicyTableProps> = ({
const rows = useMemo(() => groupPoliciesByName(policies), [policies]);
const formatDate = (dateString?: string) => {
if (!dateString) return "-";
const date = new Date(dateString);
return date.toLocaleString();
};
const columns: ColumnDef<PolicyRow>[] = [
{
header: "Name",
@ -199,14 +194,7 @@ const PolicyTable: React.FC<PolicyTableProps> = ({
header: "Created At",
id: "created_at",
accessorFn: (row) => row.primaryPolicy.created_at ?? "",
cell: ({ row }) => {
const policy = row.original.primaryPolicy;
return (
<Tooltip title={policy.created_at}>
<span className="text-xs">{formatDate(policy.created_at)}</span>
</Tooltip>
);
},
cell: ({ row }) => <DateCell value={row.original.primaryPolicy.created_at} />,
},
{
id: "actions",

View file

@ -0,0 +1,21 @@
"use client";
import * as React from "react";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
interface CellTooltipProps {
content: React.ReactNode;
trigger: React.ReactElement;
}
export function CellTooltip({ content, trigger }: CellTooltipProps) {
return (
<TooltipProvider delay={300}>
<Tooltip>
<TooltipTrigger render={trigger} />
<TooltipContent>{content}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}

View file

@ -0,0 +1,57 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { DateCell, formatCellDate, formatFullTimestamp } from "./date_cell";
const localIso = new Date(2026, 6, 7, 9, 50, 13).toISOString();
describe("formatCellDate", () => {
it("formats datetime precision as 'MMM D, HH:mm:ss' without a year", () => {
expect(formatCellDate(new Date(2026, 6, 7, 9, 50, 13), "datetime")).toBe("Jul 7, 09:50:13");
});
it("zero-pads hours, minutes and seconds", () => {
expect(formatCellDate(new Date(2026, 0, 2, 1, 2, 3), "datetime")).toBe("Jan 2, 01:02:03");
});
it("formats date precision as 'MMM D, YYYY' with no time", () => {
expect(formatCellDate(new Date(2026, 11, 31, 23, 59, 59), "date")).toBe("Dec 31, 2026");
});
});
describe("formatFullTimestamp", () => {
it("includes year, 24h time and the IANA timezone", () => {
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
expect(formatFullTimestamp(new Date(2026, 6, 7, 9, 50, 13))).toBe(`Jul 7, 2026, 09:50:13 (${timeZone})`);
});
});
describe("DateCell", () => {
it("renders the datetime format by default", () => {
render(<DateCell value={localIso} />);
expect(screen.getByText("Jul 7, 09:50:13")).toBeInTheDocument();
});
it("renders date-only when precision is 'date'", () => {
render(<DateCell value={localIso} precision="date" />);
expect(screen.getByText("Jul 7, 2026")).toBeInTheDocument();
});
it("renders '-' for null and undefined", () => {
const { rerender } = render(<DateCell value={null} />);
expect(screen.getByText("-")).toBeInTheDocument();
rerender(<DateCell value={undefined} />);
expect(screen.getByText("-")).toBeInTheDocument();
});
it("renders the custom fallback for empty values", () => {
render(<DateCell value="" fallback="Never" />);
expect(screen.getByText("Never")).toBeInTheDocument();
});
it("renders the fallback instead of 'Invalid Date' for unparseable input", () => {
render(<DateCell value="not-a-date" fallback="Unknown" />);
expect(screen.getByText("Unknown")).toBeInTheDocument();
expect(screen.queryByText(/Invalid/)).not.toBeInTheDocument();
});
});

View file

@ -0,0 +1,41 @@
"use client";
import { CellTooltip } from "./cell_tooltip";
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
export type DatePrecision = "datetime" | "date";
interface DateCellProps {
value: string | null | undefined;
precision?: DatePrecision;
fallback?: string;
}
const pad = (n: number): string => String(n).padStart(2, "0");
export const formatCellDate = (date: Date, precision: DatePrecision): string =>
precision === "date"
? `${MONTHS[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`
: `${MONTHS[date.getMonth()]} ${date.getDate()}, ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
export const formatFullTimestamp = (date: Date): string => {
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const day = `${MONTHS[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`;
const time = `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
return `${day}, ${time} (${timeZone})`;
};
export function DateCell({ value, precision = "datetime", fallback = "-" }: DateCellProps) {
const date = value ? new Date(value) : null;
if (!date || Number.isNaN(date.getTime())) {
return <span className="text-muted-foreground">{fallback}</span>;
}
return (
<CellTooltip
content={formatFullTimestamp(date)}
trigger={<span className="whitespace-nowrap">{formatCellDate(date, precision)}</span>}
/>
);
}

View file

@ -0,0 +1,78 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { IdCell } from "./id_cell";
const { copyToClipboardMock } = vi.hoisted(() => ({ copyToClipboardMock: vi.fn() }));
vi.mock("@/utils/dataUtils", async (importOriginal) => ({
...(await importOriginal<typeof import("@/utils/dataUtils")>()),
copyToClipboard: copyToClipboardMock,
}));
describe("IdCell", () => {
it("renders '-' for empty values", () => {
render(<IdCell value={null} />);
expect(screen.getByText("-")).toBeInTheDocument();
});
it("renders the custom fallback for empty values", () => {
render(<IdCell value="" fallback="—" />);
expect(screen.getByText("—")).toBeInTheDocument();
});
it("renders the full id as a non-interactive pill by default", () => {
render(<IdCell value="sk-1234567890abcdef" />);
const el = screen.getByText("sk-1234567890abcdef");
expect(el.tagName).toBe("SPAN");
expect(el.className).toContain("bg-blue-50");
expect(el.className).toContain("font-mono");
expect(el.className).toContain("max-w-[15ch]");
expect(el.className).toContain("truncate");
});
it("renders plain mono text without pill styling for the plain variant", () => {
render(<IdCell value="req-123" variant="plain" />);
const el = screen.getByText("req-123");
expect(el.className).toContain("font-mono");
expect(el.className).not.toContain("bg-blue-50");
});
it("does not truncate when truncate is false", () => {
render(<IdCell value="audit-object-id" truncate={false} />);
expect(screen.getByText("audit-object-id").className).not.toContain("truncate");
});
it("becomes a button that fires onClick with the id value", async () => {
const user = userEvent.setup();
const onClick = vi.fn();
render(<IdCell value="team-42" onClick={onClick} />);
await user.click(screen.getByRole("button", { name: "team-42" }));
expect(onClick).toHaveBeenCalledWith("team-42");
});
it("stays non-interactive when disabled, even with onClick", () => {
const onClick = vi.fn();
render(<IdCell value="tag-1" onClick={onClick} disabled />);
expect(screen.queryByRole("button", { name: "tag-1" })).not.toBeInTheDocument();
});
it("copies the id via the trailing copy button without triggering row clicks", async () => {
const user = userEvent.setup();
const rowClick = vi.fn();
render(
<div onClick={rowClick}>
<IdCell value="key-hash-9" copyable />
</div>,
);
await user.click(screen.getByRole("button", { name: "Copy ID" }));
expect(copyToClipboardMock).toHaveBeenCalledWith("key-hash-9");
expect(rowClick).not.toHaveBeenCalled();
});
it("passes dataTestId through to the id element", () => {
render(<IdCell value="k-1" dataTestId="key-id-cell" />);
expect(screen.getByTestId("key-id-cell")).toHaveTextContent("k-1");
});
});

View file

@ -0,0 +1,94 @@
"use client";
import { Copy } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/cva.config";
import { copyToClipboard } from "@/utils/dataUtils";
import { CellTooltip } from "./cell_tooltip";
export type IdCellVariant = "pill" | "plain";
interface IdCellProps {
value: string | null | undefined;
variant?: IdCellVariant;
onClick?: (value: string) => void;
copyable?: boolean;
truncate?: boolean;
fallback?: string;
tooltip?: React.ReactNode;
disabled?: boolean;
dataTestId?: string;
className?: string;
}
const VARIANT_CLASS: Record<IdCellVariant, { base: string; clickable: string }> = {
pill: {
base: "font-mono text-xs font-normal px-2 py-0.5 rounded-md text-left bg-blue-50 text-blue-500",
clickable: "hover:bg-blue-100 cursor-pointer",
},
plain: {
base: "font-mono text-xs text-left",
clickable: "hover:text-blue-600 cursor-pointer",
},
};
export function IdCell({
value,
variant = "pill",
onClick,
copyable = false,
truncate = true,
fallback = "-",
tooltip,
disabled = false,
dataTestId,
className,
}: IdCellProps) {
if (!value) {
return <span className="text-muted-foreground">{fallback}</span>;
}
const clickable = !!onClick && !disabled;
const classes = cn(
VARIANT_CLASS[variant].base,
clickable && VARIANT_CLASS[variant].clickable,
truncate && "block max-w-[15ch] truncate",
disabled && "opacity-50",
className,
);
const idElement = clickable ? (
<button type="button" className={classes} data-testid={dataTestId} onClick={() => onClick(value)}>
{value}
</button>
) : (
<span className={classes} data-testid={dataTestId}>
{value}
</span>
);
const withTooltip = <CellTooltip content={tooltip ?? value} trigger={idElement} />;
if (!copyable) {
return withTooltip;
}
return (
<span className="inline-flex max-w-full items-center gap-1">
{withTooltip}
<button
type="button"
aria-label="Copy ID"
className="shrink-0 cursor-pointer text-muted-foreground hover:text-foreground"
onClick={(event) => {
event.stopPropagation();
void copyToClipboard(value);
}}
>
<Copy className="size-3" />
</button>
</span>
);
}

View file

@ -0,0 +1,5 @@
export { CellTooltip } from "./cell_tooltip";
export { DateCell, formatCellDate, formatFullTimestamp, type DatePrecision } from "./date_cell";
export { IdCell, type IdCellVariant } from "./id_cell";
export { MoneyCell } from "./money_cell";
export { StatusBadge, type StatusTone } from "./status_badge";

View file

@ -0,0 +1,44 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { MoneyCell } from "./money_cell";
describe("MoneyCell", () => {
it("renders '-' for null and undefined", () => {
const { rerender } = render(<MoneyCell value={null} />);
expect(screen.getByText("-")).toBeInTheDocument();
rerender(<MoneyCell value={undefined} />);
expect(screen.getByText("-")).toBeInTheDocument();
});
it("renders the custom emptyText for null budgets", () => {
render(<MoneyCell value={null} emptyText="Unlimited" />);
expect(screen.getByText("Unlimited")).toBeInTheDocument();
});
it("renders '-' for zero by default", () => {
render(<MoneyCell value={0} />);
expect(screen.getByText("-")).toBeInTheDocument();
});
it("renders a formatted zero when showZero is set, never the emptyText", () => {
render(<MoneyCell value={0} showZero emptyText="Unlimited" decimals={2} />);
expect(screen.getByText("$0.00")).toBeInTheDocument();
expect(screen.queryByText("Unlimited")).not.toBeInTheDocument();
});
it("formats amounts with commas, a dollar sign and the given decimals", () => {
render(<MoneyCell value={1234.5678} decimals={2} />);
expect(screen.getByText("$1,234.57")).toBeInTheDocument();
});
it("defaults to 4 decimals", () => {
render(<MoneyCell value={42} />);
expect(screen.getByText("$42.0000")).toBeInTheDocument();
});
it("renders the sub-threshold form for amounts that round to zero", () => {
render(<MoneyCell value={0.0000001} decimals={6} />);
expect(screen.getByText("< $0.000001")).toBeInTheDocument();
});
});

View file

@ -0,0 +1,23 @@
"use client";
import { formatNumberWithCommas, getSpendString } from "@/utils/dataUtils";
interface MoneyCellProps {
value: number | null | undefined;
decimals?: number;
emptyText?: string;
showZero?: boolean;
}
export function MoneyCell({ value, decimals = 4, emptyText = "-", showZero = false }: MoneyCellProps) {
if (value === null || value === undefined || Number.isNaN(value)) {
return <span className="text-muted-foreground">{emptyText}</span>;
}
if (value === 0) {
if (!showZero) {
return <span className="text-muted-foreground">-</span>;
}
return <span className="whitespace-nowrap">{`$${formatNumberWithCommas(0, decimals, false, true)}`}</span>;
}
return <span className="whitespace-nowrap">{getSpendString(value, decimals)}</span>;
}

View file

@ -0,0 +1,42 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { StatusBadge, type StatusTone } from "./status_badge";
describe("StatusBadge", () => {
const toneClasses: Record<StatusTone, string[]> = {
success: ["border-green-200", "bg-green-50", "text-green-600"],
error: ["border-red-200", "bg-red-50", "text-red-600"],
warning: ["border-amber-200", "bg-amber-50", "text-amber-600"],
neutral: ["border-gray-200", "bg-gray-50", "text-gray-600"],
info: ["border-blue-200", "bg-blue-50", "text-blue-600"],
};
(Object.entries(toneClasses) as [StatusTone, string[]][]).forEach(([tone, classes]) => {
it(`renders a tinted pill (${classes.join(" ")}) for the ${tone} tone`, () => {
render(<StatusBadge tone={tone} label={tone} />);
const badge = screen.getByText(tone);
classes.forEach((cls) => expect(badge.className).toContain(cls));
});
});
it("renders the label text inside an outline badge with no status dot", () => {
render(<StatusBadge tone="success" label="Active" />);
const badge = screen.getByText("Active");
expect(badge.dataset.variant).toBe("outline");
expect(badge.querySelector("[aria-hidden]")).toBeNull();
});
it("passes dataTestId through", () => {
render(<StatusBadge tone="error" label="Blocked" dataTestId="key-status" />);
expect(screen.getByTestId("key-status")).toHaveTextContent("Blocked");
});
it("opens the tooltip on hover, which requires Badge to forward its ref to the trigger", async () => {
const user = userEvent.setup();
render(<StatusBadge tone="error" label="Blocked" tooltip="This key was blocked by SCIM" />);
await user.hover(screen.getByText("Blocked"));
expect(await screen.findByText("This key was blocked by SCIM")).toBeInTheDocument();
});
});

View file

@ -0,0 +1,38 @@
"use client";
import * as React from "react";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/cva.config";
import { CellTooltip } from "./cell_tooltip";
export type StatusTone = "success" | "error" | "warning" | "neutral" | "info";
const TONE_CLASS: Record<StatusTone, string> = {
success: "border-green-200 bg-green-50 text-green-600",
error: "border-red-200 bg-red-50 text-red-600",
warning: "border-amber-200 bg-amber-50 text-amber-600",
neutral: "border-gray-200 bg-gray-50 text-gray-600",
info: "border-blue-200 bg-blue-50 text-blue-600",
};
interface StatusBadgeProps {
tone: StatusTone;
label: string;
tooltip?: React.ReactNode;
dataTestId?: string;
}
export function StatusBadge({ tone, label, tooltip, dataTestId }: StatusBadgeProps) {
const badge = (
<Badge variant="outline" data-testid={dataTestId} className={cn("whitespace-nowrap font-normal", TONE_CLASS[tone])}>
{label}
</Badge>
);
if (!tooltip) {
return badge;
}
return <CellTooltip content={tooltip} trigger={badge} />;
}

View file

@ -3,6 +3,7 @@ import { Badge, Text } from "@tremor/react";
import { Tooltip } from "antd";
import { CopyOutlined, LinkOutlined } from "@ant-design/icons";
import { Plugin } from "./claude_code_plugins/types";
import { StatusBadge } from "@/components/shared/table_cells";
export const skillHubColumns = (
showModal: (skill: Plugin) => void,
@ -104,9 +105,10 @@ export const skillHubColumns = (
accessorKey: "enabled",
enableSorting: true,
cell: ({ row }) => (
<Badge color={row.original.enabled ? "green" : "gray"} size="xs">
{row.original.enabled ? "Public" : "Draft"}
</Badge>
<StatusBadge
tone={row.original.enabled ? "success" : "neutral"}
label={row.original.enabled ? "Public" : "Draft"}
/>
),
},
];

View file

@ -1,5 +1,6 @@
import { render, screen } from "@testing-library/react";
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { formatCellDate } from "@/components/shared/table_cells";
import TagTable from "./TagTable";
import { Tag } from "./types";
@ -75,14 +76,21 @@ describe("TagTable", () => {
it("should display formatted created date", () => {
render(<TagTable {...defaultProps} data={[mockTag]} />);
const formattedDate = new Date(mockTag.created_at).toLocaleDateString();
const formattedDate = formatCellDate(new Date(mockTag.created_at), "date");
expect(screen.getByText(formattedDate)).toBeInTheDocument();
});
it("should disable tag name button for dynamic spend tags", () => {
it("should call onSelectTag when tag name is clicked", () => {
render(<TagTable {...defaultProps} data={[mockTag]} />);
fireEvent.click(screen.getByRole("button", { name: "test-tag" }));
expect(mockOnSelectTag).toHaveBeenCalledWith("test-tag");
});
it("should render tag name as non-clickable for dynamic spend tags", () => {
render(<TagTable {...defaultProps} data={[mockDynamicSpendTag]} />);
const tagButton = screen.getByRole("button", { name: "dynamic-spend-tag" });
expect(tagButton).toBeDisabled();
expect(screen.queryByRole("button", { name: "dynamic-spend-tag" })).not.toBeInTheDocument();
fireEvent.click(screen.getByText("dynamic-spend-tag"));
expect(mockOnSelectTag).not.toHaveBeenCalled();
});
it("should disable edit icon for dynamic spend tags", () => {

View file

@ -7,20 +7,10 @@ import {
SortingState,
useReactTable,
} from "@tanstack/react-table";
import {
Badge,
Button,
Icon,
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
Text,
} from "@tremor/react";
import { Badge, Icon, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Text } from "@tremor/react";
import { Tooltip } from "antd";
import React from "react";
import { DateCell, IdCell } from "@/components/shared/table_cells";
import { Tag } from "./types";
interface TagTableProps {
@ -45,21 +35,15 @@ const TagTable: React.FC<TagTableProps> = ({ data, onEdit, onDelete, onSelectTag
const isDynamicSpendTag = tag.description === DYNAMIC_SPEND_TAG_DESCRIPTION;
return (
<div className="overflow-hidden">
<Tooltip
title={
<IdCell
value={tag.name}
truncate={false}
onClick={onSelectTag}
disabled={isDynamicSpendTag}
tooltip={
isDynamicSpendTag ? "You cannot view the information of a dynamically generated spend tag" : tag.name
}
>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5"
onClick={() => onSelectTag(tag.name)}
disabled={isDynamicSpendTag}
>
{tag.name}
</Button>
</Tooltip>
/>
</div>
);
},
@ -104,10 +88,7 @@ const TagTable: React.FC<TagTableProps> = ({ data, onEdit, onDelete, onSelectTag
header: "Created",
accessorKey: "created_at",
sortingFn: "datetime",
cell: ({ row }) => {
const tag = row.original;
return <span className="text-xs">{new Date(tag.created_at).toLocaleDateString()}</span>;
},
cell: ({ row }) => <DateCell value={row.original.created_at} precision="date" />,
},
{
id: "actions",

View file

@ -27,6 +27,8 @@ const mockSetSelectedEditMember = vi.fn();
const mockSetIsEditMemberModalVisible = vi.fn();
const mockSetIsAddMemberModalVisible = vi.fn();
const budgetResetIso = new Date(2026, 6, 15, 12, 0, 0).toISOString();
const createMockTeamData = (overrides: Partial<TeamData> = {}): TeamData => ({
team_id: "team-123",
team_info: {
@ -78,6 +80,7 @@ const createMockTeamData = (overrides: Partial<TeamData> = {}): TeamData => ({
rpm_limit: 100,
model_max_budget: null,
budget_duration: null,
budget_reset_at: budgetResetIso,
},
},
],
@ -202,7 +205,7 @@ describe("TeamMembersComponent", () => {
/>,
);
expect(screen.getByText("-")).toBeInTheDocument();
expect(screen.getAllByText("-").length).toBeGreaterThanOrEqual(1);
});
it("should display Default Proxy Admin tag for default_user_id", () => {
@ -243,12 +246,12 @@ describe("TeamMembersComponent", () => {
/>,
);
expect(screen.getByText(/\$100\.5/)).toBeInTheDocument();
expect(screen.getByText("$100.5000")).toBeInTheDocument();
expect(screen.getByText(/100 RPM/)).toBeInTheDocument();
expect(screen.getByText(/10000 TPM/)).toBeInTheDocument();
});
it("should display No Limit for budget when member has no budget", () => {
it("should display the budget reset date for member with a budget reset", () => {
renderWithProviders(
<TeamMembersComponent
teamData={createMockTeamData()}
@ -260,7 +263,23 @@ describe("TeamMembersComponent", () => {
/>,
);
expect(screen.getByText("No Limit")).toBeInTheDocument();
expect(screen.getByText("Jul 15, 2026")).toBeInTheDocument();
});
it("should display formatted budget and Unlimited for member with no budget", () => {
renderWithProviders(
<TeamMembersComponent
teamData={createMockTeamData()}
canEditTeam={false}
handleMemberDelete={mockHandleMemberDelete}
setSelectedEditMember={mockSetSelectedEditMember}
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
/>,
);
expect(screen.getByText("$1,000.0000")).toBeInTheDocument();
expect(screen.getByText("Unlimited")).toBeInTheDocument();
});
it("should display No Limits for rate limits when member has no limits", () => {

View file

@ -1,7 +1,7 @@
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { Member } from "@/components/networking";
import { formatBudgetReset } from "@/utils/budgetUtils";
import { DateCell, MoneyCell } from "@/components/shared/table_cells";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { isProxyAdminRole, isUserTeamAdminForSingleTeam } from "@/utils/roles";
import { InfoCircleOutlined } from "@ant-design/icons";
@ -58,14 +58,10 @@ export default function TeamMemberTab({
return membership?.total_spend ?? 0;
};
const getUserBudget = (userId: string | null): string | null => {
const getUserBudget = (userId: string | null): number | null => {
if (!userId) return null;
const membership = teamData.team_memberships.find((tm) => tm.user_id === userId);
const maxBudget = membership?.litellm_budget_table?.max_budget;
if (maxBudget === null || maxBudget === undefined) {
return null;
}
return formatNumber(maxBudget);
return membership?.litellm_budget_table?.max_budget ?? null;
};
// Helper function to get rate limits for a user
@ -98,7 +94,7 @@ export default function TeamMemberTab({
const getUserBudgetReset = (userId: string | null): string | null => {
if (!userId) return null;
const membership = teamData.team_memberships.find((tm) => tm.user_id === userId);
return formatBudgetReset(membership?.litellm_budget_table?.budget_reset_at);
return membership?.litellm_budget_table?.budget_reset_at ?? null;
};
const extraColumns: ColumnsType<Member> = [
@ -146,7 +142,7 @@ export default function TeamMemberTab({
),
key: "spend",
render: (_: unknown, record: Member) => (
<Typography.Text>${formatNumberWithCommas(getUserCurrentCycleSpend(record.user_id), 4)}</Typography.Text>
<MoneyCell value={getUserCurrentCycleSpend(record.user_id)} decimals={4} />
),
},
{
@ -159,31 +155,19 @@ export default function TeamMemberTab({
</Space>
),
key: "total_spend",
render: (_: unknown, record: Member) => (
<Typography.Text>${formatNumberWithCommas(getUserTotalSpend(record.user_id), 4)}</Typography.Text>
),
render: (_: unknown, record: Member) => <MoneyCell value={getUserTotalSpend(record.user_id)} decimals={4} />,
},
{
title: "Team Member Budget (USD)",
key: "budget",
render: (_: unknown, record: Member) => {
const budget = getUserBudget(record.user_id);
return (
<Typography.Text>{budget ? `$${formatNumberWithCommas(Number(budget), 4)}` : "No Limit"}</Typography.Text>
);
},
render: (_: unknown, record: Member) => (
<MoneyCell value={getUserBudget(record.user_id)} decimals={4} emptyText="Unlimited" showZero />
),
},
{
title: "Budget Reset",
key: "budget_reset",
render: (_: unknown, record: Member) => {
const reset = getUserBudgetReset(record.user_id);
return reset ? (
<Typography.Text>{reset}</Typography.Text>
) : (
<Typography.Text type="secondary">—</Typography.Text>
);
},
render: (_: unknown, record: Member) => <DateCell value={getUserBudgetReset(record.user_id)} precision="date" />,
},
{
title: (

View file

@ -2,7 +2,7 @@
"use client";
import { useKeys } from "@/app/(dashboard)/hooks/keys/useKeys";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { DateCell, IdCell, MoneyCell } from "@/components/shared/table_cells";
import { ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, SwitchVerticalIcon } from "@heroicons/react/outline";
import {
ColumnDef,
@ -12,18 +12,7 @@ import {
SortingState,
useReactTable,
} from "@tanstack/react-table";
import {
Badge,
Button,
Icon,
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
Text,
} from "@tremor/react";
import { Badge, Icon, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Text } from "@tremor/react";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Popover, Skeleton, Tooltip, Typography } from "antd";
import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag";
@ -214,23 +203,9 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
header: "Key ID",
size: 100,
enableSorting: true,
cell: (info) => {
const value = info.getValue() as string;
const width = info.cell.column.getSize();
return (
<Tooltip title={value}>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate block"
style={{ maxWidth: width, overflow: "hidden" }}
onClick={() => setSelectedKey(info.row.original)}
>
{value ?? "-"}
</Button>
</Tooltip>
);
},
cell: (info) => (
<IdCell value={info.getValue() as string | null} onClick={() => setSelectedKey(info.row.original)} />
),
},
{
id: "key_alias",
@ -310,10 +285,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
header: "Created At",
size: 120,
enableSorting: true,
cell: (info) => {
const value = info.getValue();
return value ? new Date(value as string).toLocaleDateString() : "-";
},
cell: (info) => <DateCell value={info.getValue() as string | null} precision="date" />,
},
{
id: "created_by",
@ -380,10 +352,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
header: "Updated At",
size: 120,
enableSorting: true,
cell: (info) => {
const value = info.getValue();
return value ? new Date(value as string).toLocaleDateString() : "Never";
},
cell: (info) => <DateCell value={info.getValue() as string | null} precision="date" fallback="Never" />,
},
{
id: "last_active",
@ -401,16 +370,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
),
size: 130,
enableSorting: false,
cell: (info) => {
const value = info.getValue();
if (!value) return "Unknown";
const date = new Date(value as string);
return (
<Tooltip title={date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "long" })}>
<span>{date.toLocaleDateString()}</span>
</Tooltip>
);
},
cell: (info) => <DateCell value={info.getValue() as string | null} precision="date" fallback="Unknown" />,
},
{
id: "expires",
@ -418,10 +378,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
header: "Expires",
size: 120,
enableSorting: false,
cell: (info) => {
const value = info.getValue();
return value ? new Date(value as string).toLocaleDateString() : "Never";
},
cell: (info) => <DateCell value={info.getValue() as string | null} precision="date" fallback="Never" />,
},
{
id: "spend",
@ -429,7 +386,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
header: "Spend (USD)",
size: 100,
enableSorting: true,
cell: (info) => formatNumberWithCommas(info.getValue() as number, 4),
cell: (info) => <MoneyCell value={info.getValue() as number | null} decimals={4} />,
},
{
id: "max_budget",
@ -437,11 +394,9 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
header: "Budget (USD)",
size: 110,
enableSorting: true,
cell: (info) => {
const maxBudget = info.getValue() as number | null;
if (maxBudget === null) return "Unlimited";
return `$${formatNumberWithCommas(maxBudget)}`;
},
cell: (info) => (
<MoneyCell value={info.getValue() as number | null} decimals={0} emptyText="Unlimited" showZero />
),
},
{
id: "budget_reset_at",
@ -449,10 +404,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
header: "Budget Reset",
size: 130,
enableSorting: false,
cell: (info) => {
const value = info.getValue();
return value ? new Date(value as string).toLocaleString() : "Never";
},
cell: (info) => <DateCell value={info.getValue() as string | null} fallback="Never" />,
},
{
id: "models",

View file

@ -21,14 +21,18 @@ const badgeVariants = cva({
},
});
function Badge({
className,
variant = "default",
...props
}: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
return (
<span data-slot="badge" data-variant={variant} className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
const Badge = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span"> & VariantProps<typeof badgeVariants>
>(({ className, variant = "default", ...props }, ref) => (
<span
ref={ref}
data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
));
Badge.displayName = "Badge";
export { Badge, badgeVariants };

View file

@ -50,6 +50,7 @@ import {
getProxyUISettings,
} from "./networking";
import TopKeyView from "./UsagePage/components/EntityUsage/TopKeyView";
import { MoneyCell } from "@/components/shared/table_cells";
import { formatNumberWithCommas } from "@/utils/dataUtils";
interface UsagePageProps {
@ -644,9 +645,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ accessToken, token, userRole, use
<TableRow key={provider.provider}>
<TableCell>{provider.provider}</TableCell>
<TableCell>
{parseFloat(provider.spend.toFixed(2)) < 0.00001
? "less than 0.00"
: formatNumberWithCommas(provider.spend, 2)}
<MoneyCell value={provider.spend} decimals={2} />
</TableCell>
</TableRow>
))}
@ -819,7 +818,9 @@ const UsagePage: React.FC<UsagePageProps> = ({ accessToken, token, userRole, use
{topUsers?.map((user: any, index: number) => (
<TableRow key={index}>
<TableCell>{user.end_user}</TableCell>
<TableCell>{formatNumberWithCommas(user.total_spend, 2)}</TableCell>
<TableCell>
<MoneyCell value={user.total_spend} decimals={2} />
</TableCell>
<TableCell>{user.total_count}</TableCell>
</TableRow>
))}

View file

@ -1,7 +1,8 @@
import React from "react";
import { Table, Badge, Tooltip } from "antd";
import { Table, Tooltip } from "antd";
import MessageManager from "@/components/molecules/message_manager";
import { EyeOutlined, CopyOutlined, DeleteOutlined } from "@ant-design/icons";
import { StatusBadge, type StatusTone } from "@/components/shared/table_cells";
import { DocumentUpload } from "./types";
interface DocumentsTableProps {
@ -16,15 +17,15 @@ const DocumentsTable: React.FC<DocumentsTableProps> = ({ documents, onRemove })
};
const getStatusBadge = (status: DocumentUpload["status"]) => {
const statusConfig = {
uploading: { color: "blue", text: "Uploading" },
done: { color: "green", text: "Ready" },
error: { color: "red", text: "Error" },
removed: { color: "default", text: "Removed" },
const statusConfig: Record<DocumentUpload["status"], { tone: StatusTone; label: string }> = {
uploading: { tone: "info", label: "Uploading" },
done: { tone: "success", label: "Ready" },
error: { tone: "error", label: "Error" },
removed: { tone: "neutral", label: "Removed" },
};
const config = statusConfig[status];
return <Badge color={config.color} text={config.text} />;
const config: { tone: StatusTone; label: string } = statusConfig[status] ?? { tone: "neutral", label: status };
return <StatusBadge tone={config.tone} label={config.label} />;
};
const formatFileSize = (bytes?: number) => {

View file

@ -154,9 +154,8 @@ describe("VectorStoreTable", () => {
it("should truncate long vector store IDs", () => {
renderComponent();
// Check that the truncated text is rendered (first 15 chars + ...)
const truncatedText = "very-long-vecto...";
expect(screen.getByText(truncatedText)).toBeInTheDocument();
const idButton = screen.getByText("very-long-vector-store-id-that-should-be-truncated");
expect(idButton).toHaveClass("truncate", "max-w-[15ch]");
});
it("should make vector store ID clickable", async () => {
@ -245,13 +244,13 @@ describe("VectorStoreTable", () => {
describe("Date Columns", () => {
it("should render created at dates", () => {
renderComponent();
const dateElements = screen.getAllByText(/1\/\d+\/2024/);
const dateElements = screen.getAllByText(/Jan \d+, 2024/);
expect(dateElements.length).toBe(6); // 3 created_at + 3 updated_at dates
});
it("should render updated at dates", () => {
renderComponent();
const dateElements = screen.getAllByText(/1\/\d+\/2024/);
const dateElements = screen.getAllByText(/Jan \d+, 2024/);
expect(dateElements.length).toBe(6); // 3 created_at + 3 updated_at dates
});
});

View file

@ -10,6 +10,7 @@ import {
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "@tremor/react";
import { Tooltip } from "antd";
import React from "react";
import { DateCell, IdCell } from "@/components/shared/table_cells";
import TableIconActionButton from "../common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
import { getProviderLogoAndName } from "../provider_info_helpers";
import { VectorStore } from "./types";
@ -28,19 +29,7 @@ const VectorStoreTable: React.FC<VectorStoreTableProps> = ({ data, onView, onEdi
{
header: "Vector Store ID",
accessorKey: "vector_store_id",
cell: ({ row }) => {
const vectorStore = row.original;
return (
<button
onClick={() => onView(vectorStore.vector_store_id)}
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left w-full truncate whitespace-nowrap cursor-pointer max-w-[15ch]"
>
{vectorStore.vector_store_id.length > 15
? `${vectorStore.vector_store_id.slice(0, 15)}...`
: vectorStore.vector_store_id}
</button>
);
},
cell: ({ row }) => <IdCell value={row.original.vector_store_id} onClick={onView} />,
},
{
header: "Name",
@ -109,19 +98,13 @@ const VectorStoreTable: React.FC<VectorStoreTableProps> = ({ data, onView, onEdi
header: "Created At",
accessorKey: "created_at",
sortingFn: "datetime",
cell: ({ row }) => {
const vectorStore = row.original;
return <span className="text-xs">{new Date(vectorStore.created_at).toLocaleDateString()}</span>;
},
cell: ({ row }) => <DateCell value={row.original.created_at} precision="date" />,
},
{
header: "Updated At",
accessorKey: "updated_at",
sortingFn: "datetime",
cell: ({ row }) => {
const vectorStore = row.original;
return <span className="text-xs">{new Date(vectorStore.updated_at).toLocaleDateString()}</span>;
},
cell: ({ row }) => <DateCell value={row.original.updated_at} precision="date" />,
},
{
id: "actions",

View file

@ -4,7 +4,7 @@ import { Table, Tag, Input, Select, Button, Pagination, Spin } from "antd";
import { ReloadOutlined, LoadingOutlined } from "@ant-design/icons";
import type { ColumnsType } from "antd/es/table";
import { resolveLogoSrc } from "@/lib/assetPaths";
import moment from "moment";
import { DateCell, IdCell } from "@/components/shared/table_cells";
import { uiAuditLogsCall } from "../networking";
import { AuditLogEntry } from "./columns";
import { AuditLogDrawer } from "./AuditLogDrawer/AuditLogDrawer";
@ -95,11 +95,7 @@ export default function AuditLogs({ userID, userRole, token, accessToken, isActi
dataIndex: "updated_at",
key: "updated_at",
width: 200,
render: (val: string) => (
<span className="font-mono text-xs whitespace-nowrap">
{moment.utc(val).local().format("MMM D, YYYY HH:mm:ss")}
</span>
),
render: (val: string) => <DateCell value={val} />,
},
{
title: "Action",
@ -123,7 +119,7 @@ export default function AuditLogs({ userID, userRole, token, accessToken, isActi
title: "Object ID",
dataIndex: "object_id",
key: "object_id",
render: (val: string) => <span className="font-mono text-xs">{val}</span>,
render: (val: string) => <IdCell value={val} variant="plain" truncate={false} />,
},
{
title: "Changed By",
@ -137,7 +133,7 @@ export default function AuditLogs({ userID, userRole, token, accessToken, isActi
dataIndex: "changed_by_api_key",
key: "changed_by_api_key",
width: 140,
render: (val: string) => (val ? <span className="font-mono text-xs">{val.slice(0, 12)}…</span> : "—"),
render: (val: string) => <IdCell value={val} variant="plain" />,
},
];

View file

@ -0,0 +1,56 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { createColumns, type LogEntry } from "./columns";
import { DataTable } from "./table";
const logEntry = (overrides: Partial<LogEntry>): LogEntry => ({
request_id: "req-1",
api_key: "key-1",
team_id: "team-1",
model: "gpt-4o",
model_id: "model-1",
call_type: "acompletion",
spend: 0,
total_tokens: 10,
prompt_tokens: 5,
completion_tokens: 5,
startTime: "2026-07-07T09:50:13Z",
endTime: "2026-07-07T09:50:14Z",
cache_hit: "false",
messages: [],
response: {},
...overrides,
});
describe("Cost column", () => {
it("renders '-' for zero spend with no tooltip, so hovering never shows a contradictory $0", async () => {
const user = userEvent.setup();
render(
<DataTable
data={[logEntry({ request_id: "req-zero", spend: 0 })]}
columns={createColumns()}
getRowId={(r) => r.request_id}
/>,
);
for (const dash of screen.getAllByText("-")) {
await user.hover(dash);
}
expect(screen.queryByText("$0")).not.toBeInTheDocument();
});
it("shows the full-precision raw value in the tooltip for a real spend", async () => {
const user = userEvent.setup();
render(
<DataTable
data={[logEntry({ request_id: "req-spend", spend: 0.00012345678 })]}
columns={createColumns()}
getRowId={(r) => r.request_id}
/>,
);
const formatted = screen.getByText("$0.000123");
await user.hover(formatted);
expect(await screen.findByText("$0.00012345678")).toBeInTheDocument();
});
});

View file

@ -1,11 +1,10 @@
import { DateCell, IdCell, MoneyCell, StatusBadge } from "@/components/shared/table_cells";
import { getSpendString } from "@/utils/dataUtils";
import type { ColumnDef } from "@tanstack/react-table";
import { Badge, Button } from "@tremor/react";
import { Tooltip } from "antd";
import React, { useState } from "react";
import React from "react";
import { getProviderLogoAndName } from "../provider_info_helpers";
import { TableHeaderSortDropdown } from "../common_components/TableHeaderSortDropdown/TableHeaderSortDropdown";
import { TimeCell } from "./time_cell";
import { AGENT_CALL_TYPES, MCP_CALL_TYPES } from "./constants";
import { AgentBadge, AgentIcon, LlmBadge, McpBadge, SparkleIcon, WrenchIcon } from "./TypeBadges";
@ -120,7 +119,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
: "Time",
accessorKey: "startTime",
size: 200,
cell: (info: any) => <TimeCell utcTime={info.getValue()} />,
cell: (info: any) => <DateCell value={info.getValue()} />,
},
{
header: "Type",
@ -174,48 +173,20 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
cell: (info: any) => {
const status = info.getValue() || "Success";
const isSuccess = status.toLowerCase() !== "failure";
return (
<span
className={`px-2 py-1 rounded-md text-xs font-medium inline-block text-center w-16 ${
isSuccess ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"
}`}
>
{isSuccess ? "Success" : "Failure"}
</span>
);
return <StatusBadge tone={isSuccess ? "success" : "error"} label={isSuccess ? "Success" : "Failure"} />;
},
},
{
header: "Session ID",
accessorKey: "session_id",
size: 120,
cell: (info: any) => {
const value = String(info.getValue() || "");
const onSessionClick = info.row.original.onSessionClick;
return (
<Tooltip title={String(info.getValue() || "")}>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal text-xs max-w-[15ch] truncate block"
onClick={() => onSessionClick?.(value)}
>
{String(info.getValue() || "")}
</Button>
</Tooltip>
);
},
cell: (info: any) => <IdCell value={info.getValue()} onClick={info.row.original.onSessionClick} />,
},
{
header: "Request ID",
accessorKey: "request_id",
cell: (info: any) => (
<Tooltip title={String(info.getValue() || "")}>
<span className="font-mono text-xs max-w-[15ch] truncate block">{String(info.getValue() || "")}</span>
</Tooltip>
),
cell: (info: any) => <IdCell value={info.getValue()} variant="plain" />,
},
{
header: sortProps
@ -236,11 +207,14 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
const row = info.row.original;
const mcpCount = row.mcp_tool_call_count || 0;
const mcpSpend = row.mcp_tool_call_spend || 0;
const spend = info.getValue();
return (
<div className="flex flex-col items-end">
<Tooltip title={`$${String(info.getValue() || 0)}`}>
<span>{getSpendString(info.getValue() || 0)}</span>
<Tooltip title={spend ? `$${String(spend)}` : undefined}>
<span>
<MoneyCell value={spend} decimals={6} />
</span>
</Tooltip>
{mcpCount > 0 && mcpSpend > 0 && (
<span className="text-[10px] text-amber-600">
@ -320,21 +294,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
header: "Key Hash",
accessorKey: "metadata.user_api_key",
size: 110,
cell: (info: any) => {
const value = String(info.getValue() || "-");
const onKeyHashClick = info.row.original.onKeyHashClick;
return (
<Tooltip title={value}>
<span
className="font-mono max-w-[15ch] truncate block cursor-pointer hover:text-blue-600"
onClick={() => onKeyHashClick?.(value)}
>
{value}
</span>
</Tooltip>
);
},
cell: (info: any) => <IdCell value={info.getValue()} variant="plain" onClick={info.row.original.onKeyHashClick} />,
},
{
header: "Key Alias",
@ -589,141 +549,3 @@ export type AuditLogEntry = {
before_value: Record<string, any>;
updated_values: Record<string, any>;
};
const getActionBadge = (action: string) => {
return (
<Badge color="gray" className="flex items-center gap-1">
<span className="whitespace-nowrap text-xs">{action}</span>
</Badge>
);
};
export const auditLogColumns: ColumnDef<AuditLogEntry>[] = [
{
id: "expander",
header: () => null,
cell: ({ row }) => {
const ExpanderCell = () => {
const [localExpanded, setLocalExpanded] = React.useState(row.getIsExpanded());
const toggleHandler = React.useCallback(() => {
setLocalExpanded((prev) => !prev);
row.getToggleExpandedHandler()();
}, [row]);
return row.getCanExpand() ? (
<button
onClick={toggleHandler}
style={{ cursor: "pointer" }}
aria-label={localExpanded ? "Collapse row" : "Expand row"}
className="w-6 h-6 flex items-center justify-center focus:outline-hidden"
>
<svg
className={`w-4 h-4 transform transition-transform ${localExpanded ? "rotate-90" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
) : (
<span className="w-6 h-6 flex items-center justify-center">●</span>
);
};
return <ExpanderCell />;
},
},
{
header: "Timestamp",
accessorKey: "updated_at",
cell: (info: any) => <TimeCell utcTime={info.getValue()} />,
},
{
header: "Table Name",
accessorKey: "table_name",
cell: (info: any) => {
const tableName = info.getValue();
let displayValue = tableName;
switch (tableName) {
case "LiteLLM_VerificationToken":
displayValue = "Keys";
break;
case "LiteLLM_TeamTable":
displayValue = "Teams";
break;
case "LiteLLM_OrganizationTable":
displayValue = "Organizations";
break;
case "LiteLLM_UserTable":
displayValue = "Users";
break;
case "LiteLLM_ProxyModelTable":
displayValue = "Models";
break;
default:
displayValue = tableName;
}
return <span>{displayValue}</span>;
},
},
{
header: "Action",
accessorKey: "action",
cell: (info: any) => <span>{getActionBadge(info.getValue())}</span>,
},
{
header: "Changed By",
accessorKey: "changed_by",
cell: (info: any) => {
const changedBy = info.row.original.changed_by;
const apiKey = info.row.original.changed_by_api_key;
return (
<div className="space-y-1">
<div className="font-medium">{changedBy}</div>
{apiKey && ( // Only show API key if it exists
<Tooltip title={apiKey}>
<div className="text-xs text-muted-foreground max-w-[15ch] truncate">
{" "}
{/* Apply max-width and truncate */}
{apiKey}
</div>
</Tooltip>
)}
</div>
);
},
},
{
header: "Affected Item ID",
accessorKey: "object_id",
cell: (props) => {
const ObjectIdDisplay = () => {
const objectId = props.getValue();
const [copied, setCopied] = useState(false);
if (!objectId) return <>-</>;
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(String(objectId));
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch (err) {
console.error("Failed to copy object ID: ", err);
}
};
return (
<Tooltip title={copied ? "Copied!" : String(objectId)}>
<span className="max-w-[20ch] truncate block cursor-pointer hover:text-blue-600" onClick={handleCopy}>
{String(objectId)}
</span>
</Tooltip>
);
};
return <ObjectIdDisplay />;
},
},
];

View file

@ -1,36 +0,0 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { TimeCell, getTimeZone } from "./time_cell";
describe("TimeCell", () => {
it("should render a formatted time string", () => {
render(<TimeCell utcTime="2025-06-15T14:30:00Z" />);
// The global toLocaleString mock in setupTests returns "YYYY-MM-DD HH:MM:SS"
expect(screen.getByText(/2025/)).toBeInTheDocument();
});
it("should render 'Error converting time' for invalid dates", () => {
// toLocaleString on an Invalid Date returns "Invalid Date", not throwing,
// but the component catches exceptions. Force an error by passing something
// that causes Date constructor to produce NaN.
render(<TimeCell utcTime="not-a-date" />);
// The mock returns "NaN-NaN-NaN NaN:NaN:NaN" for invalid dates
// The component has a try/catch that returns "Error converting time" on exception
const el = screen.getByText(/NaN|Error/);
expect(el).toBeInTheDocument();
});
it("should render with monospace font", () => {
render(<TimeCell utcTime="2025-06-15T14:30:00Z" />);
const span = screen.getByText(/2025/);
expect(span).toHaveStyle({ fontFamily: "monospace" });
});
});
describe("getTimeZone", () => {
it("should return a non-empty timezone string", () => {
const tz = getTimeZone();
expect(typeof tz).toBe("string");
expect(tz.length).toBeGreaterThan(0);
});
});

View file

@ -1,42 +0,0 @@
import * as React from "react";
interface TimeCellProps {
utcTime: string;
}
const getLocalTime = (utcTime: string): string => {
try {
const date = new Date(utcTime);
return date
.toLocaleString("en-US", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: true,
})
.replace(",", "");
} catch (e) {
return "Error converting time";
}
};
export const TimeCell: React.FC<TimeCellProps> = ({ utcTime }) => {
return (
<span
style={{
fontFamily: "monospace",
width: "180px",
display: "inline-block",
}}
>
{getLocalTime(utcTime)}
</span>
);
};
export const getTimeZone = (): string => {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
};