diff --git a/ui/litellm-dashboard/src/components/team/team_info.test.tsx b/ui/litellm-dashboard/src/components/team/team_info.test.tsx new file mode 100644 index 0000000000..526f0972d9 --- /dev/null +++ b/ui/litellm-dashboard/src/components/team/team_info.test.tsx @@ -0,0 +1,81 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import TeamInfoView from "./team_info"; +import { render, waitFor } from "@testing-library/react"; +import * as networking from "@/components/networking"; + +// Mock the networking module +vi.mock("@/components/networking", () => ({ + teamInfoCall: vi.fn(), + teamMemberDeleteCall: vi.fn(), + teamMemberAddCall: vi.fn(), + teamMemberUpdateCall: vi.fn(), + teamUpdateCall: vi.fn(), + getGuardrailsList: vi.fn(), + fetchMCPAccessGroups: vi.fn(), + getTeamPermissionsCall: vi.fn(), +})); + +describe("TeamInfoView", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should render", async () => { + // Mock the team info response + vi.mocked(networking.teamInfoCall).mockResolvedValue({ + team_id: "123", + team_info: { + team_alias: "Test Team", + team_id: "123", + organization_id: null, + admins: ["admin@test.com"], + members: ["user1@test.com", "user2@test.com"], + members_with_roles: [ + { + user_id: "user1@test.com", + user_email: "user1@test.com", + role: "member", + spend: 0, + budget_id: "budget1", + }, + ], + metadata: {}, + tpm_limit: null, + rpm_limit: null, + max_budget: null, + budget_duration: null, + models: [], + blocked: false, + spend: 0, + max_parallel_requests: null, + budget_reset_at: null, + model_id: null, + litellm_model_table: null, + created_at: "2024-01-01T00:00:00Z", + team_member_budget_table: null, + }, + keys: [], + team_memberships: [], + }); + + vi.mocked(networking.getGuardrailsList).mockResolvedValue([]); + vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue([]); + + const { getByText } = render( + {}} + onClose={() => {}} + accessToken="123" + is_team_admin={true} + is_proxy_admin={true} + userModels={[]} + editTeam={false} + premiumUser={false} + />, + ); + await waitFor(() => { + expect(getByText("User ID")).toBeInTheDocument(); + }); + }); +}); diff --git a/ui/litellm-dashboard/src/components/team/team_info.tsx b/ui/litellm-dashboard/src/components/team/team_info.tsx index 2aec5a3b76..34446c5166 100644 --- a/ui/litellm-dashboard/src/components/team/team_info.tsx +++ b/ui/litellm-dashboard/src/components/team/team_info.tsx @@ -25,7 +25,7 @@ import { teamUpdateCall, getGuardrailsList, } from "@/components/networking"; -import { Button, Form, Input, Select, message, Tooltip } from "antd"; +import { Button, Form, Input, Select, message, Modal, Tooltip } from "antd"; import { InfoCircleOutlined } from "@ant-design/icons"; import { ArrowLeftIcon } from "@heroicons/react/outline"; import MemberModal from "./edit_membership"; @@ -138,6 +138,8 @@ const TeamInfoView: React.FC = ({ const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false); const [copiedStates, setCopiedStates] = useState>({}); const [guardrailsList, setGuardrailsList] = useState([]); + const [memberToDelete, setMemberToDelete] = useState(null); + const [isDeleting, setIsDeleting] = useState(false); console.log("userModels in team info", userModels); @@ -268,13 +270,16 @@ const TeamInfoView: React.FC = ({ } }; - const handleMemberDelete = async (member: Member) => { - try { - if (accessToken == null) { - return; - } + const handleMemberDelete = (member: Member) => { + setMemberToDelete(member); + }; - await teamMemberDeleteCall(accessToken, teamId, member); + const handleDeleteConfirm = async () => { + if (!memberToDelete || !accessToken) return; + + setIsDeleting(true); + try { + await teamMemberDeleteCall(accessToken, teamId, memberToDelete); NotificationsManager.success("Team member removed successfully"); @@ -287,9 +292,16 @@ const TeamInfoView: React.FC = ({ } catch (error) { NotificationsManager.fromBackend("Failed to remove team member"); console.error("Error removing team member:", error); + } finally { + setIsDeleting(false); + setMemberToDelete(null); } }; + const handleDeleteCancel = () => { + setMemberToDelete(null); + }; + const handleTeamUpdate = async (values: any) => { try { if (!accessToken) return; @@ -885,6 +897,30 @@ const TeamInfoView: React.FC = ({ onSubmit={handleMemberCreate} accessToken={accessToken} /> + + {/* Delete Member Confirmation Modal */} + {memberToDelete && ( + +

Are you sure you want to remove this member from the team?

+

+ User ID: {memberToDelete.user_id} +

+ {memberToDelete.user_email && ( +

+ Email: {memberToDelete.user_email} +

+ )} +

This action cannot be undone.

+
+ )} ); };