From a02ec3bfa0d72fcf674c86828974bd6d5853c5c4 Mon Sep 17 00:00:00 2001 From: Lucas Song Date: Tue, 7 Apr 2026 23:06:13 -0700 Subject: [PATCH 01/12] fix(ui): delete policy attachments via controlled modal Replace static Modal.confirm with DeleteResourceModal so attachment delete reliably triggers the API call. Add a regression test covering the confirm->delete flow. Made-with: Cursor --- .../src/components/policies/index.tsx | 70 ++++--- .../policies/policies_panel.test.tsx | 185 ++++++++++++++++++ 2 files changed, 232 insertions(+), 23 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/policies/policies_panel.test.tsx diff --git a/ui/litellm-dashboard/src/components/policies/index.tsx b/ui/litellm-dashboard/src/components/policies/index.tsx index f47b8d78b9..00ad187484 100644 --- a/ui/litellm-dashboard/src/components/policies/index.tsx +++ b/ui/litellm-dashboard/src/components/policies/index.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect, useCallback } from "react"; import { Button, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react"; -import { Modal, Alert } from "antd"; +import { Alert } from "antd"; import MessageManager from "@/components/molecules/message_manager"; -import { ExclamationCircleOutlined, InfoCircleOutlined } from "@ant-design/icons"; +import { InfoCircleOutlined } from "@ant-design/icons"; import { isAdminRole } from "@/utils/roles"; import PolicyTable from "./policy_table"; import PolicyInfoView from "./policy_info"; @@ -57,6 +57,9 @@ const PoliciesPanel: React.FC = ({ const [isDeleting, setIsDeleting] = useState(false); const [policyToDelete, setPolicyToDelete] = useState(null); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isDeletingAttachment, setIsDeletingAttachment] = useState(false); + const [attachmentToDelete, setAttachmentToDelete] = useState(null); + const [isDeleteAttachmentModalOpen, setIsDeleteAttachmentModalOpen] = useState(false); const [isGuardrailSelectionModalOpen, setIsGuardrailSelectionModalOpen] = useState(false); const [selectedTemplate, setSelectedTemplate] = useState(null); const [existingGuardrailNames, setExistingGuardrailNames] = useState>(new Set()); @@ -166,26 +169,32 @@ const PoliciesPanel: React.FC = ({ setPolicyToDelete(null); }; - const handleDeleteAttachment = (attachmentId: string) => { - Modal.confirm({ - title: "Delete Attachment", - icon: , - content: "Are you sure you want to delete this attachment? This action cannot be undone.", - okText: "Delete", - okType: "danger", - cancelText: "Cancel", - onOk: async () => { - if (!accessToken) return; - try { - await deletePolicyAttachmentCall(accessToken, attachmentId); - MessageManager.success("Attachment deleted successfully"); - fetchAttachments(); - } catch (error) { - console.error("Error deleting attachment:", error); - MessageManager.error("Failed to delete attachment"); - } - }, - }); + const handleDeleteAttachmentClick = (attachmentId: string, policyName?: string) => { + const attachment = attachmentsList.find((a) => a.attachment_id === attachmentId) || null; + setAttachmentToDelete(attachment); + setIsDeleteAttachmentModalOpen(true); + }; + + const handleAttachmentDeleteCancel = () => { + setIsDeleteAttachmentModalOpen(false); + setAttachmentToDelete(null); + }; + + const handleAttachmentDeleteConfirm = async () => { + if (!attachmentToDelete || !accessToken) return; + setIsDeletingAttachment(true); + try { + await deletePolicyAttachmentCall(accessToken, attachmentToDelete.attachment_id); + MessageManager.success("Attachment deleted successfully"); + await fetchAttachments(); + } catch (error) { + console.error("Error deleting attachment:", error); + MessageManager.error("Failed to delete attachment"); + } finally { + setIsDeletingAttachment(false); + setIsDeleteAttachmentModalOpen(false); + setAttachmentToDelete(null); + } }; const handleAttachmentSuccess = () => { @@ -579,7 +588,7 @@ const PoliciesPanel: React.FC = ({ @@ -600,6 +609,21 @@ const PoliciesPanel: React.FC = ({ + + { diff --git a/ui/litellm-dashboard/src/components/policies/policies_panel.test.tsx b/ui/litellm-dashboard/src/components/policies/policies_panel.test.tsx new file mode 100644 index 0000000000..ea1fdd9aa6 --- /dev/null +++ b/ui/litellm-dashboard/src/components/policies/policies_panel.test.tsx @@ -0,0 +1,185 @@ +import React from "react"; +import { screen, waitFor, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { renderWithProviders } from "../../../tests/test-utils"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import PoliciesPanel from "./index"; + +/** + * Ant Design's static Modal.confirm often does not run onOk in the real app (React 18+). + * In jsdom it may still run; we mock confirm as a no-op so the test fails until the panel + * uses a controlled DeleteResourceModal instead of Modal.confirm. + */ +vi.mock("antd", async (importOriginal) => { + const mod = await importOriginal(); + return { + ...mod, + Modal: Object.assign(mod.Modal, { + confirm: vi.fn(), + }), + }; +}); + +const EXPECTED_ATTACHMENT_ID = "att-11111111-2222-3333-4444-555555555555" as const; + +const networkingMocks = vi.hoisted(() => ({ + deletePolicyAttachmentCall: vi.fn().mockResolvedValue(undefined), + getPoliciesList: vi.fn().mockResolvedValue({ policies: [] }), + getPolicyAttachmentsList: vi.fn().mockResolvedValue({ + attachments: [ + { + attachment_id: "att-11111111-2222-3333-4444-555555555555", + policy_name: "test-policy", + scope: null, + teams: [], + keys: [], + models: [], + tags: [], + }, + ], + }), + getGuardrailsList: vi.fn().mockResolvedValue({ guardrails: [] }), + getPolicyInfo: vi.fn().mockResolvedValue({}), + deletePolicyCall: vi.fn().mockResolvedValue(undefined), + createPolicyCall: vi.fn(), + updatePolicyCall: vi.fn(), + createPolicyAttachmentCall: vi.fn(), + createGuardrailCall: vi.fn(), + enrichPolicyTemplate: vi.fn(), +})); + +vi.mock("../networking", () => ({ + ...networkingMocks, +})); + +vi.mock("./impact_popover", () => ({ + default: () =>