+
- {`Type `}
- {requiredConfirmation}
- {` to confirm deletion:`}
+ Type
+
+ {requiredConfirmation}
+
+ to confirm deletion:
setRequiredConfirmationInput(e.target.value)}
placeholder={requiredConfirmation}
- className="rounded-md"
+ className="rounded-md text-base border-gray-200"
autoFocus
/>
diff --git a/ui/litellm-dashboard/src/components/guardrails.test.tsx b/ui/litellm-dashboard/src/components/guardrails.test.tsx
new file mode 100644
index 0000000000..8cafc18eb9
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/guardrails.test.tsx
@@ -0,0 +1,104 @@
+import { render, screen } from "@testing-library/react";
+import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
+import GuardrailsPanel from "./guardrails";
+import { getGuardrailsList } from "./networking";
+
+vi.mock("./networking", () => ({
+ getGuardrailsList: vi.fn(),
+ deleteGuardrailCall: vi.fn(),
+}));
+
+vi.mock("./guardrails/add_guardrail_form", () => ({
+ __esModule: true,
+ default: () =>
Mock Add Guardrail Form
,
+}));
+
+vi.mock("./guardrails/guardrail_table", () => ({
+ __esModule: true,
+ default: ({ guardrailsList, onDeleteClick }: any) => (
+
+
Mock Guardrail Table
+ {guardrailsList.length > 0 && (
+
+ )}
+
+ ),
+}));
+
+vi.mock("./guardrails/guardrail_info", () => ({
+ __esModule: true,
+ default: () =>
Mock Guardrail Info View
,
+}));
+
+vi.mock("./guardrails/GuardrailTestPlayground", () => ({
+ __esModule: true,
+ default: () =>
Mock Guardrail Test Playground
,
+}));
+
+vi.mock("@/utils/roles", () => ({
+ isAdminRole: vi.fn((role: string) => role === "admin"),
+}));
+
+vi.mock("./guardrails/guardrail_info_helpers", () => ({
+ getGuardrailLogoAndName: vi.fn(() => ({
+ logo: null,
+ displayName: "Test Provider",
+ })),
+}));
+
+beforeAll(() => {
+ Object.defineProperty(window, "matchMedia", {
+ writable: true,
+ value: vi.fn().mockImplementation((query: string) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+ });
+});
+
+describe("GuardrailsPanel", () => {
+ const defaultProps = {
+ accessToken: "test-token",
+ userRole: "admin",
+ };
+
+ const mockGetGuardrailsList = vi.mocked(getGuardrailsList);
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockGetGuardrailsList.mockResolvedValue({
+ guardrails: [
+ {
+ guardrail_id: "test-guardrail-1",
+ guardrail_name: "Test Guardrail",
+ litellm_params: {
+ guardrail: "test-provider",
+ mode: "async",
+ default_on: true,
+ },
+ guardrail_info: null,
+ created_at: "2024-01-01T00:00:00Z",
+ updated_at: "2024-01-01T00:00:00Z",
+ guardrail_definition_location: "database" as any,
+ },
+ ],
+ });
+ });
+
+ it("should render the component", async () => {
+ render(
);
+ expect(screen.getByText("Guardrails")).toBeInTheDocument();
+ expect(screen.getByText("+ Add New Guardrail")).toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/guardrails.tsx b/ui/litellm-dashboard/src/components/guardrails.tsx
index 3861545f8c..26b9acb6a2 100644
--- a/ui/litellm-dashboard/src/components/guardrails.tsx
+++ b/ui/litellm-dashboard/src/components/guardrails.tsx
@@ -1,6 +1,5 @@
import React, { useState, useEffect } from "react";
import { Button, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
-import { Modal } from "antd";
import { getGuardrailsList, deleteGuardrailCall } from "./networking";
import AddGuardrailForm from "./guardrails/add_guardrail_form";
import GuardrailTable from "./guardrails/guardrail_table";
@@ -9,6 +8,8 @@ import GuardrailInfoView from "./guardrails/guardrail_info";
import GuardrailTestPlayground from "./guardrails/GuardrailTestPlayground";
import NotificationsManager from "./molecules/notifications_manager";
import { Guardrail, GuardrailDefinitionLocation } from "./guardrails/types";
+import DeleteResourceModal from "./common_components/DeleteResourceModal";
+import { getGuardrailLogoAndName } from "./guardrails/guardrail_info_helpers";
interface GuardrailsPanelProps {
accessToken: string | null;
@@ -38,7 +39,8 @@ const GuardrailsPanel: React.FC
= ({ accessToken, userRole
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
- const [guardrailToDelete, setGuardrailToDelete] = useState<{ id: string; name: string } | null>(null);
+ const [guardrailToDelete, setGuardrailToDelete] = useState(null);
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedGuardrailId, setSelectedGuardrailId] = useState(null);
const [activeTab, setActiveTab] = useState(0);
@@ -81,7 +83,9 @@ const GuardrailsPanel: React.FC = ({ accessToken, userRole
};
const handleDeleteClick = (guardrailId: string, guardrailName: string) => {
- setGuardrailToDelete({ id: guardrailId, name: guardrailName });
+ const guardrail = guardrailsList.find((g) => g.guardrail_id === guardrailId) || null;
+ setGuardrailToDelete(guardrail);
+ setIsDeleteModalOpen(true);
};
const handleDeleteConfirm = async () => {
@@ -90,22 +94,29 @@ const GuardrailsPanel: React.FC = ({ accessToken, userRole
// Log removed to maintain clean production code
setIsDeleting(true);
try {
- await deleteGuardrailCall(accessToken, guardrailToDelete.id);
- NotificationsManager.success(`Guardrail "${guardrailToDelete.name}" deleted successfully`);
- fetchGuardrails(); // Refresh the list
+ await deleteGuardrailCall(accessToken, guardrailToDelete.guardrail_id);
+ NotificationsManager.success(`Guardrail "${guardrailToDelete.guardrail_name}" deleted successfully`);
+ await fetchGuardrails(); // Refresh the list
} catch (error) {
console.error("Error deleting guardrail:", error);
NotificationsManager.fromBackend("Failed to delete guardrail");
} finally {
setIsDeleting(false);
+ setIsDeleteModalOpen(false);
setGuardrailToDelete(null);
}
};
const handleDeleteCancel = () => {
+ setIsDeleteModalOpen(false);
setGuardrailToDelete(null);
};
+ const providerDisplayName =
+ guardrailToDelete && guardrailToDelete.litellm_params
+ ? getGuardrailLogoAndName(guardrailToDelete.litellm_params.guardrail).displayName
+ : undefined;
+
return (
@@ -148,20 +159,25 @@ const GuardrailsPanel: React.FC = ({ accessToken, userRole
onSuccess={handleSuccess}
/>
- {guardrailToDelete && (
-
- Are you sure you want to delete guardrail: {guardrailToDelete.name} ?
- This action cannot be undone.
-
- )}
+
diff --git a/ui/litellm-dashboard/src/components/organizations.tsx b/ui/litellm-dashboard/src/components/organizations.tsx
index 71e9030a24..5f7275091e 100644
--- a/ui/litellm-dashboard/src/components/organizations.tsx
+++ b/ui/litellm-dashboard/src/components/organizations.tsx
@@ -32,6 +32,7 @@ import VectorStoreSelector from "./vector_store_management/VectorStoreSelector";
import MCPServerSelector from "./mcp_server_management/MCPServerSelector";
import { formatNumberWithCommas } from "../utils/dataUtils";
import NotificationsManager from "./molecules/notifications_manager";
+import DeleteResourceModal from "./common_components/DeleteResourceModal";
interface OrganizationsTableProps {
organizations: Organization[];
@@ -70,6 +71,7 @@ const OrganizationsTable: React.FC = ({
const [editOrg, setEditOrg] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [orgToDelete, setOrgToDelete] = useState(null);
+ const [isDeleting, setIsDeleting] = useState(false);
const [isOrgModalVisible, setIsOrgModalVisible] = useState(false);
const [form] = Form.useForm();
const [expandedAccordions, setExpandedAccordions] = useState>({});
@@ -91,15 +93,18 @@ const OrganizationsTable: React.FC = ({
if (!orgToDelete || !accessToken) return;
try {
+ setIsDeleting(true);
await organizationDeleteCall(accessToken, orgToDelete);
NotificationsManager.success("Organization deleted successfully");
setIsDeleteModalOpen(false);
setOrgToDelete(null);
// Refresh organizations list
- fetchOrganizations(accessToken, setOrganizations);
+ await fetchOrganizations(accessToken, setOrganizations);
} catch (error) {
console.error("Error deleting organization:", error);
+ } finally {
+ setIsDeleting(false);
}
};
@@ -506,40 +511,16 @@ const OrganizationsTable: React.FC = ({
- {isDeleteModalOpen ? (
-
-
-
-
-
-
-
-
-
-
-
-
-
Delete Organization
-
-
Are you sure you want to delete this organization?
-
-
-
-
-
-
-
-
-
-
-
- ) : (
- <>>
- )}
+
);
};
diff --git a/ui/litellm-dashboard/src/components/settings.tsx b/ui/litellm-dashboard/src/components/settings.tsx
index f8a7bafce5..80b5653e75 100644
--- a/ui/litellm-dashboard/src/components/settings.tsx
+++ b/ui/litellm-dashboard/src/components/settings.tsx
@@ -38,6 +38,7 @@ import {
import { LoggingCallbacksTable } from "./Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable";
import { AlertingObject } from "./Settings/LoggingAndAlerts/LoggingCallbacks/types";
import { parseErrorMessage } from "./shared/errorUtils";
+import DeleteResourceModal from "./common_components/DeleteResourceModal";
interface SettingsPageProps {
accessToken: string | null;
userRole: string | null;
@@ -240,9 +241,10 @@ const Settings: React.FC = ({ accessToken, userRole, userID,
const [showEditCallback, setShowEditCallback] = useState(false);
const [selectedEditCallback, setSelectedEditCallback] = useState(null);
const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false);
- const [callbackToDelete, setCallbackToDelete] = useState(null);
+ const [callbackToDelete, setCallbackToDelete] = useState(null);
const [isUpdatingCallback, setIsUpdatingCallback] = useState(false);
const [isAddingCallback, setIsAddingCallback] = useState(false);
+ const [isDeletingCallback, setIsDeletingCallback] = useState(false);
useEffect(() => {
if (!accessToken) {
@@ -525,8 +527,8 @@ const Settings: React.FC = ({ accessToken, userRole, userID,
});
};
- const handleDeleteCallback = (callbackName: string) => {
- setCallbackToDelete(callbackName);
+ const handleDeleteCallback = (callback: any) => {
+ setCallbackToDelete(callback);
setShowDeleteConfirmModal(true);
};
@@ -536,8 +538,9 @@ const Settings: React.FC = ({ accessToken, userRole, userID,
}
try {
- await deleteCallback(accessToken, callbackToDelete);
- NotificationsManager.success(`Callback ${callbackToDelete} deleted successfully`);
+ setIsDeletingCallback(true);
+ await deleteCallback(accessToken, callbackToDelete.name);
+ NotificationsManager.success(`Callback ${callbackToDelete.name} deleted successfully`);
// Refresh the callbacks list
if (userID && userRole) {
@@ -550,6 +553,8 @@ const Settings: React.FC = ({ accessToken, userRole, userID,
} catch (error) {
console.error("Failed to delete callback:", error);
NotificationsManager.fromBackend(error);
+ } finally {
+ setIsDeletingCallback(false);
}
};
@@ -577,7 +582,7 @@ const Settings: React.FC = ({ accessToken, userRole, userID,
setSelectedEditCallback(cb);
setShowEditCallback(true);
}}
- onDelete={(cb) => handleDeleteCallback(cb.name)}
+ onDelete={(cb) => handleDeleteCallback(cb)}
onTest={async (cb) => {
try {
await serviceHealthCheck(accessToken, cb.name);
@@ -804,20 +809,22 @@ const Settings: React.FC = ({ accessToken, userRole, userID,
- {
setShowDeleteConfirmModal(false);
setCallbackToDelete(null);
}}
- okText="Delete"
- cancelText="Cancel"
- okButtonProps={{ danger: true }}
- >
- Are you sure you want to delete the callback - {callbackToDelete}? This action cannot be undone.
-
+ onOk={confirmDeleteCallback}
+ confirmLoading={isDeletingCallback}
+ />
);
};
diff --git a/ui/litellm-dashboard/src/components/team/team_info.tsx b/ui/litellm-dashboard/src/components/team/team_info.tsx
index 0dada8bb79..6558889d28 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, Switch, message, Modal, Tooltip } from "antd";
+import { Button, Form, Input, Select, Switch, message, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { ArrowLeftIcon } from "@heroicons/react/outline";
import MemberModal from "./edit_membership";
@@ -44,6 +44,7 @@ import { copyToClipboard as utilCopyToClipboard } from "../../utils/dataUtils";
import NotificationsManager from "../molecules/notifications_manager";
import PassThroughRoutesSelector from "../common_components/PassThroughRoutesSelector";
import { mapEmptyStringToNull } from "@/utils/keyUpdateUtils";
+import DeleteResourceModal from "../common_components/DeleteResourceModal";
export interface TeamMembership {
user_id: string;
@@ -139,6 +140,7 @@ const TeamInfoView: React.FC