feat(dashboard): refine navbar zones and Agent Platform notice

Restructure the admin navbar for production users: clear product vs community
vs personal columns with vertical dividers, icon-only Slack/GitHub in a
shared chip, and Docs/Blog typography aligned on an 8px rhythm.

Add a notifications bell with popover linking to the LiteLLM Agent Platform
repo and optional mark-as-read persistence.

Promote the account control with initials avatar, single-line display name,
and navDisplayName mapping for placeholder user ids (e.g. default_user_id).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Krrish Dholakia 2026-05-09 11:34:17 -07:00
parent 8c9830eef9
commit 37645756c7
11 changed files with 360 additions and 120 deletions

View file

@ -1,6 +1,7 @@
import { useDisableBlogPosts } from "@/app/(dashboard)/hooks/useDisableBlogPosts";
import { useBlogPosts, type BlogPost } from "@/app/(dashboard)/hooks/blogPosts/useBlogPosts";
import { LoadingOutlined } from "@ant-design/icons";
import { NAV_PRODUCT_LINK_CLASS } from "@/components/Navbar/navProductLinkClass";
import { DownOutlined, LoadingOutlined } from "@ant-design/icons";
import { Button, Dropdown, Space, Typography } from "antd";
import type { MenuProps } from "antd";
import React from "react";
@ -74,9 +75,13 @@ export const BlogDropdown: React.FC = () => {
];
}
// Blog opens a post list; Docs is a single outbound link — navbar adds a layout-only chevron there for alignment.
return (
<Dropdown menu={{ items }} trigger={["hover"]} placement="bottomRight">
<Button type="text">Blog</Button>
<button type="button" className={`${NAV_PRODUCT_LINK_CLASS} cursor-pointer border-0 bg-transparent`}>
Blog
<DownOutlined className="text-[10px] text-gray-500" aria-hidden />
</button>
</Dropdown>
);
};

View file

@ -1,36 +1,45 @@
import { useDisableShowPrompts } from "@/app/(dashboard)/hooks/useDisableShowPrompts";
import { GithubOutlined, SlackOutlined } from "@ant-design/icons";
import { Button } from "antd";
import { Tooltip } from "antd";
import React from "react";
const iconBtnClass =
"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md border-0 bg-transparent text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 cursor-pointer";
export const CommunityEngagementButtons: React.FC = () => {
const disableShowPrompts = useDisableShowPrompts();
// Hide buttons if prompts are disabled
if (disableShowPrompts) {
return null;
}
return (
<>
<Button
href="https://www.litellm.ai/support"
target="_blank"
rel="noopener noreferrer"
icon={<SlackOutlined />}
className="shadow-md shadow-indigo-500/20 hover:shadow-indigo-500/50 transition-shadow"
>
Join Slack
</Button>
<Button
href="https://github.com/BerriAI/litellm"
target="_blank"
rel="noopener noreferrer"
className="shadow-md shadow-indigo-500/20 hover:shadow-indigo-500/50 transition-shadow"
icon={<GithubOutlined />}
>
Star us on GitHub
</Button>
</>
<div
className="flex items-center gap-0.5 rounded-md border border-gray-200/80 bg-gray-50 px-0.5 py-0"
aria-label="Community links"
>
<Tooltip title="LiteLLM Slack community">
<a
href="https://www.litellm.ai/support"
target="_blank"
rel="noopener noreferrer"
className={iconBtnClass}
aria-label="Join Slack"
>
<SlackOutlined className="text-lg" />
</a>
</Tooltip>
<Tooltip title="LiteLLM on GitHub">
<a
href="https://github.com/BerriAI/litellm"
target="_blank"
rel="noopener noreferrer"
className={iconBtnClass}
aria-label="LiteLLM on GitHub"
>
<GithubOutlined className="text-lg" />
</a>
</Tooltip>
</div>
);
};

View file

