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}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/layout.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/layout.test.tsx
index 0fda48b9d5a..92ea152190d 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/layout.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/layout.test.tsx
@@ -33,7 +33,7 @@ describe("RouterSettingsLayout", () => {
};
});
- it("renders the tab bar and the active tab's page content", () => {
+ it("renders the five tabs and the active tab's page content", () => {
const { getByRole, getByTestId } = renderLayout();
for (const name of ["Loadbalancing", "Routing Groups", "Fallbacks", "Prompt Caching", "General"]) {
expect(getByRole("tab", { name })).toBeInTheDocument();
@@ -44,24 +44,13 @@ describe("RouterSettingsLayout", () => {
it("marks the base route's Loadbalancing tab active", () => {
const { getByRole } = renderLayout();
expect(getByRole("tab", { name: "Loadbalancing" })).toHaveAttribute("aria-selected", "true");
- expect(getByRole("tab", { name: "General" })).toHaveAttribute("aria-selected", "false");
});
- it("navigates to a tab's path when its tab is clicked", async () => {
+ it("derives the active tab from a nested pathname", () => {
+ navState.pathname = "/ui/router-settings/general";
const { getByRole } = renderLayout();
- await act(async () => {
- getByRole("tab", { name: "Prompt Caching" }).click();
- });
- expect(mockPush).toHaveBeenCalledWith(expect.stringMatching(/\/router-settings\/prompt-caching\/$/));
- });
-
- it("routes the base tab back to the router-settings root (no slug)", async () => {
- navState.pathname = "/router-settings/general";
- const { getByRole } = renderLayout();
- await act(async () => {
- getByRole("tab", { name: "Loadbalancing" }).click();
- });
- expect(mockPush).toHaveBeenCalledWith(expect.stringMatching(/\/router-settings\/$/));
+ expect(getByRole("tab", { name: "General" })).toHaveAttribute("aria-selected", "true");
+ expect(getByRole("tab", { name: "Loadbalancing" })).toHaveAttribute("aria-selected", "false");
});
it("redirects to the base router-settings path when the tab slug is unknown", async () => {
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/layout.tsx b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/layout.tsx
index 6f5cfef99d3..14d7010d017 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/layout.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/layout.tsx
@@ -1,63 +1,36 @@
"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 {
- routerSettingsTabHref,
- slugFromPathname,
- type RouterSettingsTabSlug,
-} from "@/app/(dashboard)/router-settings/tabRoutes";
+import { routerSettingsRoutes } from "@/app/(dashboard)/router-settings/tabRoutes";
+import { useTabRouting } from "@/app/(dashboard)/hooks/useTabRouting";
+import { TabRouteBar } from "@/app/(dashboard)/components/TabRouteBar";
const BASE_TAB_KEY = "loadbalancing";
-const ORDERED_KEYS: Array<"" | RouterSettingsTabSlug> = [
- "",
- "routing-groups",
- "fallbacks",
- "prompt-caching",
- "general",
-];
-
-const TAB_LABELS: Record<"" | RouterSettingsTabSlug, string> = {
- "": "Loadbalancing",
- "routing-groups": "Routing Groups",
- fallbacks: "Fallbacks",
- "prompt-caching": "Prompt Caching",
- general: "General",
-};
+const TABS = [
+ { key: BASE_TAB_KEY, label: "Loadbalancing" },
+ { key: "routing-groups", label: "Routing Groups" },
+ { key: "fallbacks", label: "Fallbacks" },
+ { key: "prompt-caching", label: "Prompt Caching" },
+ { key: "general", label: "General" },
+] as const;
export default function RouterSettingsLayout({ 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(routerSettingsTabHref(""));
- }
- }, [activeSlug, isKnownSlug]);
+ const { activeKey } = useTabRouting({
+ routes: routerSettingsRoutes,
+ baseTabKey: BASE_TAB_KEY,
+ visibleKeys: routerSettingsRoutes.slugs,
+ });
return (
-
router.push(routerSettingsTabHref(key === BASE_TAB_KEY ? "" : key))}
+
-
- {ORDERED_KEYS.map((slug) => (
-
- {TAB_LABELS[slug]}
-
- ))}
-
-
-
+ />
{children}
);
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/tabRoutes.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/tabRoutes.test.ts
deleted file mode 100644
index e4d1880c5e4..00000000000
--- a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/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 { ROUTER_SETTINGS_TAB_SLUGS, routerSettingsTabHref, slugFromPathname } from "./tabRoutes";
-
-describe("slugFromPathname", () => {
- it("returns empty string for the base path with or without a trailing slash", () => {
- expect(slugFromPathname("/router-settings")).toBe("");
- expect(slugFromPathname("/router-settings/")).toBe("");
- });
-
- it("extracts the tab slug from dev and proxy-mounted (/ui) paths", () => {
- expect(slugFromPathname("/router-settings/fallbacks")).toBe("fallbacks");
- expect(slugFromPathname("/ui/router-settings/prompt-caching/")).toBe("prompt-caching");
- });
-
- it("returns the raw segment for an unknown tab so the layout can redirect to base", () => {
- expect(slugFromPathname("/ui/router-settings/bogus")).toBe("bogus");
- });
-
- it("returns empty string when the router-settings base segment is not in the path", () => {
- expect(slugFromPathname("/teams")).toBe("");
- });
-});
-
-describe("routerSettingsTabHref", () => {
- it("builds the trailing-slash base href for the empty slug", () => {
- expect(routerSettingsTabHref("")).toBe("/ui/router-settings/");
- });
-
- it("builds a trailing-slash href for every tab slug (required by static export)", () => {
- for (const slug of ROUTER_SETTINGS_TAB_SLUGS) {
- expect(routerSettingsTabHref(slug)).toBe(`/ui/router-settings/${slug}/`);
- }
- });
-});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/tabRoutes.ts b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/tabRoutes.ts
index 9761d6f43ab..3d46e6d23db 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/tabRoutes.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/tabRoutes.ts
@@ -1,21 +1,10 @@
-import { migratedHref } from "@/utils/migratedPages";
+import { createTabRoutes } from "@/utils/tabRoutes";
-export const ROUTER_SETTINGS_BASE_SEGMENT = "router-settings";
+export const routerSettingsRoutes = createTabRoutes("router-settings", [
+ "routing-groups",
+ "fallbacks",
+ "prompt-caching",
+ "general",
+] as const);
-export const ROUTER_SETTINGS_TAB_SLUGS = ["routing-groups", "fallbacks", "prompt-caching", "general"] as const;
-
-export type RouterSettingsTabSlug = (typeof ROUTER_SETTINGS_TAB_SLUGS)[number];
-
-export function routerSettingsTabHref(slug: string): string {
- const base = migratedHref(ROUTER_SETTINGS_BASE_SEGMENT);
- return slug ? `${base}/${slug}/` : `${base}/`;
-}
-
-export function slugFromPathname(pathname: string): string {
- const parts = pathname.split("/").filter(Boolean);
- const idx = parts.indexOf(ROUTER_SETTINGS_BASE_SEGMENT);
- if (idx === -1) {
- return "";
- }
- return parts[idx + 1] ?? "";
-}
+export type RouterSettingsTabSlug = (typeof routerSettingsRoutes.slugs)[number];