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();