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/LicenseExpiryBanner.test.tsx b/ui/litellm-dashboard/src/components/LicenseExpiryBanner.test.tsx
new file mode 100644
index 00000000000..d6b419ace7c
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/LicenseExpiryBanner.test.tsx
@@ -0,0 +1,90 @@
+import React from "react";
+import { describe, it, expect, beforeEach, afterEach } from "vitest";
+import { render, screen, fireEvent } from "@testing-library/react";
+import { LicenseExpiryBannerView } from "./LicenseExpiryBanner";
+import { LicenseInfo } from "./networking";
+
+const daysFromNow = (n: number): string => {
+ const date = new Date();
+ date.setUTCDate(date.getUTCDate() + n);
+ return date.toISOString().slice(0, 10);
+};
+
+const licenseWith = (expiration_date: string | null): LicenseInfo => ({
+ has_license: expiration_date !== null,
+ license_type: expiration_date !== null ? "enterprise" : "community",
+ expiration_date,
+ allowed_features: [],
+ limits: { max_users: null, max_teams: null },
+});
+
+describe("LicenseExpiryBannerView", () => {
+ beforeEach(() => {
+ sessionStorage.clear();
+ });
+
+ afterEach(() => {
+ sessionStorage.clear();
+ });
+
+ it("renders nothing when there is no license info", () => {
+ const { container } = render();
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("renders nothing when expiration_date is null (community or remote-validated)", () => {
+ const { container } = render();
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("renders nothing when expiry is more than 30 days out", () => {
+ const { container } = render();
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("shows a dismissible amber warning within 30 days", () => {
+ const { container } = render();
+ expect(screen.getByText(/expires in 20 days/)).toBeInTheDocument();
+ expect(container.querySelector(".ant-alert-warning")).toBeInTheDocument();
+ expect(screen.queryByRole("button")).toBeInTheDocument();
+ expect(screen.getByRole("link", { name: "sales@berri.ai" })).toHaveAttribute("href", "mailto:sales@berri.ai");
+ });
+
+ it("shows a non-dismissible red critical alert within 7 days", () => {
+ const { container } = render();
+ expect(screen.getByText(/expires in 5 days/)).toBeInTheDocument();
+ expect(container.querySelector(".ant-alert-error")).toBeInTheDocument();
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
+ });
+
+ it("says 'expires today' on the expiration day", () => {
+ render();
+ expect(screen.getByText(/expires today/)).toBeInTheDocument();
+ });
+
+ it("shows a non-dismissible red expired alert stating features are disabled", () => {
+ const { container } = render();
+ expect(screen.getByText(/expired on/)).toBeInTheDocument();
+ expect(screen.getByText(/features are now disabled/i)).toBeInTheDocument();
+ expect(container.querySelector(".ant-alert-error")).toBeInTheDocument();
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
+ });
+
+ it("hides the warning after dismissal and stays hidden within the session", () => {
+ const expiration = daysFromNow(20);
+ const { unmount } = render();
+ fireEvent.click(screen.getByRole("button"));
+ expect(screen.queryByText(/expires in 20 days/)).not.toBeInTheDocument();
+
+ unmount();
+ render();
+ expect(screen.queryByText(/expires in 20 days/)).not.toBeInTheDocument();
+ });
+
+ it("still shows a critical alert even when its date was previously dismissed", () => {
+ const expiration = daysFromNow(5);
+ sessionStorage.setItem(`litellm:licenseExpiryBannerDismissed:${expiration}`, "true");
+ render();
+ expect(screen.getByText(/expires in 5 days/)).toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/LicenseExpiryBanner.tsx b/ui/litellm-dashboard/src/components/LicenseExpiryBanner.tsx
new file mode 100644
index 00000000000..c3b20b5fac0
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/LicenseExpiryBanner.tsx
@@ -0,0 +1,95 @@
+"use client";
+
+import React, { useState } from "react";
+import { Alert } from "antd";
+import { LicenseInfo } from "@/components/networking";
+import { useLicenseInfo } from "@/app/(dashboard)/hooks/license/useLicenseInfo";
+import { formatExpiryDate, getDaysUntilExpiration, getLicenseExpiryTier } from "@/utils/licenseUtils";
+
+const DISMISS_KEY_PREFIX = "litellm:licenseExpiryBannerDismissed:";
+const SALES_EMAIL = "sales@berri.ai";
+
+const salesLink = {SALES_EMAIL};
+
+interface LicenseExpiryBannerProps {
+ accessToken: string | null;
+}
+
+interface LicenseExpiryBannerViewProps {
+ licenseInfo: LicenseInfo | null;
+}
+
+const describeCountdown = (days: number): string => {
+ if (days <= 0) {
+ return "expires today";
+ }
+ if (days === 1) {
+ return "expires in 1 day";
+ }
+ return `expires in ${days} days`;
+};
+
+const expiryDescription = (tier: "warning" | "critical" | "expired"): React.ReactNode => {
+ if (tier === "expired") {
+ return <>Enterprise features are now disabled. Reach out to {salesLink} to restore access>;
+ }
+ if (tier === "critical") {
+ return <>Renew now to avoid losing enterprise features. Reach out to {salesLink}>;
+ }
+ return <>Renew before it lapses to keep enterprise features. Reach out to {salesLink}>;
+};
+
+export const LicenseExpiryBannerView: React.FC = ({ licenseInfo }) => {
+ const [locallyDismissed, setLocallyDismissed] = useState(false);
+
+ const expirationDate = licenseInfo?.expiration_date ?? null;
+ const tier = getLicenseExpiryTier(expirationDate);
+ const days = getDaysUntilExpiration(expirationDate);
+
+ if (expirationDate === null || tier === "none" || days === null) {
+ return null;
+ }
+
+ const isDismissible = tier === "warning";
+ const dismissKey = `${DISMISS_KEY_PREFIX}${expirationDate}`;
+ const previouslyDismissed =
+ isDismissible && typeof window !== "undefined" ? sessionStorage.getItem(dismissKey) === "true" : false;
+
+ if (isDismissible && (locallyDismissed || previouslyDismissed)) {
+ return null;
+ }
+
+ const formattedDate = formatExpiryDate(expirationDate);
+
+ const message =
+ tier === "expired"
+ ? `Your LiteLLM Enterprise license expired on ${formattedDate}`
+ : `Your LiteLLM Enterprise license ${describeCountdown(days)} (${formattedDate})`;
+
+ const description = expiryDescription(tier);
+
+ const handleClose = () => {
+ if (typeof window !== "undefined") {
+ sessionStorage.setItem(dismissKey, "true");
+ }
+ setLocallyDismissed(true);
+ };
+
+ return (
+
+ );
+};
+
+export const LicenseExpiryBanner: React.FC = ({ accessToken }) => {
+ const { data } = useLicenseInfo(accessToken);
+ return ;
+};
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) => (
-
+