Merge pull request #17625 from BerriAI/litellm_user_info_delete_modal

[Feature] User Info Delete Modal
This commit is contained in:
yuneng-jiang
2025-12-06 19:56:10 -08:00
committed by GitHub
3 changed files with 59 additions and 46 deletions
@@ -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">
&#8203;
</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">