@ -0,0 +1,46 @@
import { renderWithProviders, screen } from "../../../../tests/test-utils";
import { NotificationsBell, AGENT_PLATFORM_URL } from "./NotificationsBell";
import React from "react";
import userEvent from "@testing-library/user-event";
describe("NotificationsBell", () => {
beforeEach(() => {
localStorage.clear();
});
it("should open notifications with Agent Platform details and GitHub link", async () => {
const user = userEvent.setup();
renderWithProviders(<NotificationsBell />);
await user.click(screen.getByRole("button", { name: /^notifications$/i }));
expect(screen.getByText(/LiteLLM Agent Platform/i)).toBeInTheDocument();
const githubBtn = screen.getByRole("link", { name: /^GitHub$/i });
expect(githubBtn).toHaveAttribute("href", AGENT_PLATFORM_URL);
expect(githubBtn).toHaveAttribute("target", "_blank");
expect(githubBtn).toHaveAttribute("rel", "noopener noreferrer");
});
it("should offer mark as read when announcement is unread", async () => {
const user = userEvent.setup();
renderWithProviders(<NotificationsBell />);
await user.click(screen.getByRole("button", { name: /^notifications$/i }));
expect(screen.getByRole("button", { name: /^mark as read$/i })).toBeInTheDocument();
});
it("should hide mark as read and persist after marking read", async () => {
const user = userEvent.setup();
renderWithProviders(<NotificationsBell />);
await user.click(screen.getByRole("button", { name: /^notifications$/i }));
await user.click(screen.getByRole("button", { name: /^mark as read$/i }));
expect(localStorage.getItem("litellmHideAgentPlatformBanner")).toBe("true");
await user.click(screen.getByRole("button", { name: /^notifications$/i }));
expect(screen.queryByRole("button", { name: /^mark as read$/i })).not.toBeInTheDocument();
});
it("should not show mark as read when previously dismissed", async () => {
localStorage.setItem("litellmHideAgentPlatformBanner", "true");
const user = userEvent.setup();
renderWithProviders(<NotificationsBell />);
await user.click(screen.getByRole("button", { name: /^notifications$/i }));
expect(screen.queryByRole("button", { name: /^mark as read$/i })).not.toBeInTheDocument();
});
});

View file

@ -0,0 +1,66 @@
"use client";
import { BellOutlined } from "@ant-design/icons";
import { Badge, Button, Popover, Typography } from "antd";
import React, { useEffect, useState } from "react";
const STORAGE_KEY = "litellmHideAgentPlatformBanner";
export const AGENT_PLATFORM_URL = "https://github.com/BerriAI/litellm-agent-platform";
export const NotificationsBell: React.FC = () => {
const [hasUnread, setHasUnread] = useState(false);
const [open, setOpen] = useState(false);
useEffect(() => {
try {
setHasUnread(localStorage.getItem(STORAGE_KEY) !== "true");
} catch {
setHasUnread(true);
}
}, []);
const markDismissed = () => {
try {
localStorage.setItem(STORAGE_KEY, "true");
} catch {
/* ignore */
}
setHasUnread(false);
setOpen(false);
};
const content = (
<div className="max-w-[280px]">
<Typography.Title level={5} className="!mt-0 !mb-2">
LiteLLM Agent Platform
</Typography.Title>
<Typography.Paragraph type="secondary" className="!mb-3 text-sm leading-snug">
Open-source agent infra sandboxes, durable sessions, and workers on AWS Fargate.
</Typography.Paragraph>
<div className="flex flex-wrap items-center gap-2">
<Button type="primary" size="small" href={AGENT_PLATFORM_URL} target="_blank" rel="noopener noreferrer">
GitHub
</Button>
{hasUnread ? (
<Button type="link" size="small" className="!px-1" onClick={markDismissed}>
Mark as read
</Button>
) : null}
</div>
</div>
);
return (
<Popover content={content} trigger="click" open={open} onOpenChange={setOpen} placement="bottomRight">
<button
type="button"
className="flex h-9 w-9 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
aria-label="Notifications"
>
<Badge dot={hasUnread} color="#1677ff" size="small" offset={[8, 2]}>
<BellOutlined className="text-base" aria-hidden />
</Badge>
</button>
</Popover>
);
};

View file

