This commit is contained in:
jesus-berri 2026-10-19 19:10:10 +00:00 • committed by GitHub
commit 7e3aa5617e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 489 additions and 82 deletions

View file

@ -0,0 +1,78 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { GuardrailDeleteModal } from "./GuardrailDeleteModal";
import { deleteGuardrailCall } from "@/components/networking";
import { toast } from "@/lib/toast";
import { fireEvent, renderWithProviders, screen, waitFor } from "@/../tests/test-utils";
vi.mock("@/components/networking", () => ({ deleteGuardrailCall: vi.fn() }));
vi.mock("@/lib/toast", () => ({ toast: { success: vi.fn(), fromError: vi.fn() } }));
const GUARDRAIL = {
guardrail_id: "gr-1",
guardrail_name: "word-block",
litellm_params: { guardrail: "litellm_content_filter", mode: "pre_call", default_on: true },
};
const renderModal = () => {
const onClose = vi.fn();
const onDeleted = vi.fn();
renderWithProviders(
<GuardrailDeleteModal guardrail={GUARDRAIL} accessToken="tok" onClose={onClose} onDeleted={onDeleted} />,
);
return { onClose, onDeleted };
};
const confirmDelete = () => {
fireEvent.change(screen.getByPlaceholderText(GUARDRAIL.guardrail_name), {
target: { value: GUARDRAIL.guardrail_name },
});
fireEvent.click(screen.getByRole("button", { name: "Delete" }));
};
describe("GuardrailDeleteModal", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("warns about name references and only enables Delete once the exact name is typed", () => {
renderModal();
const warning = screen.getByText(/silently stop running it/);
expect(warning).toHaveTextContent("Keys, teams, and policies that list this guardrail");
expect(warning).toHaveTextContent("Policy pipeline steps that use it will block every request with a 400 error");
expect(warning).toHaveTextContent("unless the step's on_error is next or allow");
const confirmButton = screen.getByRole("button", { name: "Delete" });
expect(confirmButton).toBeDisabled();
fireEvent.change(screen.getByPlaceholderText(GUARDRAIL.guardrail_name), { target: { value: "word" } });
expect(confirmButton).toBeDisabled();
fireEvent.change(screen.getByPlaceholderText(GUARDRAIL.guardrail_name), {
target: { value: GUARDRAIL.guardrail_name },
});
expect(confirmButton).toBeEnabled();
});
it("shows a success toast naming the guardrail and calls onDeleted after a successful delete", async () => {
vi.mocked(deleteGuardrailCall).mockResolvedValue({});
const { onClose, onDeleted } = renderModal();
confirmDelete();
await waitFor(() => expect(onDeleted).toHaveBeenCalledTimes(1));
expect(deleteGuardrailCall).toHaveBeenCalledWith("tok", GUARDRAIL.guardrail_id);
expect(toast.success).toHaveBeenCalledWith('Guardrail "word-block" deleted successfully');
expect(toast.fromError).not.toHaveBeenCalled();
expect(onClose).toHaveBeenCalledTimes(1);
});
it("shows an error toast and does not call onDeleted when the delete fails", async () => {
vi.mocked(deleteGuardrailCall).mockRejectedValue(new Error("boom"));
const { onClose, onDeleted } = renderModal();
confirmDelete();
await waitFor(() => expect(onClose).toHaveBeenCalledTimes(1));
expect(toast.fromError).toHaveBeenCalledWith("Failed to delete guardrail");
expect(toast.success).not.toHaveBeenCalled();
expect(onDeleted).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,68 @@
import React, { useState } from "react";
import DeleteResourceModal from "@/components/common_components/DeleteResourceModal";
import { deleteGuardrailCall } from "@/components/networking";
import { toast } from "@/lib/toast";
import { formatGuardrailMode, getGuardrailLogoAndName } from "./guardrail_info_helpers";
const DANGLING_REFERENCE_WARNING =
"Keys, teams, and policies that list this guardrail will silently stop running it. Policy pipeline steps that use it will block every request with a 400 error, unless the step's on_error is next or allow (or its on_fail, when on_error is not set).";
export interface DeletableGuardrail {
guardrail_id: string;
guardrail_name?: string | null;
litellm_params?: {
guardrail?: string;
mode?: unknown;
default_on?: boolean | null;
} | null;
}
interface GuardrailDeleteModalProps {
guardrail: DeletableGuardrail | null;
accessToken: string | null;
onClose: () => void;
onDeleted: () => void;
}
export function GuardrailDeleteModal({ guardrail, accessToken, onClose, onDeleted }: GuardrailDeleteModalProps) {
const [isDeleting, setIsDeleting] = useState(false);
const guardrailName = guardrail ? guardrail.guardrail_name || guardrail.guardrail_id : "";
const providerDisplayName = getGuardrailLogoAndName(guardrail?.litellm_params?.guardrail || "").displayName;
const handleDelete = async () => {
if (!guardrail || !accessToken) return;
setIsDeleting(true);
try {
await deleteGuardrailCall(accessToken, guardrail.guardrail_id);
toast.success(`Guardrail "${guardrailName}" deleted successfully`);
onClose();
onDeleted();
} catch {
toast.fromError("Failed to delete guardrail");
onClose();
} finally {
setIsDeleting(false);
}
};
return (
<DeleteResourceModal
isOpen={guardrail !== null}
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={onClose}
onOk={handleDelete}
confirmLoading={isDeleting}
requiredConfirmation={guardrailName}
/>
);
}

View file

@ -0,0 +1,51 @@
import { Code, Trash2 } from "lucide-react";
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { DeletableGuardrail, GuardrailDeleteModal } from "./GuardrailDeleteModal";
interface GuardrailSettingsActionsProps {
guardrail: DeletableGuardrail & { guardrail_definition_location?: string };
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);
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>
<GuardrailDeleteModal
guardrail={isModalOpen ? guardrail : null}
accessToken={accessToken}
onClose={() => setIsModalOpen(false)}
onDeleted={onDeleted}
/>
</>
);
}

View file

@ -0,0 +1,135 @@
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(),
})),
});
});
const UI_SETTINGS = {
supported_entities: [],
supported_actions: [],
pii_entity_categories: [],
supported_modes: ["pre_call", "post_call"],
};
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(UI_SETTINGS);
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(/pipeline steps that use it will block every request with a 400 error/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

@ -2,6 +2,7 @@ import { type UrlUpdateEvent } from "nuqs/adapters/testing";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import GuardrailsPanel from "./GuardrailsPanel";
import { getGuardrailsList, deleteGuardrailCall } from "@/components/networking";
import { toast } from "@/lib/toast";
import { fireEvent, renderWithProviders, screen, waitFor, within } from "@/../tests/test-utils";
vi.mock("@/components/networking", () => ({
@ -9,6 +10,8 @@ vi.mock("@/components/networking", () => ({
deleteGuardrailCall: vi.fn(),
}));
vi.mock("@/lib/toast", () => ({ toast: { success: vi.fn(), fromError: vi.fn() } }));
vi.mock("./add_guardrail_form", () => ({
__esModule: true,
default: () => <div>Mock Add Guardrail Form</div>,
@ -140,13 +143,36 @@ describe("GuardrailsPanel", () => {
expect(modal.getByText("Delete Guardrail")).toBeInTheDocument();
expect(modal.getByText("test-guardrail-1")).toBeInTheDocument();
expect(modal.getByText("Test Provider")).toBeInTheDocument();
expect(
modal.getByText(/pipeline steps that use it will block every request with a 400 error/i),
).toBeInTheDocument();
fireEvent.click(modal.getByRole("button", { name: "Delete" }));
const confirmButton = modal.getByRole("button", { name: "Delete" });
expect(confirmButton).toBeDisabled();
fireEvent.change(modal.getByPlaceholderText("Test Guardrail"), { target: { value: "Test Guardrail" } });
fireEvent.click(confirmButton);
await waitFor(() => {
expect(mockDeleteGuardrailCall).toHaveBeenCalledWith("test-token", "test-guardrail-1");
});
expect(mockGetGuardrailsList).toHaveBeenCalledTimes(2);
await waitFor(() => expect(mockGetGuardrailsList).toHaveBeenCalledTimes(2));
expect(toast.success).toHaveBeenCalledWith('Guardrail "Test Guardrail" deleted successfully');
});
it("should show an error toast and not refetch when the row delete fails", async () => {
mockDeleteGuardrailCall.mockRejectedValue(new Error("boom"));
renderWithProviders(<GuardrailsPanel {...defaultProps} />);
fireEvent.click(screen.getByText("Guardrails"));
fireEvent.click(await screen.findByTestId("delete-button"));
const modal = within(await screen.findByRole("dialog"));
fireEvent.change(modal.getByPlaceholderText("Test Guardrail"), { target: { value: "Test Guardrail" } });
fireEvent.click(modal.getByRole("button", { name: "Delete" }));
await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
expect(toast.fromError).toHaveBeenCalledWith("Failed to delete guardrail");
expect(toast.success).not.toHaveBeenCalled();
expect(mockGetGuardrailsList).toHaveBeenCalledTimes(1);
});
it("should mount every tab panel up front so panel state survives tab switches", async () => {

View file

@ -2,7 +2,7 @@ import { parseAsString, useQueryState } from "nuqs";
import React, { useState, useEffect } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ChevronDown, Code, Plus } from "lucide-react";
import { getGuardrailsList, deleteGuardrailCall } from "@/components/networking";
import { getGuardrailsList } from "@/components/networking";
import { buttonVariants } from "@/components/ui/button";
import {
DropdownMenu,
@ -16,10 +16,8 @@ import GuardrailTable from "./guardrail_table";
import { isAdminRole } from "@/utils/roles";
import GuardrailInfoView from "./guardrail_info";
import GuardrailTestPlayground from "./GuardrailTestPlayground";
import { toast } from "@/lib/toast";
import { Guardrail } from "@/components/guardrails/types";
import DeleteResourceModal from "@/components/common_components/DeleteResourceModal";
import { formatGuardrailMode, getGuardrailLogoAndName } from "./guardrail_info_helpers";
import { GuardrailDeleteModal } from "./GuardrailDeleteModal";
import { CustomCodeModal } from "./custom_code";
import GuardrailGarden from "./guardrail_garden";
import { TeamGuardrailsTab } from "./TeamGuardrailsTab";
@ -38,9 +36,7 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
const [isCustomCodeModalVisible, setIsCustomCodeModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [guardrailToDelete, setGuardrailToDelete] = useState<Guardrail | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedGuardrailId, setSelectedGuardrailId] = useQueryState(
"guardrail",
parseAsString.withOptions({ history: "push" }),
@ -97,40 +93,10 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
fetchGuardrails();
};
const handleDeleteClick = (guardrailId: string, guardrailName: string) => {
const guardrail = guardrailsList.find((g) => g.guardrail_id === guardrailId) || null;
setGuardrailToDelete(guardrail);
setIsDeleteModalOpen(true);
const handleDeleteClick = (guardrailId: string) => {
setGuardrailToDelete(guardrailsList.find((g) => g.guardrail_id === guardrailId) || null);
};
const handleDeleteConfirm = async () => {
if (!guardrailToDelete || !accessToken) return;
setIsDeleting(true);
try {
await deleteGuardrailCall(accessToken, guardrailToDelete.guardrail_id);
toast.success(`Guardrail "${guardrailToDelete.guardrail_name}" deleted successfully`);
await fetchGuardrails();
} catch (error) {
console.error("Error deleting guardrail:", error);
toast.fromError("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 (
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
<Tabs defaultValue="guardrails">
@ -184,6 +150,10 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
<GuardrailInfoView
guardrailId={selectedGuardrailId}
onClose={closeGuardrailDetail}
onDeleted={() => {
closeGuardrailDetail();
fetchGuardrails();
}}
accessToken={accessToken}
isAdmin={isAdmin}
/>
@ -210,24 +180,11 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
onSuccess={handleSuccess}
/>
<DeleteResourceModal
isOpen={isDeleteModalOpen}
title="Delete Guardrail"
message={`Are you sure you want to delete guardrail: ${guardrailToDelete?.guardrail_name}? This action cannot be undone.`}
resourceInformationTitle="Guardrail Information"
resourceInformation={[
{ label: "Name", value: guardrailToDelete?.guardrail_name },
{ label: "ID", value: guardrailToDelete?.guardrail_id, code: true },
{ label: "Provider", value: providerDisplayName },
{ label: "Mode", value: formatGuardrailMode(guardrailToDelete?.litellm_params.mode) },
{
label: "Default On",
value: guardrailToDelete?.litellm_params.default_on ? "Yes" : "No",
},
]}
onCancel={handleDeleteCancel}
onOk={handleDeleteConfirm}
confirmLoading={isDeleting}
<GuardrailDeleteModal
guardrail={guardrailToDelete}
accessToken={accessToken}
onClose={() => setGuardrailToDelete(null)}
onDeleted={fetchGuardrails}
/>
</TabsContent>

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,90 @@ 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 dbGuardrail = {
guardrail_id: "123",
guardrail_name: "Test Guardrail",
litellm_params: { guardrail: "presidio", mode: "pre_call", default_on: true },
guardrail_definition_location: "db",
};
const uiSettings = {
supported_entities: [],
supported_actions: [],
pii_entity_categories: [],
supported_modes: ["pre_call"],
};
const mockDbGuardrail = () => {
vi.mocked(networking.getGuardrailInfo).mockResolvedValue(dbGuardrail);
vi.mocked(networking.getGuardrailUISettings).mockResolvedValue(uiSettings);
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("hides Delete while the settings are being edited", async () => {
mockDbGuardrail();
render(<GuardrailInfoView guardrailId="123" onClose={() => {}} onDeleted={() => {}} accessToken="tok" isAdmin />);
fireEvent.click(await screen.findByRole("tab", { name: "Settings" }));
fireEvent.click(await screen.findByRole("button", { name: "Edit Settings" }));
expect(await screen.findByRole("button", { name: "Save Changes" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Delete" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Edit Settings" })).not.toBeInTheDocument();
});
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 ? (