diff --git a/ui/litellm-dashboard/src/components/UsageIndicator.tsx b/ui/litellm-dashboard/src/components/UsageIndicator.tsx index 3976e4d3d6..47da9f78bd 100644 --- a/ui/litellm-dashboard/src/components/UsageIndicator.tsx +++ b/ui/litellm-dashboard/src/components/UsageIndicator.tsx @@ -1,8 +1,8 @@ import { useDisableUsageIndicator } from "@/app/(dashboard)/hooks/useDisableUsageIndicator"; import { Badge } from "@tremor/react"; -import { AlertTriangle, ChevronDown, ChevronUp, Loader2, Minus, TrendingUp, UserCheck, Users } from "lucide-react"; +import { AlertTriangle, Calendar, ChevronDown, ChevronUp, Loader2, Minus, TrendingUp, UserCheck, Users } from "lucide-react"; import { useEffect, useState } from "react"; -import { getRemainingUsers } from "./networking"; +import { getRemainingUsers, getLicenseInfo, LicenseInfo } from "./networking"; // Simple utility function to combine class names const cn = (...classes: (string | boolean | undefined)[]) => { @@ -23,11 +23,35 @@ interface UsageData { total_teams_remaining: number | null; } +// Calculate days until expiration +const getDaysUntilExpiration = (expirationDate: string | null): number | null => { + if (!expirationDate) return null; + const expDate = new Date(expirationDate + 'T00:00:00Z'); // Force UTC midnight + const now = new Date(); + now.setHours(0, 0, 0, 0); // Normalize to local midnight + const diffTime = expDate.getTime() - now.getTime(); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + return diffDays; +}; + +// 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); const [isMinimized, setIsMinimized] = useState(false); const [data, setData] = useState(null); + const [licenseInfo, setLicenseInfo] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -39,8 +63,12 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica setError(null); try { - const result = await getRemainingUsers(accessToken); - setData(result); + const [usageResult, licenseResult] = await Promise.all([ + getRemainingUsers(accessToken), + getLicenseInfo(accessToken).catch(() => null), // Don't fail if license endpoint unavailable + ]); + setData(usageResult); + setLicenseInfo(licenseResult); } catch (err) { console.error("Failed to fetch usage data:", err); setError("Failed to load usage data"); @@ -52,6 +80,13 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica fetchData(); }, [accessToken]); + // Calculate license expiration metrics + const daysUntilExpiration = licenseInfo?.expiration_date + ? getDaysUntilExpiration(licenseInfo.expiration_date) + : null; + const isLicenseExpired = daysUntilExpiration !== null && daysUntilExpiration < 0; + const isLicenseExpiringSoon = daysUntilExpiration !== null && daysUntilExpiration >= 0 && daysUntilExpiration < 30; + // Calculate derived values from data const getUsageMetrics = (data: UsageData | null) => { if (!data) { @@ -106,35 +141,38 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica const { isOverLimit, isNearLimit, usagePercentage, userMetrics, teamMetrics } = getUsageMetrics(data); + // Include license status in overall status + const hasAnyIssue = isOverLimit || isNearLimit || isLicenseExpired || isLicenseExpiringSoon; + const hasError = isOverLimit || isLicenseExpired; + const hasWarning = (isNearLimit || isLicenseExpiringSoon) && !hasError; + const getStatusColor = () => { - if (isOverLimit) return "red"; - if (isNearLimit) return "yellow"; + if (hasError) return "red"; + if (hasWarning) return "yellow"; return "green"; }; const getStatusIcon = () => { - if (isOverLimit) return ; - if (isNearLimit) return ; + if (hasError) return ; + if (hasWarning) return ; return null; }; // Minimized view - just a small restore button const MinimizedView = () => { - const hasIssues = isOverLimit || isNearLimit; - return (
@@ -198,13 +245,13 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica onClick={() => setIsExpanded(!isExpanded)} className={cn( "flex items-center gap-3 text-left hover:bg-gray-50 rounded-md px-0 py-1 transition-colors flex-1 min-w-0", - isOverLimit && "text-red-600", - isNearLimit && "text-yellow-600", + hasError && "text-red-600", + hasWarning && "text-yellow-600", )} > Usage Status - {(isOverLimit || isNearLimit) && ( + {hasAnyIssue && ( {getStatusIcon()} @@ -229,6 +276,28 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica {/* Expanded details - simple and compact */} {isExpanded && (
+ {/* License expiration section */} + {licenseInfo?.has_license && licenseInfo.expiration_date && ( +
+
+ + License +
+
+ {isLicenseExpired ? ( + + ) : isLicenseExpiringSoon ? ( + + ) : null} + {formatExpirationDisplay(daysUntilExpiration)} +
+
+ )} + {/* Users section */} {data.total_users !== null && (
@@ -323,7 +392,6 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica // Optimized CardStyleView for 220px width const CardStyleView = () => { if (isMinimized) { - const hasIssues = isOverLimit || isNearLimit; return ( @@ -416,6 +496,50 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica {/* Compact stats optimized for 220px */}
+ {/* License expiration section */} + {licenseInfo?.has_license && licenseInfo.expiration_date && ( +
+
+ + License + + {isLicenseExpired ? "Expired" : isLicenseExpiringSoon ? "Expiring soon" : "OK"} + +
+
+ Status: + + {formatExpirationDisplay(daysUntilExpiration)} + +
+ {licenseInfo.license_type && ( +
+ Type: + {licenseInfo.license_type} +
+ )} +
+ )} + {/* Users section */} {data.total_users !== null && (
=> { + try { + const url = proxyBaseUrl ? `${proxyBaseUrl}/health/license` : `/health/license`; + + const response = await fetch(url, { + method: "GET", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + }, + }); + + if (!response.ok) { + // if 404 - return null (endpoint not available) + if (response.status === 404) { + return null; + } + const errorData = await response.text(); + handleError(errorData); + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Failed to fetch license info:", error); + throw error; + } +}; + export const updatePassThroughEndpoint = async ( accessToken: string, endpointPath: string,