diff --git a/ui/litellm-dashboard/src/components/CreateUserButton.test.tsx b/ui/litellm-dashboard/src/components/CreateUserButton.test.tsx index 03d982ca7c2..d3e2e452909 100644 --- a/ui/litellm-dashboard/src/components/CreateUserButton.test.tsx +++ b/ui/litellm-dashboard/src/components/CreateUserButton.test.tsx @@ -354,4 +354,126 @@ describe("CreateUserButton", () => { expect(mockOrganizationMemberAddCall).not.toHaveBeenCalled(); }); }); + + describe("send invitation email toggle", () => { + it("should send send_invite_email true by default in embedded mode", async () => { + const user = userEvent.setup(); + mockUserCreateCall.mockResolvedValue({ data: { user_id: "default-on-user" } }); + + renderWithProviders( + , + ); + + await user.type(screen.getByLabelText(/user email/i), "default@example.com"); + await user.click(screen.getByRole("combobox", { name: /user role/i })); + await user.click(screen.getByText("User")); + await user.click(screen.getByRole("button", { name: /create user/i })); + + await waitFor(() => { + expect(mockUserCreateCall).toHaveBeenCalledWith("token", null, expect.objectContaining({ + send_invite_email: true, + })); + }); + }); + + it("should send send_invite_email false when the checkbox is unchecked in embedded mode", async () => { + const user = userEvent.setup(); + mockUserCreateCall.mockResolvedValue({ data: { user_id: "unchecked-user" } }); + + renderWithProviders( + , + ); + + await user.type(screen.getByLabelText(/user email/i), "off@example.com"); + await user.click(screen.getByRole("combobox", { name: /user role/i })); + await user.click(screen.getByText("User")); + await user.click(screen.getByRole("checkbox", { name: /send invitation email/i })); + await user.click(screen.getByRole("button", { name: /create user/i })); + + await waitFor(() => { + expect(mockUserCreateCall).toHaveBeenCalledWith("token", null, expect.objectContaining({ + send_invite_email: false, + })); + }); + }); + + it("should send send_invite_email true by default in standalone mode", async () => { + const user = userEvent.setup(); + mockUserCreateCall.mockResolvedValue({ data: { user_id: "standalone-default-user" } }); + mockInvitationCreateCall.mockResolvedValue({ + id: "inv-default", + user_id: "standalone-default-user", + has_user_setup_sso: false, + } as any); + + renderWithProviders( + , + ); + + await waitFor(() => { + expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument(); + }); + await user.click(screen.getByRole("button", { name: /\+ invite user/i })); + + const dialog = screen.getByRole("dialog", { name: /invite user/i }); + await user.type(within(dialog).getByLabelText(/user email/i), "standalone-default@example.com"); + await user.click(within(dialog).getByRole("combobox", { name: /global proxy role/i })); + await user.click(screen.getByText("User")); + await user.click(within(dialog).getByRole("button", { name: /invite user/i })); + + await waitFor(() => { + expect(mockUserCreateCall).toHaveBeenCalledWith("token", null, expect.objectContaining({ + send_invite_email: true, + })); + }); + }); + + it("should send send_invite_email false when the checkbox is unchecked in standalone mode", async () => { + const user = userEvent.setup(); + mockUserCreateCall.mockResolvedValue({ data: { user_id: "standalone-off-user" } }); + mockInvitationCreateCall.mockResolvedValue({ + id: "inv-off", + user_id: "standalone-off-user", + has_user_setup_sso: false, + } as any); + + renderWithProviders( + , + ); + + await waitFor(() => { + expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument(); + }); + await user.click(screen.getByRole("button", { name: /\+ invite user/i })); + + const dialog = screen.getByRole("dialog", { name: /invite user/i }); + await user.type(within(dialog).getByLabelText(/user email/i), "standalone-off@example.com"); + await user.click(within(dialog).getByRole("combobox", { name: /global proxy role/i })); + await user.click(screen.getByText("User")); + await user.click(within(dialog).getByRole("checkbox", { name: /send invitation email/i })); + await user.click(within(dialog).getByRole("button", { name: /invite user/i })); + + await waitFor(() => { + expect(mockUserCreateCall).toHaveBeenCalledWith("token", null, expect.objectContaining({ + send_invite_email: false, + })); + }); + }); + + it("should keep the checkbox checked by default when the modal is opened in standalone mode", async () => { + const user = userEvent.setup(); + + renderWithProviders( + , + ); + + await waitFor(() => { + expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument(); + }); + await user.click(screen.getByRole("button", { name: /\+ invite user/i })); + + const dialog = screen.getByRole("dialog", { name: /invite user/i }); + expect(within(dialog).getByRole("checkbox", { name: /send invitation email/i })).toBeChecked(); + }); + }); }); diff --git a/ui/litellm-dashboard/src/components/CreateUserButton.tsx b/ui/litellm-dashboard/src/components/CreateUserButton.tsx index b65caec26d0..9f5c1aa13c6 100644 --- a/ui/litellm-dashboard/src/components/CreateUserButton.tsx +++ b/ui/litellm-dashboard/src/components/CreateUserButton.tsx @@ -2,7 +2,7 @@ import { InfoCircleOutlined, UserAddOutlined } from "@ant-design/icons"; import { useQueryClient } from "@tanstack/react-query"; import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations"; import { Accordion, AccordionBody, AccordionHeader, SelectItem, TextInput } from "@tremor/react"; -import { Alert, Button, Form, Input, Modal, Select, Select as Select2, Space, Tooltip, Typography } from "antd"; +import { Alert, Button, Checkbox, Form, Input, Modal, Select, Select as Select2, Space, Tooltip, Typography } from "antd"; import React, { useEffect, useMemo, useState } from "react"; import BulkCreateUsers from "./bulk_create_users_button"; import TeamDropdown from "./common_components/team_dropdown"; @@ -113,6 +113,7 @@ export const CreateUserButton: React.FC = ({ user_role: string; organization_ids?: string[]; organizations?: string[]; + send_invite_email?: boolean; }) => { try { NotificationsManager.info("Making API Call"); @@ -181,7 +182,7 @@ export const CreateUserButton: React.FC = ({ labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} labelAlign="left" - initialValues={{ user_role: "internal_user_viewer" }} + initialValues={{ user_role: "internal_user_viewer", send_invite_email: true }} > = ({ + + + +
@@ -270,7 +279,7 @@ export const CreateUserButton: React.FC = ({ labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} labelAlign="left" - initialValues={{ user_role: "internal_user_viewer" }} + initialValues={{ user_role: "internal_user_viewer", send_invite_email: true }} > @@ -326,6 +335,13 @@ export const CreateUserButton: React.FC = ({ + + + Personal Key Creation @@ -360,6 +376,7 @@ export const CreateUserButton: React.FC = ({ +