diff --git a/ui/litellm-dashboard/src/components/SidebarUsageCard.test.tsx b/ui/litellm-dashboard/src/components/SidebarUsageCard.test.tsx index 3244906ed8e..71ed3094a20 100644 --- a/ui/litellm-dashboard/src/components/SidebarUsageCard.test.tsx +++ b/ui/litellm-dashboard/src/components/SidebarUsageCard.test.tsx @@ -128,6 +128,29 @@ describe("SidebarUsageCard", () => { expect(container.querySelector('[data-slot="meter"]')).toBeNull(); }); + it("shows the exact license expiration date as the subtitle instead of time remaining", async () => { + mockUseLicenseInfo.mockReturnValue(licenseResult({ ...ACTIVE_LICENSE, expiration_date: "2099-12-31" })); + + renderWithClient( {}} />); + + expect(await screen.findByText("Expires Dec 31, 2099")).toBeInTheDocument(); + expect(screen.queryByText(/(day|days|month|months) remaining/)).not.toBeInTheDocument(); + }); + + it("shows the exact date as the subtitle when the license is expired", async () => { + mockUseLicenseInfo.mockReturnValue(licenseResult({ ...ACTIVE_LICENSE, expiration_date: "2020-01-01" })); + + renderWithClient( {}} />); + + expect(await screen.findByText("Expired Jan 1, 2020")).toBeInTheDocument(); + }); + + it("falls back to Active plan when the license has no expiration date", async () => { + renderWithClient( {}} />); + + expect(await screen.findByText("Active plan")).toBeInTheDocument(); + }); + it("shows a collapsed rail button that expands the sidebar", async () => { const onExpandRail = vi.fn(); const user = userEvent.setup(); diff --git a/ui/litellm-dashboard/src/components/SidebarUsageCard.tsx b/ui/litellm-dashboard/src/components/SidebarUsageCard.tsx index 4fb0e1dac21..2a6d6b43f38 100644 --- a/ui/litellm-dashboard/src/components/SidebarUsageCard.tsx +++ b/ui/litellm-dashboard/src/components/SidebarUsageCard.tsx @@ -1,6 +1,6 @@ import { useDisableUsageIndicator } from "@/app/(dashboard)/hooks/useDisableUsageIndicator"; import { useLicenseInfo } from "@/app/(dashboard)/hooks/license/useLicenseInfo"; -import { getDaysUntilExpiration } from "@/utils/licenseUtils"; +import { formatExpirationStatus } from "@/utils/licenseUtils"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Meter, MeterIndicator, MeterLabel, MeterTrack } from "@/components/ui/meter"; @@ -20,16 +20,6 @@ interface MeterData { total: number; } -const formatExpiration = (daysRemaining: number | null): string => { - if (daysRemaining === null) return "No expiration"; - if (daysRemaining < 0) return "Expired"; - if (daysRemaining === 0) return "Expires today"; - if (daysRemaining === 1) return "1 day remaining"; - if (daysRemaining < 30) return `${daysRemaining} days remaining`; - if (daysRemaining < 60) return "1 month remaining"; - return `${Math.floor(daysRemaining / 30)} months remaining`; -}; - const meterTone = (pct: number): "default" | "warning" | "over" => { if (pct > 100) return "over"; if (pct >= 80) return "warning"; @@ -104,8 +94,7 @@ export default function SidebarUsageCard({ accessToken, collapsed, onExpandRail ); } - const daysUntilExpiration = licenseInfo?.expiration_date ? getDaysUntilExpiration(licenseInfo.expiration_date) : null; - const subtitle = licenseInfo?.expiration_date ? formatExpiration(daysUntilExpiration) : "Active plan"; + const subtitle = licenseInfo?.expiration_date ? formatExpirationStatus(licenseInfo.expiration_date) : "Active plan"; const meters = buildMeters(data); return ( diff --git a/ui/litellm-dashboard/src/components/UsageIndicator.test.tsx b/ui/litellm-dashboard/src/components/UsageIndicator.test.tsx index ad27fbcd74f..c587ebde57f 100644 --- a/ui/litellm-dashboard/src/components/UsageIndicator.test.tsx +++ b/ui/litellm-dashboard/src/components/UsageIndicator.test.tsx @@ -14,9 +14,19 @@ vi.mock("@/app/(dashboard)/hooks/useDisableUsageIndicator", () => ({ useDisableUsageIndicator: vi.fn(() => false), })); -import { getRemainingUsers } from "./networking"; +import { getLicenseInfo, getRemainingUsers } from "./networking"; +import type { LicenseInfo } from "./networking"; const mockGetRemainingUsers = vi.mocked(getRemainingUsers); +const mockGetLicenseInfo = vi.mocked(getLicenseInfo); + +const licenseWithExpiry = (expiration_date: string): LicenseInfo => ({ + has_license: true, + license_type: "enterprise", + expiration_date, + allowed_features: [], + limits: { max_users: null, max_teams: null }, +}); const renderWithClient = (ui: React.ReactElement) => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); @@ -36,6 +46,7 @@ describe("UsageIndicator", () => { beforeEach(() => { vi.clearAllMocks(); mockGetRemainingUsers.mockResolvedValue(DEFAULT_USAGE_DATA); + mockGetLicenseInfo.mockResolvedValue(null); }); it("should render when given access token and usage data loads", async () => { @@ -126,6 +137,23 @@ describe("UsageIndicator", () => { expect(screen.getByText("Over limit")).toBeInTheDocument(); }); + it("should show the exact license expiration date instead of time remaining", async () => { + mockGetLicenseInfo.mockResolvedValue(licenseWithExpiry("2099-12-31")); + + renderWithClient(); + + expect(await screen.findByText("Expires Dec 31, 2099")).toBeInTheDocument(); + expect(screen.queryByText(/(day|days|month|months) remaining/)).not.toBeInTheDocument(); + }); + + it("should show the exact date when the license is expired", async () => { + mockGetLicenseInfo.mockResolvedValue(licenseWithExpiry("2020-01-01")); + + renderWithClient(); + + expect(await screen.findByText("Expired Jan 1, 2020")).toBeInTheDocument(); + }); + it("should render nothing when accessToken is null", () => { renderWithClient(); diff --git a/ui/litellm-dashboard/src/components/UsageIndicator.tsx b/ui/litellm-dashboard/src/components/UsageIndicator.tsx index 6e7b5e9ec60..6ff4008c2cf 100644 --- a/ui/litellm-dashboard/src/components/UsageIndicator.tsx +++ b/ui/litellm-dashboard/src/components/UsageIndicator.tsx @@ -15,7 +15,7 @@ import { useEffect, useState } from "react"; import { getRemainingUsers } from "./networking"; import { cn } from "@/lib/cva.config"; -import { getDaysUntilExpiration } from "@/utils/licenseUtils"; +import { formatExpirationStatus, getDaysUntilExpiration } from "@/utils/licenseUtils"; import { useLicenseInfo } from "@/app/(dashboard)/hooks/license/useLicenseInfo"; interface UsageIndicatorProps { @@ -32,18 +32,6 @@ interface UsageData { total_teams_remaining: number | null; } -// Format expiration for display -const formatExpirationDisplay = (daysRemaining: number | null): string => { - if (daysRemaining === null) return "No expiration"; - if (daysRemaining < 0) return "Expired"; - if (daysRemaining === 0) return "Expires today"; - if (daysRemaining === 1) return "1 day remaining"; - if (daysRemaining < 30) return `${daysRemaining} days remaining`; - if (daysRemaining < 60) return "1 month remaining"; - const months = Math.floor(daysRemaining / 30); - return `${months} months remaining`; -}; - export default function UsageIndicator({ accessToken, width = 220 }: UsageIndicatorProps) { const disableUsageIndicator = useDisableUsageIndicator(); const [isExpanded, setIsExpanded] = useState(false); @@ -292,7 +280,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica ) : isLicenseExpiringSoon ? ( ) : null} - {formatExpirationDisplay(daysUntilExpiration)} + {formatExpirationStatus(licenseInfo.expiration_date)} )} @@ -529,7 +517,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica isLicenseExpiringSoon && "text-yellow-600", )} > - {formatExpirationDisplay(daysUntilExpiration)} + {formatExpirationStatus(licenseInfo.expiration_date)} {licenseInfo.license_type && ( diff --git a/ui/litellm-dashboard/src/utils/licenseUtils.test.ts b/ui/litellm-dashboard/src/utils/licenseUtils.test.ts index 717b8f0d90d..8a489c30c2d 100644 --- a/ui/litellm-dashboard/src/utils/licenseUtils.test.ts +++ b/ui/litellm-dashboard/src/utils/licenseUtils.test.ts @@ -1,5 +1,11 @@ import { describe, it, expect } from "vitest"; -import { type LicenseExpiryTier, formatExpiryDate, getDaysUntilExpiration, getLicenseExpiryTier } from "./licenseUtils"; +import { + type LicenseExpiryTier, + formatExpirationStatus, + formatExpiryDate, + getDaysUntilExpiration, + getLicenseExpiryTier, +} from "./licenseUtils"; const NOW = new Date("2026-07-08T00:00:00Z"); @@ -59,3 +65,25 @@ describe("formatExpiryDate", () => { expect(formatExpiryDate("bogus")).toBe("bogus"); }); }); + +describe("formatExpirationStatus", () => { + it("shows the exact date for a future expiration", () => { + expect(formatExpirationStatus("2026-08-07", NOW)).toBe("Expires Aug 7, 2026"); + }); + + it("still reads as upcoming on the expiration day itself", () => { + expect(formatExpirationStatus("2026-07-08", NOW)).toBe("Expires Jul 8, 2026"); + }); + + it("shows the exact date for a past expiration", () => { + expect(formatExpirationStatus("2026-07-07", NOW)).toBe("Expired Jul 7, 2026"); + }); + + it("returns No expiration for a null date", () => { + expect(formatExpirationStatus(null, NOW)).toBe("No expiration"); + }); + + it("returns No expiration for an unparseable date", () => { + expect(formatExpirationStatus("not-a-date", NOW)).toBe("No expiration"); + }); +}); diff --git a/ui/litellm-dashboard/src/utils/licenseUtils.ts b/ui/litellm-dashboard/src/utils/licenseUtils.ts index 57acad85508..4ba39dabe0b 100644 --- a/ui/litellm-dashboard/src/utils/licenseUtils.ts +++ b/ui/litellm-dashboard/src/utils/licenseUtils.ts @@ -49,3 +49,11 @@ export const formatExpiryDate = (expirationDate: string): string => { } return date.toLocaleDateString("en-US", EXPIRY_DATE_FORMAT); }; + +export const formatExpirationStatus = (expirationDate: string | null, now: Date = new Date()): string => { + const days = getDaysUntilExpiration(expirationDate, now); + if (expirationDate === null || days === null) { + return "No expiration"; + } + return days < 0 ? `Expired ${formatExpiryDate(expirationDate)}` : `Expires ${formatExpiryDate(expirationDate)}`; +};