From daa15ba0747b66d4c5ae70ade65d293e00adb2b7 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Wed, 15 Jul 2026 17:09:24 -0700 Subject: [PATCH] chore(ui): remove unmounted UsageIndicator component --- .../src/components/UsageIndicator.test.tsx | 193 ----- .../src/components/UsageIndicator.tsx | 681 ------------------ 2 files changed, 874 deletions(-) delete mode 100644 ui/litellm-dashboard/src/components/UsageIndicator.test.tsx delete mode 100644 ui/litellm-dashboard/src/components/UsageIndicator.tsx diff --git a/ui/litellm-dashboard/src/components/UsageIndicator.test.tsx b/ui/litellm-dashboard/src/components/UsageIndicator.test.tsx deleted file mode 100644 index ad27fbcd74f..00000000000 --- a/ui/litellm-dashboard/src/components/UsageIndicator.test.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import React from "react"; -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import UsageIndicator from "./UsageIndicator"; - -vi.mock("./networking", () => ({ - getRemainingUsers: vi.fn(), - getLicenseInfo: vi.fn().mockResolvedValue(null), -})); - -vi.mock("@/app/(dashboard)/hooks/useDisableUsageIndicator", () => ({ - useDisableUsageIndicator: vi.fn(() => false), -})); - -import { getRemainingUsers } from "./networking"; - -const mockGetRemainingUsers = vi.mocked(getRemainingUsers); - -const renderWithClient = (ui: React.ReactElement) => { - const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); - return render({ui}); -}; - -const DEFAULT_USAGE_DATA = { - total_users: 100, - total_users_used: 1, - total_users_remaining: 99, - total_teams: null, - total_teams_used: 0, - total_teams_remaining: null, -}; - -describe("UsageIndicator", () => { - beforeEach(() => { - vi.clearAllMocks(); - mockGetRemainingUsers.mockResolvedValue(DEFAULT_USAGE_DATA); - }); - - it("should render when given access token and usage data loads", async () => { - renderWithClient(); - - await screen.findByText("Usage"); - - expect(screen.getByText("Usage")).toBeInTheDocument(); - }); - - it("should not show Near limit when users usage is below 80% (1/100 -> 1%)", async () => { - renderWithClient(); - - await screen.findByText("Usage"); - - expect(screen.queryByText("Near limit")).not.toBeInTheDocument(); - }); - - it("should render nothing when both total_users and total_teams are null", async () => { - mockGetRemainingUsers.mockResolvedValue({ - total_users: null, - total_teams: null, - total_users_used: 520, - total_teams_used: 4, - total_teams_remaining: null, - total_users_remaining: null, - }); - - renderWithClient(); - - await waitFor(() => { - expect(screen.queryByText("Usage")).not.toBeInTheDocument(); - expect(screen.queryByText("Loading...")).not.toBeInTheDocument(); - }); - }); - - it("should show Near limit for Teams when at 80% usage (4/5)", async () => { - mockGetRemainingUsers.mockResolvedValue({ - total_users: null, - total_users_used: 0, - total_users_remaining: null, - total_teams: 5, - total_teams_used: 4, - total_teams_remaining: 1, - }); - - renderWithClient(); - - await screen.findByText("Usage"); - - expect(screen.getByText("Teams")).toBeInTheDocument(); - expect(screen.getByText("Near limit")).toBeInTheDocument(); - }); - - it("should show Over limit for Users when usage exceeds 100% (105/100)", async () => { - mockGetRemainingUsers.mockResolvedValue({ - total_users: 100, - total_users_used: 105, - total_users_remaining: -5, - total_teams: null, - total_teams_used: 0, - total_teams_remaining: null, - }); - - renderWithClient(); - - await screen.findByText("Usage"); - - expect(screen.getByText("Users")).toBeInTheDocument(); - expect(screen.getByText("Over limit")).toBeInTheDocument(); - }); - - it("should show Over limit for Teams when usage exceeds 100%", async () => { - mockGetRemainingUsers.mockResolvedValue({ - total_users: null, - total_users_used: 0, - total_users_remaining: null, - total_teams: 10, - total_teams_used: 12, - total_teams_remaining: -2, - }); - - renderWithClient(); - - await screen.findByText("Usage"); - - expect(screen.getByText("Teams")).toBeInTheDocument(); - expect(screen.getByText("Over limit")).toBeInTheDocument(); - }); - - it("should render nothing when accessToken is null", () => { - renderWithClient(); - - expect(mockGetRemainingUsers).not.toHaveBeenCalled(); - expect(screen.queryByText("Usage")).not.toBeInTheDocument(); - }); - - it("should render nothing when disableUsageIndicator is true", async () => { - const { useDisableUsageIndicator } = await import("@/app/(dashboard)/hooks/useDisableUsageIndicator"); - (useDisableUsageIndicator as ReturnType).mockReturnValue(true); - - renderWithClient(); - - await waitFor(() => { - expect(screen.queryByText("Usage")).not.toBeInTheDocument(); - }); - - (useDisableUsageIndicator as ReturnType).mockReturnValue(false); - }); - - it("should show Loading while fetching", () => { - mockGetRemainingUsers.mockImplementation(() => new Promise(() => {})); - - renderWithClient(); - - expect(screen.getByText("Loading...")).toBeInTheDocument(); - }); - - it("should show error message when fetch fails", async () => { - const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - mockGetRemainingUsers.mockRejectedValue(new Error("Network error")); - - renderWithClient(); - - expect(await screen.findByText("Failed to load usage data")).toBeInTheDocument(); - - consoleSpy.mockRestore(); - }); - - it("should minimize when user clicks minimize button", async () => { - const user = userEvent.setup(); - renderWithClient(); - - await screen.findByText("Usage"); - - const minimizeButton = screen.getByTitle("Minimize"); - await user.click(minimizeButton); - - expect(screen.queryByText("Users")).not.toBeInTheDocument(); - expect(screen.getByTitle("Show usage details")).toBeInTheDocument(); - }); - - it("should restore from minimized when user clicks restore button", async () => { - const user = userEvent.setup(); - renderWithClient(); - - await screen.findByText("Usage"); - - await user.click(screen.getByTitle("Minimize")); - await user.click(screen.getByTitle("Show usage details")); - - expect(screen.getByText("Usage")).toBeInTheDocument(); - expect(screen.getByText("Users")).toBeInTheDocument(); - }); -}); diff --git a/ui/litellm-dashboard/src/components/UsageIndicator.tsx b/ui/litellm-dashboard/src/components/UsageIndicator.tsx deleted file mode 100644 index 6e7b5e9ec60..00000000000 --- a/ui/litellm-dashboard/src/components/UsageIndicator.tsx +++ /dev/null @@ -1,681 +0,0 @@ -import { useDisableUsageIndicator } from "@/app/(dashboard)/hooks/useDisableUsageIndicator"; -import { Badge } from "@tremor/react"; -import { - AlertTriangle, - Calendar, - ChevronDown, - ChevronUp, - Loader2, - Minus, - TrendingUp, - UserCheck, - Users, -} from "lucide-react"; -import { useEffect, useState } from "react"; -import { getRemainingUsers } from "./networking"; - -import { cn } from "@/lib/cva.config"; -import { getDaysUntilExpiration } from "@/utils/licenseUtils"; -import { useLicenseInfo } from "@/app/(dashboard)/hooks/license/useLicenseInfo"; - -interface UsageIndicatorProps { - accessToken: string | null; - width: number; -} - -interface UsageData { - total_users: number | null; - total_users_used: number; - total_users_remaining: number | null; - total_teams: number | null; - total_teams_used: number; - 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); - const [isMinimized, setIsMinimized] = useState(false); - const [data, setData] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const licenseInfo = useLicenseInfo(accessToken).data ?? null; - - useEffect(() => { - const fetchData = async () => { - if (!accessToken) return; - - setIsLoading(true); - setError(null); - - try { - const usageResult = await getRemainingUsers(accessToken); - setData(usageResult); - } catch (err) { - console.error("Failed to fetch usage data:", err); - setError("Failed to load usage data"); - } finally { - setIsLoading(false); - } - }; - - 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) { - return { - isOverLimit: false, - isNearLimit: false, - usagePercentage: 0, - userMetrics: { - isOverLimit: false, - isNearLimit: false, - usagePercentage: 0, - }, - teamMetrics: { - isOverLimit: false, - isNearLimit: false, - usagePercentage: 0, - }, - }; - } - - // User metrics - const userUsagePercentage = data.total_users ? (data.total_users_used / data.total_users) * 100 : 0; - const userIsOverLimit = userUsagePercentage > 100; - const userIsNearLimit = userUsagePercentage >= 80 && userUsagePercentage <= 100; - - // Team metrics - const teamUsagePercentage = data.total_teams ? (data.total_teams_used / data.total_teams) * 100 : 0; - const teamIsOverLimit = teamUsagePercentage > 100; - const teamIsNearLimit = teamUsagePercentage >= 80 && teamUsagePercentage <= 100; - - // Combined status (worst case scenario) - const isOverLimit = userIsOverLimit || teamIsOverLimit; - const isNearLimit = (userIsNearLimit || teamIsNearLimit) && !isOverLimit; - const usagePercentage = Math.max(userUsagePercentage, teamUsagePercentage); - - return { - isOverLimit, - isNearLimit, - usagePercentage, - userMetrics: { - isOverLimit: userIsOverLimit, - isNearLimit: userIsNearLimit, - usagePercentage: userUsagePercentage, - }, - teamMetrics: { - isOverLimit: teamIsOverLimit, - isNearLimit: teamIsNearLimit, - usagePercentage: teamUsagePercentage, - }, - }; - }; - - 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 (hasError) return "red"; - if (hasWarning) return "yellow"; - return "green"; - }; - - const getStatusIcon = () => { - if (hasError) return ; - if (hasWarning) return ; - return null; - }; - - // Minimized view - just a small restore button - const MinimizedView = () => { - return ( -
- -
- ); - }; - - // Sidebar/nav style component - const NavStyleView = () => { - if (isMinimized) { - return ; - } - - if (isLoading) { - return ( -
- - Loading... -
- ); - } - - if (error || !data) { - return ( -
-
- - {error || "No data"} -
- -
- ); - } - - return ( -
- {/* Main nav item style */} -
- - - {/* Minimize button */} - -
- - {/* 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 && ( -
-
- - - {data.total_users_used}/{data.total_users} - - users -
- - {/* User progress bar */} -
-
-
- - {(userMetrics.isOverLimit || userMetrics.isNearLimit) && ( -
- {userMetrics.isOverLimit ? ( - - ) : ( - - )} - Users {userMetrics.isOverLimit ? "Over Limit" : "Near Limit"} -
- )} -
- )} - - {/* Teams section */} - {data.total_teams !== null && ( -
-
- - - {data.total_teams_used}/{data.total_teams} - - teams -
- - {/* Team progress bar */} -
-
-
- - {(teamMetrics.isOverLimit || teamMetrics.isNearLimit) && ( -
- {teamMetrics.isOverLimit ? ( - - ) : ( - - )} - Teams {teamMetrics.isOverLimit ? "Over Limit" : "Near Limit"} -
- )} -
- )} -
- )} -
- ); - }; - - // Optimized CardStyleView for 220px width - const CardStyleView = () => { - if (isMinimized) { - return ( - - ); - } - - if (isLoading) { - return ( -
-
- - Loading... -
-
- ); - } - - if (error || !data) { - return ( -
-
-
- {error || "No data"} -
- -
-
- ); - } - - return ( -
-
-
- - Usage -
- -
- - {/* 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 && ( -
-
- - Users - - {userMetrics.isOverLimit ? "Over limit" : userMetrics.isNearLimit ? "Near limit" : "OK"} - -
-
- Used: - - {data.total_users_used}/{data.total_users} - -
-
- Remaining: - - {data.total_users_remaining} - -
-
- Usage: - {Math.round(userMetrics.usagePercentage)}% -
- - {/* User progress bar */} -
-
-
-
- )} - - {/* Teams section */} - {data.total_teams !== null && ( -
-
- - Teams - - {teamMetrics.isOverLimit ? "Over limit" : teamMetrics.isNearLimit ? "Near limit" : "OK"} - -
-
- Used: - - {data.total_teams_used}/{data.total_teams} - -
-
- Remaining: - - {data.total_teams_remaining} - -
-
- Usage: - {Math.round(teamMetrics.usagePercentage)}% -
- - {/* Team progress bar */} -
-
-
-
- )} -
-
- ); - }; - - // Don't render anything if disabled, no access token, or if both total_users and total_teams are null - if (disableUsageIndicator || !accessToken || (data?.total_users === null && data?.total_teams === null)) { - return null; - } - - // Fixed positioning with proper spacing from edges - return ( -
- -
- ); -}