Config Guardrails should not be deletable from table (#16540)

This commit is contained in:
yuneng-jiang
2025-11-12 18:23:31 -08:00
committed by GitHub
parent b30439257b
commit c23b2c5023
8 changed files with 161 additions and 42 deletions
@@ -8,6 +8,7 @@ import { isAdminRole } from "@/utils/roles";
import GuardrailInfoView from "./guardrails/guardrail_info";
import GuardrailTestPlayground from "./guardrails/GuardrailTestPlayground";
import NotificationsManager from "./molecules/notifications_manager";
import { Guardrail, GuardrailDefinitionLocation } from "./guardrails/types";
interface GuardrailsPanelProps {
accessToken: string | null;
@@ -25,14 +26,15 @@ interface GuardrailItem {
guardrail_info: Record<string, any> | null;
created_at?: string;
updated_at?: string;
guardrail_definition_location: GuardrailDefinitionLocation;
}
interface GuardrailsResponse {
guardrails: GuardrailItem[];
guardrails: Guardrail[];
}
const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole }) => {
const [guardrailsList, setGuardrailsList] = useState<GuardrailItem[]>([]);
const [guardrailsList, setGuardrailsList] = useState<Guardrail[]>([]);
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
@@ -0,0 +1,56 @@
import GuardrailTable from "./guardrail_table";
import { render } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { GuardrailDefinitionLocation } from "./types";
describe("GuardrailTable", () => {
it("should render", () => {
const { getByText } = render(
<GuardrailTable
guardrailsList={[]}
isLoading={false}
onDeleteClick={() => {}}
accessToken={null}
onGuardrailUpdated={() => {}}
onGuardrailClick={() => {}}
/>,
);
expect(getByText("Guardrail ID")).toBeInTheDocument();
expect(getByText("Name")).toBeInTheDocument();
expect(getByText("Provider")).toBeInTheDocument();
expect(getByText("Mode")).toBeInTheDocument();
expect(getByText("Default On")).toBeInTheDocument();
expect(getByText("Created At")).toBeInTheDocument();
expect(getByText("Updated At")).toBeInTheDocument();
});
it("should not allow deletion of config guardrails", () => {
const { getByTestId } = render(
<GuardrailTable
guardrailsList={[
{
guardrail_id: "1",
guardrail_name: "Guardrail 1",
litellm_params: { guardrail: "presidio", mode: "pre_call", default_on: true },
guardrail_info: null,
created_at: "2021-01-01",
updated_at: "2021-01-01",
guardrail_definition_location: GuardrailDefinitionLocation.CONFIG,
},
]}
isLoading={false}
onDeleteClick={() => {}}
accessToken={null}
onGuardrailUpdated={() => {}}
onGuardrailClick={() => {}}
/>,
);
const deleteGuardrailButton = getByTestId("config-delete-icon");
expect(deleteGuardrailButton).toBeInTheDocument();
expect(deleteGuardrailButton).toHaveClass("cursor-not-allowed text-gray-400");
expect(deleteGuardrailButton).toHaveAttribute(
"title",
"Config guardrail cannot be deleted on the dashboard. Please delete it from the config file.",
);
});
});
@@ -13,24 +13,10 @@ import {
} from "@tanstack/react-table";
import { getGuardrailLogoAndName, guardrail_provider_map } from "./guardrail_info_helpers";
import EditGuardrailForm from "./edit_guardrail_form";
interface GuardrailItem {
guardrail_id?: string;
guardrail_name: string | null;
litellm_params: {
guardrail: string;
mode: string;
default_on: boolean;
pii_entities_config?: { [key: string]: string };
[key: string]: any;
};
guardrail_info: Record<string, any> | null;
created_at?: string;
updated_at?: string;
}
import { Guardrail, GuardrailDefinitionLocation } from "./types";
interface GuardrailTableProps {
guardrailsList: GuardrailItem[];
guardrailsList: Guardrail[];
isLoading: boolean;
onDeleteClick: (guardrailId: string, guardrailName: string) => void;
accessToken: string | null;
@@ -50,7 +36,7 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
}) => {
const [sorting, setSorting] = useState<SortingState>([{ id: "created_at", desc: true }]);
const [editModalVisible, setEditModalVisible] = useState(false);
const [selectedGuardrail, setSelectedGuardrail] = useState<GuardrailItem | null>(null);
const [selectedGuardrail, setSelectedGuardrail] = useState<Guardrail | null>(null);
// Format date helper function
const formatDate = (dateString?: string) => {
@@ -59,7 +45,7 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
return date.toLocaleString();
};
const handleEditClick = (guardrail: GuardrailItem) => {
const handleEditClick = (guardrail: Guardrail) => {
setSelectedGuardrail(guardrail);
setEditModalVisible(true);
};
@@ -70,7 +56,7 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
onGuardrailUpdated();
};
const columns: ColumnDef<GuardrailItem>[] = [
const columns: ColumnDef<Guardrail>[] = [
{
header: "Guardrail ID",
accessorKey: "guardrail_id",
@@ -176,18 +162,32 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
header: "",
cell: ({ row }) => {
const guardrail = row.original;
const isConfigGuardrail = guardrail.guardrail_definition_location === GuardrailDefinitionLocation.CONFIG;
return (
<div className="flex space-x-2">
<Icon
icon={TrashIcon}
size="sm"
onClick={() =>
guardrail.guardrail_id &&
onDeleteClick(guardrail.guardrail_id, guardrail.guardrail_name || "Unnamed Guardrail")
}
className="cursor-pointer hover:text-red-500"
tooltip="Delete guardrail"
/>
{isConfigGuardrail ? (
<Tooltip title="Config guardrail cannot be deleted on the dashboard. Please delete it from the config file.">
<Icon
data-testid="config-delete-icon"
icon={TrashIcon}
size="sm"
className="cursor-not-allowed text-gray-400"
title="Config guardrail cannot be deleted on the dashboard. Please delete it from the config file."
aria-label="Delete guardrail (config)"
/>
</Tooltip>
) : (
<Icon
icon={TrashIcon}
size="sm"
onClick={() =>
guardrail.guardrail_id &&
onDeleteClick(guardrail.guardrail_id, guardrail.guardrail_name || "Unnamed Guardrail")
}
className="cursor-pointer hover:text-red-500"
tooltip="Delete guardrail"
/>
)}
</div>
);
},
@@ -0,0 +1,40 @@
import { render } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { CategoryFilter, QuickActions, PiiEntityList } from "./pii_components";
import type { PiiEntityCategory } from "./types";
describe("CategoryFilter", () => {
it("should render", () => {
const emptyCategories: PiiEntityCategory[] = [];
const { getByText } = render(
<CategoryFilter categories={emptyCategories} selectedCategories={[]} onChange={() => {}} />,
);
expect(getByText("Filter by category")).toBeInTheDocument();
});
});
describe("QuickActions", () => {
it("should render", () => {
const { getByText } = render(
<QuickActions onSelectAll={() => {}} onUnselectAll={() => {}} hasSelectedEntities={false} />,
);
expect(getByText("Quick Actions")).toBeInTheDocument();
});
});
describe("PiiEntityList", () => {
it("should render", () => {
const { getByText } = render(
<PiiEntityList
entities={[]}
selectedEntities={[]}
selectedActions={{}}
actions={[]}
onEntitySelect={() => {}}
onActionSelect={() => {}}
entityToCategoryMap={new Map()}
/>,
);
expect(getByText("No PII types match your filter criteria")).toBeInTheDocument();
});
});
@@ -83,6 +83,7 @@ export const QuickActions: React.FC<QuickActionsProps> = ({ onSelectAll, onUnsel
</div>
<Button
type="default"
danger
onClick={onUnselectAll}
disabled={!hasSelectedEntities}
icon={<CloseOutlined />}
@@ -103,6 +104,7 @@ export const QuickActions: React.FC<QuickActionsProps> = ({ onSelectAll, onUnsel
</Button>
<Button
type="default"
danger
onClick={() => onSelectAll("BLOCK")}
className="flex items-center justify-center h-10 border-red-200 hover:border-red-300 hover:text-red-700 bg-red-50 hover:bg-red-100 text-red-600"
block
@@ -0,0 +1,20 @@
import { render } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import PiiConfiguration from "./pii_configuration";
describe("PiiConfiguration", () => {
it("should render", () => {
const { getByText } = render(
<PiiConfiguration
entities={[]}
actions={[]}
selectedEntities={[]}
selectedActions={{}}
onEntitySelect={() => {}}
onActionSelect={() => {}}
entityCategories={[]}
/>,
);
expect(getByText("Configure PII Protection")).toBeInTheDocument();
});
});
@@ -1,7 +1,7 @@
import { Typography } from "antd";
import React, { useState } from "react";
import { Typography, Badge } from "antd";
import { CategoryFilter, PiiEntityList, QuickActions } from "./pii_components";
import { PiiConfigurationProps } from "./types";
import { CategoryFilter, QuickActions, PiiEntityList } from "./pii_components";
const { Title, Text } = Typography;
@@ -57,18 +57,11 @@ const PiiConfiguration: React.FC<PiiConfigurationProps> = ({
<div className="pii-configuration">
<div className="flex justify-between items-center mb-5">
<div className="flex items-center">
<Title level={4} className="mb-0 font-semibold text-gray-800">
<Title level={4} className="!m-0 font-semibold text-gray-800">
Configure PII Protection
</Title>
</div>
<Badge
count={selectedEntities.length}
showZero
style={{ backgroundColor: selectedEntities.length > 0 ? "#4f46e5" : "#d9d9d9" }}
overflowCount={999}
>
<Text className="text-gray-500">{selectedEntities.length} items selected</Text>
</Badge>
<Text className="text-gray-500">{selectedEntities.length} items selected</Text>
</div>
<div className="mb-6">
@@ -31,4 +31,10 @@ export interface Guardrail {
guardrail_info: Record<string, any> | null;
created_at?: string;
updated_at?: string;
guardrail_definition_location: GuardrailDefinitionLocation;
}
export enum GuardrailDefinitionLocation {
DB = "db",
CONFIG = "config",
}