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() { );