From f9ed4f8aeaa602256faf979d1ca79b322322e2c4 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Sat, 11 Jul 2026 14:30:04 -0700 Subject: [PATCH] feat(ui): add redesigned sidebar account menu (#32931) * feat(ui): add redesigned sidebar account menu Introduce SidebarAccountMenu, a sidebar-only account/logout menu built on shadcn Popover/Switch/Badge/Separator/Button, and wire it into leftnav in place of the shared UserDropdown. The panel has a LiteLLM header with the bouncing moon and a clickable version tag, Tier/Role/Email/User ID rows with copy actions, the five display toggles, and Logout. UserDropdown is left untouched so the control-plane / chat navbar keeps its existing menu. The version tag links to the same release notes page as the navbar tag, and the bouncing icon reuses the existing header animation gated by the Hide Bouncing Icon toggle. * test(ui): point account-menu e2e specs at the migrated sidebar menu The sidebar account menu moved from an antd Dropdown to a Base UI popover (SidebarAccountMenu), so the login, logout, proxy-logout-url, and internal user identity specs were still waiting on antd-era locators (.ant-dropdown, the popupRender wrapper class, the user-dropdown-panel test id, and a menuitem-role Logout). Point them at the new panel test id (sidebar-account-menu-panel) and the button-role Logout instead. The logout behavior is unchanged since both menus call the same useLogout handler. --- .../e2e_tests/tests/auth/logout.spec.ts | 15 +- .../tests/auth/proxyLogoutUrl.spec.ts | 10 +- .../tests/login/internalUserIdentity.spec.ts | 10 +- .../e2e_tests/tests/login/login.spec.ts | 18 +- .../SidebarAccountMenu.test.tsx | 302 ++++++++++++++++++ .../SidebarAccountMenu/SidebarAccountMenu.tsx | 295 +++++++++++++++++ .../src/components/leftnav.tsx | 4 +- 7 files changed, 619 insertions(+), 35 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.test.tsx create mode 100644 ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx diff --git a/ui/litellm-dashboard/e2e_tests/tests/auth/logout.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/auth/logout.spec.ts index 1e2f2269dbe..351ba91e8d7 100644 --- a/ui/litellm-dashboard/e2e_tests/tests/auth/logout.spec.ts +++ b/ui/litellm-dashboard/e2e_tests/tests/auth/logout.spec.ts @@ -9,22 +9,17 @@ test.describe("Logout", () => { // Scope to the sidebar; the top-bar breadcrumb also shows "Virtual Keys". await expect(page.getByRole("complementary").getByText("Virtual Keys")).toBeVisible({ timeout: 10_000 }); - // Open the navbar User dropdown. The trigger button exposes an aria-label - // of "Account menu — — signed in as ", and the antd Dropdown - // is declared with trigger={["click"]}, so a plain click opens the popup. + // Open the sidebar account menu. The trigger button exposes an aria-label + // of "Account menu — — signed in as "; clicking it opens the + // Base UI popover panel. await page.getByRole("button", { name: /Account menu/i }).click(); - const popup = page - .locator(".ant-dropdown:visible") - .filter({ - has: page.locator(".bg-white.rounded-lg.shadow-lg"), - }) - .first(); + const popup = page.getByTestId("sidebar-account-menu-panel"); await expect(popup).toBeVisible({ timeout: 5_000 }); // Click Logout — the handler clears the auth cookie and navigates via // window.location.href = PROXY_LOGOUT_URL (empty string in the e2e env). - await popup.getByText("Logout", { exact: true }).click(); + await popup.getByRole("button", { name: "Logout" }).click(); // The cookie is now gone — visiting a protected page must redirect to /ui/login. await page.goto("/ui?page=llm-playground", { waitUntil: "domcontentloaded" }); diff --git a/ui/litellm-dashboard/e2e_tests/tests/auth/proxyLogoutUrl.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/auth/proxyLogoutUrl.spec.ts index 9faf6741333..7f6cc6f2f87 100644 --- a/ui/litellm-dashboard/e2e_tests/tests/auth/proxyLogoutUrl.spec.ts +++ b/ui/litellm-dashboard/e2e_tests/tests/auth/proxyLogoutUrl.spec.ts @@ -34,8 +34,8 @@ test.describe("PROXY_LOGOUT_URL redirect", () => { }), ); - // navbar.tsx populates the logout target only after the proxy UI settings - // fetch (/sso/get/ui_settings) resolves. Clicking Logout before that lands + // The logout handler populates the logout target only after the proxy UI + // settings fetch (/sso/get/ui_settings) resolves. Clicking Logout before that lands // runs `window.location.href = ""` — a same-origin reload, not a redirect — // so gate the click on the settings response, not just on first paint. const settingsLoaded = page.waitForResponse((r) => r.url().includes("/sso/get/ui_settings") && r.ok(), { @@ -51,10 +51,10 @@ test.describe("PROXY_LOGOUT_URL redirect", () => { const tokensBefore = (await page.context().cookies()).filter((c) => c.name === "token"); expect(tokensBefore.length, "should start logged in with a token cookie").toBeGreaterThan(0); - // Open the navbar account dropdown (trigger=click) and click Logout by role - // rather than internal Ant Design CSS classes, which are not a stable API. + // Open the sidebar account menu and click Logout by role rather than by + // internal CSS classes, which are not a stable API. await page.getByRole("button", { name: /^Account menu/ }).click(); - const logout = page.getByRole("menuitem", { name: "Logout" }); + const logout = page.getByRole("button", { name: "Logout" }); await expect(logout).toBeVisible({ timeout: 5_000 }); // handleLogout clears cookies/local storage, then assigns window.location.href. diff --git a/ui/litellm-dashboard/e2e_tests/tests/login/internalUserIdentity.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/login/internalUserIdentity.spec.ts index 569908c5f75..101b91daeb1 100644 --- a/ui/litellm-dashboard/e2e_tests/tests/login/internalUserIdentity.spec.ts +++ b/ui/litellm-dashboard/e2e_tests/tests/login/internalUserIdentity.spec.ts @@ -27,13 +27,13 @@ test.describe("Navbar identity scoping", () => { { timeout: 5_000 }, ); - // Open the dropdown (UserDropdown configures trigger=["click"]). + // Open the account menu (click to open the Base UI popover). await accountButton.click(); - // Locate the panel by its test id (data-testid on the popupRender div in - // UserDropdown.tsx) rather than Ant/Tailwind class names, so styling - // refactors don't silently break the identity-scoping assertions below. - const popup = page.getByTestId("user-dropdown-panel"); + // Locate the panel by its test id (data-testid on SidebarAccountMenu's + // popover content) rather than class names, so styling refactors don't + // silently break the identity-scoping assertions below. + const popup = page.getByTestId("sidebar-account-menu-panel"); await expect(popup).toBeVisible({ timeout: 5_000 }); // The popup must show the internal user's identity — not the seeded diff --git a/ui/litellm-dashboard/e2e_tests/tests/login/login.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/login/login.spec.ts index 11febf0ed48..88378df36c3 100644 --- a/ui/litellm-dashboard/e2e_tests/tests/login/login.spec.ts +++ b/ui/litellm-dashboard/e2e_tests/tests/login/login.spec.ts @@ -12,22 +12,14 @@ test("user can log in", async ({ page }) => { // Scope to the sidebar; the top-bar breadcrumb also shows "Virtual Keys". await expect(page.getByRole("complementary").getByText("Virtual Keys")).toBeVisible(); - // Match the navbar account button by its stable aria-label (UserDropdown.tsx - // emits "Account menu — — signed in as "). Earlier this used - // `hasText: /^User$/`, which never matched the rendered button (text is - // displayName = "Account" for the master-key admin), so the trigger evaluate - // would time out in CI. + // Match the sidebar account button by its stable aria-label + // (SidebarAccountMenu emits "Account menu — — signed in as "; + // displayName is "Account" for the master-key admin, so match on the label). const userTrigger = page.locator('button[aria-label^="Account menu"]').first(); await userTrigger.click(); - // Filter by the popupRender wrapper class to disambiguate from other - // ant-dropdown popups. - const popup = page - .locator(".ant-dropdown:visible") - .filter({ - has: page.locator(".bg-white.rounded-lg.shadow-lg"), - }) - .first(); + // The account menu is a Base UI popover; locate its panel by test id. + const popup = page.getByTestId("sidebar-account-menu-panel"); await expect(popup).toBeVisible({ timeout: 5_000 }); await expect(popup.getByText("Admin", { exact: true })).toBeVisible({ timeout: 5_000 }); await expect(popup.getByText("default_user_id", { exact: true })).toBeVisible({ timeout: 5_000 }); diff --git a/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.test.tsx b/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.test.tsx new file mode 100644 index 00000000000..716a2ebe6e2 --- /dev/null +++ b/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.test.tsx @@ -0,0 +1,302 @@ +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { renderWithProviders, screen, waitFor } from "../../../tests/test-utils"; +import SidebarAccountMenu from "./SidebarAccountMenu"; + +interface AuthMock { + userId: string | null; + userEmail: string | null; + userRole: string; + premiumUser: boolean; + accessToken: string; +} + +let mockUseAuthorizedImpl: () => AuthMock = () => ({ + userId: "test-user-id", + userEmail: "test@example.com", + userRole: "Admin", + premiumUser: false, + accessToken: "test-token", +}); + +let mockUseDisableShowPromptsImpl = () => false; +let mockUseDisableBouncingIconImpl = () => false; +let mockHealthDataImpl = (): { litellm_version?: string } | undefined => ({ litellm_version: "1.99.0" }); + +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("@/app/(dashboard)/hooks/useDisableUsageIndicator", () => ({ + useDisableUsageIndicator: () => false, +})); + +vi.mock("@/app/(dashboard)/hooks/useDisableBlogPosts", () => ({ + useDisableBlogPosts: () => false, +})); + +vi.mock("@/app/(dashboard)/hooks/useDisableBouncingIcon", () => ({ + useDisableBouncingIcon: () => mockUseDisableBouncingIconImpl(), +})); + +vi.mock("@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails", () => ({ + useHealthReadinessDetails: () => ({ data: mockHealthDataImpl() }), +})); + +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("SidebarAccountMenu", () => { + const mockOnLogout = vi.fn(); + + const getAccountTrigger = () => screen.getByRole("button", { name: /account menu/i }); + + const openMenu = async (user: ReturnType) => { + await user.click(getAccountTrigger()); + await waitFor(() => { + expect(screen.getByTestId("sidebar-account-menu-panel")).toBeInTheDocument(); + }); + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockUseAuthorizedImpl = () => ({ + userId: "test-user-id", + userEmail: "test@example.com", + userRole: "Admin", + premiumUser: false, + accessToken: "test-token", + }); + mockUseDisableShowPromptsImpl = () => false; + mockUseDisableBouncingIconImpl = () => false; + mockHealthDataImpl = () => ({ litellm_version: "1.99.0" }); + mockGetLocalStorageItemImpl = (key: string): string | null => { + if (key === "disableShowNewBadge") return null; + if (key === "disableShowPrompts") return null; + return null; + }; + }); + + it("should render the account trigger with initials", () => { + renderWithProviders(); + expect(getAccountTrigger()).toBeInTheDocument(); + expect(screen.getByText("TE")).toBeInTheDocument(); + }); + + it("should render only the avatar (no name/role) when collapsed", () => { + renderWithProviders(); + expect(getAccountTrigger()).toBeInTheDocument(); + expect(screen.getByText("TE")).toBeInTheDocument(); + expect(screen.queryByText("Admin")).not.toBeInTheDocument(); + expect(screen.queryByText("test@example.com")).not.toBeInTheDocument(); + }); + + it("should show email, user ID, and role when the menu is opened", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await openMenu(user); + + expect(screen.getAllByText("test@example.com").length).toBeGreaterThan(0); + expect(screen.getByText("test-user-id")).toBeInTheDocument(); + expect(screen.getAllByText("Admin").length).toBeGreaterThan(0); + }); + + it("should display Standard tier for non-premium users", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await openMenu(user); + + expect(screen.getByText("Standard")).toBeInTheDocument(); + }); + + it("should display Premium tier for premium users", async () => { + const user = userEvent.setup(); + mockUseAuthorizedImpl = () => ({ + userId: "test-user-id", + userEmail: "test@example.com", + userRole: "Admin", + premiumUser: true, + accessToken: "test-token", + }); + + renderWithProviders(); + + await openMenu(user); + + expect(screen.getByText("Premium")).toBeInTheDocument(); + }); + + it("should render a clickable version badge linking to the release notes", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await openMenu(user); + + const versionLink = screen.getByRole("link", { name: /v1\.99\.0/ }); + expect(versionLink).toHaveAttribute("href", "https://docs.litellm.ai/release_notes"); + expect(versionLink).toHaveAttribute("target", "_blank"); + }); + + it("should not render the version badge when the version is unavailable", async () => { + const user = userEvent.setup(); + mockHealthDataImpl = () => undefined; + renderWithProviders(); + + await openMenu(user); + + expect(screen.queryByRole("link", { name: /^v/ })).not.toBeInTheDocument(); + }); + + it("should show the bouncing icon by default", async () => { + const user = userEvent.setup(); + renderWithProviders(); + await openMenu(user); + expect(screen.getByTitle("Thanks for using LiteLLM!")).toBeInTheDocument(); + }); + + it("should hide the bouncing icon when Hide Bouncing Icon is enabled", async () => { + const user = userEvent.setup(); + mockUseDisableBouncingIconImpl = () => true; + renderWithProviders(); + await openMenu(user); + expect(screen.queryByTitle("Thanks for using LiteLLM!")).not.toBeInTheDocument(); + }); + + it("should copy the email to the clipboard", async () => { + const user = userEvent.setup(); + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, "clipboard", { value: { writeText }, configurable: true }); + + renderWithProviders(); + await openMenu(user); + + await user.click(screen.getByRole("button", { name: "Copy email" })); + + expect(writeText).toHaveBeenCalledWith("test@example.com"); + }); + + it("should call onLogout when logout is clicked", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await openMenu(user); + + await user.click(screen.getByRole("button", { name: /logout/i })); + + expect(mockOnLogout).toHaveBeenCalledTimes(1); + }); + + it("should toggle hide new feature indicators on", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await openMenu(user); + + 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 off", async () => { + const user = userEvent.setup(); + mockGetLocalStorageItemImpl = (key: string): string | null => { + if (key === "disableShowNewBadge") return "true"; + return null; + }; + + renderWithProviders(); + + await openMenu(user); + + 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 on", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await openMenu(user); + + 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 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 openMenu(user); + + const toggle = screen.getByLabelText("Toggle hide new feature indicators"); + expect(toggle).toBeChecked(); + }); + + it("should show Account in the trigger for the default placeholder user id", () => { + mockUseAuthorizedImpl = () => ({ + userId: "default_user_id", + userEmail: null, + userRole: "Admin", + premiumUser: false, + accessToken: "test-token", + }); + renderWithProviders(); + expect(screen.getByText("Account")).toBeInTheDocument(); + }); + + it("should display a dash when email is unavailable", async () => { + const user = userEvent.setup(); + mockUseAuthorizedImpl = () => ({ + userId: "test-user-id", + userEmail: null, + userRole: "Admin", + premiumUser: false, + accessToken: "test-token", + }); + + renderWithProviders(); + + await openMenu(user); + + expect(screen.getByText("-")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx b/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx new file mode 100644 index 00000000000..46d7271c9df --- /dev/null +++ b/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx @@ -0,0 +1,295 @@ +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { useHealthReadinessDetails } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails"; +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 { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Separator } from "@/components/ui/separator"; +import { Switch } from "@/components/ui/switch"; +import { cn } from "@/lib/cva.config"; +import { Check, ChevronsUpDown, Copy, Crown, IdCard, LogOut, Mail, ShieldCheck } from "lucide-react"; +import React, { useEffect, useState } from "react"; + +const RELEASE_NOTES_URL = "https://docs.litellm.ai/release_notes"; + +function hueFromString(seed: string): number { + let h = 0; + for (let i = 0; i < seed.length; i += 1) { + h = seed.charCodeAt(i) + ((h << 5) - h); + } + return Math.abs(h) % 360; +} + +function initialsFromIdentity(email: string | null, userId: string | null): string { + const local = email?.split("@")[0]?.trim(); + if (local) { + const parts = local + .replace(/[^a-zA-Z0-9]+/g, " ") + .trim() + .split(/\s+/) + .filter(Boolean); + if (parts.length >= 2) { + return `${parts[0]!.charAt(0)}${parts[1]!.charAt(0)}`.toUpperCase(); + } + if (parts.length === 1) { + const p = parts[0]!; + return p.length >= 2 ? p.slice(0, 2).toUpperCase() : `${p.charAt(0)}`.toUpperCase(); + } + } + if (userId && userId.length >= 2) { + return userId.slice(0, 2).toUpperCase(); + } + if (userId && userId.length === 1) { + return `${userId.toUpperCase()}•`; + } + return "?"; +} + +const CopyButton: React.FC<{ value: string | null; label: string }> = ({ value, label }) => { + const [copied, setCopied] = useState(false); + + useEffect(() => { + if (!copied) return; + const timer = setTimeout(() => setCopied(false), 1200); + return () => clearTimeout(timer); + }, [copied]); + + if (!value) return null; + + return ( + + ); +}; + +const InfoRow: React.FC<{ icon: React.ReactNode; label: string; children: React.ReactNode }> = ({ + icon, + label, + children, +}) => ( +
+ + {icon} + {label} + + {children} +
+); + +const MonoValue: React.FC<{ value: string | null; copyLabel: string }> = ({ value, copyLabel }) => ( + + + {value || "-"} + + + +); + +interface SidebarAccountMenuProps { + onLogout: () => void; + collapsed?: boolean; +} + +const SidebarAccountMenu: React.FC = ({ onLogout, collapsed = false }) => { + const { userId, userEmail, userRole, premiumUser, accessToken } = useAuthorized(); + 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(); + + const setFlag = (key: string, checked: boolean) => { + if (checked) { + setLocalStorageItem(key, "true"); + } else { + removeLocalStorageItem(key); + } + emitLocalStorageChange(key); + }; + + const toggles = [ + { + key: "disableShowNewBadge", + label: "Hide New Feature Indicators", + ariaLabel: "Toggle hide new feature indicators", + checked: disableShowNewBadge, + onCheckedChange: (checked: boolean) => setFlag("disableShowNewBadge", checked), + }, + { + key: "disableShowPrompts", + label: "Hide All Prompts", + ariaLabel: "Toggle hide all prompts", + 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", + ariaLabel: "Toggle hide blog posts", + checked: disableBlogPosts, + onCheckedChange: (checked: boolean) => setFlag("disableBlogPosts", checked), + }, + { + key: "disableBouncingIcon", + label: "Hide Bouncing Icon", + ariaLabel: "Toggle hide bouncing icon", + checked: disableBouncingIcon, + onCheckedChange: (checked: boolean) => setFlag("disableBouncingIcon", checked), + }, + ]; + + const seed = userEmail || userId || "user"; + const initials = initialsFromIdentity(userEmail, userId); + const hue = hueFromString(seed); + const displayName = navAccountDisplayName(userEmail, userId); + const triggerLabel = `Account menu — ${userRole ?? "Unknown role"} — signed in as ${userEmail || userId || "unknown"}`; + + return ( + + + + + {initials} + + + {!collapsed && ( + <> + + {displayName} + {userRole && {userRole}} + + + + )} + + + +
+ LiteLLM + {!disableBouncingIcon && ( + + 🌑 + + )} + + {version && ( + } + className="px-1.5 py-0 font-mono text-[10px] font-medium text-muted-foreground" + > + v{version} + + )} +
+ +
+ } label="Tier"> + {premiumUser ? ( + + + Premium + + ) : ( + + + Standard + + )} + + } label="Role"> + {userRole} + + } label="Email"> + + + } label="User ID"> + + +
+ + + +
+ {toggles.map((toggle) => ( +
+ {toggle.label} + +
+ ))} +
+ + + + +
+
+ ); +}; + +export default SidebarAccountMenu; diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index 0fb9fc84a31..c24cebe7e7d 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -74,8 +74,8 @@ import { } from "../utils/roles"; import NewBadge from "./common_components/NewBadge"; import type { Organization } from "./networking"; +import SidebarAccountMenu from "./SidebarAccountMenu/SidebarAccountMenu"; import SidebarUsageCard from "./SidebarUsageCard"; -import UserDropdown from "./Navbar/UserDropdown/UserDropdown"; import { MIGRATED_PAGES, migratedHref, legacyPageHref } from "@/utils/migratedPages"; const ICON = { strokeWidth: 1.75 } as const; @@ -626,7 +626,7 @@ const Sidebar_: React.FC = ({ onExpandRail={() => onToggleCollapsed?.()} /> )} - + );