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