Skip to content

Commit babee43

Browse files
authored
Merge pull request #17068 from BerriAI/litellm_additional_delete_resource_modal
[Feature] Change Delete Modals to Common Component
2 parents 046b7ef + d53bc7b commit babee43

File tree

6 files changed

+210
-103
lines changed

6 files changed

+210
-103
lines changed

ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,6 @@ export default function DeleteResourceModal({
5757
>
5858
<div className="space-y-4">
5959
{alertMessage && <Alert message={alertMessage} type="warning" />}
60-
<div>
61-
<Text>{message}</Text>
62-
</div>
63-
6460
<div className="mt-4 p-4 bg-red-50 rounded-lg border border-red-200">
6561
<Title level={5} className="mb-3 text-gray-900">
6662
{resourceInformationTitle}
@@ -74,18 +70,23 @@ export default function DeleteResourceModal({
7470
))}
7571
</Descriptions>
7672
</div>
73+
<div>
74+
<Text>{message}</Text>
75+
</div>
7776
{requiredConfirmation && (
78-
<div className="mb-5">
77+
<div className="mb-6 mt-4 pt-4 border-t border-gray-200">
7978
<Text className="block text-base font-medium text-gray-700 mb-2">
80-
{`Type `}
81-
<span className="underline">{requiredConfirmation}</span>
82-
{` to confirm deletion:`}
79+
<Text>Type </Text>
80+
<Text strong type="danger">
81+
{requiredConfirmation}
82+
</Text>
83+
<Text> to confirm deletion:</Text>
8384
</Text>
8485
<Input
8586
value={requiredConfirmationInput}
8687
onChange={(e) => setRequiredConfirmationInput(e.target.value)}
8788
placeholder={requiredConfirmation}
88-
className="rounded-md"
89+
className="rounded-md text-base border-gray-200"
8990
autoFocus
9091
/>
9192
</div>
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { render, screen } from "@testing-library/react";
2+
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
3+
import GuardrailsPanel from "./guardrails";
4+
import { getGuardrailsList } from "./networking";
5+
6+
vi.mock("./networking", () => ({
7+
getGuardrailsList: vi.fn(),
8+
deleteGuardrailCall: vi.fn(),
9+
}));
10+
11+
vi.mock("./guardrails/add_guardrail_form", () => ({
12+
__esModule: true,
13+
default: () => <div>Mock Add Guardrail Form</div>,
14+
}));
15+
16+
vi.mock("./guardrails/guardrail_table", () => ({
17+
__esModule: true,
18+
default: ({ guardrailsList, onDeleteClick }: any) => (
19+
<div>
20+
<div>Mock Guardrail Table</div>
21+
{guardrailsList.length > 0 && (
22+
<button
23+
data-testid="delete-button"
24+
onClick={() => onDeleteClick(guardrailsList[0].guardrail_id, guardrailsList[0].guardrail_name)}
25+
>
26+
Delete
27+
</button>
28+
)}
29+
</div>
30+
),
31+
}));
32+
33+
vi.mock("./guardrails/guardrail_info", () => ({
34+
__esModule: true,
35+
default: () => <div>Mock Guardrail Info View</div>,
36+
}));
37+
38+
vi.mock("./guardrails/GuardrailTestPlayground", () => ({
39+
__esModule: true,
40+
default: () => <div>Mock Guardrail Test Playground</div>,
41+
}));
42+
43+
vi.mock("@/utils/roles", () => ({
44+
isAdminRole: vi.fn((role: string) => role === "admin"),
45+
}));
46+
47+
vi.mock("./guardrails/guardrail_info_helpers", () => ({
48+
getGuardrailLogoAndName: vi.fn(() => ({
49+
logo: null,
50+
displayName: "Test Provider",
51+
})),
52+
}));
53+
54+
beforeAll(() => {
55+
Object.defineProperty(window, "matchMedia", {
56+
writable: true,
57+
value: vi.fn().mockImplementation((query: string) => ({
58+
matches: false,
59+
media: query,
60+
onchange: null,
61+
addListener: vi.fn(),
62+
removeListener: vi.fn(),
63+
addEventListener: vi.fn(),
64+
removeEventListener: vi.fn(),
65+
dispatchEvent: vi.fn(),
66+
})),
67+
});
68+
});
69+
70+
describe("GuardrailsPanel", () => {
71+
const defaultProps = {
72+
accessToken: "test-token",
73+
userRole: "admin",
74+
};
75+
76+
const mockGetGuardrailsList = vi.mocked(getGuardrailsList);
77+
78+
beforeEach(() => {
79+
vi.clearAllMocks();
80+
mockGetGuardrailsList.mockResolvedValue({
81+
guardrails: [
82+
{
83+
guardrail_id: "test-guardrail-1",
84+
guardrail_name: "Test Guardrail",
85+
litellm_params: {
86+
guardrail: "test-provider",
87+
mode: "async",
88+
default_on: true,
89+
},
90+
guardrail_info: null,
91+
created_at: "2024-01-01T00:00:00Z",
92+
updated_at: "2024-01-01T00:00:00Z",
93+
guardrail_definition_location: "database" as any,
94+
},
95+
],
96+
});
97+
});
98+
99+
it("should render the component", async () => {
100+
render(<GuardrailsPanel {...defaultProps} />);
101+
expect(screen.getByText("Guardrails")).toBeInTheDocument();
102+
expect(screen.getByText("+ Add New Guardrail")).toBeInTheDocument();
103+
});
104+
});

ui/litellm-dashboard/src/components/guardrails.tsx

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import React, { useState, useEffect } from "react";
22
import { Button, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
3-
import { Modal } from "antd";
43
import { getGuardrailsList, deleteGuardrailCall } from "./networking";
54
import AddGuardrailForm from "./guardrails/add_guardrail_form";
65
import GuardrailTable from "./guardrails/guardrail_table";
@@ -9,6 +8,8 @@ import GuardrailInfoView from "./guardrails/guardrail_info";
98
import GuardrailTestPlayground from "./guardrails/GuardrailTestPlayground";
109
import NotificationsManager from "./molecules/notifications_manager";
1110
import { Guardrail, GuardrailDefinitionLocation } from "./guardrails/types";
11+
import DeleteResourceModal from "./common_components/DeleteResourceModal";
12+
import { getGuardrailLogoAndName } from "./guardrails/guardrail_info_helpers";
1213

1314
interface GuardrailsPanelProps {
1415
accessToken: string | null;
@@ -38,7 +39,8 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
3839
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
3940
const [isLoading, setIsLoading] = useState(false);
4041
const [isDeleting, setIsDeleting] = useState(false);
41-
const [guardrailToDelete, setGuardrailToDelete] = useState<{ id: string; name: string } | null>(null);
42+
const [guardrailToDelete, setGuardrailToDelete] = useState<Guardrail | null>(null);
43+
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
4244
const [selectedGuardrailId, setSelectedGuardrailId] = useState<string | null>(null);
4345
const [activeTab, setActiveTab] = useState<number>(0);
4446

@@ -81,7 +83,9 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
8183
};
8284

8385
const handleDeleteClick = (guardrailId: string, guardrailName: string) => {
84-
setGuardrailToDelete({ id: guardrailId, name: guardrailName });
86+
const guardrail = guardrailsList.find((g) => g.guardrail_id === guardrailId) || null;
87+
setGuardrailToDelete(guardrail);
88+
setIsDeleteModalOpen(true);
8589
};
8690

8791
const handleDeleteConfirm = async () => {
@@ -90,22 +94,29 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
9094
// Log removed to maintain clean production code
9195
setIsDeleting(true);
9296
try {
93-
await deleteGuardrailCall(accessToken, guardrailToDelete.id);
94-
NotificationsManager.success(`Guardrail "${guardrailToDelete.name}" deleted successfully`);
95-
fetchGuardrails(); // Refresh the list
97+
await deleteGuardrailCall(accessToken, guardrailToDelete.guardrail_id);
98+
NotificationsManager.success(`Guardrail "${guardrailToDelete.guardrail_name}" deleted successfully`);
99+
await fetchGuardrails(); // Refresh the list
96100
} catch (error) {
97101
console.error("Error deleting guardrail:", error);
98102
NotificationsManager.fromBackend("Failed to delete guardrail");
99103
} finally {
100104
setIsDeleting(false);
105+
setIsDeleteModalOpen(false);
101106
setGuardrailToDelete(null);
102107
}
103108
};
104109

105110
const handleDeleteCancel = () => {
111+
setIsDeleteModalOpen(false);
106112
setGuardrailToDelete(null);
107113
};
108114

115+
const providerDisplayName =
116+
guardrailToDelete && guardrailToDelete.litellm_params
117+
? getGuardrailLogoAndName(guardrailToDelete.litellm_params.guardrail).displayName
118+
: undefined;
119+
109120
return (
110121
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
111122
<TabGroup index={activeTab} onIndexChange={setActiveTab}>
@@ -148,20 +159,25 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
148159
onSuccess={handleSuccess}
149160
/>
150161

151-
{guardrailToDelete && (
152-
<Modal
153-
title="Delete Guardrail"
154-
open={guardrailToDelete !== null}
155-
onOk={handleDeleteConfirm}
156-
onCancel={handleDeleteCancel}
157-
confirmLoading={isDeleting}
158-
okText="Delete"
159-
okButtonProps={{ danger: true }}
160-
>
161-
<p>Are you sure you want to delete guardrail: {guardrailToDelete.name} ?</p>
162-
<p>This action cannot be undone.</p>
163-
</Modal>
164-
)}
162+
<DeleteResourceModal
163+
isOpen={isDeleteModalOpen}
164+
title="Delete Guardrail"
165+
message={`Are you sure you want to delete guardrail: ${guardrailToDelete?.guardrail_name}? This action cannot be undone.`}
166+
resourceInformationTitle="Guardrail Information"
167+
resourceInformation={[
168+
{ label: "Name", value: guardrailToDelete?.guardrail_name },
169+
{ label: "ID", value: guardrailToDelete?.guardrail_id, code: true },
170+
{ label: "Provider", value: providerDisplayName },
171+
{ label: "Mode", value: guardrailToDelete?.litellm_params.mode },
172+
{
173+
label: "Default On",
174+
value: guardrailToDelete?.litellm_params.default_on ? "Yes" : "No",
175+
},
176+
]}
177+
onCancel={handleDeleteCancel}
178+
onOk={handleDeleteConfirm}
179+
confirmLoading={isDeleting}
180+
/>
165181
</TabPanel>
166182

167183
<TabPanel>

ui/litellm-dashboard/src/components/organizations.tsx

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import VectorStoreSelector from "./vector_store_management/VectorStoreSelector";
3232
import MCPServerSelector from "./mcp_server_management/MCPServerSelector";
3333
import { formatNumberWithCommas } from "../utils/dataUtils";
3434
import NotificationsManager from "./molecules/notifications_manager";
35+
import DeleteResourceModal from "./common_components/DeleteResourceModal";
3536

3637
interface OrganizationsTableProps {
3738
organizations: Organization[];
@@ -70,6 +71,7 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
7071
const [editOrg, setEditOrg] = useState(false);
7172
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
7273
const [orgToDelete, setOrgToDelete] = useState<string | null>(null);
74+
const [isDeleting, setIsDeleting] = useState(false);
7375
const [isOrgModalVisible, setIsOrgModalVisible] = useState(false);
7476
const [form] = Form.useForm();
7577
const [expandedAccordions, setExpandedAccordions] = useState<Record<string, boolean>>({});
@@ -91,15 +93,18 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
9193
if (!orgToDelete || !accessToken) return;
9294

9395
try {
96+
setIsDeleting(true);
9497
await organizationDeleteCall(accessToken, orgToDelete);
9598
NotificationsManager.success("Organization deleted successfully");
9699

97100
setIsDeleteModalOpen(false);
98101
setOrgToDelete(null);
99102
// Refresh organizations list
100-
fetchOrganizations(accessToken, setOrganizations);
103+
await fetchOrganizations(accessToken, setOrganizations);
101104
} catch (error) {
102105
console.error("Error deleting organization:", error);
106+
} finally {
107+
setIsDeleting(false);
103108
}
104109
};
105110

@@ -506,40 +511,16 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
506511
</Form>
507512
</Modal>
508513

509-
{isDeleteModalOpen ? (
510-
<div className="fixed z-10 inset-0 overflow-y-auto">
511-
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
512-
<div className="fixed inset-0 transition-opacity" aria-hidden="true">
513-
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
514-
</div>
515-
516-
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
517-
&#8203;
518-
</span>
519-
520-
<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">
521-
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
522-
<div className="sm:flex sm:items-start">
523-
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
524-
<h3 className="text-lg leading-6 font-medium text-gray-900">Delete Organization</h3>
525-
<div className="mt-2">
526-
<p className="text-sm text-gray-500">Are you sure you want to delete this organization?</p>
527-
</div>
528-
</div>
529-
</div>
530-
</div>
531-
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
532-
<Button onClick={confirmDelete} color="red" className="ml-2">
533-
Delete
534-
</Button>
535-
<Button onClick={cancelDelete}>Cancel</Button>
536-
</div>
537-
</div>
538-
</div>
539-
</div>
540-
) : (
541-
<></>
542-
)}
514+
<DeleteResourceModal
515+
isOpen={isDeleteModalOpen}
516+
title="Delete Organization?"
517+
message="Are you sure you want to delete this organization? This action cannot be undone."
518+
resourceInformationTitle="Organization Information"
519+
resourceInformation={[{ label: "Organization ID", value: orgToDelete, code: true }]}
520+
onCancel={cancelDelete}
521+
onOk={confirmDelete}
522+
confirmLoading={isDeleting}
523+
/>
543524
</div>
544525
);
545526
};

0 commit comments

Comments
 (0)