refactor(ui): extract shared tab-routing helpers and adopt them in Models + Endpoints (#34435)

* refactor(ui): extract shared tab-routing helpers

Every per-tab-routed page copy-pastes the same URL<->slug logic and the
same active-tab/redirect engine. Extract two reusable pieces:

- createTabRoutes(baseSegment, slugs) in utils/tabRoutes.ts returns
  { baseSegment, slugs, tabHref, slugFromPathname }, the trailing-slash
  href builder (via migratedHref) and the pathname->slug reader.
- useTabRouting({ routes, baseTabKey, visibleKeys, ready }) derives the
  active tab from the pathname, redirects an unknown/forbidden slug to
  base once ready, and returns an onTabChange navigator.

visibleKeys + ready exist so a role-gated page can pass its filtered tab
set and defer the redirect until permissions resolve, rather than
bouncing a user off a still-loading valid tab. Both are pure/unit-tested.
No page consumes them yet.

* refactor(ui): migrate Models + Endpoints onto the shared tab-routing helpers

Replace the page's hand-rolled tabRoutes.ts (base segment + slug tuple +
href builder + slugFromPathname) with createTabRoutes, keeping the
existing named exports as thin re-exports so callers and tests are
unchanged. The layout drops its local activeSlug/isKnownSlug/activeKey
derivation, its redirect useEffect and its router.push onChange in favor
of useTabRouting, passing the role-filtered visibleKeys and a ready flag
(!teamsLoading && !uiSettingsLoading) so the permission-gated redirect
behavior is preserved exactly. The antd tab bar, role-gated tab set, the
refresh button and the ?model=/?team= drill-in overlay are untouched; the
file's pre-existing antd import is now recorded in the suppressions
baseline since editing it makes it a linted-as-changed file.

The existing models-and-endpoints layout.test.tsx and tabRoutes.test.ts
pass unchanged, which is the regression guarantee.
This commit is contained in:
ryan-crabbe-berri 2026-07-23 16:36:25 -07:00 committed by GitHub
parent 07726b4f60
commit e906a7e796
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 217 additions and 37 deletions

View file

@ -1142,6 +1142,11 @@
"count": 1
}
},
"src/app/(dashboard)/models-and-endpoints/layout.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer.ts": {
"prefer-const": {
"count": 6

View file

@ -0,0 +1,82 @@
/* @vitest-environment jsdom */
import { renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { mockPush, navState } = vi.hoisted(() => ({
mockPush: vi.fn(),
navState: { pathname: "/logs" },
}));
vi.mock("next/navigation", () => ({
usePathname: () => navState.pathname,
useRouter: () => ({ push: mockPush }),
}));
vi.mock("@/components/networking", () => ({ serverRootPath: "" }));
import { createTabRoutes } from "@/utils/tabRoutes";
import { useTabRouting } from "./useTabRouting";
const routes = createTabRoutes("logs", ["audit", "deleted-keys", "deleted-teams"] as const);
const render = (ready = true) => {
const config = {
routes,
baseTabKey: "request-logs",
visibleKeys: ["audit", "deleted-keys", "deleted-teams"],
ready,
};
return renderHook(() => useTabRouting(config));
};
describe("useTabRouting", () => {
beforeEach(() => {
navState.pathname = "/logs";
mockPush.mockClear();
});
it("maps the base path to the base tab key", () => {
const { result } = render();
expect(result.current.activeSlug).toBe("");
expect(result.current.activeKey).toBe("request-logs");
});
it("uses the slug itself as the active key for a known nested tab", () => {
navState.pathname = "/ui/logs/audit";
const { result } = render();
expect(result.current.activeKey).toBe("audit");
});
it("falls back to the base tab key for an unknown slug", () => {
navState.pathname = "/ui/logs/bogus";
const { result } = render();
expect(result.current.activeKey).toBe("request-logs");
});
it("redirects an unknown slug to the base href once ready", () => {
const replaceMock = vi.fn();
const originalLocation = window.location;
Object.defineProperty(window, "location", { configurable: true, value: { replace: replaceMock } });
navState.pathname = "/ui/logs/bogus";
render(true);
expect(replaceMock).toHaveBeenCalledWith("/ui/logs/");
Object.defineProperty(window, "location", { configurable: true, value: originalLocation });
});
it("does not redirect while not ready (role/creds still loading)", () => {
const replaceMock = vi.fn();
const originalLocation = window.location;
Object.defineProperty(window, "location", { configurable: true, value: { replace: replaceMock } });
navState.pathname = "/ui/logs/bogus";
render(false);
expect(replaceMock).not.toHaveBeenCalled();
Object.defineProperty(window, "location", { configurable: true, value: originalLocation });
});
it("pushes the tab href on change, mapping the base key back to the empty slug", () => {
const { result } = render();
result.current.onTabChange("audit");
expect(mockPush).toHaveBeenCalledWith("/ui/logs/audit/");
result.current.onTabChange("request-logs");
expect(mockPush).toHaveBeenCalledWith("/ui/logs/");
});
});

View file

@ -0,0 +1,38 @@
import { useEffect } from "react";
import { usePathname, useRouter } from "next/navigation";
import type { TabRoutes } from "@/utils/tabRoutes";
interface UseTabRoutingArgs {
routes: Pick<TabRoutes<string>, "tabHref" | "slugFromPathname">;
baseTabKey: string;
visibleKeys: readonly string[];
ready?: boolean;
}
interface TabRoutingState {
activeSlug: string;
activeKey: string;
onTabChange: (key: string) => void;
}
export function useTabRouting({ routes, baseTabKey, visibleKeys, ready = true }: UseTabRoutingArgs): TabRoutingState {
const { tabHref, slugFromPathname } = routes;
const pathname = usePathname();
const router = useRouter();
const activeSlug = slugFromPathname(pathname);
const isKnownSlug = activeSlug === "" || visibleKeys.includes(activeSlug);
const activeKey = isKnownSlug ? activeSlug || baseTabKey : baseTabKey;
useEffect(() => {
if (ready && activeSlug !== "" && !isKnownSlug) {
window.location.replace(tabHref(""));
}
}, [ready, activeSlug, isKnownSlug, tabHref]);
const onTabChange = (key: string) => {
router.push(tabHref(key === baseTabKey ? "" : key));
};
return { activeSlug, activeKey, onTabChange };
}

View file

@ -1,19 +1,19 @@
"use client";
import type { ReactNode } from "react";
import { useEffect, useMemo, useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import { useMemo, useState } from "react";
import { Tabs } from "antd";
import { RefreshIcon } from "@heroicons/react/outline";
import { useQueryClient } from "@tanstack/react-query";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
import { useTabRouting } from "@/app/(dashboard)/hooks/useTabRouting";
import { all_admin_roles, internalUserRoles, isProxyAdminRole, isUserTeamAdminForAnyTeam } from "@/utils/roles";
import CostOptimizationFeedbackBanner from "@/components/molecules/cost_optimization_feedback_banner";
import ModelInfoView from "@/components/model_info_view";
import TeamInfoView from "@/components/team/TeamInfo";
import { modelTabHref, slugFromPathname, type ModelTabSlug } from "@/app/(dashboard)/models-and-endpoints/tabRoutes";
import { modelsRoutes, type ModelTabSlug } from "@/app/(dashboard)/models-and-endpoints/tabRoutes";
import { useModelDetailRouting } from "@/app/(dashboard)/models-and-endpoints/detailNavigation";
import { useModelDashboardData } from "@/app/(dashboard)/models-and-endpoints/useModelDashboardData";
@ -33,8 +33,6 @@ export default function ModelsAndEndpointsLayout({ children }: { children: React
const { accessToken, userRole, userId: userID, premiumUser } = useAuthorized();
const { data: teams, isLoading: teamsLoading } = useTeams();
const { data: uiSettings, isLoading: uiSettingsLoading } = useUISettings();
const pathname = usePathname();
const router = useRouter();
const queryClient = useQueryClient();
const { modelId, teamId, close } = useModelDetailRouting();
const { availableModelAccessGroups, allModelsOnProxy } = useModelDashboardData();
@ -60,18 +58,13 @@ export default function ModelsAndEndpointsLayout({ children }: { children: React
[shouldHideAddModelTab, isAdmin],
);
const activeSlug = slugFromPathname(pathname);
const isKnownSlug = visibleSlugs.some((slug) => slug === activeSlug);
const activeKey = isKnownSlug ? activeSlug || BASE_TAB_KEY : BASE_TAB_KEY;
useEffect(() => {
if (teamsLoading || uiSettingsLoading) {
return;
}
if (activeSlug !== "" && !isKnownSlug) {
window.location.replace(modelTabHref(""));
}
}, [activeSlug, isKnownSlug, teamsLoading, uiSettingsLoading]);
const tabRoutingConfig = {
routes: modelsRoutes,
baseTabKey: BASE_TAB_KEY,
visibleKeys: visibleSlugs.filter(Boolean),
ready: !teamsLoading && !uiSettingsLoading,
};
const { activeKey, onTabChange } = useTabRouting(tabRoutingConfig);
const allModelsLabel = isAdmin ? "All Models" : "Your Models";
const tabItems = visibleSlugs.map((slug) => {
@ -137,7 +130,7 @@ export default function ModelsAndEndpointsLayout({ children }: { children: React
) : (
<Tabs
activeKey={activeKey}
onChange={(key) => router.push(modelTabHref(key === BASE_TAB_KEY ? "" : key))}
onChange={onTabChange}
items={tabItems}
tabBarExtraContent={{
right: (

View file

@ -1,8 +1,6 @@
import { migratedHref } from "@/utils/migratedPages";
import { createTabRoutes } from "@/utils/tabRoutes";
export const MODELS_BASE_SEGMENT = "models-and-endpoints";
export const MODEL_TAB_SLUGS = [
export const modelsRoutes = createTabRoutes("models-and-endpoints", [
"add",
"llm-credentials",
"pass-through",
@ -10,20 +8,11 @@ export const MODEL_TAB_SLUGS = [
"retry-settings",
"model-group-alias",
"price-data",
] as const;
] as const);
export type ModelTabSlug = (typeof MODEL_TAB_SLUGS)[number];
export type ModelTabSlug = (typeof modelsRoutes.slugs)[number];
export function modelTabHref(slug: string): string {
const base = migratedHref(MODELS_BASE_SEGMENT);
return slug ? `${base}/${slug}/` : `${base}/`;
}
export function slugFromPathname(pathname: string): string {
const parts = pathname.split("/").filter(Boolean);
const idx = parts.indexOf(MODELS_BASE_SEGMENT);
if (idx === -1) {
return "";
}
return parts[idx + 1] ?? "";
}
export const MODELS_BASE_SEGMENT = modelsRoutes.baseSegment;
export const MODEL_TAB_SLUGS = modelsRoutes.slugs;
export const modelTabHref = modelsRoutes.tabHref;
export const slugFromPathname = modelsRoutes.slugFromPathname;

View file

@ -0,0 +1,47 @@
/* @vitest-environment jsdom */
import { describe, expect, it, vi } from "vitest";
vi.mock("@/components/networking", () => ({ serverRootPath: "" }));
import { createTabRoutes } from "./tabRoutes";
const routes = createTabRoutes("logs", ["audit", "deleted-keys", "deleted-teams"] as const);
describe("createTabRoutes.slugFromPathname", () => {
it("returns empty string for the base path with or without a trailing slash", () => {
expect(routes.slugFromPathname("/logs")).toBe("");
expect(routes.slugFromPathname("/logs/")).toBe("");
});
it("extracts the tab slug from dev and proxy-mounted (/ui) paths", () => {
expect(routes.slugFromPathname("/logs/audit")).toBe("audit");
expect(routes.slugFromPathname("/ui/logs/deleted-teams/")).toBe("deleted-teams");
});
it("returns the raw segment for an unknown tab so the caller can redirect to base", () => {
expect(routes.slugFromPathname("/ui/logs/bogus")).toBe("bogus");
});
it("returns empty string when the base segment is not in the path", () => {
expect(routes.slugFromPathname("/teams")).toBe("");
});
});
describe("createTabRoutes.tabHref", () => {
it("builds the trailing-slash base href for the empty slug", () => {
expect(routes.tabHref("")).toBe("/ui/logs/");
});
it("builds a trailing-slash href for every tab slug (required by static export)", () => {
for (const slug of routes.slugs) {
expect(routes.tabHref(slug)).toBe(`/ui/logs/${slug}/`);
}
});
});
describe("createTabRoutes metadata", () => {
it("preserves the base segment and slug tuple", () => {
expect(routes.baseSegment).toBe("logs");
expect(routes.slugs).toEqual(["audit", "deleted-keys", "deleted-teams"]);
});
});

View file

@ -0,0 +1,26 @@
import { migratedHref } from "@/utils/migratedPages";
export interface TabRoutes<Slug extends string> {
baseSegment: string;
slugs: readonly Slug[];
tabHref: (slug: string) => string;
slugFromPathname: (pathname: string) => string;
}
export function createTabRoutes<Slug extends string>(baseSegment: string, slugs: readonly Slug[]): TabRoutes<Slug> {
const tabHref = (slug: string): string => {
const base = migratedHref(baseSegment);
return slug ? `${base}/${slug}/` : `${base}/`;
};
const slugFromPathname = (pathname: string): string => {
const parts = pathname.split("/").filter(Boolean);
const idx = parts.indexOf(baseSegment);
if (idx === -1) {
return "";
}
return parts[idx + 1] ?? "";
};
return { baseSegment, slugs, tabHref, slugFromPathname };
}