From 2e6d053932714c0a959baf8ad751f3bd2c8f7e80 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 16:09:56 +0000 Subject: [PATCH] feat(ui): policies/policy_table to shadcn - @tremor Button/Icon/Badge + @heroicons Trash/Pencil/SwitchVert/ ChevronUp/Down + antd Tooltip/Tag \u2192 shadcn Badge/Button/Tooltip + lucide ArrowUpDown/ChevronUp/ChevronDown/Pencil/Trash2. - Reusable chipList helper with tooltip for overflow. Tremor Table family whitelisted. - Blue inherit badge + emerald guardrails-add + red guardrails-remove categorical palette; file added to .eslintrc.json overrides. Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang --- ui/litellm-dashboard/.eslintrc.json | 1 + .../src/components/policies/policy_table.tsx | 323 +++++++++++------- 2 files changed, 206 insertions(+), 118 deletions(-) diff --git a/ui/litellm-dashboard/.eslintrc.json b/ui/litellm-dashboard/.eslintrc.json index 19a56de12d3..8648c7a1ff1 100644 --- a/ui/litellm-dashboard/.eslintrc.json +++ b/ui/litellm-dashboard/.eslintrc.json @@ -57,6 +57,7 @@ "src/components/pass_through_settings.tsx", "src/components/policies/attachment_table.tsx", "src/components/claude_code_plugins/plugin_table.tsx", + "src/components/policies/policy_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/policies/policy_table.tsx b/ui/litellm-dashboard/src/components/policies/policy_table.tsx index 9a3796c2395..8d5c47d4f2b 100644 --- a/ui/litellm-dashboard/src/components/policies/policy_table.tsx +++ b/ui/litellm-dashboard/src/components/policies/policy_table.tsx @@ -1,7 +1,28 @@ import React, { useMemo, useState } from "react"; -import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Icon, Button, Badge } from "@tremor/react"; -import { TrashIcon, PencilIcon, SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline"; -import { Tooltip, Tag } from "antd"; +// 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, + Pencil, + Trash2, +} from "lucide-react"; import { ColumnDef, flexRender, @@ -12,7 +33,6 @@ import { } from "@tanstack/react-table"; import { Policy } from "./types"; -/** One row per policy name; primaryPolicy is used for display and for Edit (FlowBuilder loads all versions) */ interface PolicyRow { policy_name: string; primaryPolicy: Policy; @@ -28,12 +48,17 @@ function groupPoliciesByName(policies: Policy[]): PolicyRow[] { } const rows: PolicyRow[] = []; for (const [policyName, versions] of byName) { - // Prefer production, then highest version_number const primary = versions.find((v) => v.version_status === "production") ?? - [...versions].sort((a, b) => (b.version_number ?? 0) - (a.version_number ?? 0))[0] ?? + [...versions].sort( + (a, b) => (b.version_number ?? 0) - (a.version_number ?? 0), + )[0] ?? versions[0]; - rows.push({ policy_name: policyName, primaryPolicy: primary, versionCount: versions.length }); + rows.push({ + policy_name: policyName, + primaryPolicy: primary, + versionCount: versions.length, + }); } return rows.sort((a, b) => a.policy_name.localeCompare(b.policy_name)); } @@ -47,6 +72,32 @@ interface PolicyTableProps { isAdmin?: boolean; } +const chipList = (items: string[], classes: string) => { + if (items.length === 0) + return -; + return ( +
+ {items.slice(0, 2).map((g, i) => ( + + {g} + + ))} + {items.length > 2 && ( + + + + + +{items.length - 2} + + + {items.slice(2).join(", ")} + + + )} +
+ ); +}; + const PolicyTable: React.FC = ({ policies, isLoading, @@ -55,7 +106,9 @@ const PolicyTable: React.FC = ({ onViewClick, isAdmin = false, }) => { - const [sorting, setSorting] = useState([{ id: "policy_name", desc: false }]); + const [sorting, setSorting] = useState([ + { id: "policy_name", desc: false }, + ]); const rows = useMemo(() => groupPoliciesByName(policies), [policies]); @@ -73,18 +126,28 @@ const PolicyTable: React.FC = ({ const { primaryPolicy, versionCount } = row.original; return (
- 1 ? ` (${versionCount} versions)` : ""}`}> - - + + + + + + + {primaryPolicy.policy_name || "-"} + {versionCount > 1 ? ` (${versionCount} versions)` : ""} + + + {versionCount > 1 && ( - + {versionCount} version{versionCount !== 1 ? "s" : ""} )} @@ -98,11 +161,16 @@ const PolicyTable: React.FC = ({ cell: ({ row }) => { const policy = row.original.primaryPolicy; return ( - - - {policy.description || "-"} - - + + + + + {policy.description || "-"} + + + {policy.description || "-"} + + ); }, }, @@ -112,63 +180,32 @@ const PolicyTable: React.FC = ({ cell: ({ row }) => { const policy = row.original.primaryPolicy; return policy.inherit ? ( - + {policy.inherit} ) : ( - - + - ); }, }, { header: "Guardrails (Add)", accessorFn: (row) => (row.primaryPolicy.guardrails_add ?? []).join(", "), - cell: ({ row }) => { - const policy = row.original.primaryPolicy; - const guardrails = policy.guardrails_add || []; - if (guardrails.length === 0) { - return -; - } - return ( -
- {guardrails.slice(0, 2).map((g, i) => ( - - {g} - - ))} - {guardrails.length > 2 && ( - - +{guardrails.length - 2} - - )} -
- ); - }, + cell: ({ row }) => + chipList( + row.original.primaryPolicy.guardrails_add || [], + "bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300", + ), }, { header: "Guardrails (Remove)", - accessorFn: (row) => (row.primaryPolicy.guardrails_remove ?? []).join(", "), - cell: ({ row }) => { - const policy = row.original.primaryPolicy; - const guardrails = policy.guardrails_remove || []; - if (guardrails.length === 0) { - return -; - } - return ( -
- {guardrails.slice(0, 2).map((g, i) => ( - - {g} - - ))} - {guardrails.length > 2 && ( - - +{guardrails.length - 2} - - )} -
- ); - }, + accessorFn: (row) => + (row.primaryPolicy.guardrails_remove ?? []).join(", "), + cell: ({ row }) => + chipList( + row.original.primaryPolicy.guardrails_remove || [], + "bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-300", + ), }, { header: "Model Condition", @@ -180,18 +217,27 @@ const PolicyTable: React.FC = ({ const policy = row.original.primaryPolicy; const modelCondition = policy.condition?.model; if (!modelCondition) { - return -; + return -; } + const asString = + typeof modelCondition === "string" + ? modelCondition + : JSON.stringify(modelCondition); return ( - - - {typeof modelCondition === "string" - ? modelCondition.length > 20 - ? modelCondition.slice(0, 20) + "..." - : modelCondition - : "Multiple"} - - + + + + + {typeof modelCondition === "string" + ? modelCondition.length > 20 + ? modelCondition.slice(0, 20) + "..." + : modelCondition + : "Multiple"} + + + {asString} + + ); }, }, @@ -202,9 +248,14 @@ const PolicyTable: React.FC = ({ cell: ({ row }) => { const policy = row.original.primaryPolicy; return ( - - {formatDate(policy.created_at)} - + + + + {formatDate(policy.created_at)} + + {policy.created_at} + + ); }, }, @@ -212,31 +263,49 @@ const PolicyTable: React.FC = ({ id: "actions", header: "Actions", cell: ({ row }) => { - const { primaryPolicy } = row.original; - const policy = primaryPolicy; + const policy = row.original.primaryPolicy; return ( -
+
{isAdmin && ( <> - - onEditClick(policy)} - className="cursor-pointer hover:text-blue-500" - /> - - - - policy.policy_id && - onDeleteClick(policy.policy_id, policy.policy_name || "Unnamed Policy") - } - className="cursor-pointer hover:text-red-500" - /> - + + + + + + Edit policy + + + + + + + + Delete policy + + )}
@@ -248,9 +317,7 @@ const PolicyTable: React.FC = ({ const table = useReactTable({ data: rows, columns, - state: { - sorting, - }, + state: { sorting }, onSortingChange: setSorting, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), @@ -268,23 +335,34 @@ const PolicyTable: 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] ) : ( - + )}
)} @@ -297,8 +375,11 @@ const PolicyTable: React.FC = ({ {isLoading ? ( - -
+ +

Loading...

@@ -311,19 +392,25 @@ const PolicyTable: 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 policies found