From ab7a3175cd20efc0a60265151df7b557e7578668 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Thu, 23 Jul 2026 16:47:39 -0700 Subject: [PATCH] refactor(ui): adopt shared tab-routing helpers + anchor TabRouteBar in Caching Now that createTabRoutes and useTabRouting are on staging, replace Caching's hand-written tabRoutes.ts and layout routing engine with them, and introduce the shared : a shadcn tab bar whose triggers render as real anchors (via Base UI's render prop). A plain left click soft-navigates through the router; a modifier-click falls through to the browser so open-in-new-tab / new-window work, mirroring the sidebar's own anchor convention. tabRoutes.ts collapses to one createTabRoutes call and the per-page tabRoutes.test.ts is dropped (the factory is covered centrally by utils/tabRoutes.test.ts). --- .../app/(dashboard)/caching/layout.test.tsx | 26 ++++----- .../src/app/(dashboard)/caching/layout.tsx | 49 +++++----------- .../app/(dashboard)/caching/tabRoutes.test.ts | 38 ------------- .../src/app/(dashboard)/caching/tabRoutes.ts | 22 +------- .../components/TabRouteBar.test.tsx | 56 +++++++++++++++++++ .../(dashboard)/components/TabRouteBar.tsx | 48 ++++++++++++++++ 6 files changed, 132 insertions(+), 107 deletions(-) delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/caching/tabRoutes.test.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/components/TabRouteBar.test.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/components/TabRouteBar.tsx diff --git a/ui/litellm-dashboard/src/app/(dashboard)/caching/layout.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/caching/layout.test.tsx index d4436ae2793..8cbc7a00dfc 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/caching/layout.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/caching/layout.test.tsx @@ -33,30 +33,24 @@ describe("CachingLayout", () => { }; }); - it("renders the tab bar and the active tab's page content", () => { + it("renders the four tabs and the active tab's page content", () => { const { getByRole, getByTestId } = renderLayout(); - expect(getByRole("tab", { name: "Cache Analytics" })).toBeInTheDocument(); - expect(getByRole("tab", { name: "Cache Health" })).toBeInTheDocument(); - expect(getByRole("tab", { name: "Cache Settings" })).toBeInTheDocument(); - expect(getByRole("tab", { name: "Coordination Redis" })).toBeInTheDocument(); + for (const name of ["Cache Analytics", "Cache Health", "Cache Settings", "Coordination Redis"]) { + expect(getByRole("tab", { name })).toBeInTheDocument(); + } expect(getByTestId("tab-content")).toHaveTextContent("CHILD"); }); - it("navigates to a tab's path when its tab is clicked", async () => { + it("marks the base route's Cache Analytics tab active", () => { const { getByRole } = renderLayout(); - await act(async () => { - getByRole("tab", { name: "Cache Health" }).click(); - }); - expect(mockPush).toHaveBeenCalledWith(expect.stringMatching(/\/caching\/health\/$/)); + expect(getByRole("tab", { name: "Cache Analytics" })).toHaveAttribute("aria-selected", "true"); }); - it("routes the base tab back to the caching root (no slug)", async () => { - navState.pathname = "/caching/health"; + it("derives the active tab from a nested pathname", () => { + navState.pathname = "/ui/caching/settings"; const { getByRole } = renderLayout(); - await act(async () => { - getByRole("tab", { name: "Cache Analytics" }).click(); - }); - expect(mockPush).toHaveBeenCalledWith(expect.stringMatching(/\/caching\/$/)); + expect(getByRole("tab", { name: "Cache Settings" })).toHaveAttribute("aria-selected", "true"); + expect(getByRole("tab", { name: "Cache Analytics" })).toHaveAttribute("aria-selected", "false"); }); it("redirects to the base caching path when the tab slug is unknown", async () => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/caching/layout.tsx b/ui/litellm-dashboard/src/app/(dashboard)/caching/layout.tsx index f0c75a7bb28..ff92a1ced55 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/caching/layout.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/caching/layout.tsx @@ -1,48 +1,29 @@ "use client"; import type { ReactNode } from "react"; -import { useEffect } from "react"; -import { usePathname, useRouter } from "next/navigation"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { cacheTabHref, slugFromPathname, type CacheTabSlug } from "@/app/(dashboard)/caching/tabRoutes"; +import { cachingRoutes } from "@/app/(dashboard)/caching/tabRoutes"; +import { useTabRouting } from "@/app/(dashboard)/hooks/useTabRouting"; +import { TabRouteBar } from "@/app/(dashboard)/components/TabRouteBar"; const BASE_TAB_KEY = "cache-analytics"; -const ORDERED_KEYS: Array<"" | CacheTabSlug> = ["", "health", "settings", "coordination-redis"]; - -const TAB_LABELS: Record<"" | CacheTabSlug, string> = { - "": "Cache Analytics", - health: "Cache Health", - settings: "Cache Settings", - "coordination-redis": "Coordination Redis", -}; +const TABS = [ + { key: BASE_TAB_KEY, label: "Cache Analytics" }, + { key: "health", label: "Cache Health" }, + { key: "settings", label: "Cache Settings" }, + { key: "coordination-redis", label: "Coordination Redis" }, +] as const; export default function CachingLayout({ children }: { children: ReactNode }) { - const pathname = usePathname(); - const router = useRouter(); - - const activeSlug = slugFromPathname(pathname); - const isKnownSlug = ORDERED_KEYS.some((slug) => slug === activeSlug); - const activeKey = isKnownSlug ? activeSlug || BASE_TAB_KEY : BASE_TAB_KEY; - - useEffect(() => { - if (activeSlug !== "" && !isKnownSlug) { - window.location.replace(cacheTabHref("")); - } - }, [activeSlug, isKnownSlug]); + const { activeKey } = useTabRouting({ + routes: cachingRoutes, + baseTabKey: BASE_TAB_KEY, + visibleKeys: cachingRoutes.slugs, + }); return (
- router.push(cacheTabHref(key === BASE_TAB_KEY ? "" : key))}> - - {ORDERED_KEYS.map((slug) => ( - - {TAB_LABELS[slug]} - - ))} - - - +
{children}
); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/caching/tabRoutes.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/caching/tabRoutes.test.ts deleted file mode 100644 index 842f5c5680c..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/caching/tabRoutes.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* @vitest-environment jsdom */ -import { describe, expect, it, vi } from "vitest"; - -vi.mock("@/components/networking", () => ({ serverRootPath: "" })); - -import { CACHE_TAB_SLUGS, cacheTabHref, slugFromPathname } from "./tabRoutes"; - -describe("slugFromPathname", () => { - it("returns empty string for the base path with or without a trailing slash", () => { - expect(slugFromPathname("/caching")).toBe(""); - expect(slugFromPathname("/caching/")).toBe(""); - }); - - it("extracts the tab slug from dev and proxy-mounted (/ui) paths", () => { - expect(slugFromPathname("/caching/health")).toBe("health"); - expect(slugFromPathname("/ui/caching/coordination-redis/")).toBe("coordination-redis"); - }); - - it("returns the raw segment for an unknown tab so the layout can redirect to base", () => { - expect(slugFromPathname("/ui/caching/bogus")).toBe("bogus"); - }); - - it("returns empty string when the caching base segment is not in the path", () => { - expect(slugFromPathname("/teams")).toBe(""); - }); -}); - -describe("cacheTabHref", () => { - it("builds the trailing-slash base href for the empty slug", () => { - expect(cacheTabHref("")).toBe("/ui/caching/"); - }); - - it("builds a trailing-slash href for every tab slug (required by static export)", () => { - for (const slug of CACHE_TAB_SLUGS) { - expect(cacheTabHref(slug)).toBe(`/ui/caching/${slug}/`); - } - }); -}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/caching/tabRoutes.ts b/ui/litellm-dashboard/src/app/(dashboard)/caching/tabRoutes.ts index 15806cbe813..49860f7efaf 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/caching/tabRoutes.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/caching/tabRoutes.ts @@ -1,21 +1,5 @@ -import { migratedHref } from "@/utils/migratedPages"; +import { createTabRoutes } from "@/utils/tabRoutes"; -export const CACHING_BASE_SEGMENT = "caching"; +export const cachingRoutes = createTabRoutes("caching", ["health", "settings", "coordination-redis"] as const); -export const CACHE_TAB_SLUGS = ["health", "settings", "coordination-redis"] as const; - -export type CacheTabSlug = (typeof CACHE_TAB_SLUGS)[number]; - -export function cacheTabHref(slug: string): string { - const base = migratedHref(CACHING_BASE_SEGMENT); - return slug ? `${base}/${slug}/` : `${base}/`; -} - -export function slugFromPathname(pathname: string): string { - const parts = pathname.split("/").filter(Boolean); - const idx = parts.indexOf(CACHING_BASE_SEGMENT); - if (idx === -1) { - return ""; - } - return parts[idx + 1] ?? ""; -} +export type CacheTabSlug = (typeof cachingRoutes.slugs)[number]; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/TabRouteBar.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/TabRouteBar.test.tsx new file mode 100644 index 00000000000..f4b149c9667 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/TabRouteBar.test.tsx @@ -0,0 +1,56 @@ +/* @vitest-environment jsdom */ +import { render } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { mockPush } = vi.hoisted(() => ({ mockPush: vi.fn() })); +vi.mock("next/navigation", () => ({ useRouter: () => ({ push: mockPush }) })); +vi.mock("@/components/networking", () => ({ serverRootPath: "" })); + +import { createTabRoutes } from "@/utils/tabRoutes"; +import { TabRouteBar } from "./TabRouteBar"; + +const routes = createTabRoutes("caching", ["health", "settings"] as const); +const TABS = [ + { key: "analytics", label: "Cache Analytics" }, + { key: "health", label: "Cache Health" }, + { key: "settings", label: "Cache Settings" }, +]; + +const renderBar = (activeKey = "analytics") => + render(); + +describe("TabRouteBar", () => { + beforeEach(() => { + mockPush.mockClear(); + }); + + it("renders each tab as an anchor with its trailing-slash href (base tab maps to the root)", () => { + const { getByRole } = renderBar(); + expect(getByRole("tab", { name: "Cache Analytics" })).toHaveAttribute("href", "/ui/caching/"); + expect(getByRole("tab", { name: "Cache Health" })).toHaveAttribute("href", "/ui/caching/health/"); + expect(getByRole("tab", { name: "Cache Settings" })).toHaveAttribute("href", "/ui/caching/settings/"); + }); + + it("marks the active tab selected from activeKey", () => { + const { getByRole } = renderBar("health"); + expect(getByRole("tab", { name: "Cache Health" })).toHaveAttribute("aria-selected", "true"); + expect(getByRole("tab", { name: "Cache Analytics" })).toHaveAttribute("aria-selected", "false"); + }); + + it("soft-navigates on a plain left click (preventing the full-page anchor load)", async () => { + const user = userEvent.setup(); + const { getByRole } = renderBar(); + await user.click(getByRole("tab", { name: "Cache Health" })); + expect(mockPush).toHaveBeenCalledWith("/ui/caching/health/"); + }); + + it("lets the browser handle a modifier-click so open-in-new-tab works", async () => { + const user = userEvent.setup(); + const { getByRole } = renderBar(); + await user.keyboard("[ControlLeft>]"); + await user.click(getByRole("tab", { name: "Cache Health" })); + await user.keyboard("[/ControlLeft]"); + expect(mockPush).not.toHaveBeenCalled(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/TabRouteBar.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/TabRouteBar.tsx new file mode 100644 index 00000000000..60cbe53e326 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/TabRouteBar.tsx @@ -0,0 +1,48 @@ +"use client"; + +import type { MouseEvent } from "react"; +import { useRouter } from "next/navigation"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import type { TabRoutes } from "@/utils/tabRoutes"; + +export interface TabRouteItem { + key: string; + label: string; +} + +interface TabRouteBarProps { + routes: Pick, "tabHref">; + baseTabKey: string; + activeKey: string; + tabs: readonly TabRouteItem[]; + className?: string; +} + +export function TabRouteBar({ routes, baseTabKey, activeKey, tabs, className }: TabRouteBarProps) { + const router = useRouter(); + + const navigate = (href: string) => (event: MouseEvent) => { + const commandModifier = event.metaKey || event.ctrlKey; + const otherModifier = event.shiftKey || event.altKey; + if (commandModifier || otherModifier) { + return; + } + event.preventDefault(); + router.push(href); + }; + + return ( + + + {tabs.map(({ key, label }) => { + const href = routes.tabHref(key === baseTabKey ? "" : key); + return ( + }> + {label} + + ); + })} + + + ); +}