mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
fix: add QueryClientProvider, remove stale file, use should naming in tests
- Wrap onboarding page with QueryClientProvider to prevent runtime crash (mirrors the same pattern used in LoginPage) - Stage deletion of stale litellm/ui/litellm-dashboard/src/app/onboarding/page.tsx committed at the wrong path - Rename all 16 test names to start with "should" per AGENTS.md convention
This commit is contained in:
parent
a3491490f9
commit
36f7722b0f
5 changed files with 28 additions and 173 deletions
|
|
@ -1,150 +0,0 @@
|
|||
"use client";
|
||||
import React, { Suspense, useEffect, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Card, Title, Text, TextInput, Callout, Button, Grid, Col } from "@tremor/react";
|
||||
import { RiCheckboxCircleLine } from "@remixicon/react";
|
||||
import {
|
||||
getOnboardingCredentials,
|
||||
claimOnboardingToken,
|
||||
getUiConfig,
|
||||
getProxyBaseUrl,
|
||||
} from "@/components/networking";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { Form, Button as Button2 } from "antd";
|
||||
import { getCookie } from "@/utils/cookieUtils";
|
||||
|
||||
function OnboardingContent() {
|
||||
const [form] = Form.useForm();
|
||||
const searchParams = useSearchParams()!;
|
||||
const token = getCookie("token");
|
||||
const inviteID = searchParams.get("invitation_id");
|
||||
const action = searchParams.get("action");
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const [defaultUserEmail, setDefaultUserEmail] = useState<string>("");
|
||||
const [userEmail, setUserEmail] = useState<string>("");
|
||||
const [userID, setUserID] = useState<string | null>(null);
|
||||
const [loginUrl, setLoginUrl] = useState<string>("");
|
||||
const [jwtToken, setJwtToken] = useState<string>("");
|
||||
const [getUiConfigLoading, setGetUiConfigLoading] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
getUiConfig().then((data) => {
|
||||
// get the information for constructing the proxy base url, and then set the token and auth loading
|
||||
console.log("ui config in onboarding.tsx:", data);
|
||||
setGetUiConfigLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inviteID || getUiConfigLoading) {
|
||||
// wait for the ui config to be loaded
|
||||
return;
|
||||
}
|
||||
|
||||
getOnboardingCredentials(inviteID).then((data) => {
|
||||
const login_url = data.login_url;
|
||||
console.log("login_url:", login_url);
|
||||
setLoginUrl(login_url);
|
||||
|
||||
const token = data.token;
|
||||
const decoded = jwtDecode(token) as { [key: string]: any };
|
||||
setJwtToken(token);
|
||||
|
||||
console.log("decoded:", decoded);
|
||||
setAccessToken(decoded.key);
|
||||
|
||||
console.log("decoded user email:", decoded.user_email);
|
||||
const user_email = decoded.user_email;
|
||||
setUserEmail(user_email);
|
||||
|
||||
const user_id = decoded.user_id;
|
||||
setUserID(user_id);
|
||||
});
|
||||
}, [inviteID, getUiConfigLoading]);
|
||||
|
||||
const handleSubmit = (formValues: Record<string, any>) => {
|
||||
console.log("in handle submit. accessToken:", accessToken, "token:", jwtToken, "formValues:", formValues);
|
||||
if (!accessToken || !jwtToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
formValues.user_email = userEmail;
|
||||
|
||||
if (!userID || !inviteID) {
|
||||
return;
|
||||
}
|
||||
claimOnboardingToken(accessToken, inviteID, userID, formValues.password).then((data) => {
|
||||
// set cookie "token" to jwtToken
|
||||
document.cookie = "token=" + jwtToken;
|
||||
|
||||
const proxyBaseUrl = getProxyBaseUrl();
|
||||
console.log("proxyBaseUrl:", proxyBaseUrl);
|
||||
|
||||
// Construct the full redirect URL using the proxyBaseUrl which includes the server root path
|
||||
let redirectUrl = proxyBaseUrl ? `${proxyBaseUrl}/ui/?login=success` : "/ui/?login=success";
|
||||
console.log("redirecting to:", redirectUrl);
|
||||
|
||||
window.location.href = redirectUrl;
|
||||
});
|
||||
|
||||
// redirect to login page
|
||||
};
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md mt-10">
|
||||
<Card>
|
||||
<Title className="text-sm mb-5 text-center">🚅 LiteLLM</Title>
|
||||
<Title className="text-xl">{action === "reset_password" ? "Reset Password" : "Sign up"}</Title>
|
||||
<Text>
|
||||
{action === "reset_password"
|
||||
? "Reset your password to access Admin UI."
|
||||
: "Claim your user account to login to Admin UI."}
|
||||
</Text>
|
||||
|
||||
{action !== "reset_password" && (
|
||||
<Callout className="mt-4" title="SSO" icon={RiCheckboxCircleLine} color="sky">
|
||||
<Grid numItems={2} className="flex justify-between items-center">
|
||||
<Col>SSO is under the Enterprise Tier.</Col>
|
||||
|
||||
<Col>
|
||||
<Button variant="primary" className="mb-2">
|
||||
<a href="https://forms.gle/W3U4PZpJGFHWtHyA9" target="_blank">
|
||||
Get Free Trial
|
||||
</a>
|
||||
</Button>
|
||||
</Col>
|
||||
</Grid>
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
<Form className="mt-10 mb-5 mx-auto" layout="vertical" onFinish={handleSubmit}>
|
||||
<>
|
||||
<Form.Item label="Email Address" name="user_email">
|
||||
<TextInput type="email" disabled={true} value={userEmail} defaultValue={userEmail} className="max-w-md" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Password"
|
||||
name="password"
|
||||
rules={[{ required: true, message: "password required to sign up" }]}
|
||||
help={action === "reset_password" ? "Enter your new password" : "Create a password for your account"}
|
||||
>
|
||||
<TextInput placeholder="" type="password" className="max-w-md" />
|
||||
</Form.Item>
|
||||
</>
|
||||
|
||||
<div className="mt-10">
|
||||
<Button2 htmlType="submit">{action === "reset_password" ? "Reset Password" : "Sign Up"}</Button2>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Onboarding() {
|
||||
return (
|
||||
<Suspense fallback={<div className="flex items-center justify-center min-h-screen">Loading...</div>}>
|
||||
<OnboardingContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,19 +4,19 @@ import { describe, it, expect } from "vitest";
|
|||
import { OnboardingErrorView } from "./OnboardingErrorView";
|
||||
|
||||
describe("OnboardingErrorView", () => {
|
||||
it("shows the failed to load invitation message", () => {
|
||||
it("should show the failed to load invitation message", () => {
|
||||
render(<OnboardingErrorView />);
|
||||
expect(screen.getByText("Failed to load invitation")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the expiry description", () => {
|
||||
it("should show the expiry description", () => {
|
||||
render(<OnboardingErrorView />);
|
||||
expect(
|
||||
screen.getByText("The invitation link may be invalid or expired.")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a Back to Login link pointing to /ui/login", () => {
|
||||
it("should render a Back to Login link pointing to /ui/login", () => {
|
||||
render(<OnboardingErrorView />);
|
||||
// antd Button with href renders as an <a> element
|
||||
const link = screen.getByRole("link", { name: "Back to Login" });
|
||||
|
|
|
|||
|
|
@ -17,55 +17,55 @@ describe("OnboardingFormBody", () => {
|
|||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("shows 'Sign Up' heading for signup variant", () => {
|
||||
it("should show 'Sign Up' heading for signup variant", () => {
|
||||
render(<OnboardingFormBody {...defaultProps} />);
|
||||
expect(screen.getByRole("heading", { name: "Sign Up" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows 'Reset Password' heading for reset_password variant", () => {
|
||||
it("should show 'Reset Password' heading for reset_password variant", () => {
|
||||
render(<OnboardingFormBody {...defaultProps} variant="reset_password" />);
|
||||
expect(screen.getByRole("heading", { name: "Reset Password" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows SSO alert for signup variant", () => {
|
||||
it("should show SSO alert for signup variant", () => {
|
||||
render(<OnboardingFormBody {...defaultProps} />);
|
||||
expect(screen.getByText("SSO")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides SSO alert for reset_password variant", () => {
|
||||
it("should hide SSO alert for reset_password variant", () => {
|
||||
render(<OnboardingFormBody {...defaultProps} variant="reset_password" />);
|
||||
expect(screen.queryByText("SSO")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("pre-fills the email field with userEmail", async () => {
|
||||
it("should pre-fill the email field with userEmail", async () => {
|
||||
render(<OnboardingFormBody {...defaultProps} userEmail="user@example.com" />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("Email Address")).toHaveValue("user@example.com");
|
||||
});
|
||||
});
|
||||
|
||||
it("disables the email field", () => {
|
||||
it("should disable the email field", () => {
|
||||
render(<OnboardingFormBody {...defaultProps} />);
|
||||
expect(screen.getByLabelText("Email Address")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("shows claimError message when claimError is set", () => {
|
||||
it("should show claimError message when claimError is set", () => {
|
||||
render(<OnboardingFormBody {...defaultProps} claimError="Something went wrong" />);
|
||||
expect(screen.getByText("Something went wrong")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show claimError message when claimError is null", () => {
|
||||
it("should not show claimError message when claimError is null", () => {
|
||||
render(<OnboardingFormBody {...defaultProps} claimError={null} />);
|
||||
expect(screen.queryByText("Something went wrong")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows a loading indicator on the submit button when isPending is true", () => {
|
||||
it("should show a loading indicator on the submit button when isPending is true", () => {
|
||||
render(<OnboardingFormBody {...defaultProps} isPending={true} />);
|
||||
// antd v5 renders a loading icon with aria-label="loading" inside the button
|
||||
expect(screen.getByRole("img", { name: "loading" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onSubmit with the typed password on form submit", async () => {
|
||||
it("should call onSubmit with the typed password on form submit", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSubmit = vi.fn();
|
||||
render(<OnboardingFormBody {...defaultProps} onSubmit={onSubmit} />);
|
||||
|
|
@ -80,7 +80,7 @@ describe("OnboardingFormBody", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("shows 'Reset Password' on the submit button for reset_password variant", () => {
|
||||
it("should show 'Reset Password' on the submit button for reset_password variant", () => {
|
||||
render(<OnboardingFormBody {...defaultProps} variant="reset_password" />);
|
||||
expect(
|
||||
screen.getByRole("button", { name: /reset password/i })
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import { describe, it, expect } from "vitest";
|
|||
import { OnboardingLoadingView } from "./OnboardingLoadingView";
|
||||
|
||||
describe("OnboardingLoadingView", () => {
|
||||
it("renders a spinner container", () => {
|
||||
it("should render a spinner container", () => {
|
||||
const { container } = render(<OnboardingLoadingView />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies centering layout classes", () => {
|
||||
it("should apply centering layout classes", () => {
|
||||
const { container } = render(<OnboardingLoadingView />);
|
||||
expect(container.firstChild).toHaveClass("flex", "justify-center");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
"use client";
|
||||
import React, { Suspense } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { OnboardingForm } from "./OnboardingForm";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
function OnboardingContent() {
|
||||
const searchParams = useSearchParams()!;
|
||||
const action = searchParams.get("action");
|
||||
|
|
@ -12,12 +15,14 @@ function OnboardingContent() {
|
|||
|
||||
export default function Onboarding() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex items-center justify-center min-h-screen">Loading...</div>
|
||||
}
|
||||
>
|
||||
<OnboardingContent />
|
||||
</Suspense>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex items-center justify-center min-h-screen">Loading...</div>
|
||||
}
|
||||
>
|
||||
<OnboardingContent />
|
||||
</Suspense>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue