mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
refactor(ui): adopt shared tab-routing helpers + anchor TabRouteBar in Router Settings
Replace the page's hand-written tabRoutes.ts and layout routing engine with createTabRoutes + useTabRouting + the shared <TabRouteBar>. The general_settings.tsx split (GeneralConfigTab / PromptCachingSettingsTab / useGeneralSettings) is unchanged and its regression tests stay green. The per-page tabRoutes.test.ts is dropped in favor of the central factory test.
This commit is contained in:
parent
0618b364fc
commit
27a1d7f3ea
6 changed files with 138 additions and 121 deletions
|
|
@ -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(<TabRouteBar routes={routes} baseTabKey="analytics" activeKey={activeKey} tabs={TABS} />);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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<TabRoutes<string>, "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<HTMLAnchorElement>) => {
|
||||
const commandModifier = event.metaKey || event.ctrlKey;
|
||||
const otherModifier = event.shiftKey || event.altKey;
|
||||
if (commandModifier || otherModifier) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
router.push(href);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs value={activeKey} className={className}>
|
||||
<TabsList variant="line">
|
||||
{tabs.map(({ key, label }) => {
|
||||
const href = routes.tabHref(key === baseTabKey ? "" : key);
|
||||
return (
|
||||
<TabsTrigger key={key} value={key} render={<a href={href} onClick={navigate(href)} />}>
|
||||
{label}
|
||||
</TabsTrigger>
|
||||
);
|
||||
})}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="w-full">
|
||||
<Tabs
|
||||
value={activeKey}
|
||||
onValueChange={(key) => router.push(routerSettingsTabHref(key === BASE_TAB_KEY ? "" : key))}
|
||||
<TabRouteBar
|
||||
routes={routerSettingsRoutes}
|
||||
baseTabKey={BASE_TAB_KEY}
|
||||
activeKey={activeKey}
|
||||
tabs={TABS}
|
||||
className="px-8 pt-4"
|
||||
>
|
||||
<TabsList variant="line">
|
||||
{ORDERED_KEYS.map((slug) => (
|
||||
<TabsTrigger key={slug || BASE_TAB_KEY} value={slug || BASE_TAB_KEY}>
|
||||
{TAB_LABELS[slug]}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
/>
|
||||
<div className="px-8 py-6">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}/`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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];
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue