refactor(ui): give each Router Settings tab its own route

Split the Router Settings page's five tabs (Loadbalancing, Routing
Groups, Fallbacks, Prompt Caching, General) into their own prerendered
paths under /router-settings, mirroring the earlier per-tab routing
migrations. A shared layout renders the tab bar and derives the active
tab from the pathname; each tab is its own page.tsx, so deep links and
hard-loads to /router-settings/fallbacks, /prompt-caching, /general and
/routing-groups resolve to real static HTML with no nginx change.

The former GeneralSettings god-component (a Tremor TabGroup wrapping all
five panels) is gone. Its two registry-backed tabs, Prompt Caching and
General, shared the general-settings config fetch and mutations; that
logic moves into a useGeneralSettings hook, and each tab becomes a
self-contained component (PromptCachingSettingsTab, GeneralConfigTab)
that owns its own data. The tab bar is rebuilt on the shadcn Tabs
primitive. PromptCachingPanel and generalSettingsItem stay exported
because the Cost Optimization page reuses them.
This commit is contained in:
ryan-crabbe-berri 2026-07-23 13:53:54 -07:00
parent a507394841
commit e6b2862507
12 changed files with 336 additions and 158 deletions

View file

@ -241,11 +241,6 @@
"count": 1
}
},
"src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/app/(dashboard)/cost-optimization/_components/PromptCompressionTab.tsx": {
"no-restricted-imports": {
"count": 1
@ -1804,9 +1799,6 @@
"count": 1
},
"no-restricted-imports": {
"count": 3
},
"prefer-const": {
"count": 2
}
},

View file

@ -1,7 +1,7 @@
import { renderWithProviders, screen, within } from "../../../../../tests/test-utils";
import userEvent from "@testing-library/user-event";
import { vi } from "vitest";
import GeneralSettings from "./general_settings";
import { GeneralConfigTab } from "./general_settings";
import { deleteConfigFieldSetting, getGeneralSettingsCall, updateConfigFieldSetting } from "@/components/networking";
vi.mock("@/components/networking", () => ({
@ -10,10 +10,6 @@ vi.mock("@/components/networking", () => ({
deleteConfigFieldSetting: vi.fn().mockResolvedValue({}),
}));
vi.mock("@/components/router_settings", () => ({ default: () => null }));
vi.mock("@/components/Settings/RouterSettings/Fallbacks/Fallbacks", () => ({ default: () => null }));
vi.mock("@/components/routing_groups", () => ({ default: () => null }));
// Mirrors the /config/list ordering: the two prompt-caching rows sit between the
// General-tab rows in the unfiltered response but are filtered out of the General
// tab's table, so any index-based lookup into the unfiltered array reads the wrong
@ -63,7 +59,7 @@ const settingsRow = async (fieldName: string) => {
return row as HTMLElement;
};
describe("GeneralSettings General tab", () => {
describe("GeneralConfigTab", () => {
beforeEach(() => {
vi.mocked(getGeneralSettingsCall).mockResolvedValue([...SETTINGS_FIXTURE.map((s) => ({ ...s }))]);
vi.mocked(updateConfigFieldSetting).mockClear();
@ -72,9 +68,8 @@ describe("GeneralSettings General tab", () => {
it("updates max_ui_session_budget with its own value, not the value at its filtered index", async () => {
const user = userEvent.setup();
renderWithProviders(<GeneralSettings accessToken="token" userRole="Admin" userID="user" />);
renderWithProviders(<GeneralConfigTab accessToken="token" />);
await user.click(screen.getByText("General"));
const row = await settingsRow("max_ui_session_budget");
await user.click(within(row).getByRole("button", { name: /update/i }));
@ -84,9 +79,8 @@ describe("GeneralSettings General tab", () => {
it("reset shows the field's default value instead of an empty input", async () => {
const user = userEvent.setup();
renderWithProviders(<GeneralSettings accessToken="token" userRole="Admin" userID="user" />);
renderWithProviders(<GeneralConfigTab accessToken="token" />);
await user.click(screen.getByText("General"));
const row = await settingsRow("max_ui_session_budget");
expect(within(row).getByRole("spinbutton")).toHaveValue("7.50");
@ -98,4 +92,12 @@ describe("GeneralSettings General tab", () => {
expect(deleteConfigFieldSetting).toHaveBeenCalledWith("token", "max_ui_session_budget");
expect(within(row).getByRole("spinbutton")).toHaveValue("1.00");
});
it("hides prompt-caching-tab rows from the General table", async () => {
renderWithProviders(<GeneralConfigTab accessToken="token" />);
await settingsRow("max_ui_session_budget");
expect(screen.queryByText("enable_anthropic_prompt_caching")).not.toBeInTheDocument();
expect(screen.queryByText("anthropic_prompt_caching_ttl")).not.toBeInTheDocument();
});
});

View file

@ -13,26 +13,15 @@ import {
Icon,
Switch,
} from "@tremor/react";
import { TabPanel, TabPanels, TabGroup, TabList, Tab } from "@tremor/react";
import { getGeneralSettingsCall, updateConfigFieldSetting, deleteConfigFieldSetting } from "@/components/networking";
import { InputNumber, Select as AntdSelect } from "antd";
import { TrashIcon } from "@heroicons/react/outline";
import { StatusBadge } from "@/components/shared/table_cells";
import RouterSettings from "@/components/router_settings";
import Fallbacks from "@/components/Settings/RouterSettings/Fallbacks/Fallbacks";
import RoutingGroups from "@/components/routing_groups";
const PROMPT_CACHING_TAB = "prompt_caching";
const ENABLE_ANTHROPIC_PROMPT_CACHING = "enable_anthropic_prompt_caching";
const ANTHROPIC_PROMPT_CACHING_TTL = "anthropic_prompt_caching_ttl";
interface GeneralSettingsPageProps {
accessToken: string | null;
userRole: string | null;
userID: string | null;
}
export interface generalSettingsItem {
field_name: string;
field_type: string;
@ -162,154 +151,105 @@ export const PromptCachingPanel: React.FC<{
);
};
const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, userRole, userID }) => {
const useGeneralSettings = (accessToken: string) => {
const [generalSettings, setGeneralSettings] = useState<generalSettingsItem[]>([]);
useEffect(() => {
if (!accessToken) {
return;
}
getGeneralSettingsCall(accessToken).then((data) => {
let general_settings = data;
setGeneralSettings(general_settings);
setGeneralSettings(data);
});
}, [accessToken]);
const handleInputChange = (fieldName: string, newValue: any) => {
// Update the value in the state
const updatedSettings = generalSettings.map((setting) =>
setting.field_name === fieldName ? { ...setting, field_value: newValue } : setting,
setGeneralSettings((prev) =>
prev.map((setting) => (setting.field_name === fieldName ? { ...setting, field_value: newValue } : setting)),
);
setGeneralSettings(updatedSettings);
};
const handleUpdateField = (fieldName: string) => {
if (!accessToken) {
const fieldValue = generalSettings.find((setting) => setting.field_name === fieldName)?.field_value;
if (fieldValue == null) {
return;
}
let fieldValue = generalSettings.find((setting) => setting.field_name === fieldName)?.field_value;
if (fieldValue == null || fieldValue == undefined) {
return;
}
try {
updateConfigFieldSetting(accessToken, fieldName, fieldValue);
// update value in state
const updatedSettings = generalSettings.map((setting) =>
setting.field_name === fieldName ? { ...setting, stored_in_db: true } : setting,
);
setGeneralSettings(updatedSettings);
} catch (error) {
// do something
}
updateConfigFieldSetting(accessToken, fieldName, fieldValue);
setGeneralSettings((prev) =>
prev.map((setting) => (setting.field_name === fieldName ? { ...setting, stored_in_db: true } : setting)),
);
};
const handleResetField = (fieldName: string) => {
if (!accessToken) {
return;
}
try {
deleteConfigFieldSetting(accessToken, fieldName);
// update value in state
const updatedSettings = generalSettings.map((setting) =>
deleteConfigFieldSetting(accessToken, fieldName);
setGeneralSettings((prev) =>
prev.map((setting) =>
setting.field_name === fieldName
? { ...setting, stored_in_db: null, field_value: setting.field_default_value ?? null }
: setting,
);
setGeneralSettings(updatedSettings);
} catch (error) {
// do something
}
),
);
};
if (!accessToken) {
return null;
}
return (
<div className="w-full">
<TabGroup className="h-[75vh] w-full">
<TabList variant="line" defaultValue="1" className="px-8 pt-4">
<Tab value="1">Loadbalancing</Tab>
<Tab value="2">Routing Groups</Tab>
<Tab value="3">Fallbacks</Tab>
<Tab value="5">Prompt Caching</Tab>
<Tab value="4">General</Tab>
</TabList>
<TabPanels className="px-8 py-6">
<TabPanel>
<RouterSettings accessToken={accessToken} userRole={userRole} userID={userID} />
</TabPanel>
<TabPanel>
<RoutingGroups />
</TabPanel>
<TabPanel>
<Fallbacks accessToken={accessToken} userRole={userRole} userID={userID} />
</TabPanel>
<TabPanel>
<PromptCachingPanel accessToken={accessToken} settings={generalSettings} onChange={handleInputChange} />
</TabPanel>
<TabPanel>
<Card>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Setting</TableHeaderCell>
<TableHeaderCell>Value</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell>Action</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{generalSettings
.filter((value) => value.field_type !== "TypedDictionary" && value.field_tab !== PROMPT_CACHING_TAB)
.map((value, index) => (
<TableRow key={index}>
<TableCell>
<Text>{value.field_name}</Text>
<p
style={{
fontSize: "0.65rem",
color: "#808080",
fontStyle: "italic",
}}
className="mt-1"
>
{value.field_description}
</p>
</TableCell>
<TableCell>
<SettingValueEditor setting={value} onChange={handleInputChange} />
</TableCell>
<TableCell>
{value.stored_in_db == true ? (
<StatusBadge tone="success" label="In DB" />
) : value.stored_in_db == false ? (
<StatusBadge tone="neutral" label="In Config" />
) : (
<StatusBadge tone="neutral" label="Not Set" />
)}
</TableCell>
<TableCell>
<Button onClick={() => handleUpdateField(value.field_name)}>Update</Button>
<Icon icon={TrashIcon} color="red" onClick={() => handleResetField(value.field_name)}>
Reset
</Icon>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
</TabPanel>
</TabPanels>
</TabGroup>
</div>
);
return { generalSettings, handleInputChange, handleUpdateField, handleResetField };
};
export default GeneralSettings;
export const PromptCachingSettingsTab: React.FC<{ accessToken: string }> = ({ accessToken }) => {
const { generalSettings, handleInputChange } = useGeneralSettings(accessToken);
return <PromptCachingPanel accessToken={accessToken} settings={generalSettings} onChange={handleInputChange} />;
};
export const GeneralConfigTab: React.FC<{ accessToken: string }> = ({ accessToken }) => {
const { generalSettings, handleInputChange, handleUpdateField, handleResetField } = useGeneralSettings(accessToken);
return (
<Card>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Setting</TableHeaderCell>
<TableHeaderCell>Value</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell>Action</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{generalSettings
.filter((value) => value.field_type !== "TypedDictionary" && value.field_tab !== PROMPT_CACHING_TAB)
.map((value, index) => (
<TableRow key={index}>
<TableCell>
<Text>{value.field_name}</Text>
<p
style={{
fontSize: "0.65rem",
color: "#808080",
fontStyle: "italic",
}}
className="mt-1"
>
{value.field_description}
</p>
</TableCell>
<TableCell>
<SettingValueEditor setting={value} onChange={handleInputChange} />
</TableCell>
<TableCell>
{value.stored_in_db == true ? (
<StatusBadge tone="success" label="In DB" />
) : value.stored_in_db == false ? (
<StatusBadge tone="neutral" label="In Config" />
) : (
<StatusBadge tone="neutral" label="Not Set" />
)}
</TableCell>
<TableCell>
<Button onClick={() => handleUpdateField(value.field_name)}>Update</Button>
<Icon icon={TrashIcon} color="red" onClick={() => handleResetField(value.field_name)}>
Reset
</Icon>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
);
};

View file

@ -0,0 +1,9 @@
"use client";
import Fallbacks from "@/components/Settings/RouterSettings/Fallbacks/Fallbacks";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
export default function FallbacksPage() {
const { accessToken, userRole, userId } = useAuthorized();
return <Fallbacks accessToken={accessToken} userRole={userRole} userID={userId} />;
}

View file

@ -0,0 +1,12 @@
"use client";
import { GeneralConfigTab } from "@/app/(dashboard)/router-settings/_components/general_settings";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
export default function GeneralConfigPage() {
const { accessToken } = useAuthorized();
if (!accessToken) {
return null;
}
return <GeneralConfigTab accessToken={accessToken} />;
}

View file

@ -0,0 +1,81 @@
/* @vitest-environment jsdom */
import { act, render } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import RouterSettingsLayout from "./layout";
const { mockPush, navState } = vi.hoisted(() => ({
mockPush: vi.fn(),
navState: { pathname: "/router-settings" },
}));
vi.mock("next/navigation", () => ({
usePathname: () => navState.pathname,
useRouter: () => ({ push: mockPush }),
}));
vi.mock("@/components/networking", () => ({ serverRootPath: "" }));
const renderLayout = () =>
render(
<RouterSettingsLayout>
<div data-testid="tab-content">CHILD</div>
</RouterSettingsLayout>,
);
describe("RouterSettingsLayout", () => {
beforeEach(() => {
navState.pathname = "/router-settings";
mockPush.mockClear();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(global as any).ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
};
});
it("renders the tab bar 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();
}
expect(getByTestId("tab-content")).toHaveTextContent("CHILD");
});
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 () => {
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\/$/));
});
it("redirects to the base router-settings path when the tab slug is unknown", async () => {
const replaceMock = vi.fn();
const originalLocation = window.location;
Object.defineProperty(window, "location", {
configurable: true,
value: { replace: replaceMock, assign: vi.fn(), href: "http://localhost/", pathname: "/", search: "" },
});
navState.pathname = "/router-settings/bogus";
await act(async () => {
renderLayout();
});
expect(replaceMock).toHaveBeenCalledWith(expect.stringMatching(/\/router-settings\/$/));
Object.defineProperty(window, "location", { configurable: true, value: originalLocation });
});
});

View file

@ -0,0 +1,64 @@
"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";
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",
};
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]);
return (
<div className="w-full">
<Tabs
value={activeKey}
onValueChange={(key) => router.push(routerSettingsTabHref(key === BASE_TAB_KEY ? "" : key))}
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>
);
}

View file

@ -1,9 +1,9 @@
"use client";
import GeneralSettings from "./_components/general_settings";
import RouterSettings from "@/components/router_settings";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
export default function RouterSettingsPage() {
const { accessToken, userRole, userId } = useAuthorized();
return <GeneralSettings userID={userId} userRole={userRole} accessToken={accessToken} />;
return <RouterSettings accessToken={accessToken} userRole={userRole} userID={userId} />;
}

View file

@ -0,0 +1,12 @@
"use client";
import { PromptCachingSettingsTab } from "@/app/(dashboard)/router-settings/_components/general_settings";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
export default function PromptCachingPage() {
const { accessToken } = useAuthorized();
if (!accessToken) {
return null;
}
return <PromptCachingSettingsTab accessToken={accessToken} />;
}

View file

@ -0,0 +1,7 @@
"use client";
import RoutingGroups from "@/components/routing_groups";
export default function RoutingGroupsPage() {
return <RoutingGroups />;
}

View file

@ -0,0 +1,38 @@
/* @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}/`);
}
});
});

View file

@ -0,0 +1,21 @@
import { migratedHref } from "@/utils/migratedPages";
export const ROUTER_SETTINGS_BASE_SEGMENT = "router-settings";
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] ?? "";
}