From b8e2ac46d1f0ab18e3bd39a686352c4e497027bf Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Fri, 30 Jan 2026 18:45:09 -0800 Subject: [PATCH] Delete resource modal dark mode --- .../DeleteResourceModal.test.tsx | 187 +++++++++++++++--- .../common_components/DeleteResourceModal.tsx | 34 +++- 2 files changed, 180 insertions(+), 41 deletions(-) diff --git a/ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.test.tsx b/ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.test.tsx index e9c4500205b..a850fa8d2df 100644 --- a/ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.test.tsx +++ b/ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.test.tsx @@ -1,68 +1,193 @@ -import { describe, it, expect, vi } from "vitest"; +import { describe, it, expect, vi, beforeEach } from "vitest"; import userEvent from "@testing-library/user-event"; -import { renderWithProviders } from "../../../tests/test-utils"; +import { renderWithProviders, screen } from "../../../tests/test-utils"; import DeleteResourceModal from "./DeleteResourceModal"; describe("DeleteResourceModal", () => { + const mockOnCancel = vi.fn(); + const mockOnOk = vi.fn(); + const defaultProps = { isOpen: true, title: "Delete Resource", message: "Are you sure you want to delete this resource?", - onCancel: vi.fn(), - onOk: vi.fn(), + onCancel: mockOnCancel, + onOk: mockOnOk, confirmLoading: false, }; - it("renders", () => { - const { getByText } = renderWithProviders(); - expect(getByText("Delete Resource")).toBeInTheDocument(); + beforeEach(() => { + vi.clearAllMocks(); }); - it("renders the title correctly", () => { - const { getByText } = renderWithProviders(); - expect(getByText("Custom Delete Title")).toBeInTheDocument(); + it("should render", () => { + renderWithProviders(); + expect(screen.getByText("Delete Resource")).toBeInTheDocument(); }); - it("renders the message correctly", () => { - const { getByText } = renderWithProviders( - , - ); - expect(getByText("This is a custom message")).toBeInTheDocument(); + it("should render the title correctly", () => { + renderWithProviders(); + expect(screen.getByText("Custom Delete Title")).toBeInTheDocument(); }); - it("renders the resourceInformation and resourceInformationTitle correctly", () => { + it("should render the message correctly", () => { + renderWithProviders(); + expect(screen.getByText("This is a custom message")).toBeInTheDocument(); + }); + + it("should render alert message when provided", () => { + renderWithProviders(); + expect(screen.getByText("Warning: This action cannot be undone")).toBeInTheDocument(); + }); + + it("should not render alert message when not provided", () => { + renderWithProviders(); + expect(screen.queryByRole("alert")).not.toBeInTheDocument(); + }); + + it("should render resourceInformation and resourceInformationTitle correctly", () => { const resourceInformation = [ { label: "Name", value: "Test Resource" }, { label: "ID", value: "123" }, ]; - const { getByText } = renderWithProviders( + renderWithProviders( , ); - expect(getByText("Resource Details")).toBeInTheDocument(); - expect(getByText("Name")).toBeInTheDocument(); - expect(getByText("Test Resource")).toBeInTheDocument(); - expect(getByText("ID")).toBeInTheDocument(); - expect(getByText("123")).toBeInTheDocument(); + expect(screen.getByText("Resource Details")).toBeInTheDocument(); + expect(screen.getByText("Name")).toBeInTheDocument(); + expect(screen.getByText("Test Resource")).toBeInTheDocument(); + expect(screen.getByText("ID")).toBeInTheDocument(); + expect(screen.getByText("123")).toBeInTheDocument(); }); - it("disables the delete button when requiredConfirmation is not in the input (empty state)", async () => { - const { getByRole } = renderWithProviders(); - const deleteButton = getByRole("button", { name: /delete/i }); + it("should render dash for null or undefined resource information values", () => { + const resourceInformation = [ + { label: "Name", value: null }, + { label: "ID", value: undefined }, + { label: "Status", value: "Active" }, + ]; + renderWithProviders( + , + ); + expect(screen.getAllByText("-")).toHaveLength(2); + expect(screen.getByText("Active")).toBeInTheDocument(); + }); + + it("should render resource information with number values", () => { + const resourceInformation = [{ label: "Count", value: 42 }]; + renderWithProviders(); + expect(screen.getByText("42")).toBeInTheDocument(); + }); + + it("should call onCancel when cancel button is clicked", async () => { + const user = userEvent.setup(); + renderWithProviders(); + const cancelButton = screen.getByRole("button", { name: "Cancel" }); + await user.click(cancelButton); + expect(mockOnCancel).toHaveBeenCalledTimes(1); + }); + + it("should call onOk when delete button is clicked", async () => { + const user = userEvent.setup(); + renderWithProviders(); + const deleteButton = screen.getByRole("button", { name: /delete/i }); + await user.click(deleteButton); + expect(mockOnOk).toHaveBeenCalledTimes(1); + }); + + it("should disable delete button when requiredConfirmation is not entered", () => { + renderWithProviders(); + const deleteButton = screen.getByRole("button", { name: /delete/i }); expect(deleteButton).toBeDisabled(); }); - it("enables the delete button when the input equals requiredConfirmation", async () => { + it("should disable delete button when requiredConfirmation input does not match exactly", async () => { const user = userEvent.setup(); - const { getByRole, getByPlaceholderText } = renderWithProviders( - , - ); - const input = getByPlaceholderText("DELETE"); + renderWithProviders(); + const input = screen.getByPlaceholderText("DELETE"); + await user.type(input, "DELET"); + const deleteButton = screen.getByRole("button", { name: /delete/i }); + expect(deleteButton).toBeDisabled(); + }); + + it("should enable delete button when requiredConfirmation input matches exactly", async () => { + const user = userEvent.setup(); + renderWithProviders(); + const input = screen.getByPlaceholderText("DELETE"); await user.type(input, "DELETE"); - const deleteButton = getByRole("button", { name: /delete/i }); + const deleteButton = screen.getByRole("button", { name: /delete/i }); expect(deleteButton).not.toBeDisabled(); }); + + it("should reset requiredConfirmation input when modal opens", async () => { + const user = userEvent.setup(); + const { rerender } = renderWithProviders( + , + ); + const input = screen.getByPlaceholderText("DELETE"); + await user.type(input, "DELETE"); + expect(input).toHaveValue("DELETE"); + + rerender(); + rerender(); + + const newInput = screen.getByPlaceholderText("DELETE"); + expect(newInput).toHaveValue(""); + }); + + it("should display deleting text on delete button when confirmLoading is true", () => { + renderWithProviders(); + expect(screen.getByText("Deleting...")).toBeInTheDocument(); + }); + + it("should display delete text on delete button when confirmLoading is false", () => { + renderWithProviders(); + const deleteButton = screen.getByRole("button", { name: /delete/i }); + expect(deleteButton).toBeInTheDocument(); + expect(screen.queryByText("Deleting...")).not.toBeInTheDocument(); + }); + + it("should disable delete button when confirmLoading is true", () => { + renderWithProviders(); + const deleteButton = screen.getByText("Deleting...").closest("button"); + expect(deleteButton).toBeDisabled(); + }); + + it("should disable cancel button when confirmLoading is true", () => { + renderWithProviders(); + const cancelButton = screen.getByRole("button", { name: "Cancel" }); + expect(cancelButton).toBeDisabled(); + }); + + it("should disable delete button when confirmLoading is true even if requiredConfirmation matches", async () => { + const user = userEvent.setup(); + renderWithProviders( + , + ); + const input = screen.getByPlaceholderText("DELETE"); + await user.type(input, "DELETE"); + const deleteButton = screen.getByText("Deleting...").closest("button"); + expect(deleteButton).toBeDisabled(); + }); + + it("should render required confirmation prompt with correct text", () => { + renderWithProviders(); + expect(screen.getByText(/Type/i)).toBeInTheDocument(); + expect(screen.getByText("DELETE")).toBeInTheDocument(); + expect(screen.getByText(/to confirm deletion/i)).toBeInTheDocument(); + }); + + it("should not render required confirmation section when not provided", () => { + renderWithProviders(); + expect(screen.queryByPlaceholderText("DELETE")).not.toBeInTheDocument(); + }); + + it("should not render modal when isOpen is false", () => { + renderWithProviders(); + expect(screen.queryByText("Delete Resource")).not.toBeInTheDocument(); + }); }); diff --git a/ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.tsx b/ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.tsx index 93de859dd7d..26419585a4c 100644 --- a/ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.tsx +++ b/ui/litellm-dashboard/src/components/common_components/DeleteResourceModal.tsx @@ -1,4 +1,5 @@ -import { Alert, Descriptions, Input, Modal, Typography } from "antd"; +import { Alert, Card, Descriptions, Input, Modal, Typography, theme } from "antd"; +import { ExclamationCircleOutlined } from "@ant-design/icons"; import React, { useState, useEffect } from "react"; interface DeleteResourceModalProps { @@ -32,6 +33,7 @@ export default function DeleteResourceModal({ requiredConfirmation, }: DeleteResourceModalProps) { const { Title, Text } = Typography; + const { token } = theme.useToken(); const [requiredConfirmationInput, setRequiredConfirmationInput] = useState(""); useEffect(() => { @@ -57,25 +59,36 @@ export default function DeleteResourceModal({ >
{alertMessage && } -
- - {resourceInformationTitle} - + {resourceInformation && resourceInformation.map(({ label, value, ...textProps }) => ( - {label}}> + {label}}> {value ?? "-"} ))} -
+
{message}
{requiredConfirmation && ( -
- +
+ Type {requiredConfirmation} @@ -86,7 +99,8 @@ export default function DeleteResourceModal({ value={requiredConfirmationInput} onChange={(e) => setRequiredConfirmationInput(e.target.value)} placeholder={requiredConfirmation} - className="rounded-md text-base border-gray-200" + className="rounded-md" + prefix={} autoFocus />