diff --git a/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/index.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/index.test.tsx index 2530ce9fda7..e5740a89822 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/index.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/index.test.tsx @@ -1,7 +1,8 @@ import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { tagListCall } from "@/components/networking"; +import { tagDeleteCall, tagListCall } from "@/components/networking"; import TagManagement from "./index"; @@ -12,10 +13,23 @@ vi.mock("@/components/networking", () => ({ modelInfoCall: vi.fn(), })); +vi.mock("@/components/molecules/notifications_manager", () => ({ + __esModule: true, + default: { + success: vi.fn(), + fromBackend: vi.fn(), + }, +})); + vi.mock("./TagTable", () => ({ __esModule: true, - default: ({ isLoading }: { isLoading?: boolean }) => ( -
{isLoading ? "table-loading" : "table-loaded"}
+ default: ({ isLoading, onDelete }: { isLoading?: boolean; onDelete: (tagName: string) => void }) => ( +
+ {isLoading ? "table-loading" : "table-loaded"} + +
), })); @@ -30,6 +44,7 @@ vi.mock("./components/CreateTagModal", () => ({ })); const mockTagListCall = vi.mocked(tagListCall); +const mockTagDeleteCall = vi.mocked(tagDeleteCall); describe("TagManagement loading state", () => { beforeEach(() => { @@ -57,3 +72,41 @@ describe("TagManagement loading state", () => { expect(mockTagListCall).toHaveBeenCalledWith("sk-test"); }); }); + +describe("TagManagement delete flow", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockTagListCall.mockResolvedValue({}); + }); + + it("should confirm deletion through the shared DeleteResourceModal and call tagDeleteCall with the tag name", async () => { + const user = userEvent.setup(); + mockTagDeleteCall.mockResolvedValue({}); + render(); + await screen.findByText("table-loaded"); + + expect(screen.queryByText("Tag Information")).not.toBeInTheDocument(); + + await user.click(screen.getByTestId("mock-delete-trigger")); + + expect(await screen.findByText("Tag Information")).toBeInTheDocument(); + expect(screen.getByText("test-tag")).toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: /delete/i })); + + expect(mockTagDeleteCall).toHaveBeenCalledWith("sk-test", "test-tag"); + }); + + it("should not call tagDeleteCall when the deletion is cancelled", async () => { + const user = userEvent.setup(); + render(); + await screen.findByText("table-loaded"); + + await user.click(screen.getByTestId("mock-delete-trigger")); + await screen.findByText("Tag Information"); + + await user.click(screen.getByRole("button", { name: "Cancel" })); + + expect(mockTagDeleteCall).not.toHaveBeenCalled(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/index.tsx b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/index.tsx index 16b92bd4553..301d9264437 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/index.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/index.tsx @@ -7,6 +7,7 @@ import { tagCreateCall, tagListCall, tagDeleteCall } from "@/components/networki import { Tag } from "@/components/tag_management/types"; import TagTable from "./TagTable"; import NotificationsManager from "@/components/molecules/notifications_manager"; +import DeleteResourceModal from "@/components/common_components/DeleteResourceModal"; import CreateTagModal from "./components/CreateTagModal"; interface ModelInfo { @@ -33,6 +34,7 @@ const TagManagement: React.FC = ({ accessToken, userID, userRole }) => const [editTag, setEditTag] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [tagToDelete, setTagToDelete] = useState(null); + const [isDeleting, setIsDeleting] = useState(false); const [lastRefreshed, setLastRefreshed] = useState(""); const [availableModels, setAvailableModels] = useState([]); @@ -87,6 +89,7 @@ const TagManagement: React.FC = ({ accessToken, userID, userRole }) => const confirmDelete = async () => { if (!accessToken || !tagToDelete) return; + setIsDeleting(true); try { await tagDeleteCall(accessToken, tagToDelete); NotificationsManager.success("Tag deleted successfully"); @@ -94,9 +97,11 @@ const TagManagement: React.FC = ({ accessToken, userID, userRole }) => } catch (error) { console.error("Error deleting tag:", error); NotificationsManager.fromBackend("Error deleting tag: " + error); + } finally { + setIsDeleting(false); + setIsDeleteModalOpen(false); + setTagToDelete(null); } - setIsDeleteModalOpen(false); - setTagToDelete(null); }; useEffect(() => { @@ -189,40 +194,19 @@ const TagManagement: React.FC = ({ accessToken, userID, userRole }) => /> {/* Delete Confirmation Modal */} - {isDeleteModalOpen && ( -
-
- -
-
-
-
-

Delete Tag

-
-

Are you sure you want to delete this tag?

-
-
-
-
-
- - -
-
-
-
- )} + { + setIsDeleteModalOpen(false); + setTagToDelete(null); + }} + onOk={confirmDelete} + confirmLoading={isDeleting} + /> )}