From ff3da21aab14dc935ef4087dfbef3ebcf5204c94 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Sat, 15 Aug 2026 15:39:50 -0700 Subject: [PATCH] refactor(ui): decouple bulk invite from the invite user button The bulk invite button was rendered from inside CreateUserButton, so the two actions were locked together and only the bulk one had been migrated, leaving the users page with an antd primary button sitting next to a shadcn one. Move BulkCreateUsersButton up to the users page toolbar so each action stands on its own, and migrate CreateUserButton's buttons to the shared shadcn Button so both triggers render identically. The teams prop only ever fed the bulk button, so it goes away from CreateUserButton and its other call site. --- .../users/_components/view_users.test.tsx | 13 +++++++ .../users/_components/view_users.tsx | 12 +++---- .../src/components/CreateUserButton.test.tsx | 13 ++++--- .../src/components/CreateUserButton.tsx | 35 ++++++------------- .../organisms/create_key_button.tsx | 1 - 5 files changed, 37 insertions(+), 37 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.test.tsx index 2632f40adbe..1a3d517df3e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.test.tsx @@ -148,12 +148,25 @@ describe("ViewUserDashboard", () => { expect(screen.getByRole("textbox", { name: "Default setting" })).toHaveValue("unsaved change"); }); + it("renders invite and bulk invite as toolbar actions alongside the other admin controls", async () => { + renderDashboard(); + + const inviteButton = await screen.findByRole("button", { name: /\+ invite user/i }); + const bulkInviteButton = screen.getByRole("button", { name: /\+ bulk invite users/i }); + const toolbar = screen.getByTestId("toggle-user-selection").parentElement; + + expect(inviteButton.parentElement).toBe(toolbar); + expect(bulkInviteButton.parentElement).toBe(toolbar); + }); + it("shows the users table without admin controls for non-proxy admins", async () => { renderDashboard({ userRole: "Internal User" }); expect(await screen.findByText("test@example.com")).toBeInTheDocument(); expect(screen.queryByRole("tab")).not.toBeInTheDocument(); expect(screen.queryByTestId("toggle-user-selection")).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /\+ invite user/i })).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /\+ bulk invite users/i })).not.toBeInTheDocument(); }); it("keeps actions unavailable while the user list is loading", () => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.tsx index 1de01e88866..df2345b0af4 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/view_users.tsx @@ -2,6 +2,7 @@ import { parseAsString, useQueryState } from "nuqs"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import BulkEditUserModal from "./BulkEditUsers"; +import BulkCreateUsersButton from "@/components/bulk_create_users_button"; import { CreateUserButton } from "@/components/CreateUserButton"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; @@ -365,12 +366,11 @@ const ViewUserDashboard: React.FC = ({ {!userListQuery.isLoading && userID && accessToken && ( <> {isProxyAdmin && ( - + + )} + + {isProxyAdmin && ( + )} {isProxyAdmin && ( diff --git a/ui/litellm-dashboard/src/components/CreateUserButton.test.tsx b/ui/litellm-dashboard/src/components/CreateUserButton.test.tsx index 246c01e9a9a..de1c3508dac 100644 --- a/ui/litellm-dashboard/src/components/CreateUserButton.test.tsx +++ b/ui/litellm-dashboard/src/components/CreateUserButton.test.tsx @@ -20,10 +20,6 @@ vi.mock("./networking", () => ({ getProxyBaseUrl: vi.fn().mockReturnValue("http://localhost"), })); -vi.mock("./bulk_create_users_button", () => ({ - default: () =>
Bulk Create Users
, -})); - vi.mock("@/app/(dashboard)/hooks/organizations/useOrganizations", () => ({ useOrganizations: vi.fn().mockReturnValue({ data: [], isLoading: false }), })); @@ -42,7 +38,6 @@ const createQueryClient = () => const defaultProps = { userID: "123", accessToken: "token", - teams: [], possibleUIRoles: null as Record> | null, }; @@ -75,6 +70,14 @@ describe("CreateUserButton", () => { }); }); + it("should not render the bulk invite button", async () => { + renderWithProviders(); + await waitFor(() => { + expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument(); + }); + expect(screen.queryByRole("button", { name: /bulk invite users/i })).not.toBeInTheDocument(); + }); + it("should open the invite modal when invite user button is clicked", async () => { const user = userEvent.setup(); renderWithProviders(); diff --git a/ui/litellm-dashboard/src/components/CreateUserButton.tsx b/ui/litellm-dashboard/src/components/CreateUserButton.tsx index 8de9f010fa3..8a9146d756f 100644 --- a/ui/litellm-dashboard/src/components/CreateUserButton.tsx +++ b/ui/litellm-dashboard/src/components/CreateUserButton.tsx @@ -1,22 +1,11 @@ -import { InfoCircleOutlined, UserAddOutlined } from "@ant-design/icons"; +import { InfoCircleOutlined } from "@ant-design/icons"; import { useQueryClient } from "@tanstack/react-query"; import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations"; +import { Button } from "@/components/ui/button"; import { Accordion, AccordionBody, AccordionHeader, SelectItem, TextInput } from "@tremor/react"; -import { - Alert, - Button, - Checkbox, - Form, - Input, - Modal, - Select, - Select as Select2, - Space, - Tooltip, - Typography, -} from "antd"; +import { Alert, Checkbox, Form, Input, Modal, Select, Select as Select2, Space, Tooltip, Typography } from "antd"; +import { UserPlus } from "lucide-react"; import React, { useEffect, useState } from "react"; -import BulkCreateUsers from "./bulk_create_users_button"; import TeamDropdown from "./common_components/team_dropdown"; import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key"; import NotificationsManager from "./molecules/notifications_manager"; @@ -46,7 +35,6 @@ const generateUUID = (): string => { interface CreateuserProps { userID: string; accessToken: string; - teams: any[] | null; possibleUIRoles: null | Record>; onUserCreated?: (userId: string) => void; isEmbedded?: boolean; @@ -63,7 +51,6 @@ interface UISettings { export const CreateUserButton: React.FC = ({ userID, accessToken, - teams, possibleUIRoles, onUserCreated, isEmbedded = false, @@ -79,8 +66,6 @@ export const CreateUserButton: React.FC = ({ const [baseUrl, setBaseUrl] = useState(null); const { data: organizations = [] } = useOrganizations(); - // Derive teams from the user's organizations, falling back to the teams prop - useEffect(() => { const fetchData = async () => { try { @@ -237,7 +222,7 @@ export const CreateUserButton: React.FC = ({
- +
); @@ -245,11 +230,10 @@ export const CreateUserButton: React.FC = ({ // Original return for standalone mode return ( -
- - = ({
-
@@ -391,6 +376,6 @@ export const CreateUserButton: React.FC = ({ invitationLinkData={invitationLinkData} /> )} -
+ ); }; diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx index c747cfe36fe..a593b2b9f95 100644 --- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx @@ -1718,7 +1718,6 @@ const CreateKey: React.FC = ({ team, teams, data, addKey, autoOp