diff --git a/ui/litellm-dashboard/src/components/DashboardHeader.test.tsx b/ui/litellm-dashboard/src/components/DashboardHeader.test.tsx index bc871085f48..aa83e70c56d 100644 --- a/ui/litellm-dashboard/src/components/DashboardHeader.test.tsx +++ b/ui/litellm-dashboard/src/components/DashboardHeader.test.tsx @@ -47,11 +47,11 @@ describe("DashboardHeader breadcrumb", () => { await waitFor(() => expect(screen.getByText("Chat")).toBeInTheDocument()); }); - it("falls back to the static section crumb when the selector has nothing to switch to", () => { + it("keeps the AI Gateway selector at the root even when there is nothing to switch to (discovery)", () => { render(); - expect(screen.getByText("Observability")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /AI Gateway/i })).toBeInTheDocument(); expect(screen.getByText("Logs")).toBeInTheDocument(); - expect(screen.queryByRole("button", { name: /AI Gateway/i })).not.toBeInTheDocument(); + expect(screen.queryByText("Observability")).not.toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/components/DashboardHeader.tsx b/ui/litellm-dashboard/src/components/DashboardHeader.tsx index a8537980dbf..8b928f8e6fe 100644 --- a/ui/litellm-dashboard/src/components/DashboardHeader.tsx +++ b/ui/litellm-dashboard/src/components/DashboardHeader.tsx @@ -13,7 +13,7 @@ import { getBreadcrumb } from "@/components/leftnav"; import { BlogDropdown } from "@/components/Navbar/BlogDropdown/BlogDropdown"; import { CommunityEngagementButtons } from "@/components/Navbar/CommunityEngagementButtons/CommunityEngagementButtons"; import { NotificationsBell } from "@/components/Navbar/NotificationsBell/NotificationsBell"; -import ViewSwitcher, { useViewSwitcherVisible } from "@/components/Navbar/ViewSwitcher"; +import ViewSwitcher from "@/components/Navbar/ViewSwitcher"; import WorkerDropdown from "@/components/Navbar/WorkerDropdown/WorkerDropdown"; import { useWorker } from "@/hooks/useWorker"; import { useDisableShowPrompts } from "@/app/(dashboard)/hooks/useDisableShowPrompts"; @@ -27,8 +27,7 @@ interface DashboardHeaderProps { // Top bar for the dashboard shell. Sits only over the content column (the brand // lives in the sidebar header); mirrors the design's breadcrumb-left / tools-right layout. export function DashboardHeader({ page }: DashboardHeaderProps) { - const { section, title } = getBreadcrumb(page); - const showSwitcher = useViewSwitcherVisible(); + const { title } = getBreadcrumb(page); const { isControlPlane, selectedWorker } = useWorker(); const showWorkerSwitch = isControlPlane && selectedWorker !== null; const hideCommunityLinks = useDisableShowPrompts(); @@ -45,14 +44,10 @@ export function DashboardHeader({ page }: DashboardHeaderProps) {
- {showSwitcher ? ( - - - - ) : ( - section && {section} - )} - {(showSwitcher || section) && } + + + + {title} diff --git a/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.test.tsx b/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.test.tsx index bdbf74cdf7d..d9a28b2ab8c 100644 --- a/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.test.tsx +++ b/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.test.tsx @@ -49,9 +49,23 @@ describe("ViewSwitcher", () => { state.setMode.mockClear(); }); - it("renders nothing with no plugins, chat disabled, and a non-admin user", () => { - const { container } = render(); - expect(container.firstChild).toBeNull(); + it("still renders the selector with a disabled Chat hint when there are no plugins and chat is off", async () => { + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveTextContent("AI Gateway"); + + act(() => { + fireEvent.click(button); + }); + await waitFor(() => expect(screen.getByText("Chat")).toBeInTheDocument()); + expect(screen.getByText(/Admins can enable in Settings/i)).toBeInTheDocument(); + + act(() => { + fireEvent.click(screen.getByText("Chat")); + }); + expect(assignSpy).not.toHaveBeenCalled(); + expect(state.setMode).not.toHaveBeenCalled(); }); it("labels the button from the active plugin and lists AI Gateway + each plugin", async () => { @@ -95,7 +109,7 @@ describe("ViewSwitcher", () => { fireEvent.click(screen.getByRole("button")); }); await waitFor(() => expect(screen.getByText("Chat")).toBeInTheDocument()); - expect(screen.queryByText(/Enable in Admin Settings/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/Admins can enable in Settings/i)).not.toBeInTheDocument(); act(() => { fireEvent.click(screen.getByText("Chat")); @@ -121,7 +135,7 @@ describe("ViewSwitcher", () => { expect(assignSpy).toHaveBeenCalledWith("/ui/"); }); - it("hides the Chat entry from everyone when disabled", async () => { + it("shows Chat as a disabled, non-navigating entry with an admin hint when disabled", async () => { state.enableChatUI = false; state.plugins = [{ name: "obs", display_name: "Observability", url: "http://localhost:9000" }]; render(); @@ -130,6 +144,12 @@ describe("ViewSwitcher", () => { fireEvent.click(screen.getByRole("button")); }); await waitFor(() => expect(screen.getByText("Observability")).toBeInTheDocument()); - expect(screen.queryByText("Chat")).not.toBeInTheDocument(); + expect(screen.getByText("Chat")).toBeInTheDocument(); + expect(screen.getByText(/Admins can enable in Settings/i)).toBeInTheDocument(); + + act(() => { + fireEvent.click(screen.getByText("Chat")); + }); + expect(assignSpy).not.toHaveBeenCalled(); }); }); diff --git a/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx b/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx index b4876f791eb..09a4538ae18 100644 --- a/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx +++ b/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx @@ -11,12 +11,6 @@ import { migratedHref } from "@/utils/migratedPages"; const GATEWAY = "ai-gateway"; const CHAT = "chat"; -export function useViewSwitcherVisible(): boolean { - const { plugins } = usePluginMode(); - const { data: uiSettings } = useUISettings(); - return plugins.length > 0 || Boolean(uiSettings?.values?.enable_chat_ui); -} - export default function ViewSwitcher() { const { mode, setMode, plugins } = usePluginMode(); const { data: uiSettings } = useUISettings(); @@ -24,8 +18,6 @@ export default function ViewSwitcher() { const chatEnabled = Boolean(uiSettings?.values?.enable_chat_ui); - if (plugins.length === 0 && !chatEnabled) return null; - const chatHref = migratedHref(CHAT); const normalizedPathname = (pathname ?? "").replace(/\/+$/, ""); const isChatRoute = chatEnabled && (normalizedPathname === chatHref || normalizedPathname.startsWith(`${chatHref}/`)); @@ -37,6 +29,29 @@ export default function ViewSwitcher() { ...plugins.map((p) => ({ key: p.name, label: p.display_name })), ]; + const chatItem = chatEnabled + ? { + key: CHAT, + label: ( +
+ Chat + {isChatRoute && } +
+ ), + } + : { + key: CHAT, + disabled: true, + label: ( +
+ Chat + + Admins can enable in Settings + +
+ ), + }; + const items: MenuProps["items"] = [ ...modeEntries.map((e) => ({ key: e.key, @@ -47,19 +62,7 @@ export default function ViewSwitcher() { ), })), - ...(chatEnabled - ? [ - { - key: CHAT, - label: ( -
- Chat - {isChatRoute && } -
- ), - }, - ] - : []), + chatItem, ]; const onClick: MenuProps["onClick"] = ({ key }) => {