diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 05ad536b260..9b55bf6ca0d 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -1655,17 +1655,6 @@ "count": 1 } }, - "src/components/UsageIndicator.tsx": { - "no-nested-ternary": { - "count": 4 - }, - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/static-components": { - "count": 1 - } - }, "src/components/activity_metrics.tsx": { "no-nested-ternary": { "count": 1 diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useDisableUsageIndicator.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useDisableUsageIndicator.test.ts deleted file mode 100644 index bd0e69c0de3..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useDisableUsageIndicator.test.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; -import { act, renderHook, waitFor } from "@testing-library/react"; -import { useDisableUsageIndicator } from "./useDisableUsageIndicator"; -import { LOCAL_STORAGE_EVENT } from "@/utils/localStorageUtils"; - -describe("useDisableUsageIndicator", () => { - const STORAGE_KEY = "disableUsageIndicator"; - - beforeEach(() => { - localStorage.clear(); - vi.clearAllMocks(); - }); - - afterEach(() => { - localStorage.clear(); - }); - - it("should return false when localStorage is empty", () => { - const { result } = renderHook(() => useDisableUsageIndicator()); - - expect(result.current).toBe(false); - }); - - it("should return false when localStorage value is not 'true'", () => { - localStorage.setItem(STORAGE_KEY, "false"); - - const { result } = renderHook(() => useDisableUsageIndicator()); - - expect(result.current).toBe(false); - }); - - it("should return true when localStorage value is 'true'", () => { - localStorage.setItem(STORAGE_KEY, "true"); - - const { result } = renderHook(() => useDisableUsageIndicator()); - - expect(result.current).toBe(true); - }); - - it("should return false when localStorage value is an empty string", () => { - localStorage.setItem(STORAGE_KEY, ""); - - const { result } = renderHook(() => useDisableUsageIndicator()); - - expect(result.current).toBe(false); - }); - - it("should update when storage event fires for the correct key", async () => { - const { result } = renderHook(() => useDisableUsageIndicator()); - - expect(result.current).toBe(false); - - await act(async () => { - localStorage.setItem(STORAGE_KEY, "true"); - const storageEvent = new StorageEvent("storage", { - key: STORAGE_KEY, - newValue: "true", - }); - window.dispatchEvent(storageEvent); - }); - - await waitFor(() => { - expect(result.current).toBe(true); - }); - }); - - it("should not update when storage event fires for a different key", () => { - localStorage.setItem(STORAGE_KEY, "false"); - const { result } = renderHook(() => useDisableUsageIndicator()); - - expect(result.current).toBe(false); - - const storageEvent = new StorageEvent("storage", { - key: "otherKey", - newValue: "true", - }); - window.dispatchEvent(storageEvent); - - expect(result.current).toBe(false); - }); - - it("should update when custom LOCAL_STORAGE_EVENT fires for the correct key", async () => { - const { result } = renderHook(() => useDisableUsageIndicator()); - - expect(result.current).toBe(false); - - await act(async () => { - localStorage.setItem(STORAGE_KEY, "true"); - const customEvent = new CustomEvent(LOCAL_STORAGE_EVENT, { - detail: { key: STORAGE_KEY }, - }); - window.dispatchEvent(customEvent); - }); - - await waitFor(() => { - expect(result.current).toBe(true); - }); - }); - - it("should not update when custom LOCAL_STORAGE_EVENT fires for a different key", () => { - localStorage.setItem(STORAGE_KEY, "false"); - const { result } = renderHook(() => useDisableUsageIndicator()); - - expect(result.current).toBe(false); - - const customEvent = new CustomEvent(LOCAL_STORAGE_EVENT, { - detail: { key: "otherKey" }, - }); - window.dispatchEvent(customEvent); - - expect(result.current).toBe(false); - }); - - it("should update when localStorage changes from false to true via custom event", async () => { - localStorage.setItem(STORAGE_KEY, "false"); - const { result } = renderHook(() => useDisableUsageIndicator()); - - expect(result.current).toBe(false); - - await act(async () => { - localStorage.setItem(STORAGE_KEY, "true"); - const customEvent = new CustomEvent(LOCAL_STORAGE_EVENT, { - detail: { key: STORAGE_KEY }, - }); - window.dispatchEvent(customEvent); - }); - - await waitFor(() => { - expect(result.current).toBe(true); - }); - }); - - it("should update when localStorage changes from true to false via storage event", async () => { - localStorage.setItem(STORAGE_KEY, "true"); - const { result } = renderHook(() => useDisableUsageIndicator()); - - expect(result.current).toBe(true); - - await act(async () => { - localStorage.setItem(STORAGE_KEY, "false"); - const storageEvent = new StorageEvent("storage", { - key: STORAGE_KEY, - newValue: "false", - }); - window.dispatchEvent(storageEvent); - }); - - await waitFor(() => { - expect(result.current).toBe(false); - }); - }); - - it("should cleanup event listeners on unmount", () => { - const addEventListenerSpy = vi.spyOn(window, "addEventListener"); - const removeEventListenerSpy = vi.spyOn(window, "removeEventListener"); - - const { unmount } = renderHook(() => useDisableUsageIndicator()); - - expect(addEventListenerSpy).toHaveBeenCalledTimes(2); - expect(addEventListenerSpy).toHaveBeenCalledWith("storage", expect.any(Function)); - expect(addEventListenerSpy).toHaveBeenCalledWith(LOCAL_STORAGE_EVENT, expect.any(Function)); - - unmount(); - - expect(removeEventListenerSpy).toHaveBeenCalledTimes(2); - expect(removeEventListenerSpy).toHaveBeenCalledWith("storage", expect.any(Function)); - expect(removeEventListenerSpy).toHaveBeenCalledWith(LOCAL_STORAGE_EVENT, expect.any(Function)); - }); - - it("should handle multiple hooks independently", async () => { - const { result: result1 } = renderHook(() => useDisableUsageIndicator()); - const { result: result2 } = renderHook(() => useDisableUsageIndicator()); - - expect(result1.current).toBe(false); - expect(result2.current).toBe(false); - - await act(async () => { - localStorage.setItem(STORAGE_KEY, "true"); - const customEvent = new CustomEvent(LOCAL_STORAGE_EVENT, { - detail: { key: STORAGE_KEY }, - }); - window.dispatchEvent(customEvent); - }); - - await waitFor(() => { - expect(result1.current).toBe(true); - expect(result2.current).toBe(true); - }); - }); -}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useDisableUsageIndicator.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useDisableUsageIndicator.ts deleted file mode 100644 index 7f4e2295090..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useDisableUsageIndicator.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { getLocalStorageItem, LOCAL_STORAGE_EVENT } from "@/utils/localStorageUtils"; -import { useSyncExternalStore } from "react"; - -function subscribe(callback: () => void) { - const onStorage = (e: StorageEvent) => { - if (e.key === "disableUsageIndicator") { - callback(); - } - }; - - const onCustom = (e: Event) => { - const { key } = (e as CustomEvent).detail; - if (key === "disableUsageIndicator") { - callback(); - } - }; - - window.addEventListener("storage", onStorage); - window.addEventListener(LOCAL_STORAGE_EVENT, onCustom); - - return () => { - window.removeEventListener("storage", onStorage); - window.removeEventListener(LOCAL_STORAGE_EVENT, onCustom); - }; -} - -function getSnapshot() { - return getLocalStorageItem("disableUsageIndicator") === "true"; -} - -export function useDisableUsageIndicator() { - return useSyncExternalStore(subscribe, getSnapshot); -} diff --git a/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx b/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx index 983ab980d2f..a71fc1b97a8 100644 --- a/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx +++ b/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx @@ -2,7 +2,6 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { useDisableBlogPosts } from "@/app/(dashboard)/hooks/useDisableBlogPosts"; import { useDisableBouncingIcon } from "@/app/(dashboard)/hooks/useDisableBouncingIcon"; import { useDisableShowPrompts } from "@/app/(dashboard)/hooks/useDisableShowPrompts"; -import { useDisableUsageIndicator } from "@/app/(dashboard)/hooks/useDisableUsageIndicator"; import { emitLocalStorageChange, getLocalStorageItem, @@ -72,7 +71,6 @@ interface UserDropdownProps { const UserDropdown: React.FC = ({ onLogout, variant = "navbar", collapsed = false }) => { const { userId, userEmail, userRole, premiumUser } = useAuthorized(); const disableShowPrompts = useDisableShowPrompts(); - const disableUsageIndicator = useDisableUsageIndicator(); const disableBlogPosts = useDisableBlogPosts(); const disableBouncingIcon = useDisableBouncingIcon(); const [disableShowNewBadge, setDisableShowNewBadge] = useState(false); @@ -165,23 +163,6 @@ const UserDropdown: React.FC = ({ onLogout, variant = "navbar aria-label="Toggle hide all prompts" /> - - Hide Usage Indicator - { - if (checked) { - setLocalStorageItem("disableUsageIndicator", "true"); - emitLocalStorageChange("disableUsageIndicator"); - } else { - removeLocalStorageItem("disableUsageIndicator"); - emitLocalStorageChange("disableUsageIndicator"); - } - }} - aria-label="Toggle hide usage indicator" - /> - Hide Blog Posts ({ useDisableShowPrompts: () => mockUseDisableShowPromptsImpl(), })); -vi.mock("@/app/(dashboard)/hooks/useDisableUsageIndicator", () => ({ - useDisableUsageIndicator: () => false, -})); - vi.mock("@/app/(dashboard)/hooks/useDisableBlogPosts", () => ({ useDisableBlogPosts: () => false, })); diff --git a/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx b/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx index 4842f25f36b..b7a16bcf09a 100644 --- a/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx +++ b/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx @@ -4,7 +4,6 @@ import { useDisableBlogPosts } from "@/app/(dashboard)/hooks/useDisableBlogPosts import { useDisableBouncingIcon } from "@/app/(dashboard)/hooks/useDisableBouncingIcon"; import { useDisableShowNewBadge } from "@/app/(dashboard)/hooks/useDisableShowNewBadge"; import { useDisableShowPrompts } from "@/app/(dashboard)/hooks/useDisableShowPrompts"; -import { useDisableUsageIndicator } from "@/app/(dashboard)/hooks/useDisableUsageIndicator"; import { emitLocalStorageChange, removeLocalStorageItem, setLocalStorageItem } from "@/utils/localStorageUtils"; import { navAccountDisplayName } from "@/components/Navbar/navDisplayName"; import CopyButton from "@/components/shared/CopyButton"; @@ -86,7 +85,6 @@ const SidebarAccountMenu: React.FC = ({ onLogout, colla const { data: healthData } = useHealthReadinessDetails(accessToken); const version = healthData?.litellm_version; const disableShowPrompts = useDisableShowPrompts(); - const disableUsageIndicator = useDisableUsageIndicator(); const disableBlogPosts = useDisableBlogPosts(); const disableBouncingIcon = useDisableBouncingIcon(); const disableShowNewBadge = useDisableShowNewBadge(); @@ -115,13 +113,6 @@ const SidebarAccountMenu: React.FC = ({ onLogout, colla checked: disableShowPrompts, onCheckedChange: (checked: boolean) => setFlag("disableShowPrompts", checked), }, - { - key: "disableUsageIndicator", - label: "Hide Usage Indicator", - ariaLabel: "Toggle hide usage indicator", - checked: disableUsageIndicator, - onCheckedChange: (checked: boolean) => setFlag("disableUsageIndicator", checked), - }, { key: "disableBlogPosts", label: "Hide Blog Posts", diff --git a/ui/litellm-dashboard/src/components/SidebarUsageCard.test.tsx b/ui/litellm-dashboard/src/components/SidebarUsageCard.test.tsx index 71ed3094a20..09e56d8fb5a 100644 --- a/ui/litellm-dashboard/src/components/SidebarUsageCard.test.tsx +++ b/ui/litellm-dashboard/src/components/SidebarUsageCard.test.tsx @@ -8,10 +8,6 @@ import type { LicenseInfo } from "./networking"; vi.mock("./networking", () => ({ getRemainingUsers: vi.fn() })); -vi.mock("@/app/(dashboard)/hooks/useDisableUsageIndicator", () => ({ - useDisableUsageIndicator: vi.fn(() => false), -})); - vi.mock("@/app/(dashboard)/hooks/license/useLicenseInfo", () => ({ useLicenseInfo: vi.fn(), })); diff --git a/ui/litellm-dashboard/src/components/SidebarUsageCard.tsx b/ui/litellm-dashboard/src/components/SidebarUsageCard.tsx index 2a6d6b43f38..fad8eb092cb 100644 --- a/ui/litellm-dashboard/src/components/SidebarUsageCard.tsx +++ b/ui/litellm-dashboard/src/components/SidebarUsageCard.tsx @@ -1,4 +1,3 @@ -import { useDisableUsageIndicator } from "@/app/(dashboard)/hooks/useDisableUsageIndicator"; import { useLicenseInfo } from "@/app/(dashboard)/hooks/license/useLicenseInfo"; import { formatExpirationStatus } from "@/utils/licenseUtils"; import { Button } from "@/components/ui/button"; @@ -69,7 +68,6 @@ const buildMeters = (data: RemainingUsage | null): MeterData[] => { * design's Spend / API-request meters are intentionally omitted. */ export default function SidebarUsageCard({ accessToken, collapsed, onExpandRail }: SidebarUsageCardProps) { - const disableUsageIndicator = useDisableUsageIndicator(); const licenseInfo = useLicenseInfo(accessToken).data ?? null; const { data: usageData, isLoading } = useQuery(remainingUsersQuery(accessToken)); const data = usageData ?? null; @@ -77,7 +75,7 @@ export default function SidebarUsageCard({ accessToken, collapsed, onExpandRail const hasData = data !== null && (data.total_users !== null || data.total_teams !== null); const noUsableData = !isLoading && !hasData; const noLicensedUsage = !licenseInfo?.has_license || noUsableData; - if (disableUsageIndicator || !accessToken || noLicensedUsage) { + if (!accessToken || noLicensedUsage) { return null; } 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 c587ebde57f..00000000000 --- a/ui/litellm-dashboard/src/components/UsageIndicator.test.tsx +++ /dev/null @@ -1,221 +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 { 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 } } }); - 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); - mockGetLicenseInfo.mockResolvedValue(null); - }); - - 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 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(); - - 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 6ff4008c2cf..00000000000 --- a/ui/litellm-dashboard/src/components/UsageIndicator.tsx +++ /dev/null @@ -1,669 +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 { formatExpirationStatus, 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; -} - -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} - {formatExpirationStatus(licenseInfo.expiration_date)} -
-
- )} - - {/* 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: - - {formatExpirationStatus(licenseInfo.expiration_date)} - -
- {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 ( -
- -
- ); -}