mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
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:
parent
a507394841
commit
e6b2862507
12 changed files with 336 additions and 158 deletions
|
|
@ -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
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import RoutingGroups from "@/components/routing_groups";
|
||||
|
||||
export default function RoutingGroupsPage() {
|
||||
return <RoutingGroups />;
|
||||
}
|
||||
|
|
@ -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}/`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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] ?? "";
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue