diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx
index 405f8329b67..a74d3c108d6 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx
@@ -31,7 +31,7 @@ import {
import * as React from "react";
import { useRouter, usePathname } from "next/navigation";
import { all_admin_roles, internalUserRoles, isAdminRole, rolesWithWriteAccess } from "@/utils/roles";
-import UsageIndicator from "@/components/usage_indicator";
+import UsageIndicator from "@/components/UsageIndicator";
import { serverRootPath } from "@/components/networking";
const { Sider } = Layout;
@@ -64,7 +64,7 @@ const getBasePath = () => {
const raw = process.env.NEXT_PUBLIC_BASE_URL ?? "";
const trimmed = raw.replace(/^\/+|\/+$/g, ""); // strip leading/trailing slashes
const uiPath = trimmed ? `/${trimmed}/` : "/";
-
+
// If serverRootPath is set and not "/", prepend it to the UI path
if (serverRootPath && serverRootPath !== "/") {
// Remove trailing slash from serverRootPath and ensure uiPath has no leading slash for proper joining
@@ -72,7 +72,7 @@ const getBasePath = () => {
const cleanUiPath = uiPath.replace(/^\/+/, "");
return `${cleanServerRoot}/${cleanUiPath}`;
}
-
+
return uiPath;
};
@@ -153,170 +153,170 @@ const toHref = (slugOrPath: string) => {
// ----- Menu config (unchanged labels/icons; same appearance) -----
const menuItems: MenuItemCfg[] = [
- { key: "1", page: "api-keys", label: "Virtual Keys", icon: },
- {
- key: "3",
- page: "llm-playground",
- label: "Test Key",
- icon: ,
- roles: rolesWithWriteAccess,
- },
- {
- key: "2",
- page: "models",
- label: "Models + Endpoints",
- icon: ,
- roles: rolesWithWriteAccess,
- },
- {
- key: "12",
- page: "new_usage",
- label: "Usage",
- icon: ,
- roles: [...all_admin_roles, ...internalUserRoles],
- },
- { key: "6", page: "teams", label: "Teams", icon: },
- {
- key: "17",
- page: "organizations",
- label: "Organizations",
- icon: ,
- roles: all_admin_roles,
- },
- {
- key: "5",
- page: "users",
- label: "Internal Users",
- icon: ,
- roles: all_admin_roles,
- },
- { key: "14", page: "api_ref", label: "API Reference", icon: },
- {
- key: "16",
- page: "model-hub-table",
- label: "Model Hub",
- icon: ,
- },
- { key: "15", page: "logs", label: "Logs", icon: },
- {
- key: "11",
- page: "guardrails",
- label: "Guardrails",
- icon: ,
- roles: all_admin_roles,
- },
- {
- key: "28",
- page: "policies",
- label: "Policies",
- icon: ,
- roles: all_admin_roles,
- },
- {
- key: "26",
- page: "tools",
- label: "Tools",
- icon: ,
- children: [
- { key: "18", page: "mcp-servers", label: "MCP Servers", icon: },
- {
- key: "21",
- page: "vector-stores",
- label: "Vector Stores",
- icon: ,
- roles: all_admin_roles,
- },
- ],
- },
- {
- key: "experimental",
- page: "experimental",
- label: "Experimental",
- icon: ,
- children: [
- {
- key: "9",
- page: "caching",
- label: "Caching",
- icon: ,
- roles: all_admin_roles,
- },
- {
- key: "25",
- page: "prompts",
- label: "Prompts",
- icon: ,
- roles: all_admin_roles,
- },
- {
- key: "10",
- page: "budgets",
- label: "Budgets",
- icon: ,
- roles: all_admin_roles,
- },
- {
- key: "20",
- page: "transform-request",
- label: "API Playground",
- icon: ,
- roles: [...all_admin_roles, ...internalUserRoles],
- },
- {
- key: "19",
- page: "tag-management",
- label: "Tag Management",
- icon: ,
- roles: all_admin_roles,
- },
- {
- key: "27",
- page: "claude-code-plugins",
- label: "Claude Code Plugins",
- icon: ,
- roles: all_admin_roles,
- },
- { key: "4", page: "usage", label: "Old Usage", icon: },
- ],
- },
- {
- key: "settings",
- page: "settings",
- label: "Settings",
- icon: ,
- roles: all_admin_roles,
- children: [
- {
- key: "11",
- page: "general-settings",
- label: "Router Settings",
- icon: ,
- roles: all_admin_roles,
- },
- {
- key: "8",
- page: "settings",
- label: "Logging & Alerts",
- icon: ,
- roles: all_admin_roles,
- },
- {
- key: "13",
- page: "admin-panel",
- label: "Admin Settings",
- icon: ,
- roles: all_admin_roles,
- },
- {
- key: "14",
- page: "ui-theme",
- label: "UI Theme",
- icon: ,
- roles: all_admin_roles,
- },
- ],
- },
- ];
+ { key: "1", page: "api-keys", label: "Virtual Keys", icon: },
+ {
+ key: "3",
+ page: "llm-playground",
+ label: "Test Key",
+ icon: ,
+ roles: rolesWithWriteAccess,
+ },
+ {
+ key: "2",
+ page: "models",
+ label: "Models + Endpoints",
+ icon: ,
+ roles: rolesWithWriteAccess,
+ },
+ {
+ key: "12",
+ page: "new_usage",
+ label: "Usage",
+ icon: ,
+ roles: [...all_admin_roles, ...internalUserRoles],
+ },
+ { key: "6", page: "teams", label: "Teams", icon: },
+ {
+ key: "17",
+ page: "organizations",
+ label: "Organizations",
+ icon: ,
+ roles: all_admin_roles,
+ },
+ {
+ key: "5",
+ page: "users",
+ label: "Internal Users",
+ icon: ,
+ roles: all_admin_roles,
+ },
+ { key: "14", page: "api_ref", label: "API Reference", icon: },
+ {
+ key: "16",
+ page: "model-hub-table",
+ label: "Model Hub",
+ icon: ,
+ },
+ { key: "15", page: "logs", label: "Logs", icon: },
+ {
+ key: "11",
+ page: "guardrails",
+ label: "Guardrails",
+ icon: ,
+ roles: all_admin_roles,
+ },
+ {
+ key: "28",
+ page: "policies",
+ label: "Policies",
+ icon: ,
+ roles: all_admin_roles,
+ },
+ {
+ key: "26",
+ page: "tools",
+ label: "Tools",
+ icon: ,
+ children: [
+ { key: "18", page: "mcp-servers", label: "MCP Servers", icon: },
+ {
+ key: "21",
+ page: "vector-stores",
+ label: "Vector Stores",
+ icon: ,
+ roles: all_admin_roles,
+ },
+ ],
+ },
+ {
+ key: "experimental",
+ page: "experimental",
+ label: "Experimental",
+ icon: ,
+ children: [
+ {
+ key: "9",
+ page: "caching",
+ label: "Caching",
+ icon: ,
+ roles: all_admin_roles,
+ },
+ {
+ key: "25",
+ page: "prompts",
+ label: "Prompts",
+ icon: ,
+ roles: all_admin_roles,
+ },
+ {
+ key: "10",
+ page: "budgets",
+ label: "Budgets",
+ icon: ,
+ roles: all_admin_roles,
+ },
+ {
+ key: "20",
+ page: "transform-request",
+ label: "API Playground",
+ icon: ,
+ roles: [...all_admin_roles, ...internalUserRoles],
+ },
+ {
+ key: "19",
+ page: "tag-management",
+ label: "Tag Management",
+ icon: ,
+ roles: all_admin_roles,
+ },
+ {
+ key: "27",
+ page: "claude-code-plugins",
+ label: "Claude Code Plugins",
+ icon: ,
+ roles: all_admin_roles,
+ },
+ { key: "4", page: "usage", label: "Old Usage", icon: },
+ ],
+ },
+ {
+ key: "settings",
+ page: "settings",
+ label: "Settings",
+ icon: ,
+ roles: all_admin_roles,
+ children: [
+ {
+ key: "11",
+ page: "general-settings",
+ label: "Router Settings",
+ icon: ,
+ roles: all_admin_roles,
+ },
+ {
+ key: "8",
+ page: "settings",
+ label: "Logging & Alerts",
+ icon: ,
+ roles: all_admin_roles,
+ },
+ {
+ key: "13",
+ page: "admin-panel",
+ label: "Admin Settings",
+ icon: ,
+ roles: all_admin_roles,
+ },
+ {
+ key: "14",
+ page: "ui-theme",
+ label: "UI Theme",
+ icon: ,
+ roles: all_admin_roles,
+ },
+ ],
+ },
+];
const Sidebar2: React.FC = ({ accessToken, userRole, defaultSelectedKey, collapsed = false }) => {
const router = useRouter();
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useDisableUsageIndicator.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useDisableUsageIndicator.test.ts
new file mode 100644
index 00000000000..bd0e69c0de3
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useDisableUsageIndicator.test.ts
@@ -0,0 +1,190 @@
+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
new file mode 100644
index 00000000000..7f4e2295090
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useDisableUsageIndicator.ts
@@ -0,0 +1,33 @@
+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 f80af33f9e2..90e02ae447b 100644
--- a/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx
+++ b/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx
@@ -1,5 +1,6 @@
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useDisableShowPrompts } from "@/app/(dashboard)/hooks/useDisableShowPrompts";
+import { useDisableUsageIndicator } from "@/app/(dashboard)/hooks/useDisableUsageIndicator";
import {
emitLocalStorageChange,
getLocalStorageItem,
@@ -27,6 +28,7 @@ interface UserDropdownProps {
const UserDropdown: React.FC = ({ onLogout }) => {
const { userId, userEmail, userRole, premiumUser } = useAuthorized();
const disableShowPrompts = useDisableShowPrompts();
+ const disableUsageIndicator = useDisableUsageIndicator();
const [disableShowNewBadge, setDisableShowNewBadge] = useState(false);
useEffect(() => {
@@ -129,6 +131,23 @@ const UserDropdown: React.FC = ({ onLogout }) => {
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"
+ />
+
);
diff --git a/ui/litellm-dashboard/src/components/UsageIndicator.test.tsx b/ui/litellm-dashboard/src/components/UsageIndicator.test.tsx
new file mode 100644
index 00000000000..71a37263980
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/UsageIndicator.test.tsx
@@ -0,0 +1,186 @@
+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 UsageIndicator from "./UsageIndicator";
+
+vi.mock("./networking", () => ({
+ getRemainingUsers: vi.fn(),
+}));
+
+vi.mock("@/app/(dashboard)/hooks/useDisableUsageIndicator", () => ({
+ useDisableUsageIndicator: vi.fn(() => false),
+}));
+
+import { getRemainingUsers } from "./networking";
+
+const mockGetRemainingUsers = vi.mocked(getRemainingUsers);
+
+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 () => {
+ render();
+
+ 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 () => {
+ render();
+
+ 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,
+ });
+
+ render();
+
+ 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,
+ });
+
+ render();
+
+ 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,
+ });
+
+ render();
+
+ 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,
+ });
+
+ render();
+
+ await screen.findByText("Usage");
+
+ expect(screen.getByText("Teams")).toBeInTheDocument();
+ expect(screen.getByText("Over limit")).toBeInTheDocument();
+ });
+
+ it("should render nothing when accessToken is null", () => {
+ render();
+
+ 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);
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.queryByText("Usage")).not.toBeInTheDocument();
+ });
+
+ (useDisableUsageIndicator as ReturnType).mockReturnValue(false);
+ });
+
+ it("should show Loading while fetching", () => {
+ mockGetRemainingUsers.mockImplementation(() => new Promise(() => {}));
+
+ render();
+
+ 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"));
+
+ render();
+
+ 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();
+ render();
+
+ 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();
+ render();
+
+ 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/usage_indicator.tsx b/ui/litellm-dashboard/src/components/UsageIndicator.tsx
similarity index 98%
rename from ui/litellm-dashboard/src/components/usage_indicator.tsx
rename to ui/litellm-dashboard/src/components/UsageIndicator.tsx
index ed5e8e07555..3976e4d3d6f 100644
--- a/ui/litellm-dashboard/src/components/usage_indicator.tsx
+++ b/ui/litellm-dashboard/src/components/UsageIndicator.tsx
@@ -1,3 +1,4 @@
+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 { useEffect, useState } from "react";
@@ -23,7 +24,7 @@ interface UsageData {
}
export default function UsageIndicator({ accessToken, width = 220 }: UsageIndicatorProps) {
- const position = "bottom-left";
+ const disableUsageIndicator = useDisableUsageIndicator();
const [isExpanded, setIsExpanded] = useState(false);
const [isMinimized, setIsMinimized] = useState(false);
const [data, setData] = useState(null);
@@ -541,8 +542,8 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
);
};
- // Don't render anything if no access token or if both total_users and total_teams are null
- if (!accessToken || (data?.total_users === null && data?.total_teams === null)) {
+ // 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;
}
diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx
index b26590989d2..e35d70a65bb 100644
--- a/ui/litellm-dashboard/src/components/leftnav.tsx
+++ b/ui/litellm-dashboard/src/components/leftnav.tsx
@@ -29,9 +29,9 @@ import type { MenuProps } from "antd";
import { ConfigProvider, Layout, Menu } from "antd";
import { useMemo } from "react";
import { all_admin_roles, internalUserRoles, isAdminRole, rolesWithWriteAccess } from "../utils/roles";
-import type { Organization } from "./networking";
-import UsageIndicator from "./usage_indicator";
import NewBadge from "./common_components/NewBadge";
+import type { Organization } from "./networking";
+import UsageIndicator from "./UsageIndicator";
const { Sider } = Layout;
// Define the props type
diff --git a/ui/litellm-dashboard/src/components/usage_indicator.test.tsx b/ui/litellm-dashboard/src/components/usage_indicator.test.tsx
deleted file mode 100644
index 5d82d5f1540..00000000000
--- a/ui/litellm-dashboard/src/components/usage_indicator.test.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import React from "react";
-import { describe, it, expect, vi } from "vitest";
-import { render, waitFor } from "@testing-library/react";
-import UsageIndicator from "./usage_indicator";
-
-vi.mock("./networking", () => {
- return {
- getRemainingUsers: vi.fn(),
- };
-});
-
-import { getRemainingUsers } from "./networking";
-
-describe("UsageIndicator", () => {
- it("does not show Near limit when users usage is below 80% (1/100 -> 1%)", async () => {
- (getRemainingUsers as unknown as ReturnType).mockResolvedValue({
- total_users: 100,
- total_users_used: 1,
- total_users_remaining: 99,
- total_teams: null,
- total_teams_used: 0,
- total_teams_remaining: null,
- });
-
- const { queryByText, findByText } = render();
-
- await findByText("Usage");
-
- expect(queryByText("Near limit")).toBeNull();
- });
-
- it("handles null totals shape by rendering nothing", async () => {
- (getRemainingUsers as unknown as ReturnType).mockResolvedValue({
- total_users: null,
- total_teams: null,
- total_users_used: 520,
- total_teams_used: 4,
- total_teams_remaining: null,
- total_users_remaining: null,
- });
-
- const { container } = render();
-
- await waitFor(() => {
- expect(container.firstChild).toBeNull();
- });
- });
-
- it("shows Near limit for Teams at 80% usage (4/5)", async () => {
- (getRemainingUsers as unknown as ReturnType).mockResolvedValue({
- total_users: null,
- total_users_used: 0,
- total_users_remaining: null,
- total_teams: 5,
- total_teams_used: 4,
- total_teams_remaining: 1,
- });
-
- const { findByText, getByText } = render();
-
- await findByText("Usage");
-
- // Teams section should show Near limit indicator
- expect(getByText("Teams")).toBeTruthy();
- expect(getByText("Near limit")).toBeTruthy();
- });
-
- it("shows Over limit for Users when usage exceeds 100% (105/100)", async () => {
- (getRemainingUsers as unknown as ReturnType).mockResolvedValue({
- total_users: 100,
- total_users_used: 105,
- total_users_remaining: -5,
- total_teams: null,
- total_teams_used: 0,
- total_teams_remaining: null,
- });
-
- const { findByText, getByText } = render();
-
- await findByText("Usage");
-
- // Users section should show Over limit indicator
- expect(getByText("Users")).toBeTruthy();
- expect(getByText("Over limit")).toBeTruthy();
- });
-});