@ -37,6 +37,8 @@ vi.mock("@/utils/localStorageUtils", () => ({
describe("UserDropdown", () => {
const mockOnLogout = vi.fn();
const getAccountTrigger = () => screen.getByRole("button", { name: /account menu/i });
beforeEach(() => {
vi.clearAllMocks();
mockUseAuthorizedImpl = () => ({
@ -55,22 +57,23 @@ describe("UserDropdown", () => {
it("should render", () => {
renderWithProviders(<UserDropdown onLogout={mockOnLogout} />);
expect(screen.getByRole("button")).toBeInTheDocument();
expect(getAccountTrigger()).toBeInTheDocument();
});
it("should display user button with User text", () => {
it("should surface initials and account menu affordance", () => {
renderWithProviders(<UserDropdown onLogout={mockOnLogout} />);
expect(screen.getByText("User")).toBeInTheDocument();
expect(getAccountTrigger()).toBeInTheDocument();
expect(screen.getByText("TE")).toBeInTheDocument();
});
it("should show user email when dropdown is opened", async () => {
const user = userEvent.setup();
renderWithProviders(<UserDropdown onLogout={mockOnLogout} />);
await user.click(screen.getByText("User"));
await user.click(getAccountTrigger());
await waitFor(() => {
expect(screen.getByText("test@example.com")).toBeInTheDocument();
expect(screen.getAllByText("test@example.com").length).toBeGreaterThan(0);
});
});
@ -78,7 +81,7 @@ describe("UserDropdown", () => {
const user = userEvent.setup();
renderWithProviders(<UserDropdown onLogout={mockOnLogout} />);
await user.click(screen.getByText("User"));
await user.click(getAccountTrigger());
await waitFor(() => {
expect(screen.getByText("test-user-id")).toBeInTheDocument();
@ -89,10 +92,10 @@ describe("UserDropdown", () => {
const user = userEvent.setup();
renderWithProviders(<UserDropdown onLogout={mockOnLogout} />);
await user.click(screen.getByText("User"));
await user.click(getAccountTrigger());
await waitFor(() => {
expect(screen.getByText("Admin")).toBeInTheDocument();
expect(screen.getAllByText("Admin").length).toBeGreaterThan(0);
});
});
@ -100,7 +103,7 @@ describe("UserDropdown", () => {
const user = userEvent.setup();
renderWithProviders(<UserDropdown onLogout={mockOnLogout} />);
await user.click(screen.getByText("User"));
await user.click(getAccountTrigger());
await waitFor(() => {
expect(screen.getByText("Standard")).toBeInTheDocument();
@ -118,7 +121,7 @@ describe("UserDropdown", () => {
renderWithProviders(<UserDropdown onLogout={mockOnLogout} />);
await user.click(screen.getByText("User"));
await user.click(getAccountTrigger());
await waitFor(() => {
expect(screen.getByText("Premium")).toBeInTheDocument();
@ -129,10 +132,10 @@ describe("UserDropdown", () => {
const user = userEvent.setup();
renderWithProviders(<UserDropdown onLogout={mockOnLogout} />);
await user.click(screen.getByText("User"));
await user.click(getAccountTrigger());
await waitFor(() => {
expect(screen.getByText("test@example.com")).toBeInTheDocument();
expect(screen.getAllByText("test@example.com").length).toBeGreaterThan(0);
});
await user.click(screen.getByText("Logout"));
@ -144,10 +147,10 @@ describe("UserDropdown", () => {
const user = userEvent.setup();
renderWithProviders(<UserDropdown onLogout={mockOnLogout} />);
await user.click(screen.getByText("User"));
await user.click(getAccountTrigger());
await waitFor(() => {
expect(screen.getByText("test@example.com")).toBeInTheDocument();
expect(screen.getAllByText("test@example.com").length).toBeGreaterThan(0);
});
const toggle = screen.getByLabelText("Toggle hide new feature indicators");
@ -169,10 +172,10 @@ describe("UserDropdown", () => {
renderWithProviders(<UserDropdown onLogout={mockOnLogout} />);
await user.click(screen.getByText("User"));
await user.click(getAccountTrigger());
await waitFor(() => {
expect(screen.getByText("test@example.com")).toBeInTheDocument();
expect(screen.getAllByText("test@example.com").length).toBeGreaterThan(0);
});
const toggle = screen.getByLabelText("Toggle hide new feature indicators");
@ -189,10 +192,10 @@ describe("UserDropdown", () => {
const user = userEvent.setup();
renderWithProviders(<UserDropdown onLogout={mockOnLogout} />);
await user.click(screen.getByText("User"));
await user.click(getAccountTrigger());
await waitFor(() => {
expect(screen.getByText("test@example.com")).toBeInTheDocument();
expect(screen.getAllByText("test@example.com").length).toBeGreaterThan(0);
});
const toggle = screen.getByLabelText("Toggle hide all prompts");
@ -215,10 +218,10 @@ describe("UserDropdown", () => {
renderWithProviders(<UserDropdown onLogout={mockOnLogout} />);
await user.click(screen.getByText("User"));
await user.click(getAccountTrigger());
await waitFor(() => {
expect(screen.getByText("test@example.com")).toBeInTheDocument();
expect(screen.getAllByText("test@example.com").length).toBeGreaterThan(0);
});
const toggle = screen.getByLabelText("Toggle hide all prompts");
@ -231,6 +234,17 @@ describe("UserDropdown", () => {
expect(localStorageUtils.emitLocalStorageChange).toHaveBeenCalledWith("disableShowPrompts");
});
it("should show Account in the trigger when user id is the default placeholder", () => {
mockUseAuthorizedImpl = () => ({
userId: "default_user_id",
userEmail: null as any,
userRole: "Admin",
premiumUser: false,
});
renderWithProviders(<UserDropdown onLogout={mockOnLogout} />);
expect(screen.getByText("Account")).toBeInTheDocument();
});
it("should display dash when user email is not available", async () => {
const user = userEvent.setup();
mockUseAuthorizedImpl = () => ({
@ -242,7 +256,7 @@ describe("UserDropdown", () => {
renderWithProviders(<UserDropdown onLogout={mockOnLogout} />);
await user.click(screen.getByText("User"));
await user.click(getAccountTrigger());
await waitFor(() => {
expect(screen.getByText("-")).toBeInTheDocument();
@ -260,7 +274,7 @@ describe("UserDropdown", () => {
renderWithProviders(<UserDropdown onLogout={mockOnLogout} />);
await user.click(screen.getByText("User"));
await user.click(getAccountTrigger());
await waitFor(() => {
const dashElements = screen.getAllByText("-");
@ -277,10 +291,10 @@ describe("UserDropdown", () => {
renderWithProviders(<UserDropdown onLogout={mockOnLogout} />);
await user.click(screen.getByText("User"));
await user.click(getAccountTrigger());
await waitFor(() => {
expect(screen.getByText("test@example.com")).toBeInTheDocument();
expect(screen.getAllByText("test@example.com").length).toBeGreaterThan(0);
});
const toggle = screen.getByLabelText("Toggle hide new feature indicators");

View file

@ -9,6 +9,7 @@ import {
removeLocalStorageItem,
setLocalStorageItem,
} from "@/utils/localStorageUtils";
import { navAccountDisplayName } from "@/components/Navbar/navDisplayName";
import {
CrownOutlined,
DownOutlined,
@ -18,11 +19,44 @@ import {
UserOutlined,
} from "@ant-design/icons";
import type { MenuProps } from "antd";
import { Button, Divider, Dropdown, Space, Switch, Tag, Tooltip, Typography } from "antd";
import { Divider, Dropdown, Space, Switch, Tag, Tooltip, Typography } from "antd";
import React, { useEffect, useState } from "react";
const { Text } = Typography;
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 "?";
}
interface UserDropdownProps {
onLogout: () => void;
}
@ -61,19 +95,12 @@ const UserDropdown: React.FC<UserDropdownProps> = ({ onLogout }) => {
<Text type="secondary">{userEmail || "-"}</Text>
</Space>
{premiumUser ? (
<Tag
icon={<CrownOutlined />}
color="gold"
>
<Tag icon={<CrownOutlined />} color="gold">
Premium
</Tag>
) : (
<Tooltip title="Upgrade to Premium for advanced features" placement="left">
<Tag
icon={<CrownOutlined />}
>
Standard
</Tag>
<Tag icon={<CrownOutlined />}>Standard</Tag>
</Tooltip>
)}
</Space>
@ -83,12 +110,7 @@ const UserDropdown: React.FC<UserDropdownProps> = ({ onLogout }) => {
<UserOutlined />
<Text type="secondary">User ID</Text>
</Space>
<Text
copyable
ellipsis
style={{ maxWidth: "150px" }}
title={userId || "-"}
>
<Text copyable ellipsis style={{ maxWidth: "150px" }} title={userId || "-"}>
{userId || "-"}
</Text>
</Space>
@ -189,13 +211,17 @@ const UserDropdown: React.FC<UserDropdownProps> = ({ onLogout }) => {
</Space>
);
const seed = userEmail || userId || "user";
const initials = initialsFromIdentity(userEmail, userId);
const hue = hueFromString(seed);
const displayName = navAccountDisplayName(userEmail, userId);
return (
<Dropdown
trigger={["click"]}
menu={{ items: userItems }}
popupRender={(menu) => (
<div
className="bg-white rounded-lg shadow-lg"
>
<div className="rounded-lg bg-white shadow-lg">
{renderUserInfoSection()}
<Divider style={{ margin: 0 }} />
{React.cloneElement(menu as React.ReactElement, {
@ -204,13 +230,24 @@ const UserDropdown: React.FC<UserDropdownProps> = ({ onLogout }) => {
</div>
)}
>
<Button type="text" >
<Space>
<UserOutlined />
<Text>User</Text>
<DownOutlined />
</Space>
</Button>
<button
type="button"
className="flex max-w-[min(200px,34vw)] items-center gap-2 rounded-md py-0.5 pl-1 pr-2 transition-colors hover:bg-gray-100"
aria-label={`Account menu — ${userRole ?? "Unknown role"} — signed in as ${userEmail || userId || "unknown"}`}
aria-haspopup="menu"
>
<span
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-xs font-semibold text-white shadow-inner ring-1 ring-black/5"
style={{ backgroundColor: `hsl(${hue} 46% 38%)` }}
aria-hidden
>
{initials}
</span>
<span className="hidden min-w-0 truncate text-left text-sm font-medium leading-none text-gray-900 md:inline">
{displayName}
</span>
<DownOutlined className="hidden shrink-0 text-[10px] text-gray-400 md:inline" aria-hidden />
</button>
</Dropdown>
);
};

View file

@ -0,0 +1,17 @@
import { describe, expect, it } from "vitest";
import { navAccountDisplayName } from "./navDisplayName";
describe("navAccountDisplayName", () => {
it("should prefer email when present", () => {
expect(navAccountDisplayName("x@y.com", "ignored")).toBe("x@y.com");
});
it("should map default_user_id placeholder to Account", () => {
expect(navAccountDisplayName(null, "default_user_id")).toBe("Account");
expect(navAccountDisplayName(null, "DEFAULT_USER_ID")).toBe("Account");
});
it("should show a sensible token when user id is non-placeholder", () => {
expect(navAccountDisplayName(null, "user-uuid-123")).toBe("user-uuid-123");
});
});

View file

@ -0,0 +1,20 @@
/** Primary label for the navbar account control — avoids raw placeholder JWT/user IDs in the UI. */
export function navAccountDisplayName(userEmail: string | null, userId: string | null): string {
const email = userEmail?.trim();
if (email) {
return email;
}
const id = userId?.trim();
if (!id) {
return "Account";
}
const lower = id.toLowerCase();
if (
lower === "default_user_id" ||
lower === "default-user-id" ||
/^default[_\s-]?user[_\s-]?id$/i.test(id)
) {
return "Account";
}
return id;
}

View file

@ -0,0 +1,3 @@
/** Shared styling for Docs / Blog in the top nav (product navigation zone). */
export const NAV_PRODUCT_LINK_CLASS =
"inline-flex h-9 shrink-0 items-center justify-center gap-1 rounded-md px-2 text-sm font-medium leading-none text-gray-800 transition-colors hover:bg-gray-100 hover:text-gray-950";

View file

@ -37,8 +37,8 @@ vi.mock("./Navbar/UserDropdown/UserDropdown", async (importOriginal) => {
const [open, setOpen] = useState(false);
return (
<div>
<button type="button" onClick={() => setOpen(!open)}>
User
<button type="button" aria-label="Open account menu" onClick={() => setOpen(!open)}>
Account
</button>
{open && (
<div data-testid="user-dropdown-content">
@ -146,15 +146,16 @@ describe("Navbar", () => {
it("should render without crashing", () => {
renderWithProviders(<Navbar {...defaultProps} />);
expect(screen.getByRole("button", { name: /^notifications$/i })).toBeInTheDocument();
expect(screen.getByText("Docs")).toBeInTheDocument();
expect(screen.getByText("User")).toBeInTheDocument();
expect(screen.getByRole("button", { name: /open account menu/i })).toBeInTheDocument();
});
it("should display user information in dropdown", async () => {
const user = userEvent.setup();
renderWithProviders(<Navbar {...defaultProps} />);
await user.click(screen.getByText("User"));
await user.click(screen.getByRole("button", { name: /open account menu/i }));
await waitFor(() => {
expect(screen.getByText("test-user")).toBeInTheDocument();
@ -193,7 +194,7 @@ describe("Navbar", () => {
});
renderWithProviders(<Navbar {...defaultProps} />);
await user.click(screen.getByText("User"));
await user.click(screen.getByRole("button", { name: /open account menu/i }));
await waitFor(() => {
expect(screen.getByText("Premium")).toBeInTheDocument();
@ -230,7 +231,7 @@ describe("Navbar", () => {
const publicPageProps = { ...defaultProps, isPublicPage: true };
renderWithProviders(<Navbar {...publicPageProps} />);
expect(screen.queryByText("User")).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /open account menu/i })).not.toBeInTheDocument();
});
it("should handle hide new features toggle", async () => {
@ -244,7 +245,7 @@ describe("Navbar", () => {
renderWithProviders(<Navbar {...defaultProps} />);
await user.click(screen.getByText("User"));
await user.click(screen.getByRole("button", { name: /open account menu/i }));
await waitFor(() => {
expect(screen.getByText("test-user")).toBeInTheDocument();
@ -269,7 +270,7 @@ describe("Navbar", () => {
renderWithProviders(<Navbar {...defaultProps} />);
await user.click(screen.getByText("User"));
await user.click(screen.getByRole("button", { name: /open account menu/i }));
await waitFor(() => {
expect(screen.getByText("test-user")).toBeInTheDocument();

View file

@ -1,16 +1,20 @@
import { useHealthReadiness } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadiness";
import { useDisableBouncingIcon } from "@/app/(dashboard)/hooks/useDisableBouncingIcon";
import { useDisableShowPrompts } from "@/app/(dashboard)/hooks/useDisableShowPrompts";
import { useHealthReadiness } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadiness";
import { useWorker } from "@/hooks/useWorker";
import { getProxyBaseUrl } from "@/components/networking";
import { useTheme } from "@/contexts/ThemeContext";
import { clearTokenCookies } from "@/utils/cookieUtils";
import { clearStoredReturnUrl } from "@/utils/returnUrlUtils";
import { fetchProxySettings } from "@/utils/proxyUtils";
import { MenuFoldOutlined, MenuUnfoldOutlined, MoonOutlined, SunOutlined } from "@ant-design/icons";
import { Button, Switch, Tag } from "antd";
import { DownOutlined, MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons";
import { Tag } from "antd";
import Link from "next/link";
import React, { useEffect, useState } from "react";
import { BlogDropdown } from "./Navbar/BlogDropdown/BlogDropdown";
import { CommunityEngagementButtons } from "./Navbar/CommunityEngagementButtons/CommunityEngagementButtons";
import { NAV_PRODUCT_LINK_CLASS } from "./Navbar/navProductLinkClass";
import { NotificationsBell } from "./Navbar/NotificationsBell/NotificationsBell";
import UserDropdown from "./Navbar/UserDropdown/UserDropdown";
import WorkerDropdown from "./Navbar/WorkerDropdown/WorkerDropdown";
@ -30,18 +34,12 @@ interface NavbarProps {
}
const Navbar: React.FC<NavbarProps> = ({
userID,
userEmail,
userRole,
premiumUser,
proxySettings,
setProxySettings,
accessToken,
isPublicPage = false,
sidebarCollapsed = false,
onToggleSidebar,
isDarkMode,
toggleDarkMode,
}) => {
const baseUrl = getProxyBaseUrl();
const [logoutUrl, setLogoutUrl] = useState("");
@ -49,8 +47,10 @@ const Navbar: React.FC<NavbarProps> = ({
const { data: healthData } = useHealthReadiness();
const version = healthData?.litellm_version;
const disableBouncingIcon = useDisableBouncingIcon();
const hideCommunityLinks = useDisableShowPrompts();
const { isControlPlane, selectedWorker } = useWorker();
const showWorkerSwitch = isControlPlane && selectedWorker !== null;
// Simple logo URL: use custom logo if available, otherwise default
const imageUrl = logoUrl || `${baseUrl}/get_image`;
useEffect(() => {
@ -87,14 +87,14 @@ const Navbar: React.FC<NavbarProps> = ({
};
return (
<nav className="bg-white border-b border-gray-200 sticky top-0 z-10">
<nav className="sticky top-0 z-10 border-b border-gray-200 bg-white">
<div className="w-full">
<div className="flex items-center h-14 px-4">
<div className="flex items-center flex-shrink-0">
<div className="flex h-14 items-center px-4">
<div className="flex flex-shrink-0 items-center">
{onToggleSidebar && (
<button
onClick={onToggleSidebar}
className="flex items-center justify-center w-10 h-10 mr-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded transition-colors"
className="mr-2 flex h-9 w-9 items-center justify-center rounded-md text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
title={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
>
<span className="text-lg">{sidebarCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}</span>
@ -104,11 +104,11 @@ const Navbar: React.FC<NavbarProps> = ({
<div className="flex items-center gap-2">
<Link href={baseUrl ? baseUrl : "/"} className="flex items-center">
<div className="relative">
<div className="h-10 max-w-48 flex items-center justify-center overflow-hidden">
<div className="flex h-10 max-w-48 items-center justify-center overflow-hidden">
<img
src={imageUrl}
alt="LiteLLM Brand"
className="max-w-full max-h-full w-auto h-auto object-contain"
className="h-auto max-h-full w-auto max-w-full object-contain"
/>
</div>
</div>
@ -117,14 +117,14 @@ const Navbar: React.FC<NavbarProps> = ({
<div className="relative">
{!disableBouncingIcon && (
<span
className="absolute -top-1 -left-2 text-lg animate-bounce"
className="absolute -left-2 -top-1 animate-bounce text-lg"
style={{ animationDuration: "2s" }}
title="Thanks for using LiteLLM!"
>
🌑
</span>
)}
<Tag className="relative text-xs font-medium cursor-pointer z-10">
<Tag className="relative z-10 cursor-pointer text-xs font-medium">
<a
href="https://docs.litellm.ai/release_notes"
target="_blank"
@ -138,28 +138,50 @@ const Navbar: React.FC<NavbarProps> = ({
)}
</div>
</div>
{/* Right side nav items */}
<div className="flex items-center space-x-5 ml-auto">
<WorkerDropdown onWorkerSwitch={handleWorkerSwitch} />
<CommunityEngagementButtons />
{/* Dark mode is currently a work in progress. To test, you can change 'false' to 'true' below.
Do not set this to true by default until all components are confirmed to support dark mode styles. */}
{false && (
<Switch
data-testid="dark-mode-toggle"
checked={isDarkMode}
onChange={toggleDarkMode}
checkedChildren={<MoonOutlined />}
unCheckedChildren={<SunOutlined />}
/>
)}
<Button type="text" href="https://docs.litellm.ai/docs/" target="_blank" rel="noopener noreferrer">
Docs
</Button>
<BlogDropdown />
{!isPublicPage && <UserDropdown onLogout={handleLogout} />}
<div className="ml-auto flex min-w-0 flex-1 items-center justify-end gap-4">
{showWorkerSwitch && (
<div className="flex shrink-0 items-center">
<WorkerDropdown onWorkerSwitch={handleWorkerSwitch} />
</div>
)}
<nav
aria-label="Product documentation"
className={`flex min-w-0 items-center gap-2 ${showWorkerSwitch ? "border-l border-gray-200 pl-4" : ""}`}
>
<a
href="https://docs.litellm.ai/docs/"
target="_blank"
rel="noopener noreferrer"
className={NAV_PRODUCT_LINK_CLASS}
>
Docs
{/* Layout parity with Blog chevron — intentional single-level link */}
<DownOutlined className="pointer-events-none text-[10px] opacity-0" aria-hidden />
</a>
<BlogDropdown />
</nav>
{!hideCommunityLinks && (
<div className="flex shrink-0 items-center border-l border-gray-200 pl-4">
<CommunityEngagementButtons />
</div>
)}
<div className="flex shrink-0 items-center border-l border-gray-200 pl-4">
<div className="flex items-center gap-0.5 rounded-lg bg-gray-50 px-1 py-0 transition-colors hover:bg-gray-100">
<NotificationsBell />
{!isPublicPage && (
<>
<span className="mx-0.5 h-6 w-px shrink-0 bg-gray-200" aria-hidden />
<UserDropdown onLogout={handleLogout} />
</>
)}
</div>
</div>
</div>
{/* Dark mode toggle: keep disabled until the dashboard supports dark styles end-to-end. */}
</div>
</div>
</nav>