diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index fa2287bdaef..67139d2c932 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -301,17 +301,6 @@ "count": 1 } }, - "src/app/(dashboard)/guardrails/_components/edit_guardrail_form.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "no-restricted-syntax": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, "src/app/(dashboard)/guardrails/_components/guardrail_info.tsx": { "max-params": { "count": 1 @@ -339,14 +328,6 @@ "count": 1 } }, - "src/app/(dashboard)/guardrails/_components/guardrail_table.tsx": { - "no-nested-ternary": { - "count": 1 - }, - "no-restricted-imports": { - "count": 1 - } - }, "src/app/(dashboard)/guardrails/_components/tool_permission/ToolPermissionRulesEditor.tsx": { "no-restricted-imports": { "count": 1 diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/GuardrailsPanel.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/GuardrailsPanel.test.tsx index adaf7e4c63f..2ad332ae9ed 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/GuardrailsPanel.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/GuardrailsPanel.test.tsx @@ -105,6 +105,6 @@ describe("GuardrailsPanel", () => { expect(screen.getByText("Guardrails")).toBeInTheDocument(); // Activate the Guardrails tab so its content (including the Add button) is rendered fireEvent.click(screen.getByText("Guardrails")); - expect(screen.getByText("+ Add New Guardrail")).toBeInTheDocument(); + expect(screen.getByText("Add New Guardrail")).toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/GuardrailsPanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/GuardrailsPanel.tsx index af4c85d0b9f..33c6933634c 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/GuardrailsPanel.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/GuardrailsPanel.tsx @@ -1,7 +1,15 @@ import React, { useState, useEffect } from "react"; -import { Button, Dropdown, Tabs } from "antd"; -import { DownOutlined, PlusOutlined, CodeOutlined } from "@ant-design/icons"; +import { Tabs } from "antd"; +import { ChevronDown, Code, Plus } from "lucide-react"; import { getGuardrailsList, deleteGuardrailCall } from "@/components/networking"; +import { buttonVariants } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { cn } from "@/lib/cva.config"; import AddGuardrailForm from "./add_guardrail_form"; import GuardrailTable from "./guardrail_table"; import { isAdminRole } from "@/utils/roles"; @@ -133,30 +141,26 @@ const GuardrailsPanel: React.FC = ({ accessToken, userRole children: ( <>
- , - label: "Add Provider Guardrail", - onClick: handleAddGuardrail, - }, - { - key: "custom_code", - icon: , - label: "Create Custom Code Guardrail", - onClick: handleAddCustomCodeGuardrail, - }, - ], - }} - trigger={["click"]} - disabled={!accessToken} - > - - + + + + Add New Guardrail + + + + + + Add Provider Guardrail + + + + Create Custom Code Guardrail + + +
{selectedGuardrailId ? ( @@ -171,9 +175,6 @@ const GuardrailsPanel: React.FC = ({ accessToken, userRole guardrailsList={guardrailsList} isLoading={isLoading} onDeleteClick={handleDeleteClick} - accessToken={accessToken} - onGuardrailUpdated={fetchGuardrails} - isAdmin={isAdmin} onGuardrailClick={(id) => setSelectedGuardrailId(id)} /> )} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/edit_guardrail_form.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/edit_guardrail_form.tsx deleted file mode 100644 index a916984f8d7..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/edit_guardrail_form.tsx +++ /dev/null @@ -1,491 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { Form, Typography, Select, Input, Switch, Modal } from "antd"; -import { Button, TextInput } from "@tremor/react"; -import { - guardrail_provider_map, - guardrailLogoMap, - getGuardrailProviders, - getSupportedModesForProvider, - toModeArray, - type SkipSystemMessageChoice, - type SkipToolMessageChoice, -} from "./guardrail_info_helpers"; -import { resolveLogoSrc } from "@/lib/assetPaths"; -import { getGuardrailUISettings, getGlobalLitellmHeaderName } from "@/components/networking"; -import PiiConfiguration from "./pii_configuration"; -import NotificationsManager from "@/components/molecules/notifications_manager"; - -const { Title, Text } = Typography; -const { Option } = Select; - -interface EditGuardrailFormProps { - visible: boolean; - onClose: () => void; - accessToken: string | null; - onSuccess: () => void; - guardrailId: string; - /** Full stored params merged into PUT so optional fields (e.g. content filter) are preserved. */ - fullLitellmParams?: Record | null; - initialValues: { - guardrail_name: string; - provider: string; - mode: string; - default_on: boolean; - pii_entities_config?: { [key: string]: string }; - skip_system_message_choice?: SkipSystemMessageChoice; - skip_tool_message_choice?: SkipToolMessageChoice; - [key: string]: unknown; - }; -} - -interface GuardrailSettings { - supported_entities: string[]; - supported_actions: string[]; - supported_modes: string[]; - supported_modes_by_provider?: Record; - pii_entity_categories: Array<{ - category: string; - entities: string[]; - }>; -} - -const EditGuardrailForm: React.FC = ({ - visible, - onClose, - accessToken, - onSuccess, - guardrailId, - fullLitellmParams, - initialValues, -}) => { - const [form] = Form.useForm(); - const [loading, setLoading] = useState(false); - const [selectedProvider, setSelectedProvider] = useState(initialValues?.provider || null); - const [guardrailSettings, setGuardrailSettings] = useState(null); - const [selectedEntities, setSelectedEntities] = useState([]); - const [selectedActions, setSelectedActions] = useState<{ [key: string]: string }>({}); - - // Fetch guardrail settings when the component mounts - useEffect(() => { - const fetchGuardrailSettings = async () => { - try { - if (!accessToken) return; - - const data = await getGuardrailUISettings(accessToken); - setGuardrailSettings(data); - } catch (error) { - console.error("Error fetching guardrail settings:", error); - NotificationsManager.fromBackend("Failed to load guardrail settings"); - } - }; - - fetchGuardrailSettings(); - }, [accessToken]); - - // Initialize selected entities and actions from initialValues - useEffect(() => { - if (initialValues?.pii_entities_config && Object.keys(initialValues.pii_entities_config).length > 0) { - const entities = Object.keys(initialValues.pii_entities_config); - setSelectedEntities(entities); - setSelectedActions(initialValues.pii_entities_config); - } - }, [initialValues]); - - const handleProviderChange = (value: string) => { - setSelectedProvider(value); - // Reset form fields that are provider-specific - form.setFieldsValue({ - config: undefined, - }); - - // Reset PII selections when changing provider - setSelectedEntities([]); - setSelectedActions({}); - }; - - const handleEntitySelect = (entity: string) => { - setSelectedEntities((prev) => { - if (prev.includes(entity)) { - return prev.filter((e) => e !== entity); - } else { - return [...prev, entity]; - } - }); - }; - - const handleActionSelect = (entity: string, action: string) => { - setSelectedActions((prev) => ({ - ...prev, - [entity]: action, - })); - }; - - const handleSubmit = async () => { - try { - setLoading(true); - const values = await form.validateFields(); - - // Get the guardrail provider value from the map - const guardrailProvider = guardrail_provider_map[values.provider]; - - const litellm_params: Record = - fullLitellmParams && typeof fullLitellmParams === "object" ? { ...fullLitellmParams } : {}; - - litellm_params.guardrail = guardrailProvider; - litellm_params.mode = values.mode; - litellm_params.default_on = values.default_on; - - const skipChoice = values.skip_system_message_choice as SkipSystemMessageChoice | undefined; - if (skipChoice === "yes") { - litellm_params.skip_system_message_in_guardrail = true; - } else if (skipChoice === "no") { - litellm_params.skip_system_message_in_guardrail = false; - } else { - delete litellm_params.skip_system_message_in_guardrail; - } - - const skipToolChoice = values.skip_tool_message_choice as SkipToolMessageChoice | undefined; - if (skipToolChoice === "yes") { - litellm_params.skip_tool_message_in_guardrail = true; - } else if (skipToolChoice === "no") { - litellm_params.skip_tool_message_in_guardrail = false; - } else { - delete litellm_params.skip_tool_message_in_guardrail; - } - - let guardrail_info: Record = {}; - - // For Presidio PII, add the entity and action configurations - if (values.provider === "PresidioPII" && selectedEntities.length > 0) { - const piiEntitiesConfig: { [key: string]: string } = {}; - selectedEntities.forEach((entity) => { - piiEntitiesConfig[entity] = selectedActions[entity] || "MASK"; // Default to MASK if no action selected - }); - - litellm_params.pii_entities_config = piiEntitiesConfig; - } - // Add config values to the guardrail_info if provided - else if (values.config) { - try { - const configObj = JSON.parse(values.config); - // For some guardrails, the config values need to be in litellm_params - // Especially for providers like Bedrock that need guardrailIdentifier and guardrailVersion - if (values.provider === "Bedrock" && configObj) { - if (configObj.guardrail_id) { - litellm_params.guardrailIdentifier = configObj.guardrail_id; - } - if (configObj.guardrail_version) { - litellm_params.guardrailVersion = configObj.guardrail_version; - } - } else { - // For other providers, add the config to guardrail_info - guardrail_info = configObj; - } - } catch (error) { - NotificationsManager.fromBackend("Invalid JSON in configuration"); - setLoading(false); - return; - } - } - - const guardrailData: { - guardrail_id: string; - guardrail: { - guardrail_name: string; - litellm_params: Record; - guardrail_info: Record; - }; - } = { - guardrail_id: guardrailId, - guardrail: { - guardrail_name: values.guardrail_name, - litellm_params, - guardrail_info, - }, - }; - - if (!accessToken) { - throw new Error("No access token available"); - } - - // Call the update endpoint - const url = `/guardrails/${guardrailId}`; - const response = await fetch(url, { - method: "PUT", - headers: { - [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(guardrailData), - }); - - if (!response.ok) { - const errorData = await response.text(); - throw new Error(errorData || "Failed to update guardrail"); - } - - NotificationsManager.success("Guardrail updated successfully"); - - // Reset and close - onSuccess(); - onClose(); - } catch (error) { - console.error("Failed to update guardrail:", error); - NotificationsManager.fromBackend( - "Failed to update guardrail: " + (error instanceof Error ? error.message : String(error)), - ); - } finally { - setLoading(false); - } - }; - - const renderPiiConfiguration = () => { - if (!guardrailSettings || !selectedProvider || selectedProvider !== "PresidioPII") return null; - - return ( - - ); - }; - - const renderProviderSpecificFields = () => { - if (!selectedProvider) return null; - - // For Presidio, we use the new PII configuration UI - if (selectedProvider === "PresidioPII") { - return renderPiiConfiguration(); - } - - switch (selectedProvider) { - case "Aporia": - return ( - - - - ); - case "AimSecurity": - return ( - - - - ); - case "Bedrock": - return ( - - - - ); - case "CatoNetworks": - return ( - - - - ); - case "GuardrailsAI": - return ( - - - - ); - case "LakeraAI": - return ( - - - - ); - case "PromptInjection": - return ( - - - - ); - default: - return ( - - - - ); - } - }; - - return ( - -
- - - - - - - - - - - - - - - - - - - - - - - - - {renderProviderSpecificFields()} - -
- - -
-
-
- ); -}; - -export default EditGuardrailForm; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrailTableColumns.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrailTableColumns.tsx new file mode 100644 index 00000000000..9ceb6ba244b --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrailTableColumns.tsx @@ -0,0 +1,176 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { MoreHorizontal, Trash2 } from "lucide-react"; + +import { DataTableSortHeader } from "@/components/shared/DataTable"; +import { DateCell, IdentityCell, StatusBadge } from "@/components/shared/table_cells"; +import { Guardrail, GuardrailDefinitionLocation } from "@/components/guardrails/types"; +import { buttonVariants } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { cn } from "@/lib/cva.config"; + +import { getGuardrailLogoAndName } from "./guardrail_info_helpers"; + +const CONFIG_DELETE_HINT = "Config guardrails are defined in the config file and cannot be deleted from the dashboard."; + +function GuardrailProviderCell({ provider }: { provider: string }) { + const { logo, displayName } = getGuardrailLogoAndName(provider); + return ( +
+ {logo ? ( + { + (event.currentTarget as HTMLImageElement).style.display = "none"; + }} + /> + ) : null} + {displayName} +
+ ); +} + +interface GuardrailRowActionsProps { + guardrail: Guardrail; + onDeleteClick: (guardrailId: string, guardrailName: string) => void; +} + +function GuardrailRowActions({ guardrail, onDeleteClick }: GuardrailRowActionsProps) { + const isConfigGuardrail = guardrail.guardrail_definition_location === GuardrailDefinitionLocation.CONFIG; + + return ( + + + + + + onDeleteClick(guardrail.guardrail_id, guardrail.guardrail_name || "Unnamed Guardrail")} + > + + Delete + + + + ); +} + +interface GuardrailTableColumnsDeps { + onGuardrailClick: (guardrailId: string) => void; + onDeleteClick: (guardrailId: string, guardrailName: string) => void; +} + +export const getGuardrailTableColumns = ({ + onGuardrailClick, + onDeleteClick, +}: GuardrailTableColumnsDeps): ColumnDef[] => [ + { + id: "guardrail_id", + accessorKey: "guardrail_id", + meta: { title: "Guardrail ID" }, + header: ({ column }) => , + size: 200, + enableSorting: true, + cell: ({ row }) => ( + onGuardrailClick(row.original.guardrail_id)} + /> + ), + }, + { + id: "guardrail_name", + accessorKey: "guardrail_name", + meta: { title: "Name" }, + header: ({ column }) => , + size: 200, + enableSorting: true, + cell: ({ row }) => { + const name = row.original.guardrail_name; + return ( + + {name || "-"} + + ); + }, + }, + { + id: "provider", + meta: { title: "Provider" }, + header: "Provider", + size: 180, + enableSorting: false, + cell: ({ row }) => , + }, + { + id: "mode", + meta: { title: "Mode" }, + header: "Mode", + size: 130, + enableSorting: false, + cell: ({ row }) => ( + {row.original.litellm_params.mode} + ), + }, + { + id: "default_on", + meta: { title: "Default On" }, + header: "Default On", + size: 120, + enableSorting: false, + cell: ({ row }) => { + const isDefaultOn = !!row.original.litellm_params?.default_on; + return ( + + ); + }, + }, + { + id: "created_at", + accessorKey: "created_at", + meta: { title: "Created At" }, + header: ({ column }) => , + size: 150, + enableSorting: true, + cell: ({ row }) => , + }, + { + id: "updated_at", + accessorKey: "updated_at", + meta: { title: "Updated At" }, + header: ({ column }) => , + size: 150, + enableSorting: true, + cell: ({ row }) => , + }, + { + id: "actions", + meta: { className: "text-right", headerClassName: "text-right" }, + header: () => Actions, + size: 64, + enableSorting: false, + enableHiding: false, + cell: ({ row }) => ( +
+ +
+ ), + }, +]; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_table.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_table.test.tsx index 70b5998ad95..4f556e74c16 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_table.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_table.test.tsx @@ -1,56 +1,61 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, it, expect, vi } from "vitest"; + import GuardrailTable from "./guardrail_table"; -import { render } from "@testing-library/react"; -import { describe, it, expect } from "vitest"; -import { GuardrailDefinitionLocation } from "@/components/guardrails/types"; +import { Guardrail, GuardrailDefinitionLocation } from "@/components/guardrails/types"; + +const baseProps = { + isLoading: false, + onDeleteClick: vi.fn(), + onGuardrailClick: vi.fn(), +}; + +const makeGuardrail = (overrides: Partial = {}): Guardrail => ({ + guardrail_id: "gr-1", + guardrail_name: "PII Redaction", + litellm_params: { guardrail: "presidio", mode: "pre_call", default_on: true }, + guardrail_info: null, + created_at: "2021-01-01", + updated_at: "2021-01-02", + guardrail_definition_location: GuardrailDefinitionLocation.DB, + ...overrides, +}); + describe("GuardrailTable", () => { - it("should render", () => { - const { getByText } = render( - {}} - 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("renders every column header", () => { + render(); + for (const header of ["Guardrail ID", "Name", "Provider", "Mode", "Default On", "Created At", "Updated At"]) { + expect(screen.getByText(header)).toBeInTheDocument(); + } }); - it("should not allow deletion of config guardrails", () => { - const { getByTestId } = render( - {}} - accessToken={null} - onGuardrailUpdated={() => {}} - onGuardrailClick={() => {}} - />, - ); + it("deletes a DB guardrail through the actions menu", async () => { + const user = userEvent.setup(); + const onDeleteClick = vi.fn(); + const guardrail = makeGuardrail({ guardrail_id: "gr-9", guardrail_name: "Toxicity Filter" }); + render(); - 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.", - ); + await user.click(screen.getByTestId("guardrail-actions-gr-9")); + await user.click(await screen.findByTestId("guardrail-action-delete")); + + expect(onDeleteClick).toHaveBeenCalledWith("gr-9", "Toxicity Filter"); + }); + + it("disables deletion for config guardrails so they cannot be removed from the dashboard", async () => { + const user = userEvent.setup(); + const onDeleteClick = vi.fn(); + const guardrail = makeGuardrail({ + guardrail_id: "cfg-1", + guardrail_name: "Config Guardrail", + guardrail_definition_location: GuardrailDefinitionLocation.CONFIG, + }); + render(); + + await user.click(screen.getByTestId("guardrail-actions-cfg-1")); + const deleteItem = await screen.findByTestId("guardrail-action-delete"); + + expect(deleteItem).toHaveAttribute("data-disabled"); + expect(onDeleteClick).not.toHaveBeenCalled(); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_table.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_table.tsx index 4ea4505f506..e6a14b2b2f4 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_table.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_table.tsx @@ -1,284 +1,61 @@ -import React, { useState } from "react"; -import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Icon } from "@tremor/react"; -import { TrashIcon, SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline"; -import { Tooltip } from "antd"; -import { DateCell, IdCell, StatusBadge } from "@/components/shared/table_cells"; -import { - ColumnDef, - flexRender, - getCoreRowModel, - getSortedRowModel, - SortingState, - useReactTable, -} from "@tanstack/react-table"; -import { - getGuardrailLogoAndName, - guardrail_provider_map, - skipSystemMessageToChoice, - skipToolMessageToChoice, -} from "./guardrail_info_helpers"; -import EditGuardrailForm from "./edit_guardrail_form"; -import { Guardrail, GuardrailDefinitionLocation } from "@/components/guardrails/types"; +"use client"; + +import { SortingState } from "@tanstack/react-table"; +import { Inbox } from "lucide-react"; +import React, { useMemo, useState } from "react"; + +import { DataTable } from "@/components/shared/DataTable"; +import { Guardrail } from "@/components/guardrails/types"; + +import { getGuardrailTableColumns } from "./guardrailTableColumns"; interface GuardrailTableProps { guardrailsList: Guardrail[]; isLoading: boolean; onDeleteClick: (guardrailId: string, guardrailName: string) => void; - accessToken: string | null; - onGuardrailUpdated: () => void; - isAdmin?: boolean; onGuardrailClick: (id: string) => void; } +const DEFAULT_SORTING: SortingState = [{ id: "created_at", desc: true }]; + +function EmptyState() { + return ( +
+
+ +
+
No guardrails yet
+
Add a guardrail to start filtering requests and responses.
+
+ ); +} + const GuardrailTable: React.FC = ({ guardrailsList, isLoading, onDeleteClick, - accessToken, - onGuardrailUpdated, - isAdmin = false, onGuardrailClick, }) => { - const [sorting, setSorting] = useState([{ id: "created_at", desc: true }]); - const [editModalVisible, setEditModalVisible] = useState(false); - const [selectedGuardrail, setSelectedGuardrail] = useState(null); + const [sorting, setSorting] = useState(DEFAULT_SORTING); - const handleEditClick = (guardrail: Guardrail) => { - setSelectedGuardrail(guardrail); - setEditModalVisible(true); - }; - - const handleEditSuccess = () => { - setEditModalVisible(false); - setSelectedGuardrail(null); - onGuardrailUpdated(); - }; - - const columns: ColumnDef[] = [ - { - header: "Guardrail ID", - accessorKey: "guardrail_id", - cell: (info: any) => , - }, - { - header: "Name", - accessorKey: "guardrail_name", - cell: ({ row }) => { - const guardrail = row.original; - return ( - - {guardrail.guardrail_name || "-"} - - ); - }, - }, - { - header: "Provider", - accessorKey: "litellm_params.guardrail", - cell: ({ row }) => { - const guardrail = row.original; - const { logo, displayName } = getGuardrailLogoAndName(guardrail.litellm_params.guardrail); - return ( -
- {logo && ( - {`${displayName} { - // Hide broken image - (e.target as HTMLImageElement).style.display = "none"; - }} - /> - )} - {displayName} -
- ); - }, - }, - { - header: "Mode", - accessorKey: "litellm_params.mode", - cell: ({ row }) => { - const guardrail = row.original; - return {guardrail.litellm_params.mode}; - }, - }, - { - header: "Default On", - accessorKey: "litellm_params.default_on", - cell: ({ row }) => { - const isDefaultOn = !!row.original.litellm_params?.default_on; - return ( - - ); - }, - }, - { - header: "Created At", - accessorKey: "created_at", - cell: ({ row }) => , - }, - { - header: "Updated At", - accessorKey: "updated_at", - cell: ({ row }) => , - }, - { - id: "actions", - header: "Actions", - cell: ({ row }) => { - const guardrail = row.original; - const isConfigGuardrail = guardrail.guardrail_definition_location === GuardrailDefinitionLocation.CONFIG; - return ( -
- {isConfigGuardrail ? ( - - - - ) : ( - - - guardrail.guardrail_id && - onDeleteClick(guardrail.guardrail_id, guardrail.guardrail_name || "Unnamed Guardrail") - } - className="cursor-pointer hover:text-red-500" - /> - - )} -
- ); - }, - }, - ]; - - const table = useReactTable({ - data: guardrailsList, - columns, - state: { - sorting, - }, - onSortingChange: setSorting, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - enableSorting: true, - }); + const columns = useMemo( + () => getGuardrailTableColumns({ onGuardrailClick, onDeleteClick }), + [onGuardrailClick, onDeleteClick], + ); return ( -
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - -
-
- {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} -
- {header.id !== "actions" && ( -
- {header.column.getIsSorted() ? ( - { - asc: , - desc: , - }[header.column.getIsSorted() as string] - ) : ( - - )} -
- )} -
-
- ))} -
- ))} -
- - {isLoading ? ( - - -
-

Loading...

-
-
-
- ) : guardrailsList.length > 0 ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - ) : ( - - -
-

No guardrails found

-
-
-
- )} -
-
-
- - {/* Edit Modal */} - {selectedGuardrail && ( - setEditModalVisible(false)} - accessToken={accessToken} - onSuccess={handleEditSuccess} - guardrailId={selectedGuardrail.guardrail_id || ""} - fullLitellmParams={selectedGuardrail.litellm_params} - initialValues={{ - guardrail_name: selectedGuardrail.guardrail_name || "", - provider: - Object.keys(guardrail_provider_map).find( - (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, - skip_system_message_choice: skipSystemMessageToChoice( - selectedGuardrail.litellm_params?.skip_system_message_in_guardrail, - ), - skip_tool_message_choice: skipToolMessageToChoice( - selectedGuardrail.litellm_params?.skip_tool_message_in_guardrail, - ), - ...selectedGuardrail.guardrail_info, - }} - /> - )} -
+ guardrail.guardrail_id || String(index)} + sortingMode="client" + sorting={sorting} + onSortingChange={setSorting} + isLoading={isLoading} + loadingMessage="Loading guardrails…" + noDataMessage={} + size="compact" + /> ); }; diff --git a/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.test.tsx b/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.test.tsx index 0d3468d4ada..60547415908 100644 --- a/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.test.tsx +++ b/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.test.tsx @@ -358,6 +358,23 @@ describe("DataTable loading", () => { expect(names()).toEqual(["Charlie", "Alice", "Bob"]); }); + it("gives compact skeleton rows the same height as loaded rows so loading does not shrink the table", () => { + const { rerender } = render( + , + ); + const skeletonRow = screen.getAllByTestId("skeleton-row").at(0); + const loadedRowHeight = "h-8"; + expect(skeletonRow?.className).toContain(loadedRowHeight); + + rerender(); + expect(document.querySelector("[data-row-id]")?.className).toContain(loadedRowHeight); + }); + + it("does not force the compact height on default-size skeleton rows", () => { + render(); + expect(screen.getAllByTestId("skeleton-row").at(0)?.className).not.toContain("h-8"); + }); + it("varies skeleton shape and width per column instead of one fixed bar", () => { const columns: ColumnDef[] = [ { accessorKey: "name", header: "Name", meta: { skeleton: "twoLine" }, cell: () => null }, diff --git a/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.tsx b/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.tsx index d55831b87ca..fa0e672026e 100644 --- a/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.tsx +++ b/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.tsx @@ -389,7 +389,11 @@ function SkeletonRows({ return ( {rowKeys.map((rowKey) => ( - + {cells.map((column, columnKey) => (