feat(ui): migrate models page to App Router path route (#30677)

* feat(ui): migrate models page to App Router path route

Cut the Models + Endpoints page over from the legacy ?page=models switch
in (dashboard)/page.tsx to a path route at (dashboard)/models-and-endpoints.
Adding the MIGRATED_PAGES entry repoints the sidebar link and redirects old
?page=models bookmarks to /ui/models-and-endpoints.

ModelsAndEndpointsView already sourced identity from useAuthorized() and its
own data via useModelsInfo(), so the token/keys/modelData/setModelData props
were dead; drop them from ModelDashboardProps (and the parent's now-unused
setModelData state) to sever the last of the shared-state coupling.

* test(ui): scope migration smoke's shell probe to the exact sidebar link

The migration smoke used a loose `locator("a", { hasText: "Virtual Keys" })`
to assert the dashboard shell rendered. The Models + Endpoints page content
itself links to the "Virtual Keys page", so on that route the substring filter
matched two anchors and tripped Playwright strict mode. Match the sidebar link
by its exact accessible name instead, which resolves to just the nav item.
This commit is contained in:
ryan-crabbe-berri 2026-06-17 18:10:31 -07:00 committed by GitHub
parent 9b1c1e9894
commit d97b17b161
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 28 additions and 56 deletions

View file

@ -11,6 +11,7 @@
* Keep this in lockstep with MIGRATED_PAGES in src/utils/migratedPages.ts.
*/
export const MIGRATED_E2E_PAGES: Record<string, string> = {
models: "models-and-endpoints",
api_ref: "api-reference",
"llm-playground": "playground",
projects: "projects",

View file

@ -17,7 +17,7 @@ const ROOT = process.env.SERVER_ROOT_PATH ?? "";
const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const pathRe = (segment: string) => new RegExp(`${esc(ROOT)}/ui/${esc(segment)}/?($|\\?)`);
const legacyAnchor = (page: Page) => page.locator("a", { hasText: "Virtual Keys" });
const legacyAnchor = (page: Page) => page.getByRole("link", { name: "Virtual Keys", exact: true });
/** The dashboard shell is present (sidebar rendered); page didn't 404 / crash. */
async function expectRendered(page: Page) {

View file

@ -120,14 +120,7 @@ describe("ModelsAndEndpointsView", () => {
const queryClient = createQueryClient();
const { findByText } = render(
<QueryClientProvider client={queryClient}>
<ModelsAndEndpointsView
token="123"
modelData={{ data: [] }}
keys={[]}
setModelData={() => {}}
premiumUser={false}
teams={[]}
/>
<ModelsAndEndpointsView premiumUser={false} teams={[]} />
</QueryClientProvider>,
);
expect(await findByText("Model Management", {}, { timeout: 10000 })).toBeInTheDocument();
@ -138,14 +131,7 @@ describe("ModelsAndEndpointsView", () => {
const queryClient = createQueryClient();
const { findByText } = render(
<QueryClientProvider client={queryClient}>
<ModelsAndEndpointsView
token="123"
modelData={{ data: [] }}
keys={[]}
setModelData={() => {}}
premiumUser={false}
teams={[]}
/>
<ModelsAndEndpointsView premiumUser={false} teams={[]} />
</QueryClientProvider>,
);
expect(await findByText("Missing a provider?", {}, { timeout: 10000 })).toBeInTheDocument();
@ -156,14 +142,7 @@ describe("ModelsAndEndpointsView", () => {
const queryClient = createQueryClient();
const { findByText, queryByText, container } = render(
<QueryClientProvider client={queryClient}>
<ModelsAndEndpointsView
token="123"
modelData={{ data: [] }}
keys={[]}
setModelData={() => {}}
premiumUser={false}
teams={[]}
/>
<ModelsAndEndpointsView premiumUser={false} teams={[]} />
</QueryClientProvider>,
);
@ -188,14 +167,7 @@ describe("ModelsAndEndpointsView", () => {
const queryClient = createQueryClient();
const { findByText, queryByText } = render(
<QueryClientProvider client={queryClient}>
<ModelsAndEndpointsView
token="123"
modelData={{ data: [] }}
keys={[]}
setModelData={() => {}}
premiumUser={false}
teams={[]}
/>
<ModelsAndEndpointsView premiumUser={false} teams={[]} />
</QueryClientProvider>,
);
@ -228,14 +200,7 @@ describe("ModelsAndEndpointsView", () => {
const queryClient = createQueryClient();
const { getByRole } = render(
<QueryClientProvider client={queryClient}>
<ModelsAndEndpointsView
token="123"
modelData={{ data: modelDataWithIds.data }}
keys={[]}
setModelData={() => {}}
premiumUser={false}
teams={[]}
/>
<ModelsAndEndpointsView premiumUser={false} teams={[]} />
</QueryClientProvider>,
);

View file

@ -30,10 +30,6 @@ import TeamInfoView from "../../../components/team/TeamInfo";
import useAuthorized from "../hooks/useAuthorized";
interface ModelDashboardProps {
token: string | null;
modelData: any;
keys: any[] | null;
setModelData: any;
premiumUser: boolean;
teams: Team[] | null;
}

View file

@ -0,0 +1,11 @@
"use client";
import ModelsAndEndpointsView from "@/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
export default function ModelsAndEndpointsPage() {
const { premiumUser } = useAuthorized();
const { data: teams } = useTeams();
return <ModelsAndEndpointsView premiumUser={premiumUser} teams={teams ?? null} />;
}

View file

@ -1,6 +1,5 @@
"use client";
import ModelsAndEndpointsView from "@/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView";
import { teamListCall as v2TeamListCall } from "@/app/(dashboard)/hooks/teams/useTeams";
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
import LoadingScreen from "@/components/common_components/LoadingScreen";
@ -34,7 +33,7 @@ function CreateKeyPageContent() {
const router = useRouter();
const searchParams = useSearchParams()!;
const [modelData, setModelData] = useState<any>({ data: [] });
const [modelData] = useState<any>({ data: [] });
const [createClicked, setCreateClicked] = useState<boolean>(false);
const { data: uiSettingsData, isLoading: uiSettingsLoading } = useUISettings();
@ -308,15 +307,6 @@ function CreateKeyPageContent() {
autoOpenCreate={autoOpenCreate}
prefillData={prefillData}
/>
) : page == "models" ? (
<ModelsAndEndpointsView
token={token}
keys={keys}
modelData={modelData}
setModelData={setModelData}
premiumUser={premiumUser}
teams={teams}
/>
) : page == "pass-through-settings" ? (
<PassThroughSettings
userID={userID}

View file

@ -48,6 +48,14 @@ describe("migratedHref / legacyPageHref", () => {
expect(MIGRATED_PAGES["llm-playground"]).toBe("playground");
});
it("maps the models sidebar id to the models-and-endpoints route and builds its redirect href", async () => {
vi.doMock("@/components/networking", () => ({ serverRootPath: "/" }));
const { MIGRATED_PAGES, migratedHref } = await import("./migratedPages");
expect(MIGRATED_PAGES.models).toBe("models-and-endpoints");
expect(migratedHref(MIGRATED_PAGES.models)).toBe("/ui/models-and-endpoints");
});
it("maps the projects and access-groups sidebar ids to their routes", async () => {
vi.doMock("@/components/networking", () => ({ serverRootPath: "/" }));
const { MIGRATED_PAGES } = await import("./migratedPages");

View file

@ -9,6 +9,7 @@ import { serverRootPath } from "@/components/networking";
* legacy `?page=` URL; remove it to roll back.
*/
export const MIGRATED_PAGES: Record<string, string> = {
models: "models-and-endpoints",
api_ref: "api-reference",
// Legacy alias: older bookmarks used the hyphenated ?page=api-reference form.
"api-reference": "api-reference",