From ba17c4526e9d066aa3bd9c507e471f285a990bc5 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Fri, 10 Jul 2026 23:25:19 -0700 Subject: [PATCH] feat(ui): root the gateway breadcrumb in the AI Gateway selector The AI Gateway select (ViewSwitcher) now sits at the root of the DashboardHeader breadcrumb instead of on the right, so the top bar reads [AI Gateway select] > Page to match the redesign. It keeps the same dropdown, including the Chat / Chat UI options. When no plugins are registered and Chat UI is disabled there is nothing to switch between, so the breadcrumb falls back to the static section crumb rather than rendering a dangling leading separator --- .../src/components/DashboardHeader.test.tsx | 57 +++++++++++++++++++ .../src/components/DashboardHeader.tsx | 17 +++--- .../src/components/Navbar/ViewSwitcher.tsx | 19 +++++-- 3 files changed, 80 insertions(+), 13 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/DashboardHeader.test.tsx diff --git a/ui/litellm-dashboard/src/components/DashboardHeader.test.tsx b/ui/litellm-dashboard/src/components/DashboardHeader.test.tsx new file mode 100644 index 00000000000..bc871085f48 --- /dev/null +++ b/ui/litellm-dashboard/src/components/DashboardHeader.test.tsx @@ -0,0 +1,57 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { DashboardHeader } from "./DashboardHeader"; + +const { mockUsePluginMode, mockUseUISettings, state } = vi.hoisted(() => { + const state = { + plugins: [] as { name: string; display_name: string; url: string }[], + enableChatUI: false, + }; + return { + state, + mockUsePluginMode: vi.fn(() => ({ mode: "ai-gateway", setMode: vi.fn(), plugins: state.plugins })), + mockUseUISettings: vi.fn(() => ({ data: { values: { enable_chat_ui: state.enableChatUI } } })), + }; +}); + +vi.mock("@/contexts/PluginModeContext", () => ({ usePluginMode: mockUsePluginMode })); +vi.mock("@/app/(dashboard)/hooks/uiSettings/useUISettings", () => ({ useUISettings: mockUseUISettings })); +vi.mock("next/navigation", () => ({ usePathname: () => "/ui/" })); +vi.mock("@/utils/migratedPages", () => ({ migratedHref: (seg: string) => `/ui/${seg}` })); +vi.mock("@/hooks/useWorker", () => ({ useWorker: () => ({ isControlPlane: false, selectedWorker: null }) })); +vi.mock("@/app/(dashboard)/hooks/useDisableShowPrompts", () => ({ useDisableShowPrompts: () => false })); +vi.mock("@/components/Navbar/BlogDropdown/BlogDropdown", () => ({ BlogDropdown: () => null })); +vi.mock("@/components/Navbar/CommunityEngagementButtons/CommunityEngagementButtons", () => ({ + CommunityEngagementButtons: () => null, +})); +vi.mock("@/components/Navbar/NotificationsBell/NotificationsBell", () => ({ NotificationsBell: () => null })); +vi.mock("@/components/Navbar/WorkerDropdown/WorkerDropdown", () => ({ default: () => null })); + +describe("DashboardHeader breadcrumb", () => { + afterEach(() => { + state.plugins = []; + state.enableChatUI = false; + }); + + it("roots the breadcrumb in the AI Gateway selector (with a Chat option) and drops the static section crumb when the selector is available", async () => { + state.enableChatUI = true; + render(); + + expect(screen.getByText("Logs")).toBeInTheDocument(); + expect(screen.queryByText("Observability")).not.toBeInTheDocument(); + + const selector = screen.getByRole("button", { name: /AI Gateway/i }); + act(() => { + fireEvent.click(selector); + }); + await waitFor(() => expect(screen.getByText("Chat")).toBeInTheDocument()); + }); + + it("falls back to the static section crumb when the selector has nothing to switch to", () => { + render(); + + expect(screen.getByText("Observability")).toBeInTheDocument(); + expect(screen.getByText("Logs")).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /AI Gateway/i })).not.toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/DashboardHeader.tsx b/ui/litellm-dashboard/src/components/DashboardHeader.tsx index 4f285cefed9..a8537980dbf 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 from "@/components/Navbar/ViewSwitcher"; +import ViewSwitcher, { useViewSwitcherVisible } from "@/components/Navbar/ViewSwitcher"; import WorkerDropdown from "@/components/Navbar/WorkerDropdown/WorkerDropdown"; import { useWorker } from "@/hooks/useWorker"; import { useDisableShowPrompts } from "@/app/(dashboard)/hooks/useDisableShowPrompts"; @@ -28,6 +28,7 @@ interface DashboardHeaderProps { // 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 { isControlPlane, selectedWorker } = useWorker(); const showWorkerSwitch = isControlPlane && selectedWorker !== null; const hideCommunityLinks = useDisableShowPrompts(); @@ -44,12 +45,14 @@ export function DashboardHeader({ page }: DashboardHeaderProps) {
- {section && ( - <> - {section} - - + {showSwitcher ? ( + + + + ) : ( + section && {section} )} + {(showSwitcher || section) && } {title} @@ -76,8 +79,6 @@ export function DashboardHeader({ page }: DashboardHeaderProps) { {!hideCommunityLinks && } - -
); diff --git a/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx b/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx index 834966aca35..b4876f791eb 100644 --- a/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx +++ b/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx @@ -1,7 +1,8 @@ import React from "react"; import { usePathname } from "next/navigation"; import { Dropdown } from "antd"; -import { AppstoreOutlined, CheckOutlined, DownOutlined } from "@ant-design/icons"; +import { AppstoreOutlined, CheckOutlined } from "@ant-design/icons"; +import { ChevronsUpDown } from "lucide-react"; import type { MenuProps } from "antd"; import { usePluginMode } from "@/contexts/PluginModeContext"; import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings"; @@ -10,6 +11,12 @@ 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(); @@ -72,11 +79,13 @@ export default function ViewSwitcher() { );