diff --git a/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.test.tsx b/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.test.tsx new file mode 100644 index 00000000000..de853303c15 --- /dev/null +++ b/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.test.tsx @@ -0,0 +1,289 @@ +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { renderWithProviders, screen, waitFor } from "../../../../tests/test-utils"; +import UserDropdown from "./UserDropdown"; + +let mockUseAuthorizedImpl = () => ({ + userId: "test-user-id", + userEmail: "test@example.com", + userRole: "Admin", + premiumUser: false, +}); + +let mockUseDisableShowPromptsImpl = () => false; + +let mockGetLocalStorageItemImpl = (key: string): string | null => { + if (key === "disableShowNewBadge") return null; + if (key === "disableShowPrompts") return null; + return null; +}; + +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: () => mockUseAuthorizedImpl(), +})); + +vi.mock("@/app/(dashboard)/hooks/useDisableShowPrompts", () => ({ + useDisableShowPrompts: () => mockUseDisableShowPromptsImpl(), +})); + +vi.mock("@/utils/localStorageUtils", () => ({ + LOCAL_STORAGE_EVENT: "local-storage-change", + getLocalStorageItem: (key: string) => mockGetLocalStorageItemImpl(key), + setLocalStorageItem: vi.fn(), + removeLocalStorageItem: vi.fn(), + emitLocalStorageChange: vi.fn(), +})); + +describe("UserDropdown", () => { + const mockOnLogout = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + mockUseAuthorizedImpl = () => ({ + userId: "test-user-id", + userEmail: "test@example.com", + userRole: "Admin", + premiumUser: false, + }); + mockUseDisableShowPromptsImpl = () => false; + mockGetLocalStorageItemImpl = (key: string): string | null => { + if (key === "disableShowNewBadge") return null; + if (key === "disableShowPrompts") return null; + return null; + }; + }); + + it("should render", () => { + renderWithProviders(); + expect(screen.getByRole("button")).toBeInTheDocument(); + }); + + it("should display user button with User text", () => { + renderWithProviders(); + expect(screen.getByText("User")).toBeInTheDocument(); + }); + + it("should show user email when dropdown is opened", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await user.click(screen.getByText("User")); + + await waitFor(() => { + expect(screen.getByText("test@example.com")).toBeInTheDocument(); + }); + }); + + it("should show user ID when dropdown is opened", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await user.click(screen.getByText("User")); + + await waitFor(() => { + expect(screen.getByText("test-user-id")).toBeInTheDocument(); + }); + }); + + it("should show user role when dropdown is opened", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await user.click(screen.getByText("User")); + + await waitFor(() => { + expect(screen.getByText("Admin")).toBeInTheDocument(); + }); + }); + + it("should display Standard badge for non-premium users", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await user.click(screen.getByText("User")); + + await waitFor(() => { + expect(screen.getByText("Standard")).toBeInTheDocument(); + }); + }); + + it("should display Premium badge for premium users", async () => { + const user = userEvent.setup(); + mockUseAuthorizedImpl = () => ({ + userId: "test-user-id", + userEmail: "test@example.com", + userRole: "Admin", + premiumUser: true, + }); + + renderWithProviders(); + + await user.click(screen.getByText("User")); + + await waitFor(() => { + expect(screen.getByText("Premium")).toBeInTheDocument(); + }); + }); + + it("should call onLogout when logout is clicked", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await user.click(screen.getByText("User")); + + await waitFor(() => { + expect(screen.getByText("test@example.com")).toBeInTheDocument(); + }); + + await user.click(screen.getByText("Logout")); + + expect(mockOnLogout).toHaveBeenCalledTimes(1); + }); + + it("should toggle hide new feature indicators switch", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await user.click(screen.getByText("User")); + + await waitFor(() => { + expect(screen.getByText("test@example.com")).toBeInTheDocument(); + }); + + const toggle = screen.getByLabelText("Toggle hide new feature indicators"); + expect(toggle).not.toBeChecked(); + + await user.click(toggle); + + const localStorageUtils = vi.mocked(await import("@/utils/localStorageUtils")); + expect(localStorageUtils.setLocalStorageItem).toHaveBeenCalledWith("disableShowNewBadge", "true"); + expect(localStorageUtils.emitLocalStorageChange).toHaveBeenCalledWith("disableShowNewBadge"); + }); + + it("should toggle hide new feature indicators switch off", async () => { + const user = userEvent.setup(); + mockGetLocalStorageItemImpl = (key: string): string | null => { + if (key === "disableShowNewBadge") return "true"; + return null; + }; + + renderWithProviders(); + + await user.click(screen.getByText("User")); + + await waitFor(() => { + expect(screen.getByText("test@example.com")).toBeInTheDocument(); + }); + + const toggle = screen.getByLabelText("Toggle hide new feature indicators"); + expect(toggle).toBeChecked(); + + await user.click(toggle); + + const localStorageUtils = vi.mocked(await import("@/utils/localStorageUtils")); + expect(localStorageUtils.removeLocalStorageItem).toHaveBeenCalledWith("disableShowNewBadge"); + expect(localStorageUtils.emitLocalStorageChange).toHaveBeenCalledWith("disableShowNewBadge"); + }); + + it("should toggle hide all prompts switch", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await user.click(screen.getByText("User")); + + await waitFor(() => { + expect(screen.getByText("test@example.com")).toBeInTheDocument(); + }); + + const toggle = screen.getByLabelText("Toggle hide all prompts"); + expect(toggle).not.toBeChecked(); + + await user.click(toggle); + + const localStorageUtils = vi.mocked(await import("@/utils/localStorageUtils")); + expect(localStorageUtils.setLocalStorageItem).toHaveBeenCalledWith("disableShowPrompts", "true"); + expect(localStorageUtils.emitLocalStorageChange).toHaveBeenCalledWith("disableShowPrompts"); + }); + + it("should toggle hide all prompts switch off", async () => { + const user = userEvent.setup(); + mockUseDisableShowPromptsImpl = () => true; + mockGetLocalStorageItemImpl = (key: string): string | null => { + if (key === "disableShowPrompts") return "true"; + return null; + }; + + renderWithProviders(); + + await user.click(screen.getByText("User")); + + await waitFor(() => { + expect(screen.getByText("test@example.com")).toBeInTheDocument(); + }); + + const toggle = screen.getByLabelText("Toggle hide all prompts"); + expect(toggle).toBeChecked(); + + await user.click(toggle); + + const localStorageUtils = vi.mocked(await import("@/utils/localStorageUtils")); + expect(localStorageUtils.removeLocalStorageItem).toHaveBeenCalledWith("disableShowPrompts"); + expect(localStorageUtils.emitLocalStorageChange).toHaveBeenCalledWith("disableShowPrompts"); + }); + + it("should display dash when user email is not available", async () => { + const user = userEvent.setup(); + mockUseAuthorizedImpl = () => ({ + userId: "test-user-id", + userEmail: null as any, + userRole: "Admin", + premiumUser: false, + }); + + renderWithProviders(); + + await user.click(screen.getByText("User")); + + await waitFor(() => { + expect(screen.getByText("-")).toBeInTheDocument(); + }); + }); + + it("should display dash when user ID is not available", async () => { + const user = userEvent.setup(); + mockUseAuthorizedImpl = () => ({ + userId: null as any, + userEmail: "test@example.com", + userRole: "Admin", + premiumUser: false, + }); + + renderWithProviders(); + + await user.click(screen.getByText("User")); + + await waitFor(() => { + const dashElements = screen.getAllByText("-"); + expect(dashElements.length).toBeGreaterThan(0); + }); + }); + + it("should initialize hide new feature indicators from localStorage", async () => { + const user = userEvent.setup(); + mockGetLocalStorageItemImpl = (key: string): string | null => { + if (key === "disableShowNewBadge") return "true"; + return null; + }; + + renderWithProviders(); + + await user.click(screen.getByText("User")); + + await waitFor(() => { + expect(screen.getByText("test@example.com")).toBeInTheDocument(); + }); + + const toggle = screen.getByLabelText("Toggle hide new feature indicators"); + expect(toggle).toBeChecked(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx b/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx new file mode 100644 index 00000000000..f80af33f9e2 --- /dev/null +++ b/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx @@ -0,0 +1,161 @@ +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { useDisableShowPrompts } from "@/app/(dashboard)/hooks/useDisableShowPrompts"; +import { + emitLocalStorageChange, + getLocalStorageItem, + removeLocalStorageItem, + setLocalStorageItem, +} from "@/utils/localStorageUtils"; +import { + CrownOutlined, + DownOutlined, + LogoutOutlined, + MailOutlined, + SafetyOutlined, + UserOutlined, +} from "@ant-design/icons"; +import type { MenuProps } from "antd"; +import { Button, Divider, Dropdown, Space, Switch, Tag, Tooltip, Typography } from "antd"; +import React, { useEffect, useState } from "react"; + +const { Text } = Typography; + +interface UserDropdownProps { + onLogout: () => void; +} + +const UserDropdown: React.FC = ({ onLogout }) => { + const { userId, userEmail, userRole, premiumUser } = useAuthorized(); + const disableShowPrompts = useDisableShowPrompts(); + const [disableShowNewBadge, setDisableShowNewBadge] = useState(false); + + useEffect(() => { + const storedValue = getLocalStorageItem("disableShowNewBadge"); + setDisableShowNewBadge(storedValue === "true"); + }, []); + + const userItems: MenuProps["items"] = [ + { + key: "logout", + label: ( + + + Logout + + ), + onClick: onLogout, + }, + ]; + + const renderUserInfoSection = () => ( + + + + + {userEmail || "-"} + + {premiumUser ? ( + } + color="gold" + > + Premium + + ) : ( + + } + > + Standard + + + )} + + + + + + User ID + + + {userId || "-"} + + + + + + Role + + {userRole} + + + + Hide New Feature Indicators + { + setDisableShowNewBadge(checked); + if (checked) { + setLocalStorageItem("disableShowNewBadge", "true"); + emitLocalStorageChange("disableShowNewBadge"); + } else { + removeLocalStorageItem("disableShowNewBadge"); + emitLocalStorageChange("disableShowNewBadge"); + } + }} + aria-label="Toggle hide new feature indicators" + /> + + + Hide All Prompts + { + if (checked) { + setLocalStorageItem("disableShowPrompts", "true"); + emitLocalStorageChange("disableShowPrompts"); + } else { + removeLocalStorageItem("disableShowPrompts"); + emitLocalStorageChange("disableShowPrompts"); + } + }} + aria-label="Toggle hide all prompts" + /> + + + ); + + return ( + ( +
+ {renderUserInfoSection()} + + {React.cloneElement(menu as React.ReactElement, { + style: { boxShadow: "none" }, + })} +
+ )} + > + +
+ ); +}; + +export default UserDropdown; diff --git a/ui/litellm-dashboard/src/components/navbar.test.tsx b/ui/litellm-dashboard/src/components/navbar.test.tsx index 9fa32cf9cb0..a2996f70587 100644 --- a/ui/litellm-dashboard/src/components/navbar.test.tsx +++ b/ui/litellm-dashboard/src/components/navbar.test.tsx @@ -15,8 +15,14 @@ vi.mock("@/utils/proxyUtils", () => ({ // Create mock functions that can be controlled in tests let mockUseThemeImpl = () => ({ logoUrl: null as string | null }); let mockUseHealthReadinessImpl = () => ({ data: null as any }); -let mockGetLocalStorageItemImpl = () => null as string | null; +let mockGetLocalStorageItemImpl = (key: string) => null as string | null; let mockUseDisableShowPromptsImpl = () => false; +let mockUseAuthorizedImpl = () => ({ + userId: "test-user", + userEmail: "test@example.com", + userRole: "Admin", + premiumUser: false, +}); vi.mock("@/contexts/ThemeContext", () => ({ useTheme: () => mockUseThemeImpl(), @@ -30,9 +36,13 @@ vi.mock("@/app/(dashboard)/hooks/useDisableShowPrompts", () => ({ useDisableShowPrompts: () => mockUseDisableShowPromptsImpl(), })); +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: () => mockUseAuthorizedImpl(), +})); + vi.mock("@/utils/localStorageUtils", () => ({ LOCAL_STORAGE_EVENT: "local-storage-change", - getLocalStorageItem: () => mockGetLocalStorageItemImpl(), + getLocalStorageItem: (key: string) => mockGetLocalStorageItemImpl(key), setLocalStorageItem: vi.fn(), removeLocalStorageItem: vi.fn(), emitLocalStorageChange: vi.fn(), @@ -123,14 +133,27 @@ describe("Navbar", () => { it("should show premium user badge when premiumUser is true", async () => { const user = userEvent.setup(); - const premiumProps = { ...defaultProps, premiumUser: true }; - renderWithProviders(); + mockUseAuthorizedImpl = () => ({ + userId: "test-user", + userEmail: "test@example.com", + userRole: "Admin", + premiumUser: true, + }); + renderWithProviders(); await user.click(screen.getByText("User")); await waitFor(() => { expect(screen.getByText("Premium")).toBeInTheDocument(); }); + + // Reset mock + mockUseAuthorizedImpl = () => ({ + userId: "test-user", + userEmail: "test@example.com", + userRole: "Admin", + premiumUser: false, + }); }); it("should show version badge when health data contains version", () => { @@ -167,7 +190,10 @@ describe("Navbar", () => { const user = userEvent.setup(); // Initially disabled - mockGetLocalStorageItemImpl = () => "false"; + mockGetLocalStorageItemImpl = (key: string) => { + if (key === "disableShowNewBadge") return "false"; + return null; + }; renderWithProviders(); @@ -186,6 +212,9 @@ describe("Navbar", () => { const localStorageUtils = vi.mocked(await import("@/utils/localStorageUtils")); expect(localStorageUtils.setLocalStorageItem).toHaveBeenCalledWith("disableShowNewBadge", "true"); expect(localStorageUtils.emitLocalStorageChange).toHaveBeenCalledWith("disableShowNewBadge"); + + // Reset mock + mockGetLocalStorageItemImpl = (key: string) => null; }); it("should handle logout functionality", async () => { diff --git a/ui/litellm-dashboard/src/components/navbar.tsx b/ui/litellm-dashboard/src/components/navbar.tsx index fca5f7e656c..3649ca76238 100644 --- a/ui/litellm-dashboard/src/components/navbar.tsx +++ b/ui/litellm-dashboard/src/components/navbar.tsx @@ -1,32 +1,20 @@ import { useHealthReadiness } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadiness"; -import { useDisableShowPrompts } from "@/app/(dashboard)/hooks/useDisableShowPrompts"; import { getProxyBaseUrl } from "@/components/networking"; import { useTheme } from "@/contexts/ThemeContext"; import { clearTokenCookies } from "@/utils/cookieUtils"; -import { - emitLocalStorageChange, - getLocalStorageItem, - removeLocalStorageItem, - setLocalStorageItem, -} from "@/utils/localStorageUtils"; import { fetchProxySettings } from "@/utils/proxyUtils"; import { - CrownOutlined, GithubOutlined, - LogoutOutlined, - MailOutlined, MenuFoldOutlined, MenuUnfoldOutlined, MoonOutlined, - SafetyOutlined, SlackOutlined, SunOutlined, - UserOutlined, } from "@ant-design/icons"; -import type { MenuProps } from "antd"; -import { Button, Dropdown, Switch, Tag, Tooltip } from "antd"; +import { Button, Switch, Tag } from "antd"; import Link from "next/link"; import React, { useEffect, useState } from "react"; +import UserDropdown from "./Navbar/UserDropdown/UserDropdown"; interface NavbarProps { userID: string | null; @@ -59,8 +47,6 @@ const Navbar: React.FC = ({ }) => { const baseUrl = getProxyBaseUrl(); const [logoutUrl, setLogoutUrl] = useState(""); - const [disableShowNewBadge, setDisableShowNewBadge] = useState(false); - const disableShowPrompts = useDisableShowPrompts(); const { logoUrl } = useTheme(); const { data: healthData } = useHealthReadiness(); const version = healthData?.litellm_version; @@ -82,11 +68,6 @@ const Navbar: React.FC = ({ initializeProxySettings(); }, [accessToken]); - useEffect(() => { - const storedValue = getLocalStorageItem("disableShowNewBadge"); - setDisableShowNewBadge(storedValue === "true"); - }, []); - useEffect(() => { setLogoutUrl(proxySettings?.PROXY_LOGOUT_URL || ""); }, [proxySettings]); @@ -96,105 +77,6 @@ const Navbar: React.FC = ({ window.location.href = logoutUrl; }; - const userItems: MenuProps["items"] = [ - { - key: "user-info", - // Prevent dropdown from closing when interacting with the toggle - onClick: (info) => info.domEvent?.stopPropagation(), - label: ( -
-
-
- - {userID} -
- {premiumUser ? ( - -
- - Premium -
-
- ) : ( - -
- - Standard -
-
- )} -
-
-
- - Role - {userRole} -
-
- - Email - - {userEmail || "Unknown"} - -
-
e.stopPropagation()} - > - Hide New Feature Indicators - { - setDisableShowNewBadge(checked); - if (checked) { - setLocalStorageItem("disableShowNewBadge", "true"); - emitLocalStorageChange("disableShowNewBadge"); - } else { - removeLocalStorageItem("disableShowNewBadge"); - emitLocalStorageChange("disableShowNewBadge"); - } - }} - aria-label="Toggle hide new feature indicators" - /> -
-
e.stopPropagation()} - > - Hide All Prompts - { - if (checked) { - setLocalStorageItem("disableShowPrompts", "true"); - emitLocalStorageChange("disableShowPrompts"); - } else { - removeLocalStorageItem("disableShowPrompts"); - emitLocalStorageChange("disableShowPrompts"); - } - }} - aria-label="Toggle hide all prompts" - /> -
-
-
- ), - }, - { - key: "logout", - label: ( -
- - Logout -
- ), - }, - ]; - return (