mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
refactor(ui): point invitation links at the dedicated /onboarding route
Invitation and reset-password links were built as /ui?invitation_id=..., which lands on the dashboard index and renders the onboarding form inline. They now point at the standalone /ui/onboarding route, so the index no longer has to special-case invitations. Old links keep working unchanged; the index still renders onboarding inline for ?invitation_id until the migration closeout removes that branch. Updates the three generators (the enterprise email builder, bulk user create, and the invitation/reset-password modal) and extracts the modal's URL building into a pure, unit-tested buildOnboardingUrl Refs LIT-3687
This commit is contained in:
parent
4847fa5dd5
commit
c7a5e211c8
5 changed files with 112 additions and 28 deletions
|
|
@ -912,9 +912,9 @@ class BaseEmailLogger(CustomLogger):
|
|||
"""
|
||||
Construct invitation link for the user
|
||||
|
||||
# http://localhost:4000/ui?invitation_id=7a096b3a-37c6-440f-9dd1-ba22e8043f6b
|
||||
# http://localhost:4000/ui/onboarding?invitation_id=7a096b3a-37c6-440f-9dd1-ba22e8043f6b
|
||||
"""
|
||||
return f"{base_url}/ui?invitation_id={invitation_id}"
|
||||
return f"{base_url}/ui/onboarding?invitation_id={invitation_id}"
|
||||
|
||||
async def send_email(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -346,7 +346,9 @@ async def test_get_invitation_link(base_email_logger):
|
|||
result = await base_email_logger._get_invitation_link(
|
||||
user_id="test-user", base_url="http://test.com"
|
||||
)
|
||||
assert result == "http://test.com/ui?invitation_id=test-invitation-id"
|
||||
assert (
|
||||
result == "http://test.com/ui/onboarding?invitation_id=test-invitation-id"
|
||||
)
|
||||
|
||||
# Test with None user_id
|
||||
result = await base_email_logger._get_invitation_link(
|
||||
|
|
@ -370,7 +372,7 @@ def test_construct_invitation_link(base_email_logger):
|
|||
result = base_email_logger._construct_invitation_link(
|
||||
invitation_id="test-id-123", base_url="http://test.com"
|
||||
)
|
||||
assert result == "http://test.com/ui?invitation_id=test-id-123"
|
||||
assert result == "http://test.com/ui/onboarding?invitation_id=test-id-123"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -406,7 +408,10 @@ async def test_get_invitation_link_creates_new_when_none_exist(base_email_logger
|
|||
assert call_args["user_api_key_dict"].user_id == "test-user"
|
||||
|
||||
# Verify the returned link uses the new invitation ID
|
||||
assert result == "http://test.com/ui?invitation_id=new-invitation-id"
|
||||
assert (
|
||||
result
|
||||
== "http://test.com/ui/onboarding?invitation_id=new-invitation-id"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -437,7 +442,10 @@ async def test_get_invitation_link_uses_existing_when_available(base_email_logge
|
|||
mock_create_invitation.assert_not_called()
|
||||
|
||||
# Verify the returned link uses the existing invitation ID
|
||||
assert result == "http://test.com/ui?invitation_id=existing-invitation-id"
|
||||
assert (
|
||||
result
|
||||
== "http://test.com/ui/onboarding?invitation_id=existing-invitation-id"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -473,7 +481,10 @@ async def test_get_invitation_link_creates_new_when_list_is_none(base_email_logg
|
|||
assert call_args["user_api_key_dict"].user_id == "test-user"
|
||||
|
||||
# Verify the returned link uses the new invitation ID
|
||||
assert result == "http://test.com/ui?invitation_id=new-invitation-from-none"
|
||||
assert (
|
||||
result
|
||||
== "http://test.com/ui/onboarding?invitation_id=new-invitation-from-none"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -493,7 +504,7 @@ async def test_get_email_params_user_invitation(
|
|||
with mock.patch.object(
|
||||
base_email_logger,
|
||||
"_get_invitation_link",
|
||||
return_value="http://test.com/ui?invitation_id=test-id",
|
||||
return_value="http://test.com/ui/onboarding?invitation_id=test-id",
|
||||
):
|
||||
# Test with user invitation event
|
||||
result = await base_email_logger._get_email_params(
|
||||
|
|
@ -507,7 +518,9 @@ async def test_get_email_params_user_invitation(
|
|||
== "https://litellm-listing.s3.amazonaws.com/litellm_logo.png"
|
||||
)
|
||||
assert result.support_contact == "support@berri.ai"
|
||||
assert result.base_url == "http://test.com/ui?invitation_id=test-id"
|
||||
assert (
|
||||
result.base_url == "http://test.com/ui/onboarding?invitation_id=test-id"
|
||||
)
|
||||
assert result.recipient_email == "test@example.com"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
if (!uiSettings?.SSO_ENABLED) {
|
||||
// Regular invitation flow
|
||||
const invitationData = await invitationCreateCall(accessToken, user_id);
|
||||
const invitationUrl = new URL(`/ui?invitation_id=${invitationData.id}`, baseUrl).toString();
|
||||
const invitationUrl = new URL(`/ui/onboarding?invitation_id=${invitationData.id}`, baseUrl).toString();
|
||||
|
||||
setParsedData((current) =>
|
||||
current.map((u, i) =>
|
||||
|
|
|
|||
59
ui/litellm-dashboard/src/components/onboarding_link.test.tsx
Normal file
59
ui/litellm-dashboard/src/components/onboarding_link.test.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { buildOnboardingUrl } from "./onboarding_link";
|
||||
|
||||
describe("buildOnboardingUrl", () => {
|
||||
it("points the invitation link at the dedicated /ui/onboarding route", () => {
|
||||
expect(
|
||||
buildOnboardingUrl({
|
||||
baseUrl: "http://localhost:4000/",
|
||||
invitationId: "inv-123",
|
||||
hasUserSetupSso: false,
|
||||
resetPassword: false,
|
||||
}),
|
||||
).toBe("http://localhost:4000/ui/onboarding?invitation_id=inv-123");
|
||||
});
|
||||
|
||||
it("preserves a server_root_path prefix before /ui/onboarding", () => {
|
||||
expect(
|
||||
buildOnboardingUrl({
|
||||
baseUrl: "https://proxy.example.com/litellm",
|
||||
invitationId: "inv-123",
|
||||
hasUserSetupSso: false,
|
||||
resetPassword: false,
|
||||
}),
|
||||
).toBe("https://proxy.example.com/litellm/ui/onboarding?invitation_id=inv-123");
|
||||
});
|
||||
|
||||
it("appends action=reset_password for the reset-password flow", () => {
|
||||
expect(
|
||||
buildOnboardingUrl({
|
||||
baseUrl: "http://localhost:4000/",
|
||||
invitationId: "inv-123",
|
||||
hasUserSetupSso: false,
|
||||
resetPassword: true,
|
||||
}),
|
||||
).toBe("http://localhost:4000/ui/onboarding?invitation_id=inv-123&action=reset_password");
|
||||
});
|
||||
|
||||
it("sends SSO users to the dashboard root, not the onboarding form", () => {
|
||||
expect(
|
||||
buildOnboardingUrl({
|
||||
baseUrl: "http://localhost:4000/",
|
||||
invitationId: "inv-123",
|
||||
hasUserSetupSso: true,
|
||||
resetPassword: false,
|
||||
}),
|
||||
).toBe("http://localhost:4000/ui");
|
||||
});
|
||||
|
||||
it("returns an empty string when no base URL is known yet", () => {
|
||||
expect(
|
||||
buildOnboardingUrl({
|
||||
baseUrl: "",
|
||||
invitationId: "inv-123",
|
||||
hasUserSetupSso: false,
|
||||
resetPassword: false,
|
||||
}),
|
||||
).toBe("");
|
||||
});
|
||||
});
|
||||
|
|
@ -25,6 +25,29 @@ interface OnboardingProps {
|
|||
modalType?: "invitation" | "resetPassword";
|
||||
}
|
||||
|
||||
export function buildOnboardingUrl({
|
||||
baseUrl,
|
||||
invitationId,
|
||||
hasUserSetupSso,
|
||||
resetPassword,
|
||||
}: {
|
||||
baseUrl: string;
|
||||
invitationId: string | undefined;
|
||||
hasUserSetupSso: boolean;
|
||||
resetPassword: boolean;
|
||||
}): string {
|
||||
if (!baseUrl) {
|
||||
return "";
|
||||
}
|
||||
const basePath = new URL(baseUrl).pathname;
|
||||
const uiPath = basePath && basePath !== "/" ? `${basePath}/ui` : "ui";
|
||||
if (hasUserSetupSso) {
|
||||
return new URL(uiPath, baseUrl).toString();
|
||||
}
|
||||
const action = resetPassword ? "&action=reset_password" : "";
|
||||
return new URL(`${uiPath}/onboarding?invitation_id=${invitationId}${action}`, baseUrl).toString();
|
||||
}
|
||||
|
||||
export default function OnboardingModal({
|
||||
isInvitationLinkModalVisible,
|
||||
setIsInvitationLinkModalVisible,
|
||||
|
|
@ -41,24 +64,13 @@ export default function OnboardingModal({
|
|||
setIsInvitationLinkModalVisible(false);
|
||||
};
|
||||
|
||||
const getInvitationUrl = () => {
|
||||
if (!baseUrl) {
|
||||
return "";
|
||||
}
|
||||
const baseUrlObj = new URL(baseUrl);
|
||||
const basePath = baseUrlObj.pathname; // This will be "/litellm" or ""
|
||||
const path = basePath && basePath !== "/" ? `${basePath}/ui` : "ui";
|
||||
// Get the path from the base URL
|
||||
if (invitationLinkData?.has_user_setup_sso) {
|
||||
return new URL(path, baseUrl).toString();
|
||||
}
|
||||
let urlPath = `${path}?invitation_id=${invitationLinkData?.id}`;
|
||||
if (modalType === "resetPassword") {
|
||||
urlPath += "&action=reset_password";
|
||||
}
|
||||
const url = new URL(urlPath, baseUrl).toString();
|
||||
return url;
|
||||
};
|
||||
const getInvitationUrl = () =>
|
||||
buildOnboardingUrl({
|
||||
baseUrl,
|
||||
invitationId: invitationLinkData?.id,
|
||||
hasUserSetupSso: invitationLinkData?.has_user_setup_sso ?? false,
|
||||
resetPassword: modalType === "resetPassword",
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue