mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-06 04:23:40 +00:00
Delete Team Member with friction (#16167)
This commit is contained in:
@@ -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(
|
||||
<TeamInfoView
|
||||
teamId="123"
|
||||
onUpdate={() => {}}
|
||||
onClose={() => {}}
|
||||
accessToken="123"
|
||||
is_team_admin={true}
|
||||
is_proxy_admin={true}
|
||||
userModels={[]}
|
||||
editTeam={false}
|
||||
premiumUser={false}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByText("User ID")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<TeamInfoProps> = ({
|
||||
const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
|
||||
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
|
||||
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
|
||||
const [memberToDelete, setMemberToDelete] = useState<Member | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
console.log("userModels in team info", userModels);
|
||||
|
||||
@@ -268,13 +270,16 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
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<TeamInfoProps> = ({
|
||||
} 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<TeamInfoProps> = ({
|
||||
onSubmit={handleMemberCreate}
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
|
||||
{/* Delete Member Confirmation Modal */}
|
||||
{memberToDelete && (
|
||||
<Modal
|
||||
title="Delete Team Member"
|
||||
open={memberToDelete !== null}
|
||||
onOk={handleDeleteConfirm}
|
||||
onCancel={handleDeleteCancel}
|
||||
confirmLoading={isDeleting}
|
||||
okText={isDeleting ? "Deleting..." : "Delete"}
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<p>Are you sure you want to remove this member from the team?</p>
|
||||
<p className="mt-2">
|
||||
<strong>User ID:</strong> {memberToDelete.user_id}
|
||||
</p>
|
||||
{memberToDelete.user_email && (
|
||||
<p>
|
||||
<strong>Email:</strong> {memberToDelete.user_email}
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-2 text-red-600">This action cannot be undone.</p>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user