diff --git a/ui/litellm-dashboard/eslint-metrics.json b/ui/litellm-dashboard/eslint-metrics.json index f4dc89c5b80..ded6ab97e1e 100644 --- a/ui/litellm-dashboard/eslint-metrics.json +++ b/ui/litellm-dashboard/eslint-metrics.json @@ -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 diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index b077338c75b..fe8f182c106 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -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 diff --git a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/components/AccessGroupsPage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/components/AccessGroupsPage.tsx index c3dd1d54b32..dbbf4e35900 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/components/AccessGroupsPage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/components/AccessGroupsPage.tsx @@ -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: () => ID, enableSorting: false, size: 170, - cell: ({ row }) => { - const record = row.original; - return ( - - setSelectedGroupId(record.id)} - > - {record.id} - - - ); - }, + cell: ({ row }) => , }, { id: "name", @@ -211,7 +198,7 @@ export function AccessGroupsPage() { header: () => Created, enableSorting: true, sortingFn: "datetime", - cell: ({ getValue }) => new Date(getValue() as string).toLocaleDateString(), + cell: ({ getValue }) => , meta: { responsive: ["lg"] }, }, { @@ -219,7 +206,7 @@ export function AccessGroupsPage() { accessorKey: "updatedAt", header: () => Updated, enableSorting: false, - cell: ({ getValue }) => new Date(getValue() as string).toLocaleDateString(), + cell: ({ getValue }) => , meta: { responsive: ["xl"] }, }, ...(canModify diff --git a/ui/litellm-dashboard/src/app/(dashboard)/budgets/components/budget_panel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/budgets/components/budget_panel.tsx index 0e601645c20..af15a99f0b4 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/budgets/components/budget_panel.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/budgets/components/budget_panel.tsx @@ -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 = ({ accessToken }) => { .map((value: budgetItem) => ( {value.budget_id} - {value.max_budget ? value.max_budget : "n/a"} + + + {value.tpm_limit ? value.tpm_limit : "n/a"} {value.rpm_limit ? value.rpm_limit : "n/a"} {canModify && ( diff --git a/ui/litellm-dashboard/src/app/(dashboard)/memory/components/MemoryView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/memory/components/MemoryView.tsx index 402de29e0c5..4ee784f4664 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/memory/components/MemoryView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/memory/components/MemoryView.tsx @@ -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 = ({ accessToken }) => { } }; - const renderIdPill = (id: string | null | undefined, onClick?: () => void) => { - if (!id) return -; - 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 ( - - {onClick ? ( - - ) : ( - {short} - )} - - ); - }; - const columns: ColumnsType = [ { title: "ID", dataIndex: "memory_id", key: "memory_id", width: 140, - render: (_: unknown, r: MemoryRow) => renderIdPill(r.memory_id, () => setDetailRow(r)), + render: (_: unknown, r: MemoryRow) => setDetailRow(r)} />, }, { title: "Name", @@ -246,21 +226,21 @@ export const MemoryView: React.FC = ({ accessToken }) => { dataIndex: "user_id", key: "user_id", width: 160, - render: (uid?: string | null) => renderIdPill(uid), + render: (uid?: string | null) => , }, { title: "Team ID", dataIndex: "team_id", key: "team_id", width: 160, - render: (tid?: string | null) => renderIdPill(tid), + render: (tid?: string | null) => , }, { title: "Updated", dataIndex: "updated_at", key: "updated_at", width: 180, - render: (ts?: string) => {formatTimestamp(ts)}, + render: (ts?: string) => , // 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. diff --git a/ui/litellm-dashboard/src/app/(dashboard)/projects/components/ProjectKeysTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/projects/components/ProjectKeysTable.tsx index 7b891078d36..8269c843b98 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/projects/components/ProjectKeysTable.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/projects/components/ProjectKeysTable.tsx @@ -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 = [ title: "Created", dataIndex: "created_at", key: "created_at", - render: (date: string) => (date ? new Date(date).toLocaleDateString() : "—"), + render: (date: string) => , }, { title: "Last Active", dataIndex: "last_active", key: "last_active", - render: (date: string | null) => (date ? new Date(date).toLocaleDateString() : "Never"), + render: (date: string | null) => , }, ]; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/projects/components/ProjectsPage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/projects/components/ProjectsPage.tsx index 8a3fc178914..be989229022 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/projects/components/ProjectsPage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/projects/components/ProjectsPage.tsx @@ -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) => ( - - setSelectedProjectId(id)} - > - {id} - - - ), + render: (id: string) => , }, { 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) => , }, { title: "Updated", dataIndex: "updated_at", key: "updated_at", responsive: ["xl"], - render: (date: string) => new Date(date).toLocaleDateString(), + render: (date: string) => , }, ]; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/components/prompt_table.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/components/prompt_table.tsx index 94b242ca4ea..51a03d19e83 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/components/prompt_table.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/components/prompt_table.tsx @@ -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 = ({ 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[] = [ { 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 ( -
- - - - - { - e.stopPropagation(); - copyToClipboard(fullId); - }} - className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs" - /> - -
- ); - }, + cell: (info: any) => , }, { header: "Model", @@ -162,26 +125,12 @@ const PromptTable: React.FC = ({ { header: "Created At", accessorKey: "created_at", - cell: ({ row }) => { - const prompt = row.original; - return ( - - {formatDate(prompt.created_at)} - - ); - }, + cell: ({ row }) => , }, { header: "Updated At", accessorKey: "updated_at", - cell: ({ row }) => { - const prompt = row.original; - return ( - - {formatDate(prompt.updated_at)} - - ); - }, + cell: ({ row }) => , }, { header: "Environment", diff --git a/ui/litellm-dashboard/src/app/(dashboard)/search-tools/_components/SearchToolColumn.tsx b/ui/litellm-dashboard/src/app/(dashboard)/search-tools/_components/SearchToolColumn.tsx index 198b3ea095f..3d9fdb2866e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/search-tools/_components/SearchToolColumn.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/search-tools/_components/SearchToolColumn.tsx @@ -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 -; } - return ( - - ); + return ; }, }, { @@ -52,7 +46,7 @@ export const searchToolColumns = ( dataIndex: "created_at", key: "created_at", render: (_, tool) => { - return {tool.created_at ? new Date(tool.created_at).toLocaleDateString() : "-"}; + return ; }, }, { @@ -60,7 +54,7 @@ export const searchToolColumns = ( dataIndex: "updated_at", key: "updated_at", render: (_, tool) => { - return {tool.updated_at ? new Date(tool.updated_at).toLocaleDateString() : "-"}; + return ; }, }, { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/BulkEditUsers.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/BulkEditUsers.test.tsx index f16f5325952..e49ac854e6d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/BulkEditUsers.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/BulkEditUsers.test.tsx @@ -91,7 +91,7 @@ describe("BulkEditUserModal", () => { it("should display budget information in table", () => { renderWithProviders(); - expect(screen.getByText("$50")).toBeInTheDocument(); + expect(screen.getByText("$50.00")).toBeInTheDocument(); expect(screen.getByText("Unlimited")).toBeInTheDocument(); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/BulkEditUsers.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/BulkEditUsers.tsx index 9bef2f3a937..7ea53352807 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/BulkEditUsers.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/BulkEditUsers.tsx @@ -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 = ({ key: "max_budget", width: "20%", render: (budget: number | null) => ( - {budget !== null ? `$${budget}` : "Unlimited"} + ), }, ]} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.test.tsx index 241cda3464e..996fd58efc1 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.test.tsx @@ -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); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users/columns.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users/columns.tsx index deeaeeb25f4..fc680cb5b1b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users/columns.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users/columns.tsx @@ -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 }) => ( -
- - {row.original.user_id ? `${row.original.user_id.slice(0, 7)}...` : "-"} - - {row.original.user_id && ( - - { - e.stopPropagation(); - copyToClipboard(row.original.user_id, "User ID copied to clipboard"); - }} - className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs" - /> - - )} -
- ), + cell: ({ row }) => , }, { header: "Email", @@ -93,17 +75,13 @@ export const columns = ( header: "Spend (USD)", accessorKey: "spend", enableSorting: true, - cell: ({ row }) => ( - {row.original.spend ? formatNumberWithCommas(row.original.spend, 4) : "-"} - ), + cell: ({ row }) => , }, { header: "Budget (USD)", accessorKey: "max_budget", enableSorting: false, - cell: ({ row }) => ( - {row.original.max_budget !== null ? row.original.max_budget : "Unlimited"} - ), + cell: ({ row }) => , }, { header: () => ( @@ -142,21 +120,13 @@ export const columns = ( header: "Created At", accessorKey: "created_at", enableSorting: true, - cell: ({ row }) => ( - - {row.original.created_at ? new Date(row.original.created_at).toLocaleDateString() : "-"} - - ), + cell: ({ row }) => , }, { header: "Updated At", accessorKey: "updated_at", enableSorting: false, - cell: ({ row }) => ( - - {row.original.updated_at ? new Date(row.original.updated_at).toLocaleDateString() : "-"} - - ), + cell: ({ row }) => , }, { id: "actions", diff --git a/ui/litellm-dashboard/src/components/AIHub/AgentHubTableColumns.tsx b/ui/litellm-dashboard/src/components/AIHub/AgentHubTableColumns.tsx index 09b1c147615..ae1a19ff95d 100644 --- a/ui/litellm-dashboard/src/components/AIHub/AgentHubTableColumns.tsx +++ b/ui/litellm-dashboard/src/components/AIHub/AgentHubTableColumns.tsx @@ -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 ? ( - - Yes - - ) : ( - - No - - ); + return ; }, meta: { className: "hidden md:table-cell", diff --git a/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysTable/DeletedKeysTable.tsx b/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysTable/DeletedKeysTable.tsx index bc52bbbe062..d4a120d0589 100644 --- a/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysTable/DeletedKeysTable.tsx +++ b/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysTable/DeletedKeysTable.tsx @@ -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 ( - - {value || "-"} - - ); - }, + cell: (info) => , }, { id: "key_alias", @@ -100,9 +93,7 @@ export function DeletedKeysTable({ header: "Spend (USD)", size: 100, maxSize: 140, - cell: (info) => ( - {formatNumberWithCommas(info.getValue() as number, 4)} - ), + cell: (info) => , }, { 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 ( - - {maxBudget === null ? "Unlimited" : `$${formatNumberWithCommas(maxBudget)}`} - - ); - }, + cell: (info) => ( + + ), }, { 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 ( - - {userId || "-"} - - ); - }, + cell: (info) => , }, { id: "created_at", @@ -155,12 +134,7 @@ export function DeletedKeysTable({ header: "Created At", size: 120, maxSize: 140, - cell: (info) => { - const value = info.getValue(); - return ( - {value ? new Date(value as string).toLocaleDateString() : "-"} - ); - }, + cell: (info) => , }, { 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 {value ? new Date(value).toLocaleDateString() : "-"}; - }, + cell: (info) => ( + + ), }, { id: "deleted_by", diff --git a/ui/litellm-dashboard/src/components/DeletedTeamsPage/DeletedTeamsTable/DeletedTeamsTable.tsx b/ui/litellm-dashboard/src/components/DeletedTeamsPage/DeletedTeamsTable/DeletedTeamsTable.tsx index 57260f6bb2d..ddfd5cf73b6 100644 --- a/ui/litellm-dashboard/src/components/DeletedTeamsPage/DeletedTeamsTable/DeletedTeamsTable.tsx +++ b/ui/litellm-dashboard/src/components/DeletedTeamsPage/DeletedTeamsTable/DeletedTeamsTable.tsx @@ -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 ( - - {value || "-"} - - ); - }, + cell: (info) => , }, { 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 ( - {value ? new Date(value as string).toLocaleDateString() : "-"} - ); - }, + cell: (info) => , }, { 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 ( - {spend !== undefined ? formatNumberWithCommas(spend, 4) : "-"} - ); - }, + cell: (info) => , }, { 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 ( - - {maxBudget === null || maxBudget === undefined ? "No limit" : `$${formatNumberWithCommas(maxBudget)}`} - - ); - }, + cell: (info) => ( + + ), }, { 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 ( - - {value || "-"} - - ); - }, + cell: (info) => , }, { 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 {value ? new Date(value).toLocaleDateString() : "-"}; - }, + cell: (info) => , }, { id: "deleted_by", diff --git a/ui/litellm-dashboard/src/components/OldTeams.tsx b/ui/litellm-dashboard/src/components/OldTeams.tsx index c2cb3d4948e..e83d1acf4e5 100644 --- a/ui/litellm-dashboard/src/components/OldTeams.tsx +++ b/ui/litellm-dashboard/src/components/OldTeams.tsx @@ -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 = ({ accessToken, userID, userRole, premiumUser key: "team_id", width: 170, ellipsis: true, - render: (id: string, record: Team) => ( - - setSelectedTeamId(record.team_id)} - data-testid="team-id-cell" - > - {id} - - + render: (id: string) => ( + setSelectedTeamId(teamId)} dataTestId="team-id-cell" /> ), }, { @@ -797,13 +788,7 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser width: 130, ellipsis: true, sorter: true, - render: (date: string | undefined) => ( - - {date - ? new Date(date).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }) - : "—"} - - ), + render: (date: string | undefined) => , }, { title: "Actions", diff --git a/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable.tsx b/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable.tsx index 70ec6599ca2..4f1889cbc99 100644 --- a/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable.tsx +++ b/ui/litellm-dashboard/src/components/Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable.tsx @@ -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 = ({ // 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 ( - - {label} - - ); + const tone: StatusTone = mode === "success" ? "success" : mode === "failure" ? "error" : "info"; + return ; }, width: 240, }, diff --git a/ui/litellm-dashboard/src/components/ToolPolicies.tsx b/ui/litellm-dashboard/src/components/ToolPolicies.tsx index 4bd028f0c8f..4468334f813 100644 --- a/ui/litellm-dashboard/src/components/ToolPolicies.tsx +++ b/ui/litellm-dashboard/src/components/ToolPolicies.tsx @@ -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 = ({ accessToken, onSelec paginated.map((tool) => ( - + - - - ), + cell: (info: any) => handleKeyClick(info.row.original)} />, }, { header: "Key Alias", @@ -165,10 +153,7 @@ const TopKeyView: React.FC = ({ 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) => , }; const columns = showTags ? [...baseColumns, tagsColumn, spendColumn] : [...baseColumns, spendColumn]; diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopModelView.test.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopModelView.test.tsx index f6014d025ae..bbf9379f5e1 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopModelView.test.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopModelView.test.tsx @@ -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); }); diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopModelView.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopModelView.tsx index 7562ef06a03..8938767c02c 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopModelView.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopModelView.tsx @@ -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) => , }, { header: "Successful", diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.test.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.test.tsx index 61968294e18..322f00a501a 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.test.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.test.tsx @@ -158,7 +158,7 @@ describe("KeyModelUsageView", () => { }, ]; render(); - expect(screen.getByText("$0.00")).toBeInTheDocument(); + expect(screen.getByText("-")).toBeInTheDocument(); expect(screen.getAllByText("0").length).toBeGreaterThan(0); }); diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.tsx index ee1a49051da..ceb00e8a19f 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.tsx @@ -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 = [ title: "Spend (USD)", dataIndex: "spend", key: "spend", - render: (value) => `$${formatNumberWithCommas(value, 2)}`, + render: (value) => , }, { title: "Successful", diff --git a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx index ba616a03fd9..02f5d588149 100644 --- a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx +++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx @@ -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(); }); diff --git a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx index a803d78ed57..00f4304c8a9 100644 --- a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx @@ -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 ( - - - - ); - }, + cell: (info) => 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 ( - - Active - - ); + return ; } const isScimBlocked = (key.metadata as Record | 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 ( - - - Blocked - - + ); }, }, @@ -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) => , }, { 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) => , }, { 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 ( - - {date.toLocaleDateString()} - - ); - }, + cell: (info) => , }, { 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) => , }, { 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) => , }, { 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) => , }, { id: "models", diff --git a/ui/litellm-dashboard/src/components/agents.tsx b/ui/litellm-dashboard/src/components/agents.tsx index 0d8916942da..e3703f6c588 100644 --- a/ui/litellm-dashboard/src/components/agents.tsx +++ b/ui/litellm-dashboard/src/components/agents.tsx @@ -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 = ({ accessToken, userRole, teams {agent.agent_name} - - - + setSelectedAgentId(id)} /> - {formatNumberWithCommas(agent.spend, 4)} + @@ -213,13 +204,13 @@ const AgentsPanel: React.FC = ({ accessToken, userRole, teams - {agent.created_at ? new Date(agent.created_at).toLocaleDateString() : "N/A"} + {(agent.keys?.length ?? 0) > 0 ? ( - Active + ) : ( - Needs Setup + )} {isAdmin && ( diff --git a/ui/litellm-dashboard/src/components/claude_code_plugins/plugin_table.tsx b/ui/litellm-dashboard/src/components/claude_code_plugins/plugin_table.tsx index 1646161891f..0932e658899 100644 --- a/ui/litellm-dashboard/src/components/claude_code_plugins/plugin_table.tsx +++ b/ui/litellm-dashboard/src/components/claude_code_plugins/plugin_table.tsx @@ -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 = ({ }) => { const [sorting, setSorting] = useState([{ 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 = ({ accessorKey: "name", cell: ({ row }) => { const plugin = row.original; - const name = plugin.name || ""; return (
- - - + onPluginClick(plugin.id)} /> { @@ -122,24 +107,13 @@ const PluginTable: React.FC = ({ accessorKey: "enabled", cell: ({ row }) => { const plugin = row.original; - return ( - - {plugin.enabled ? "Yes" : "No"} - - ); + return ; }, }, { header: "Created At", accessorKey: "created_at", - cell: ({ row }) => { - const plugin = row.original; - return ( - - {formatDate(plugin.created_at)} - - ); - }, + cell: ({ row }) => , }, ...(isAdmin ? [ diff --git a/ui/litellm-dashboard/src/components/general_settings.tsx b/ui/litellm-dashboard/src/components/general_settings.tsx index 5b8dec39505..038547c6e0e 100644 --- a/ui/litellm-dashboard/src/components/general_settings.tsx +++ b/ui/litellm-dashboard/src/components/general_settings.tsx @@ -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 = ({ accessToken, user {value.stored_in_db == true ? ( - - In DB - + ) : value.stored_in_db == false ? ( - In Config + ) : ( - Not Set + )} diff --git a/ui/litellm-dashboard/src/components/guardrails/guardrail_table.tsx b/ui/litellm-dashboard/src/components/guardrails/guardrail_table.tsx index ecf6ce48fde..99f6b2793fd 100644 --- a/ui/litellm-dashboard/src/components/guardrails/guardrail_table.tsx +++ b/ui/litellm-dashboard/src/components/guardrails/guardrail_table.tsx @@ -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 = ({ const [editModalVisible, setEditModalVisible] = useState(false); const [selectedGuardrail, setSelectedGuardrail] = useState(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 = ({ { header: "Guardrail ID", accessorKey: "guardrail_id", - cell: (info: any) => ( - - - - ), + cell: (info: any) => , }, { header: "Name", @@ -126,41 +108,21 @@ const GuardrailTable: React.FC = ({ header: "Default On", accessorKey: "litellm_params.default_on", cell: ({ row }) => { - const guardrail = row.original; + const isDefaultOn = !!row.original.litellm_params?.default_on; return ( - - {guardrail.litellm_params?.default_on ? "Default On" : "Default Off"} - + ); }, }, { header: "Created At", accessorKey: "created_at", - cell: ({ row }) => { - const guardrail = row.original; - return ( - - {formatDate(guardrail.created_at)} - - ); - }, + cell: ({ row }) => , }, { header: "Updated At", accessorKey: "updated_at", - cell: ({ row }) => { - const guardrail = row.original; - return ( - - {formatDate(guardrail.updated_at)} - - ); - }, + cell: ({ row }) => , }, { id: "actions", diff --git a/ui/litellm-dashboard/src/components/mcp_hub_table_columns.tsx b/ui/litellm-dashboard/src/components/mcp_hub_table_columns.tsx index 7cf0d48a49f..1e25f87d262 100644 --- a/ui/litellm-dashboard/src/components/mcp_hub_table_columns.tsx +++ b/ui/litellm-dashboard/src/components/mcp_hub_table_columns.tsx @@ -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 = { - active: "green", - inactive: "red", - unknown: "gray", - healthy: "green", - unhealthy: "red", + const statusTones: Record = { + active: "success", + inactive: "error", + unknown: "neutral", + healthy: "success", + unhealthy: "error", }; - const color = statusColors[server.status] || "gray"; + const tone = statusTones[server.status] || "neutral"; - return ( - - {server.status || "unknown"} - - ); + return ; }, }, { diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx b/ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx index 546df9ebc4b..5e7e99ee8ec 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx @@ -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 }) => ( - - {row.original.toolset_id.slice(0, 8)}… - - ), + cell: ({ row }) => , }, { header: "Name", @@ -359,11 +356,7 @@ function toolsetColumns( { header: "Created", accessorKey: "created_at", - cell: ({ row }) => ( - - {row.original.created_at ? new Date(row.original.created_at).toLocaleDateString() : "—"} - - ), + cell: ({ row }) => , }, ...(isAdmin ? [ diff --git a/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.tsx b/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.tsx index a2194e23b6e..6497f2686cb 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.tsx +++ b/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.tsx @@ -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 = ({ onPageChange?.(page); }; - const getStatusBadge = (status: string) => { - switch (status) { - case "healthy": - return healthy; - case "unhealthy": - return unhealthy; - case "checking": - return checking; - case "none": - return none; - default: - return unknown; - } - }; - const showErrorModal = (modelName: string, cleanedError: string, fullError: string) => { setSelectedErrorDetails({ modelName, @@ -612,7 +597,6 @@ const HealthCheckComponent: React.FC = ({ handleModelSelection, handleSelectAll, runIndividualHealthCheck, - getStatusBadge, getDisplayModelName, showErrorModal, showSuccessModal, diff --git a/ui/litellm-dashboard/src/components/model_dashboard/health_check_columns.tsx b/ui/litellm-dashboard/src/components/model_dashboard/health_check_columns.tsx index 12cc984ef91..33c97236f97 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard/health_check_columns.tsx +++ b/ui/litellm-dashboard/src/components/model_dashboard/health_check_columns.tsx @@ -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 = { + healthy: "success", + unhealthy: "error", + checking: "info", + none: "neutral", +}; + +const healthStatusBadge = (status: string): JSX.Element => { + const tone = HEALTH_STATUS_TONES[status]; + return tone ? : ; +}; + 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()} /> - -
setSelectedModelId && setSelectedModelId(model.model_info.id)} - > - {model.model_info.id} -
-
+
); }, @@ -175,7 +180,7 @@ export const healthCheckColumns = ( return (
- {getStatusBadge(healthStatus.status)} + {healthStatusBadge(healthStatus.status)} {hasSuccessResponse && showSuccessModal && ( - +
e.stopPropagation()}> +
) : ( "-" @@ -370,15 +345,10 @@ export const columns = ( minSize: 80, cell: ({ row }) => { const model = row.original; - return ( -
- {model.model_info.db_model ? "DB Model" : "Config Model"} -
+ return model.model_info.db_model ? ( + + ) : ( + ); }, }, diff --git a/ui/litellm-dashboard/src/components/organization/organization_view.tsx b/ui/litellm-dashboard/src/components/organization/organization_view.tsx index e1b8c6d5044..402cc33902f 100644 --- a/ui/litellm-dashboard/src/components/organization/organization_view.tsx +++ b/ui/litellm-dashboard/src/components/organization/organization_view.tsx @@ -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 = ({ render: (_: unknown, record: Member) => { const orgMember = record.user_id != null ? (orgData.members || []).find((m) => m.user_id === record.user_id) : undefined; - return ${formatNumberWithCommas(orgMember?.spend ?? 0, 4)}; + return ; }, }, { diff --git a/ui/litellm-dashboard/src/components/organizations.tsx b/ui/litellm-dashboard/src/components/organizations.tsx index 857a0a0c64c..edebc17087a 100644 --- a/ui/litellm-dashboard/src/components/organizations.tsx +++ b/ui/litellm-dashboard/src/components/organizations.tsx @@ -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 = ({ .map((org: Organization) => ( -
- - - -
+
{org.organization_alias} - {org.created_at ? new Date(org.created_at).toLocaleDateString() : "N/A"} + - {formatNumberWithCommas(org.spend, 4)} - {org.litellm_budget_table?.max_budget !== null && - org.litellm_budget_table?.max_budget !== undefined - ? org.litellm_budget_table?.max_budget - : "No limit"} + + + + = ({ { header: "ID", accessorKey: "id", - cell: (info: any) => ( - -
info.row.original.id && setSelectedEndpointId(info.row.original.id)} - > - {info.row.original.id} -
-
- ), + cell: (info: any) => , }, { header: "Path", @@ -192,7 +184,9 @@ const PassThroughSettings: React.FC = ({
), accessorKey: "auth", - cell: (info: any) => {info.getValue() ? "Yes" : "No"}, + cell: (info: any) => ( + + ), }, { header: "Headers", diff --git a/ui/litellm-dashboard/src/components/policies/attachment_table.test.tsx b/ui/litellm-dashboard/src/components/policies/attachment_table.test.tsx index 099aa97a433..0cfd4e41e0a 100644 --- a/ui/litellm-dashboard/src/components/policies/attachment_table.test.tsx +++ b/ui/litellm-dashboard/src/components/policies/attachment_table.test.tsx @@ -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(); - 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", () => { diff --git a/ui/litellm-dashboard/src/components/policies/attachment_table.tsx b/ui/litellm-dashboard/src/components/policies/attachment_table.tsx index d9de8378a8a..fa482552fd5 100644 --- a/ui/litellm-dashboard/src/components/policies/attachment_table.tsx +++ b/ui/litellm-dashboard/src/components/policies/attachment_table.tsx @@ -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 = ({ }) => { const [sorting, setSorting] = useState([{ 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[] = [ { header: "Attachment ID", accessorKey: "attachment_id", - cell: (info: any) => ( - - - {info.getValue() ? `${String(info.getValue()).slice(0, 7)}...` : ""} - - - ), + cell: (info: any) => , }, { header: "Policy", @@ -183,14 +171,7 @@ const AttachmentTable: React.FC = ({ { header: "Created At", accessorKey: "created_at", - cell: ({ row }) => { - const attachment = row.original; - return ( - - {formatDate(attachment.created_at)} - - ); - }, + cell: ({ row }) => , }, { id: "actions", diff --git a/ui/litellm-dashboard/src/components/policies/policy_table.tsx b/ui/litellm-dashboard/src/components/policies/policy_table.tsx index eafec442474..1716f5aea84 100644 --- a/ui/litellm-dashboard/src/components/policies/policy_table.tsx +++ b/ui/litellm-dashboard/src/components/policies/policy_table.tsx @@ -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 = ({ const rows = useMemo(() => groupPoliciesByName(policies), [policies]); - const formatDate = (dateString?: string) => { - if (!dateString) return "-"; - const date = new Date(dateString); - return date.toLocaleString(); - }; - const columns: ColumnDef[] = [ { header: "Name", @@ -199,14 +194,7 @@ const PolicyTable: React.FC = ({ header: "Created At", id: "created_at", accessorFn: (row) => row.primaryPolicy.created_at ?? "", - cell: ({ row }) => { - const policy = row.original.primaryPolicy; - return ( - - {formatDate(policy.created_at)} - - ); - }, + cell: ({ row }) => , }, { id: "actions", diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/cell_tooltip.tsx b/ui/litellm-dashboard/src/components/shared/table_cells/cell_tooltip.tsx new file mode 100644 index 00000000000..c6e10590e8f --- /dev/null +++ b/ui/litellm-dashboard/src/components/shared/table_cells/cell_tooltip.tsx @@ -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 ( + + + + {content} + + + ); +} diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/date_cell.test.tsx b/ui/litellm-dashboard/src/components/shared/table_cells/date_cell.test.tsx new file mode 100644 index 00000000000..719047e0cfe --- /dev/null +++ b/ui/litellm-dashboard/src/components/shared/table_cells/date_cell.test.tsx @@ -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(); + expect(screen.getByText("Jul 7, 09:50:13")).toBeInTheDocument(); + }); + + it("renders date-only when precision is 'date'", () => { + render(); + expect(screen.getByText("Jul 7, 2026")).toBeInTheDocument(); + }); + + it("renders '-' for null and undefined", () => { + const { rerender } = render(); + expect(screen.getByText("-")).toBeInTheDocument(); + rerender(); + expect(screen.getByText("-")).toBeInTheDocument(); + }); + + it("renders the custom fallback for empty values", () => { + render(); + expect(screen.getByText("Never")).toBeInTheDocument(); + }); + + it("renders the fallback instead of 'Invalid Date' for unparseable input", () => { + render(); + expect(screen.getByText("Unknown")).toBeInTheDocument(); + expect(screen.queryByText(/Invalid/)).not.toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/date_cell.tsx b/ui/litellm-dashboard/src/components/shared/table_cells/date_cell.tsx new file mode 100644 index 00000000000..ee4c01bb237 --- /dev/null +++ b/ui/litellm-dashboard/src/components/shared/table_cells/date_cell.tsx @@ -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 {fallback}; + } + + return ( + {formatCellDate(date, precision)}} + /> + ); +} diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/id_cell.test.tsx b/ui/litellm-dashboard/src/components/shared/table_cells/id_cell.test.tsx new file mode 100644 index 00000000000..1a87f17d50b --- /dev/null +++ b/ui/litellm-dashboard/src/components/shared/table_cells/id_cell.test.tsx @@ -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()), + copyToClipboard: copyToClipboardMock, +})); + +describe("IdCell", () => { + it("renders '-' for empty values", () => { + render(); + expect(screen.getByText("-")).toBeInTheDocument(); + }); + + it("renders the custom fallback for empty values", () => { + render(); + expect(screen.getByText("—")).toBeInTheDocument(); + }); + + it("renders the full id as a non-interactive pill by default", () => { + render(); + 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(); + 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(); + 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(); + 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(); + 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( +
+ +
, + ); + 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(); + expect(screen.getByTestId("key-id-cell")).toHaveTextContent("k-1"); + }); +}); diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/id_cell.tsx b/ui/litellm-dashboard/src/components/shared/table_cells/id_cell.tsx new file mode 100644 index 00000000000..6fbd2e2f9ed --- /dev/null +++ b/ui/litellm-dashboard/src/components/shared/table_cells/id_cell.tsx @@ -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 = { + 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 {fallback}; + } + + 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 ? ( + + ) : ( + + {value} + + ); + + const withTooltip = ; + + if (!copyable) { + return withTooltip; + } + + return ( + + {withTooltip} + + + ); +} diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/index.ts b/ui/litellm-dashboard/src/components/shared/table_cells/index.ts new file mode 100644 index 00000000000..e189413d43d --- /dev/null +++ b/ui/litellm-dashboard/src/components/shared/table_cells/index.ts @@ -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"; diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/money_cell.test.tsx b/ui/litellm-dashboard/src/components/shared/table_cells/money_cell.test.tsx new file mode 100644 index 00000000000..473785e31c4 --- /dev/null +++ b/ui/litellm-dashboard/src/components/shared/table_cells/money_cell.test.tsx @@ -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(); + expect(screen.getByText("-")).toBeInTheDocument(); + rerender(); + expect(screen.getByText("-")).toBeInTheDocument(); + }); + + it("renders the custom emptyText for null budgets", () => { + render(); + expect(screen.getByText("Unlimited")).toBeInTheDocument(); + }); + + it("renders '-' for zero by default", () => { + render(); + expect(screen.getByText("-")).toBeInTheDocument(); + }); + + it("renders a formatted zero when showZero is set, never the emptyText", () => { + render(); + 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(); + expect(screen.getByText("$1,234.57")).toBeInTheDocument(); + }); + + it("defaults to 4 decimals", () => { + render(); + expect(screen.getByText("$42.0000")).toBeInTheDocument(); + }); + + it("renders the sub-threshold form for amounts that round to zero", () => { + render(); + expect(screen.getByText("< $0.000001")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/money_cell.tsx b/ui/litellm-dashboard/src/components/shared/table_cells/money_cell.tsx new file mode 100644 index 00000000000..9d3c747b20e --- /dev/null +++ b/ui/litellm-dashboard/src/components/shared/table_cells/money_cell.tsx @@ -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 {emptyText}; + } + if (value === 0) { + if (!showZero) { + return -; + } + return {`$${formatNumberWithCommas(0, decimals, false, true)}`}; + } + return {getSpendString(value, decimals)}; +} diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/status_badge.test.tsx b/ui/litellm-dashboard/src/components/shared/table_cells/status_badge.test.tsx new file mode 100644 index 00000000000..724a362c21f --- /dev/null +++ b/ui/litellm-dashboard/src/components/shared/table_cells/status_badge.test.tsx @@ -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 = { + 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(); + 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(); + const badge = screen.getByText("Active"); + expect(badge.dataset.variant).toBe("outline"); + expect(badge.querySelector("[aria-hidden]")).toBeNull(); + }); + + it("passes dataTestId through", () => { + render(); + 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(); + await user.hover(screen.getByText("Blocked")); + expect(await screen.findByText("This key was blocked by SCIM")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/status_badge.tsx b/ui/litellm-dashboard/src/components/shared/table_cells/status_badge.tsx new file mode 100644 index 00000000000..f10f817650a --- /dev/null +++ b/ui/litellm-dashboard/src/components/shared/table_cells/status_badge.tsx @@ -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 = { + 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 = ( + + {label} + + ); + + if (!tooltip) { + return badge; + } + return ; +} diff --git a/ui/litellm-dashboard/src/components/skill_hub_table_columns.tsx b/ui/litellm-dashboard/src/components/skill_hub_table_columns.tsx index 8a8d7ae7042..8fc9adc75a2 100644 --- a/ui/litellm-dashboard/src/components/skill_hub_table_columns.tsx +++ b/ui/litellm-dashboard/src/components/skill_hub_table_columns.tsx @@ -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 }) => ( - - {row.original.enabled ? "Public" : "Draft"} - + ), }, ]; diff --git a/ui/litellm-dashboard/src/components/tag_management/TagTable.test.tsx b/ui/litellm-dashboard/src/components/tag_management/TagTable.test.tsx index a56721787d5..057f30ccee5 100644 --- a/ui/litellm-dashboard/src/components/tag_management/TagTable.test.tsx +++ b/ui/litellm-dashboard/src/components/tag_management/TagTable.test.tsx @@ -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(); - 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(); + 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(); - 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", () => { diff --git a/ui/litellm-dashboard/src/components/tag_management/TagTable.tsx b/ui/litellm-dashboard/src/components/tag_management/TagTable.tsx index ce28ac6e6f2..e34653cc702 100644 --- a/ui/litellm-dashboard/src/components/tag_management/TagTable.tsx +++ b/ui/litellm-dashboard/src/components/tag_management/TagTable.tsx @@ -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 = ({ data, onEdit, onDelete, onSelectTag const isDynamicSpendTag = tag.description === DYNAMIC_SPEND_TAG_DESCRIPTION; return (
- - - + />
); }, @@ -104,10 +88,7 @@ const TagTable: React.FC = ({ data, onEdit, onDelete, onSelectTag header: "Created", accessorKey: "created_at", sortingFn: "datetime", - cell: ({ row }) => { - const tag = row.original; - return {new Date(tag.created_at).toLocaleDateString()}; - }, + cell: ({ row }) => , }, { id: "actions", diff --git a/ui/litellm-dashboard/src/components/team/TeamMemberTab.test.tsx b/ui/litellm-dashboard/src/components/team/TeamMemberTab.test.tsx index ac0ae16a44f..a07c57eaa30 100644 --- a/ui/litellm-dashboard/src/components/team/TeamMemberTab.test.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamMemberTab.test.tsx @@ -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 => ({ team_id: "team-123", team_info: { @@ -78,6 +80,7 @@ const createMockTeamData = (overrides: Partial = {}): 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( { />, ); - 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( + , + ); + + 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", () => { diff --git a/ui/litellm-dashboard/src/components/team/TeamMemberTab.tsx b/ui/litellm-dashboard/src/components/team/TeamMemberTab.tsx index e2f108dcbf5..b884490efc0 100644 --- a/ui/litellm-dashboard/src/components/team/TeamMemberTab.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamMemberTab.tsx @@ -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 = [ @@ -146,7 +142,7 @@ export default function TeamMemberTab({ ), key: "spend", render: (_: unknown, record: Member) => ( - ${formatNumberWithCommas(getUserCurrentCycleSpend(record.user_id), 4)} + ), }, { @@ -159,31 +155,19 @@ export default function TeamMemberTab({ ), key: "total_spend", - render: (_: unknown, record: Member) => ( - ${formatNumberWithCommas(getUserTotalSpend(record.user_id), 4)} - ), + render: (_: unknown, record: Member) => , }, { title: "Team Member Budget (USD)", key: "budget", - render: (_: unknown, record: Member) => { - const budget = getUserBudget(record.user_id); - return ( - {budget ? `$${formatNumberWithCommas(Number(budget), 4)}` : "No Limit"} - ); - }, + render: (_: unknown, record: Member) => ( + + ), }, { title: "Budget Reset", key: "budget_reset", - render: (_: unknown, record: Member) => { - const reset = getUserBudgetReset(record.user_id); - return reset ? ( - {reset} - ) : ( - — - ); - }, + render: (_: unknown, record: Member) => , }, { title: ( diff --git a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx index e51c124d618..b7128e642a5 100644 --- a/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamVirtualKeysTable.tsx @@ -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 ( - - - - ); - }, + cell: (info) => ( + 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) => , }, { 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) => , }, { 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 ( - - {date.toLocaleDateString()} - - ); - }, + cell: (info) => , }, { 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) => , }, { 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) => , }, { 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) => ( + + ), }, { 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) => , }, { id: "models", diff --git a/ui/litellm-dashboard/src/components/ui/badge.tsx b/ui/litellm-dashboard/src/components/ui/badge.tsx index 87b536cc37e..2e1ebffa109 100644 --- a/ui/litellm-dashboard/src/components/ui/badge.tsx +++ b/ui/litellm-dashboard/src/components/ui/badge.tsx @@ -21,14 +21,18 @@ const badgeVariants = cva({ }, }); -function Badge({ - className, - variant = "default", - ...props -}: React.ComponentProps<"span"> & VariantProps) { - return ( - - ); -} +const Badge = React.forwardRef< + HTMLSpanElement, + React.ComponentPropsWithoutRef<"span"> & VariantProps +>(({ className, variant = "default", ...props }, ref) => ( + +)); +Badge.displayName = "Badge"; export { Badge, badgeVariants }; diff --git a/ui/litellm-dashboard/src/components/usage.tsx b/ui/litellm-dashboard/src/components/usage.tsx index 04fcbddbd37..91c12fd1fa2 100644 --- a/ui/litellm-dashboard/src/components/usage.tsx +++ b/ui/litellm-dashboard/src/components/usage.tsx @@ -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 = ({ accessToken, token, userRole, use {provider.provider} - {parseFloat(provider.spend.toFixed(2)) < 0.00001 - ? "less than 0.00" - : formatNumberWithCommas(provider.spend, 2)} + ))} @@ -819,7 +818,9 @@ const UsagePage: React.FC = ({ accessToken, token, userRole, use {topUsers?.map((user: any, index: number) => ( {user.end_user} - {formatNumberWithCommas(user.total_spend, 2)} + + + {user.total_count} ))} diff --git a/ui/litellm-dashboard/src/components/vector_store_management/DocumentsTable.tsx b/ui/litellm-dashboard/src/components/vector_store_management/DocumentsTable.tsx index c8288172d5b..eaa864ab3dc 100644 --- a/ui/litellm-dashboard/src/components/vector_store_management/DocumentsTable.tsx +++ b/ui/litellm-dashboard/src/components/vector_store_management/DocumentsTable.tsx @@ -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 = ({ 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 = { + 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 ; + const config: { tone: StatusTone; label: string } = statusConfig[status] ?? { tone: "neutral", label: status }; + return ; }; const formatFileSize = (bytes?: number) => { diff --git a/ui/litellm-dashboard/src/components/vector_store_management/VectorStoreTable.test.tsx b/ui/litellm-dashboard/src/components/vector_store_management/VectorStoreTable.test.tsx index ac790122507..54b05bc73b5 100644 --- a/ui/litellm-dashboard/src/components/vector_store_management/VectorStoreTable.test.tsx +++ b/ui/litellm-dashboard/src/components/vector_store_management/VectorStoreTable.test.tsx @@ -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 }); }); diff --git a/ui/litellm-dashboard/src/components/vector_store_management/VectorStoreTable.tsx b/ui/litellm-dashboard/src/components/vector_store_management/VectorStoreTable.tsx index 180c2485ab5..c2e47cebe69 100644 --- a/ui/litellm-dashboard/src/components/vector_store_management/VectorStoreTable.tsx +++ b/ui/litellm-dashboard/src/components/vector_store_management/VectorStoreTable.tsx @@ -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 = ({ data, onView, onEdi { header: "Vector Store ID", accessorKey: "vector_store_id", - cell: ({ row }) => { - const vectorStore = row.original; - return ( - - ); - }, + cell: ({ row }) => , }, { header: "Name", @@ -109,19 +98,13 @@ const VectorStoreTable: React.FC = ({ data, onView, onEdi header: "Created At", accessorKey: "created_at", sortingFn: "datetime", - cell: ({ row }) => { - const vectorStore = row.original; - return {new Date(vectorStore.created_at).toLocaleDateString()}; - }, + cell: ({ row }) => , }, { header: "Updated At", accessorKey: "updated_at", sortingFn: "datetime", - cell: ({ row }) => { - const vectorStore = row.original; - return {new Date(vectorStore.updated_at).toLocaleDateString()}; - }, + cell: ({ row }) => , }, { id: "actions", diff --git a/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx b/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx index 28b7afb6298..d811d3b9402 100644 --- a/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/audit_logs.tsx @@ -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) => ( - - {moment.utc(val).local().format("MMM D, YYYY HH:mm:ss")} - - ), + render: (val: string) => , }, { 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) => {val}, + render: (val: string) => , }, { 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 ? {val.slice(0, 12)}… : "—"), + render: (val: string) => , }, ]; diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.test.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.test.tsx new file mode 100644 index 00000000000..afdd17813f3 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/columns.test.tsx @@ -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 => ({ + 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( + 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( + r.request_id} + />, + ); + const formatted = screen.getByText("$0.000123"); + await user.hover(formatted); + expect(await screen.findByText("$0.00012345678")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.tsx index 7452992ed59..1d0f3f33d08 100644 --- a/ui/litellm-dashboard/src/components/view_logs/columns.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/columns.tsx @@ -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[] : "Time", accessorKey: "startTime", size: 200, - cell: (info: any) => , + cell: (info: any) => , }, { header: "Type", @@ -174,48 +173,20 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef[] cell: (info: any) => { const status = info.getValue() || "Success"; const isSuccess = status.toLowerCase() !== "failure"; - - return ( - - {isSuccess ? "Success" : "Failure"} - - ); + return ; }, }, { header: "Session ID", accessorKey: "session_id", size: 120, - cell: (info: any) => { - const value = String(info.getValue() || ""); - const onSessionClick = info.row.original.onSessionClick; - return ( - - - - ); - }, + cell: (info: any) => , }, { header: "Request ID", accessorKey: "request_id", - cell: (info: any) => ( - - {String(info.getValue() || "")} - - ), + cell: (info: any) => , }, { header: sortProps @@ -236,11 +207,14 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef[] 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 (
- - {getSpendString(info.getValue() || 0)} + + + + {mcpCount > 0 && mcpSpend > 0 && ( @@ -320,21 +294,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef[] 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 ( - - onKeyHashClick?.(value)} - > - {value} - - - ); - }, + cell: (info: any) => , }, { header: "Key Alias", @@ -589,141 +549,3 @@ export type AuditLogEntry = { before_value: Record; updated_values: Record; }; - -const getActionBadge = (action: string) => { - return ( - - {action} - - ); -}; - -export const auditLogColumns: ColumnDef[] = [ - { - 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() ? ( - - ) : ( - ● - ); - }; - return ; - }, - }, - { - header: "Timestamp", - accessorKey: "updated_at", - cell: (info: any) => , - }, - { - 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 {displayValue}; - }, - }, - { - header: "Action", - accessorKey: "action", - cell: (info: any) => {getActionBadge(info.getValue())}, - }, - { - 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 ( -
-
{changedBy}
- {apiKey && ( // Only show API key if it exists - -
- {" "} - {/* Apply max-width and truncate */} - {apiKey} -
-
- )} -
- ); - }, - }, - { - 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 ( - - - {String(objectId)} - - - ); - }; - return ; - }, - }, -]; diff --git a/ui/litellm-dashboard/src/components/view_logs/time_cell.test.tsx b/ui/litellm-dashboard/src/components/view_logs/time_cell.test.tsx deleted file mode 100644 index 95a8b43b2c1..00000000000 --- a/ui/litellm-dashboard/src/components/view_logs/time_cell.test.tsx +++ /dev/null @@ -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(); - // 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(); - // 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(); - 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); - }); -}); diff --git a/ui/litellm-dashboard/src/components/view_logs/time_cell.tsx b/ui/litellm-dashboard/src/components/view_logs/time_cell.tsx deleted file mode 100644 index 8addc702dc6..00000000000 --- a/ui/litellm-dashboard/src/components/view_logs/time_cell.tsx +++ /dev/null @@ -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 = ({ utcTime }) => { - return ( - - {getLocalTime(utcTime)} - - ); -}; - -export const getTimeZone = (): string => { - return Intl.DateTimeFormat().resolvedOptions().timeZone; -};