mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
refactor(ui): consolidate invitation flow into the dashboard layout (#32576)
The App Router migration is complete: every page is a path route and the legacy `?page=` switch is gone from the index. This closes it out. The `/ui/` index (page.tsx) kept its own duplicate copy of teams state, a teams fetch, and keys/addKey plumbing solely to feed a second `UserDashboard` render for the `invitation_id` case. That was redundant: `ApiKeysDashboard` already renders `UserDashboard` sourcing its own data, so the index is thinned to just render `<ApiKeysDashboard />`. The login redirect, the legacy `?page=` deep-link redirect for old bookmarks, and the post-login return-URL handling stay on the index. The invitation entry point now resolves in one place. Modern invitation links already point at the dedicated `/onboarding` route; the dashboard layout now redirects legacy `/ui/?invitation_id=` links there too (via `migratedHref`, the same base-aware redirect the index uses for `?page=`), instead of re-rendering that route's page component inline. This removes an import of one route's `page.tsx` into another module, and lets the now-unreachable `if (invitation_id) return <Onboarding/>` branch in the shared `user_dashboard.tsx` be deleted along with its dead `Onboarding` import and `searchParams` read. A layout test asserts the redirect and fails if it regresses. `legacyPageHref` and the sidebar's migrated-vs-legacy href fallback are left in place; they are still live for the parent-category nav nodes (agentic, tools, experimental, settings) that are not page routes. eslint-metrics.json is resynced: -2 no-explicit-any from the removed `any` casts, plus pre-existing drift the gate requires the snapshot to match.
This commit is contained in:
parent
7d63b86e00
commit
1d9a86eac4
5 changed files with 47 additions and 71 deletions
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"@typescript-eslint/no-explicit-any": 1982,
|
||||
"@typescript-eslint/no-explicit-any": 1980,
|
||||
"complexity": 128,
|
||||
"local/no-large-inline-object-arg": 519,
|
||||
"local/no-long-condition-chain": 233,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,13 @@ import { render, screen, waitFor } from "@testing-library/react";
|
|||
import { AuthProvider } from "@/contexts/AuthContext";
|
||||
import Layout from "./layout";
|
||||
|
||||
const { replaceMock } = vi.hoisted(() => ({ replaceMock: vi.fn() }));
|
||||
|
||||
let searchParamsValue = new URLSearchParams();
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: vi.fn(() => ({ push: vi.fn(), replace: vi.fn() })),
|
||||
useSearchParams: vi.fn(() => new URLSearchParams()),
|
||||
useRouter: vi.fn(() => ({ push: vi.fn(), replace: replaceMock })),
|
||||
useSearchParams: vi.fn(() => searchParamsValue),
|
||||
usePathname: vi.fn(() => "/ui/guardrails"),
|
||||
}));
|
||||
|
||||
|
|
@ -58,6 +62,7 @@ describe("(dashboard) Layout", () => {
|
|||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
pendingUiConfig = createDeferred();
|
||||
searchParamsValue = new URLSearchParams();
|
||||
});
|
||||
|
||||
it("does not mount route content until getUiConfig has resolved", async () => {
|
||||
|
|
@ -79,4 +84,25 @@ describe("(dashboard) Layout", () => {
|
|||
expect(screen.getByTestId("navbar")).toBeTruthy();
|
||||
expect(screen.queryByTestId("loading-screen")).toBeNull();
|
||||
});
|
||||
|
||||
it("redirects an invitation link to the onboarding route instead of rendering the dashboard shell", async () => {
|
||||
searchParamsValue = new URLSearchParams("invitation_id=abc123");
|
||||
|
||||
render(
|
||||
<AuthProvider>
|
||||
<Layout>
|
||||
<div data-testid="page-content" />
|
||||
</Layout>
|
||||
</AuthProvider>,
|
||||
);
|
||||
|
||||
pendingUiConfig.resolve();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(replaceMock).toHaveBeenCalledWith(expect.stringContaining("/onboarding?invitation_id=abc123")),
|
||||
);
|
||||
expect(screen.queryByTestId("page-content")).toBeNull();
|
||||
expect(screen.queryByTestId("navbar")).toBeNull();
|
||||
expect(screen.queryByTestId("sidebar")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -137,17 +137,26 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
|
|||
}
|
||||
|
||||
function LayoutContent({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { accessToken, authLoading } = useAuth();
|
||||
const isInvitationFlow = Boolean(searchParams.get("invitation_id"));
|
||||
|
||||
if (authLoading) {
|
||||
// Legacy invitation links point at /ui/?invitation_id=; the onboarding form now lives at its own
|
||||
// /onboarding route. Redirect once ui-config has loaded so migratedHref resolves the SERVER_ROOT_PATH base.
|
||||
useEffect(() => {
|
||||
if (!authLoading && isInvitationFlow) {
|
||||
router.replace(`${migratedHref("onboarding")}?${searchParams.toString()}`);
|
||||
}
|
||||
}, [authLoading, isInvitationFlow, router, searchParams]);
|
||||
|
||||
if (authLoading || isInvitationFlow) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider accessToken={accessToken}>
|
||||
{isInvitationFlow ? children : <DashboardShell>{children}</DashboardShell>}
|
||||
<DashboardShell>{children}</DashboardShell>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import ApiKeysDashboard from "@/app/(dashboard)/api-keys/ApiKeysDashboard";
|
||||
import { teamListCall as v2TeamListCall } from "@/app/(dashboard)/hooks/teams/useTeams";
|
||||
import LoadingScreen from "@/components/common_components/LoadingScreen";
|
||||
import { Team } from "@/components/key_team_helpers/key_list";
|
||||
import { proxyBaseUrl } from "@/components/networking";
|
||||
import UserDashboard from "@/components/user_dashboard";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import {
|
||||
buildLoginUrlWithReturn,
|
||||
|
|
@ -16,32 +13,20 @@ import {
|
|||
} from "@/utils/returnUrlUtils";
|
||||
import { MIGRATED_PAGES, migratedHref } from "@/utils/migratedPages";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useRef, useState } from "react";
|
||||
import { Suspense, useEffect, useRef } from "react";
|
||||
|
||||
function CreateKeyPageContent() {
|
||||
const { authLoading, token, userID, userRole, userEmail, accessToken, premiumUser, setUserRole, setUserEmail } =
|
||||
useAuth();
|
||||
|
||||
const [teams, setTeams] = useState<Team[] | null>(null);
|
||||
const [keys, setKeys] = useState<null | any[]>([]);
|
||||
const { authLoading, token } = useAuth();
|
||||
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams()!;
|
||||
const [createClicked, setCreateClicked] = useState<boolean>(false);
|
||||
|
||||
const invitation_id = searchParams.get("invitation_id");
|
||||
|
||||
const explicitPage = searchParams.get("page");
|
||||
const page = explicitPage || "api-keys";
|
||||
|
||||
// Track if we've already attempted a return URL redirect to prevent race conditions
|
||||
const hasAttemptedReturnRedirectRef = useRef(false);
|
||||
|
||||
const addKey = (data: any) => {
|
||||
setKeys((prevData) => (prevData ? [...prevData, data] : [data]));
|
||||
setCreateClicked(() => !createClicked);
|
||||
};
|
||||
const redirectToLogin = authLoading === false && token === null && invitation_id === null;
|
||||
const redirectToLogin = authLoading === false && token === null;
|
||||
|
||||
useEffect(() => {
|
||||
if (redirectToLogin) {
|
||||
|
|
@ -55,15 +40,13 @@ function CreateKeyPageContent() {
|
|||
}
|
||||
}, [redirectToLogin]);
|
||||
|
||||
// Redirect legacy query-param pages to their new path-based routes. Only when the page is
|
||||
// explicitly requested via ?page=, so the bare landing renders inline and the post-login
|
||||
// return-URL handling below stays intact.
|
||||
// Redirect legacy ?page= deep links (old bookmarks) to their path-based routes.
|
||||
const isLegacyRedirect = explicitPage !== null && explicitPage in MIGRATED_PAGES;
|
||||
useEffect(() => {
|
||||
if (!authLoading && isLegacyRedirect) {
|
||||
router.replace(migratedHref(MIGRATED_PAGES[page]));
|
||||
router.replace(migratedHref(MIGRATED_PAGES[explicitPage]));
|
||||
}
|
||||
}, [authLoading, isLegacyRedirect, page, router]);
|
||||
}, [authLoading, isLegacyRedirect, explicitPage, router]);
|
||||
|
||||
// Check for a stored return URL after successful authentication
|
||||
// This handles the case where user comes back from SSO and we need to redirect to the original URL
|
||||
|
|
@ -102,42 +85,11 @@ function CreateKeyPageContent() {
|
|||
}
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
if (accessToken && userID && userRole) {
|
||||
v2TeamListCall(accessToken, 1, 100, {
|
||||
userID: userRole !== "Admin" && userRole !== "Admin Viewer" ? userID : null,
|
||||
})
|
||||
.then((response) => setTeams(response.teams ?? []))
|
||||
.catch(console.error);
|
||||
}
|
||||
}, [accessToken, userID, userRole]);
|
||||
|
||||
if (authLoading || redirectToLogin || isLegacyRedirect) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{invitation_id ? (
|
||||
<UserDashboard
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
premiumUser={premiumUser}
|
||||
teams={teams}
|
||||
keys={keys}
|
||||
setUserRole={setUserRole}
|
||||
userEmail={userEmail}
|
||||
setUserEmail={setUserEmail}
|
||||
setTeams={setTeams}
|
||||
setKeys={setKeys}
|
||||
addKey={addKey}
|
||||
createClicked={createClicked}
|
||||
/>
|
||||
) : (
|
||||
<ApiKeysDashboard />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return <ApiKeysDashboard />;
|
||||
}
|
||||
|
||||
export default function CreateKeyPage() {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@
|
|||
import { clearTokenCookies, getCookie } from "@/utils/cookieUtils";
|
||||
import { Col, Grid } from "@tremor/react";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Onboarding from "../app/onboarding/page";
|
||||
import { fetchTeams } from "./common_components/fetch_teams";
|
||||
import { KeyResponse, Team } from "./key_team_helpers/key_list";
|
||||
import {
|
||||
|
|
@ -76,13 +74,8 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
|||
const [userSpendData, setUserSpendData] = useState<UserInfo | null>(null);
|
||||
const [currentOrg, setCurrentOrg] = useState<Organization | null>(null);
|
||||
|
||||
// Assuming useSearchParams() hook exists and works in your setup
|
||||
const searchParams = useSearchParams()!;
|
||||
|
||||
const token = getCookie("token");
|
||||
|
||||
const invitation_id = searchParams.get("invitation_id");
|
||||
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const [teamSpend, setTeamSpend] = useState<number | null>(null);
|
||||
const [userModels, setUserModels] = useState<string[]>([]);
|
||||
|
|
@ -232,10 +225,6 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
|||
}
|
||||
}, [selectedTeam]);
|
||||
|
||||
if (invitation_id != null) {
|
||||
return <Onboarding></Onboarding>;
|
||||
}
|
||||
|
||||
function gotoLogin() {
|
||||
// Clear token cookies using the utility function
|
||||
clearTokenCookies();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue