feat(ui): add Delete to the guardrail detail Settings card for DB guardrails

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
jesus 2026-10-10 23:51:47 +00:00
parent c5ac1845b0
commit c651ebcbee
6 changed files with 330 additions and 24 deletions

View file

@ -0,0 +1,93 @@
import { Code, Trash2 } from "lucide-react";
import React, { useState } from "react";
import DeleteResourceModal from "@/components/common_components/DeleteResourceModal";
import { deleteGuardrailCall } from "@/components/networking";
import { Button } from "@/components/ui/button";
import { toast } from "@/lib/toast";
import { formatGuardrailMode, getGuardrailLogoAndName } from "./guardrail_info_helpers";
const DANGLING_REFERENCE_WARNING =
"Keys, teams, or policies that reference this guardrail by name will silently stop running it after deletion. Their requests will not return an error.";
interface GuardrailSettingsActionsProps {
guardrail: {
guardrail_id: string;
guardrail_definition_location?: string;
guardrail_name?: string | null;
litellm_params?: { guardrail?: string; mode?: unknown; default_on?: boolean | null } | null;
};
isEditing: boolean;
onEdit: () => void;
onEditCode: () => void;
accessToken: string | null;
onDeleted: () => void;
}
export function GuardrailSettingsActions({
guardrail,
isEditing,
onEdit,
onEditCode,
accessToken,
onDeleted,
}: GuardrailSettingsActionsProps) {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const guardrailName = guardrail.guardrail_name || guardrail.guardrail_id;
const providerDisplayName = getGuardrailLogoAndName(guardrail.litellm_params?.guardrail || "").displayName;
const handleDelete = async () => {
if (!accessToken) return;
setIsDeleting(true);
try {
await deleteGuardrailCall(accessToken, guardrail.guardrail_id);
toast.success(`Guardrail "${guardrailName}" deleted successfully`);
setIsModalOpen(false);
onDeleted();
} catch {
toast.fromError("Failed to delete guardrail");
setIsModalOpen(false);
} finally {
setIsDeleting(false);
}
};
if (isEditing || guardrail.guardrail_definition_location === "config") return null;
return (
<>
{guardrail.litellm_params?.guardrail === "custom_code" ? (
<Button variant="outline" onClick={onEditCode}>
<Code />
Edit Code
</Button>
) : (
<Button variant="outline" onClick={onEdit}>
Edit Settings
</Button>
)}
<Button variant="destructive" onClick={() => setIsModalOpen(true)}>
<Trash2 />
Delete
</Button>
<DeleteResourceModal
isOpen={isModalOpen}
title="Delete Guardrail"
alertMessage={DANGLING_REFERENCE_WARNING}
message={`Are you sure you want to delete guardrail: ${guardrailName}? This action cannot be undone.`}
resourceInformationTitle="Guardrail Information"
resourceInformation={[
{ label: "Name", value: guardrail.guardrail_name },
{ label: "ID", value: guardrail.guardrail_id, code: true },
{ label: "Provider", value: providerDisplayName },
{ label: "Mode", value: formatGuardrailMode(guardrail.litellm_params?.mode) },
{ label: "Default On", value: guardrail.litellm_params?.default_on ? "Yes" : "No" },
]}
onCancel={() => setIsModalOpen(false)}
onOk={handleDelete}
confirmLoading={isDeleting}
requiredConfirmation={guardrailName}
/>
</>
);
}

View file

@ -0,0 +1,131 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import GuardrailsPanel from "./GuardrailsPanel";
import {
deleteGuardrailCall,
getGuardrailInfo,
getGuardrailProviderSpecificParams,
getGuardrailsList,
getGuardrailUISettings,
} from "@/components/networking";
import { fireEvent, renderWithProviders, screen, waitFor, within } from "@/../tests/test-utils";
vi.mock("@/components/networking", () => ({
getGuardrailsList: vi.fn(),
deleteGuardrailCall: vi.fn(),
getGuardrailInfo: vi.fn(),
getGuardrailUISettings: vi.fn(),
getGuardrailProviderSpecificParams: vi.fn(),
updateGuardrailCall: vi.fn(),
}));
vi.mock("./add_guardrail_form", () => ({ __esModule: true, default: () => null }));
vi.mock("./guardrail_garden", () => ({ __esModule: true, default: () => null }));
vi.mock("./GuardrailTestPlayground", () => ({ __esModule: true, default: () => null }));
vi.mock("./TeamGuardrailsTab", () => ({ TeamGuardrailsTab: () => null }));
const DB_GUARDRAIL = {
guardrail_id: "db-guardrail-1",
guardrail_name: "db-word-block",
litellm_params: { guardrail: "litellm_content_filter", mode: "pre_call", default_on: true },
guardrail_info: null,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
guardrail_definition_location: "db",
};
const CONFIG_GUARDRAIL = {
...DB_GUARDRAIL,
guardrail_id: "config-guardrail-1",
guardrail_name: "cfg-word-block",
guardrail_definition_location: "config",
};
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 delete from the guardrail detail Settings card", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(getGuardrailsList).mockResolvedValue({ guardrails: [DB_GUARDRAIL, CONFIG_GUARDRAIL] });
vi.mocked(getGuardrailInfo).mockImplementation(async (_token: string, id: string) =>
id === CONFIG_GUARDRAIL.guardrail_id ? CONFIG_GUARDRAIL : DB_GUARDRAIL,
);
vi.mocked(getGuardrailUISettings).mockResolvedValue({
supported_entities: [],
supported_actions: [],
pii_entity_categories: [],
supported_modes: ["pre_call", "post_call"],
});
vi.mocked(getGuardrailProviderSpecificParams).mockResolvedValue({});
});
const renderDetail = (guardrailId: string) =>
renderWithProviders(<GuardrailsPanel accessToken="test-token" userRole="Admin" />, {
searchParams: `?guardrail=${guardrailId}`,
});
it("deletes a DB guardrail only after the admin types its name, then returns to the refreshed table", async () => {
vi.mocked(deleteGuardrailCall).mockResolvedValue({});
renderDetail(DB_GUARDRAIL.guardrail_id);
fireEvent.click(await screen.findByRole("tab", { name: "Settings" }));
fireEvent.click(await screen.findByRole("button", { name: "Delete" }));
const modal = within(await screen.findByRole("dialog"));
expect(modal.getByText(/keys, teams, or policies that reference/i)).toBeInTheDocument();
const confirmButton = modal.getByRole("button", { name: "Delete" });
expect(confirmButton).toBeDisabled();
const confirmationInput = modal.getByPlaceholderText(DB_GUARDRAIL.guardrail_name);
fireEvent.change(confirmationInput, { target: { value: "db-word" } });
expect(confirmButton).toBeDisabled();
fireEvent.change(confirmationInput, { target: { value: DB_GUARDRAIL.guardrail_name } });
expect(confirmButton).toBeEnabled();
fireEvent.click(confirmButton);
await waitFor(() => expect(deleteGuardrailCall).toHaveBeenCalledWith("test-token", DB_GUARDRAIL.guardrail_id));
expect(await screen.findByRole("table")).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Edit Settings" })).not.toBeInTheDocument();
expect(getGuardrailsList).toHaveBeenCalledTimes(2);
});
it("keeps the admin on the detail page when the delete request fails", async () => {
vi.mocked(deleteGuardrailCall).mockRejectedValue(new Error("boom"));
renderDetail(DB_GUARDRAIL.guardrail_id);
fireEvent.click(await screen.findByRole("tab", { name: "Settings" }));
fireEvent.click(await screen.findByRole("button", { name: "Delete" }));
const modal = within(await screen.findByRole("dialog"));
fireEvent.change(modal.getByPlaceholderText(DB_GUARDRAIL.guardrail_name), {
target: { value: DB_GUARDRAIL.guardrail_name },
});
fireEvent.click(modal.getByRole("button", { name: "Delete" }));
await waitFor(() => expect(deleteGuardrailCall).toHaveBeenCalledTimes(1));
await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
expect(screen.getByRole("button", { name: "Delete" })).toBeInTheDocument();
expect(screen.queryByRole("table")).not.toBeInTheDocument();
});
it("does not offer Delete on a config guardrail's detail page", async () => {
renderDetail(CONFIG_GUARDRAIL.guardrail_id);
fireEvent.click(await screen.findByRole("tab", { name: "Settings" }));
expect(await screen.findByRole("img", { name: "Config guardrail details" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Delete" })).not.toBeInTheDocument();
});
});

View file

@ -184,6 +184,10 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
<GuardrailInfoView
guardrailId={selectedGuardrailId}
onClose={closeGuardrailDetail}
onDeleted={() => {
closeGuardrailDetail();
fetchGuardrails();
}}
accessToken={accessToken}
isAdmin={isAdmin}
/>

View file

@ -84,7 +84,8 @@ const saveChanges = async (user: ReturnType<typeof userEvent.setup>) => {
await user.click(screen.getByText("Save Changes"));
};
const renderView = () => render(<GuardrailInfoView guardrailId="123" onClose={() => {}} accessToken="123" isAdmin />);
const renderView = () =>
render(<GuardrailInfoView guardrailId="123" onClose={() => {}} onDeleted={() => {}} accessToken="123" isAdmin />);
const lastPayload = () => vi.mocked(networking.updateGuardrailCall).mock.calls.at(-1)?.[2];

View file

@ -10,6 +10,7 @@ vi.mock("@/components/networking", () => ({
getGuardrailUISettings: vi.fn(),
getGuardrailProviderSpecificParams: vi.fn(),
updateGuardrailCall: vi.fn(),
deleteGuardrailCall: vi.fn(),
}));
// Mock ContentFilterManager
@ -65,7 +66,9 @@ describe("Guardrail Info", () => {
vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue({});
render(<GuardrailInfoView guardrailId="123" onClose={() => {}} accessToken="123" isAdmin={true} />);
render(
<GuardrailInfoView guardrailId="123" onClose={() => {}} onDeleted={() => {}} accessToken="123" isAdmin={true} />,
);
// Wait for the loading to complete and data to be rendered
await waitFor(() => {
@ -103,7 +106,9 @@ describe("Guardrail Info", () => {
vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue({});
render(<GuardrailInfoView guardrailId="123" onClose={() => {}} accessToken="123" isAdmin={true} />);
render(
<GuardrailInfoView guardrailId="123" onClose={() => {}} onDeleted={() => {}} accessToken="123" isAdmin={true} />,
);
expect(await screen.findAllByText("pre_call, post_call (tag-based)")).not.toHaveLength(0);
});
@ -131,7 +136,9 @@ describe("Guardrail Info", () => {
vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue({});
render(<GuardrailInfoView guardrailId="123" onClose={() => {}} accessToken="123" isAdmin={true} />);
render(
<GuardrailInfoView guardrailId="123" onClose={() => {}} onDeleted={() => {}} accessToken="123" isAdmin={true} />,
);
const logo = await screen.findByAltText("Presidio PII logo");
expect(logo).toHaveAttribute("src", expect.stringContaining("microsoft_azure.svg"));
@ -162,7 +169,7 @@ describe("Guardrail Info", () => {
vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue({});
const { container } = render(
<GuardrailInfoView guardrailId="123" onClose={() => {}} accessToken="123" isAdmin={true} />,
<GuardrailInfoView guardrailId="123" onClose={() => {}} onDeleted={() => {}} accessToken="123" isAdmin={true} />,
);
await waitFor(() => {
@ -212,7 +219,9 @@ describe("Guardrail Info", () => {
vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue({});
render(<GuardrailInfoView guardrailId="123" onClose={() => {}} accessToken="123" isAdmin={true} />);
render(
<GuardrailInfoView guardrailId="123" onClose={() => {}} onDeleted={() => {}} accessToken="123" isAdmin={true} />,
);
await waitFor(() => {
expect(screen.getByText("PII Entity Configuration")).toBeInTheDocument();
@ -245,7 +254,9 @@ describe("Guardrail Info", () => {
vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue({});
vi.mocked(networking.updateGuardrailCall).mockResolvedValue({ status: "success" });
render(<GuardrailInfoView guardrailId="123" onClose={() => {}} accessToken="123" isAdmin={true} />);
render(
<GuardrailInfoView guardrailId="123" onClose={() => {}} onDeleted={() => {}} accessToken="123" isAdmin={true} />,
);
await waitFor(() => {
expect(screen.getByText("Settings")).toBeInTheDocument();
@ -331,7 +342,9 @@ describe("Guardrail Info", () => {
});
vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue({});
render(<GuardrailInfoView guardrailId="123" onClose={() => {}} accessToken="123" isAdmin={true} />);
render(
<GuardrailInfoView guardrailId="123" onClose={() => {}} onDeleted={() => {}} accessToken="123" isAdmin={true} />,
);
expect(await screen.findByRole("tab", { name: "Overview" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("tab", { name: "Settings" })).toHaveAttribute("aria-selected", "false");
@ -355,10 +368,76 @@ describe("Guardrail Info when the guardrail cannot be loaded", () => {
vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue({});
const onClose = vi.fn();
render(<GuardrailInfoView guardrailId="stale-id" onClose={onClose} accessToken="123" isAdmin={true} />);
render(
<GuardrailInfoView
guardrailId="stale-id"
onClose={onClose}
onDeleted={() => {}}
accessToken="123"
isAdmin={true}
/>,
);
expect(await screen.findByText("Guardrail not found")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: /back to guardrails/i }));
expect(onClose).toHaveBeenCalledTimes(1);
});
describe("Delete from the Settings card", () => {
const mockDbGuardrail = () => {
vi.mocked(networking.getGuardrailInfo).mockResolvedValue({
guardrail_id: "123",
guardrail_name: "Test Guardrail",
litellm_params: { guardrail: "presidio", mode: "pre_call", default_on: true },
guardrail_definition_location: "db",
});
vi.mocked(networking.getGuardrailUISettings).mockResolvedValue({
supported_entities: [],
supported_actions: [],
pii_entity_categories: [],
supported_modes: ["pre_call"],
});
vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue({});
};
const openDeleteModalAndConfirm = async () => {
fireEvent.click(await screen.findByRole("tab", { name: "Settings" }));
fireEvent.click(await screen.findByRole("button", { name: "Delete" }));
const modal = within(await screen.findByRole("dialog"));
expect(modal.getByText(/silently stop running it/)).toBeInTheDocument();
const confirmButton = modal.getByRole("button", { name: "Delete" });
expect(confirmButton).toBeDisabled();
fireEvent.change(modal.getByPlaceholderText("Test Guardrail"), { target: { value: "Test Guardrail" } });
fireEvent.click(confirmButton);
};
it("deletes the guardrail and calls onDeleted once the admin types its name", async () => {
mockDbGuardrail();
vi.mocked(networking.deleteGuardrailCall).mockResolvedValue({});
const onDeleted = vi.fn();
render(
<GuardrailInfoView guardrailId="123" onClose={() => {}} onDeleted={onDeleted} accessToken="tok" isAdmin />,
);
await openDeleteModalAndConfirm();
await waitFor(() => expect(onDeleted).toHaveBeenCalledTimes(1));
expect(networking.deleteGuardrailCall).toHaveBeenCalledWith("tok", "123");
});
it("does not call onDeleted when the delete request fails", async () => {
mockDbGuardrail();
vi.mocked(networking.deleteGuardrailCall).mockRejectedValue(new Error("boom"));
const onDeleted = vi.fn();
render(
<GuardrailInfoView guardrailId="123" onClose={() => {}} onDeleted={onDeleted} accessToken="tok" isAdmin />,
);
await openDeleteModalAndConfirm();
await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
expect(networking.deleteGuardrailCall).toHaveBeenCalledTimes(1);
expect(onDeleted).not.toHaveBeenCalled();
});
});
});

View file

@ -33,6 +33,7 @@ import {
} from "./GuardrailFormField";
import ContentFilterManager, { formatContentFilterDataForAPI } from "./content_filter/ContentFilterManager";
import CustomCodeModal, { EditGuardrailData } from "./custom_code/CustomCodeModal";
import { GuardrailSettingsActions } from "./GuardrailSettingsActions";
import { GuardrailModeCard } from "./GuardrailModeDisplay";
import { GuardrailReadOnlyDetails } from "./GuardrailReadOnlyDetails";
import {
@ -72,11 +73,12 @@ const SectionHeading: React.FC<{ children: React.ReactNode }> = ({ children }) =
export interface GuardrailInfoProps {
guardrailId: string;
onClose: () => void;
onDeleted: () => void;
accessToken: string | null;
isAdmin: boolean;
}
const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose, accessToken, isAdmin }) => {
const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose, onDeleted, accessToken, isAdmin }) => {
const [guardrailData, setGuardrailData] = useState<any>(null);
const [guardrailProviderSpecificParams, setGuardrailProviderSpecificParams] = useState<any>(null);
const [loading, setLoading] = useState(true);
@ -686,25 +688,21 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
{isAdmin && (
<TabsContent value="settings" keepMounted>
<Card className="block p-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium">Guardrail Settings</h3>
<div className="flex items-center gap-2 mb-4">
<h3 className="mr-auto text-lg font-medium">Guardrail Settings</h3>
{isConfigGuardrail && (
<SimpleTooltip content="Guardrail is defined in the config file and cannot be edited.">
<Info role="img" aria-label="Config guardrail details" className="size-4 text-muted-foreground" />
</SimpleTooltip>
)}
{!isEditing &&
!isConfigGuardrail &&
(guardrailData.litellm_params?.guardrail === "custom_code" ? (
<Button variant="outline" onClick={() => setCustomCodeModalVisible(true)}>
<Code />
Edit Code
</Button>
) : (
<Button variant="outline" onClick={() => setIsEditing(true)}>
Edit Settings
</Button>
))}
<GuardrailSettingsActions
guardrail={guardrailData}
isEditing={isEditing}
onEdit={() => setIsEditing(true)}
onEditCode={() => setCustomCodeModalVisible(true)}
accessToken={accessToken}
onDeleted={onDeleted}
/>
</div>
{isEditing ? (