mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-17 00:17:16 +00:00
Merge pull request #17625 from BerriAI/litellm_user_info_delete_modal
[Feature] User Info Delete Modal
This commit is contained in:
@@ -96,7 +96,9 @@ export function RequestResponsePanel({
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 overflow-auto max-h-96 w-full max-w-full box-border">
|
||||
<JsonView data={getRawRequest()} style={defaultStyles} clickToExpandNode={true} />
|
||||
<div className="[&_[role='tree']]:bg-white [&_[role='tree']]:text-slate-900">
|
||||
<JsonView data={getRawRequest()} style={defaultStyles} clickToExpandNode={true} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -131,7 +133,9 @@ export function RequestResponsePanel({
|
||||
</div>
|
||||
<div className="p-4 overflow-auto max-h-96 w-full max-w-full box-border">
|
||||
{hasResponse ? (
|
||||
<JsonView data={formattedResponse()} style={defaultStyles} clickToExpandNode={true} />
|
||||
<div className="[&_[role='tree']]:bg-white [&_[role='tree']]:text-slate-900">
|
||||
<JsonView data={getRawRequest()} style={defaultStyles} clickToExpandNode />
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-500 text-sm italic text-center py-4">Response data not available</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import UserInfoView from "./user_info_view";
|
||||
|
||||
@@ -41,19 +41,24 @@ describe("UserInfoView", () => {
|
||||
possibleUIRoles: null,
|
||||
};
|
||||
|
||||
it("should render the loading state and then the user email", async () => {
|
||||
const { getByText, findAllByText } = render(<UserInfoView {...defaultProps} />);
|
||||
it("should render the loading state", () => {
|
||||
render(<UserInfoView {...defaultProps} />);
|
||||
|
||||
expect(getByText("Loading user data...")).toBeInTheDocument();
|
||||
|
||||
const emails = await findAllByText("test@example.com");
|
||||
expect(emails.length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("Loading user data...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render the user alias", async () => {
|
||||
const { findAllByText } = render(<UserInfoView {...defaultProps} />);
|
||||
it("should render the user email after loading", async () => {
|
||||
render(<UserInfoView {...defaultProps} />);
|
||||
|
||||
const aliases = await findAllByText("Test Alias");
|
||||
const emails = await screen.findAllByText("test@example.com");
|
||||
expect(emails.length).toBeGreaterThan(0);
|
||||
expect(screen.queryByText("Loading user data...")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render the user alias after loading", async () => {
|
||||
render(<UserInfoView {...defaultProps} />);
|
||||
|
||||
const aliases = await screen.findAllByText("Test Alias");
|
||||
expect(aliases.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import { formatNumberWithCommas, copyToClipboard as utilCopyToClipboard } from "
|
||||
import { CopyIcon, CheckIcon } from "lucide-react";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import { getBudgetDurationLabel } from "../common_components/budget_duration_dropdown";
|
||||
import DeleteResourceModal from "../common_components/DeleteResourceModal";
|
||||
|
||||
interface UserInfoViewProps {
|
||||
userId: string;
|
||||
@@ -60,6 +61,7 @@ export default function UserInfoView({
|
||||
}: UserInfoViewProps) {
|
||||
const [userData, setUserData] = useState<UserInfo | null>(null);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isDeletingUser, setIsDeletingUser] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isEditing, setIsEditing] = useState(startInEditMode);
|
||||
const [userModels, setUserModels] = useState<string[]>([]);
|
||||
@@ -115,6 +117,7 @@ export default function UserInfoView({
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
if (!accessToken) return;
|
||||
setIsDeletingUser(true);
|
||||
await userDeleteCall(accessToken, [userId]);
|
||||
NotificationsManager.success("User deleted successfully");
|
||||
if (onDelete) {
|
||||
@@ -124,9 +127,16 @@ export default function UserInfoView({
|
||||
} catch (error) {
|
||||
console.error("Error deleting user:", error);
|
||||
NotificationsManager.fromBackend("Failed to delete user");
|
||||
} finally {
|
||||
setIsDeleteModalOpen(false);
|
||||
setIsDeletingUser(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelDelete = () => {
|
||||
setIsDeleteModalOpen(false);
|
||||
};
|
||||
|
||||
const handleUserUpdate = async (formValues: Record<string, any>) => {
|
||||
try {
|
||||
if (!accessToken || !userData) return;
|
||||
@@ -219,7 +229,7 @@ export default function UserInfoView({
|
||||
icon={TrashIcon}
|
||||
variant="secondary"
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
className="flex items-center"
|
||||
className="flex items-center text-red-500 border-red-500 hover:text-red-600 hover:border-red-600"
|
||||
>
|
||||
Delete User
|
||||
</Button>
|
||||
@@ -227,39 +237,33 @@ export default function UserInfoView({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{isDeleteModalOpen && (
|
||||
<div className="fixed z-10 inset-0 overflow-y-auto">
|
||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div className="fixed inset-0 transition-opacity" aria-hidden="true">
|
||||
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
|
||||
</div>
|
||||
|
||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||
​
|
||||
</span>
|
||||
|
||||
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Delete User</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">Are you sure you want to delete this user?</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<Button onClick={handleDelete} color="red" className="ml-2">
|
||||
Delete
|
||||
</Button>
|
||||
<Button onClick={() => setIsDeleteModalOpen(false)}>Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DeleteResourceModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
title="Delete User?"
|
||||
message="Are you sure you want to delete this user? This action cannot be undone."
|
||||
resourceInformationTitle="User Information"
|
||||
resourceInformation={[
|
||||
{ label: "Email", value: userData.user_info?.user_email },
|
||||
{ label: "User ID", value: userData.user_id, code: true },
|
||||
{
|
||||
label: "Global Proxy Role",
|
||||
value:
|
||||
(userData.user_info?.user_role && possibleUIRoles?.[userData.user_info.user_role]?.ui_label) ||
|
||||
userData.user_info?.user_role ||
|
||||
"-",
|
||||
},
|
||||
{
|
||||
label: "Total Spend (USD)",
|
||||
value:
|
||||
userData.user_info?.spend !== null && userData.user_info?.spend !== undefined
|
||||
? userData.user_info.spend.toFixed(2)
|
||||
: undefined,
|
||||
},
|
||||
]}
|
||||
onCancel={cancelDelete}
|
||||
onOk={handleDelete}
|
||||
confirmLoading={isDeletingUser}
|
||||
/>
|
||||
|
||||
<TabGroup defaultIndex={activeTab} onIndexChange={setActiveTab}>
|
||||
<TabList className="mb-4">
|
||||
|
||||
Reference in New Issue
Block a user