mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
feat(ui): show exact license expiration date in usage cards
This commit is contained in:
parent
ff4a40f017
commit
056f85a71e
6 changed files with 94 additions and 30 deletions
|
|
@ -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(<SidebarUsageCard accessToken="token" collapsed={false} onExpandRail={() => {}} />);
|
||||
|
||||
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(<SidebarUsageCard accessToken="token" collapsed={false} onExpandRail={() => {}} />);
|
||||
|
||||
expect(await screen.findByText("Expired Jan 1, 2020")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("falls back to Active plan when the license has no expiration date", async () => {
|
||||
renderWithClient(<SidebarUsageCard accessToken="token" collapsed={false} onExpandRail={() => {}} />);
|
||||
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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(<UsageIndicator accessToken="token" width={220} />);
|
||||
|
||||
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(<UsageIndicator accessToken="token" width={220} />);
|
||||
|
||||
expect(await screen.findByText("Expired Jan 1, 2020")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render nothing when accessToken is null", () => {
|
||||
renderWithClient(<UsageIndicator accessToken={null} width={220} />);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<TrendingUp className="h-3 w-3" />
|
||||
) : null}
|
||||
<span className="truncate">{formatExpirationDisplay(daysUntilExpiration)}</span>
|
||||
<span className="truncate">{formatExpirationStatus(licenseInfo.expiration_date)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -529,7 +517,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
isLicenseExpiringSoon && "text-yellow-600",
|
||||
)}
|
||||
>
|
||||
{formatExpirationDisplay(daysUntilExpiration)}
|
||||
{formatExpirationStatus(licenseInfo.expiration_date)}
|
||||
</span>
|
||||
</div>
|
||||
{licenseInfo.license_type && (
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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)}`;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue