diff --git a/ui/litellm-dashboard/eslint-metrics.json b/ui/litellm-dashboard/eslint-metrics.json index 37bad071081..fcf60934f64 100644 --- a/ui/litellm-dashboard/eslint-metrics.json +++ b/ui/litellm-dashboard/eslint-metrics.json @@ -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, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/layout.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/layout.test.tsx index 7573ddb5a0f..92a1d40b0e3 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/layout.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/layout.test.tsx @@ -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( + + +
+ + , + ); + + 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(); + }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx index c84209b80cf..b8eb4e66ed0 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx @@ -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 ; } return ( - {isInvitationFlow ? children : {children}} + {children} ); } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/page.tsx index cb4a4a0de03..6c0d780183a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/page.tsx @@ -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(null); - const [keys, setKeys] = useState([]); + const { authLoading, token } = useAuth(); const router = useRouter(); const searchParams = useSearchParams()!; - const [createClicked, setCreateClicked] = useState(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 ; } - return ( - <> - {invitation_id ? ( - - ) : ( - - )} - - ); + return ; } export default function CreateKeyPage() { diff --git a/ui/litellm-dashboard/src/components/user_dashboard.tsx b/ui/litellm-dashboard/src/components/user_dashboard.tsx index acb9333b051..689b5680fcc 100644 --- a/ui/litellm-dashboard/src/components/user_dashboard.tsx +++ b/ui/litellm-dashboard/src/components/user_dashboard.tsx @@ -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 = ({ const [userSpendData, setUserSpendData] = useState(null); const [currentOrg, setCurrentOrg] = useState(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(null); const [teamSpend, setTeamSpend] = useState(null); const [userModels, setUserModels] = useState([]); @@ -232,10 +225,6 @@ const UserDashboard: React.FC = ({ } }, [selectedTeam]); - if (invitation_id != null) { - return ; - } - function gotoLogin() { // Clear token cookies using the utility function clearTokenCookies();