From 45329723be672dc93cf56422c1f6412eeecb990d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 16:12:57 +0000 Subject: [PATCH] feat(ui): guardrails/guardrail_table to shadcn - @tremor Button/Icon/Badge + @heroicons Trash/SwitchVert/Chevrons + antd Tooltip \u2192 shadcn Badge/Button/Tooltip + lucide ArrowUpDown/ ChevronDown/ChevronUp/Trash2. - Config guardrail delete button is now a disabled shadcn Button (keeps data-testid='config-delete-icon' so callers can still look it up); tooltip still communicates the "delete in config" message. - Updated test assertion to check for .cursor-not-allowed + .disabled state rather than the old tremor Icon classes. - Categorical emerald "Default On" badge; file added to .eslintrc overrides. Gates: TS \u2713 | Build \u2713 | Vitest 2/2 green. Co-authored-by: yuneng-jiang --- ui/litellm-dashboard/.eslintrc.json | 1 + .../guardrails/guardrail_table.test.tsx | 7 +- .../components/guardrails/guardrail_table.tsx | 259 ++++++++++++------ 3 files changed, 182 insertions(+), 85 deletions(-) diff --git a/ui/litellm-dashboard/.eslintrc.json b/ui/litellm-dashboard/.eslintrc.json index 8648c7a1ff1..94419020d62 100644 --- a/ui/litellm-dashboard/.eslintrc.json +++ b/ui/litellm-dashboard/.eslintrc.json @@ -58,6 +58,7 @@ "src/components/policies/attachment_table.tsx", "src/components/claude_code_plugins/plugin_table.tsx", "src/components/policies/policy_table.tsx", + "src/components/guardrails/guardrail_table.tsx", "src/components/permissions/VectorStorePermissions.tsx", "src/components/permissions/AgentPermissions.tsx", "src/components/permissions/MCPServerPermissions.tsx", diff --git a/ui/litellm-dashboard/src/components/guardrails/guardrail_table.test.tsx b/ui/litellm-dashboard/src/components/guardrails/guardrail_table.test.tsx index dcc67677357..331e7984a17 100644 --- a/ui/litellm-dashboard/src/components/guardrails/guardrail_table.test.tsx +++ b/ui/litellm-dashboard/src/components/guardrails/guardrail_table.test.tsx @@ -47,10 +47,7 @@ describe("GuardrailTable", () => { 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.", - ); + expect(deleteGuardrailButton).toHaveClass("cursor-not-allowed"); + expect(deleteGuardrailButton).toBeDisabled(); }); }); diff --git a/ui/litellm-dashboard/src/components/guardrails/guardrail_table.tsx b/ui/litellm-dashboard/src/components/guardrails/guardrail_table.tsx index 5bb2da78fa2..293d4e5985c 100644 --- a/ui/litellm-dashboard/src/components/guardrails/guardrail_table.tsx +++ b/ui/litellm-dashboard/src/components/guardrails/guardrail_table.tsx @@ -1,8 +1,27 @@ import React, { useState } from "react"; -import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Icon, Button } from "@tremor/react"; -import { TrashIcon, SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline"; -import { Tooltip } from "antd"; -import { Badge } from "@tremor/react"; +// eslint-disable-next-line litellm-ui/no-banned-ui-imports +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeaderCell, + TableRow, +} from "@tremor/react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { + ArrowUpDown, + ChevronDown, + ChevronUp, + Trash2, +} from "lucide-react"; import { ColumnDef, flexRender, @@ -11,7 +30,11 @@ import { SortingState, useReactTable, } from "@tanstack/react-table"; -import { getGuardrailLogoAndName, guardrail_provider_map, skipSystemMessageToChoice } from "./guardrail_info_helpers"; +import { + getGuardrailLogoAndName, + guardrail_provider_map, + skipSystemMessageToChoice, +} from "./guardrail_info_helpers"; import EditGuardrailForm from "./edit_guardrail_form"; import { Guardrail, GuardrailDefinitionLocation } from "./types"; @@ -31,25 +54,24 @@ const GuardrailTable: React.FC = ({ onDeleteClick, accessToken, onGuardrailUpdated, + // eslint-disable-next-line @typescript-eslint/no-unused-vars isAdmin = false, onGuardrailClick, }) => { - const [sorting, setSorting] = useState([{ id: "created_at", desc: true }]); + const [sorting, setSorting] = useState([ + { id: "created_at", desc: true }, + ]); const [editModalVisible, setEditModalVisible] = useState(false); - const [selectedGuardrail, setSelectedGuardrail] = useState(null); + const [selectedGuardrail, setSelectedGuardrail] = useState( + null, + ); - // Format date helper function const formatDate = (dateString?: string) => { if (!dateString) return "-"; const date = new Date(dateString); return date.toLocaleString(); }; - const handleEditClick = (guardrail: Guardrail) => { - setSelectedGuardrail(guardrail); - setEditModalVisible(true); - }; - const handleEditSuccess = () => { setEditModalVisible(false); setSelectedGuardrail(null); @@ -60,18 +82,25 @@ const GuardrailTable: React.FC = ({ { header: "Guardrail ID", accessorKey: "guardrail_id", - cell: (info: any) => ( - - - - ), + cell: (info) => { + const v = String(info.getValue() || ""); + return ( + + + + + + {v} + + + ); + }, }, { header: "Name", @@ -79,9 +108,16 @@ const GuardrailTable: React.FC = ({ cell: ({ row }) => { const guardrail = row.original; return ( - - {guardrail.guardrail_name || "-"} - + + + + + {guardrail.guardrail_name || "-"} + + + {guardrail.guardrail_name} + + ); }, }, @@ -90,16 +126,18 @@ const GuardrailTable: React.FC = ({ accessorKey: "litellm_params.guardrail", cell: ({ row }) => { const guardrail = row.original; - const { logo, displayName } = getGuardrailLogoAndName(guardrail.litellm_params.guardrail); + const { logo, displayName } = getGuardrailLogoAndName( + guardrail.litellm_params.guardrail, + ); return (
{logo && ( + // eslint-disable-next-line @next/next/no-img-element {`${displayName} { - // Hide broken image (e.target as HTMLImageElement).style.display = "none"; }} /> @@ -124,11 +162,15 @@ const GuardrailTable: React.FC = ({ const guardrail = row.original; return ( - {guardrail.litellm_params?.default_on ? "Default On" : "Default Off"} + {guardrail.litellm_params?.default_on + ? "Default On" + : "Default Off"} ); }, @@ -139,9 +181,16 @@ const GuardrailTable: React.FC = ({ cell: ({ row }) => { const guardrail = row.original; return ( - - {formatDate(guardrail.created_at)} - + + + + + {formatDate(guardrail.created_at)} + + + {guardrail.created_at} + + ); }, }, @@ -151,9 +200,16 @@ const GuardrailTable: React.FC = ({ cell: ({ row }) => { const guardrail = row.original; return ( - - {formatDate(guardrail.updated_at)} - + + + + + {formatDate(guardrail.updated_at)} + + + {guardrail.updated_at} + + ); }, }, @@ -162,32 +218,55 @@ const GuardrailTable: React.FC = ({ header: "Actions", cell: ({ row }) => { const guardrail = row.original; - const isConfigGuardrail = guardrail.guardrail_definition_location === GuardrailDefinitionLocation.CONFIG; + const isConfigGuardrail = + guardrail.guardrail_definition_location === + GuardrailDefinitionLocation.CONFIG; return ( -
+
{isConfigGuardrail ? ( - - - + + + + + + + Config guardrail cannot be deleted on the dashboard. Please + delete it from the config file. + + + ) : ( - - - guardrail.guardrail_id && - onDeleteClick(guardrail.guardrail_id, guardrail.guardrail_name || "Unnamed Guardrail") - } - className="cursor-pointer hover:text-red-500" - /> - + + + + + + Delete guardrail + + )}
); @@ -198,9 +277,7 @@ const GuardrailTable: React.FC = ({ const table = useReactTable({ data: guardrailsList, columns, - state: { - sorting, - }, + state: { sorting }, onSortingChange: setSorting, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), @@ -218,23 +295,34 @@ const GuardrailTable: React.FC = ({
- {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )}
{header.id !== "actions" && (
{header.column.getIsSorted() ? ( { - asc: , - desc: , + asc: ( + + ), + desc: ( + + ), }[header.column.getIsSorted() as string] ) : ( - + )}
)} @@ -247,8 +335,11 @@ const GuardrailTable: React.FC = ({ {isLoading ? ( - -
+ +

Loading...

@@ -261,19 +352,25 @@ const GuardrailTable: React.FC = ({ key={cell.id} className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${ cell.column.id === "actions" - ? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]" + ? "sticky right-0 bg-background shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]" : "" }`} > - {flexRender(cell.column.columnDef.cell, cell.getContext())} + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} ))} )) ) : ( - -
+ +

No guardrails found

@@ -283,7 +380,6 @@ const GuardrailTable: React.FC = ({
- {/* Edit Modal */} {selectedGuardrail && ( = ({ guardrail_name: selectedGuardrail.guardrail_name || "", provider: Object.keys(guardrail_provider_map).find( - (key) => guardrail_provider_map[key] === selectedGuardrail?.litellm_params.guardrail, + (key) => + guardrail_provider_map[key] === + selectedGuardrail?.litellm_params.guardrail, ) || "", mode: selectedGuardrail.litellm_params.mode, default_on: selectedGuardrail.litellm_params.default_on, - pii_entities_config: selectedGuardrail.litellm_params.pii_entities_config, + pii_entities_config: + selectedGuardrail.litellm_params.pii_entities_config, skip_system_message_choice: skipSystemMessageToChoice( selectedGuardrail.litellm_params?.skip_system_message_in_guardrail, ),