From b0ef2c70d4526e71dd48e7eba441fe602662161f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 07:11:23 +0000 Subject: [PATCH] feat(ui): migrate tool-policies (PolicySelect) to shadcn - PolicySelect.tsx: antd Select \u2192 shadcn Select + SelectTrigger/ Content/Item. Preserves categorical amber/emerald/red palette for the untrusted/trusted/blocked policy badges (these are non-semantic values by design). PolicySelect.tsx added to .eslintrc.json raw-colors override list. Deviation recorded in DEVIATIONS.md. - PolicySelect.test.tsx: replace `.ant-select` class assertion with semantic `toBeDisabled()` against getByRole('combobox'), agnostic of underlying Select primitive. Gates: TS \u2713 | Lint \u2713 | Vitest 12/12 \u2713 | Build \u2713. Co-authored-by: yuneng-jiang --- ui/litellm-dashboard/.eslintrc.json | 3 +- ui/litellm-dashboard/docs/CYCLES.md | 9 ++ ui/litellm-dashboard/docs/DEVIATIONS.md | 15 +++ .../ToolPolicies/PolicySelect.test.tsx | 7 +- .../components/ToolPolicies/PolicySelect.tsx | 121 ++++++++++-------- 5 files changed, 100 insertions(+), 55 deletions(-) diff --git a/ui/litellm-dashboard/.eslintrc.json b/ui/litellm-dashboard/.eslintrc.json index 450e8d04267..f9e8316eeed 100644 --- a/ui/litellm-dashboard/.eslintrc.json +++ b/ui/litellm-dashboard/.eslintrc.json @@ -22,7 +22,8 @@ "src/components/ui/**/*.{ts,tsx}", "tailwind.config.ts", "src/components/molecules/notifications_manager.tsx", - "src/components/molecules/message_manager.tsx" + "src/components/molecules/message_manager.tsx", + "src/components/ToolPolicies/PolicySelect.tsx" ], "rules": { "litellm-ui/no-banned-ui-imports": "off", diff --git a/ui/litellm-dashboard/docs/CYCLES.md b/ui/litellm-dashboard/docs/CYCLES.md index acb0f9a426d..5bc5b0f01ae 100644 --- a/ui/litellm-dashboard/docs/CYCLES.md +++ b/ui/litellm-dashboard/docs/CYCLES.md @@ -43,3 +43,12 @@ Layer abbreviations: **TS** (tsc --noEmit), **Lint** (eslint), **Vitest** - cycle 1: TS ✓ | Lint ✓ (pre-existing unused-imports in test excluded) | Vitest ✓ (6/6) | Parity ⏭ | Snap ⏭ - Final status: **done** +## 6. Tool Policies (tool-policies) + +- Cycles used: 1 / 7 +- Layer outcomes per cycle: + - cycle 1: TS ✓ | Lint ✓ (PolicySelect.tsx added to raw-colors override list) | Vitest ✓ (12/12) | Parity ⏭ | Snap ⏭ +- Final status: **done** +- Scope: PolicySelect.tsx only (the single banned-import file under ToolPolicies/). The ToolPoliciesView.tsx top-level wrapper has no antd imports already. Top-level ToolPolicies.tsx legacy wrapper is not in scope. +- Section decision: keep amber/emerald/red categorical colors for policy badges (not theme-semantic). Documented in DEVIATIONS.md. + diff --git a/ui/litellm-dashboard/docs/DEVIATIONS.md b/ui/litellm-dashboard/docs/DEVIATIONS.md index e365ead5c87..ce9d873f0e6 100644 --- a/ui/litellm-dashboard/docs/DEVIATIONS.md +++ b/ui/litellm-dashboard/docs/DEVIATIONS.md @@ -45,6 +45,21 @@ Format per entry: --- +## Section 6 (Tool Policies) — categorical colors (amber/emerald/red) on policy badges + +- **Type:** `blueprint deviation` +- **Blueprint rule deviated from:** §1 "semantic Tailwind tokens only". +- **What the agent did instead:** `PolicySelect.tsx` uses raw palette + classes (`bg-amber-100 text-amber-800 border-amber-300`, + `bg-emerald-100 ...`, `bg-red-100 ...`) and is added to the + `litellm-ui/no-raw-tailwind-colors` override list in `.eslintrc.json`. +- **Why:** The policy badges encode a **categorical** (untrusted / + trusted / blocked), not a theme state. Semantic tokens (`primary`, + `muted`, `destructive`, ...) don't have three distinct hues suitable + for three categorical values. A phase-2 task could introduce semantic + policy tokens (`--policy-untrusted`, `--policy-trusted`, + `--policy-blocked`) backed by CSS variables; out of scope for phase 1. + ## Section 1 (Access Groups) — custom MultiSelect shim in lieu of antd Select mode=multiple - **Type:** `blueprint deviation` diff --git a/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.test.tsx b/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.test.tsx index cde59fe9c6d..c6cf9ac78db 100644 --- a/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.test.tsx +++ b/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.test.tsx @@ -55,8 +55,9 @@ describe("PolicySelect", () => { onChange={vi.fn()} /> ); - expect(screen.getByRole("combobox")).toHaveAttribute("aria-expanded", "false"); - expect(screen.getByRole("combobox").closest(".ant-select")).toHaveClass("ant-select-disabled"); + // shadcn Select's combobox trigger exposes `data-disabled` when disabled. + const combobox = screen.getByRole("combobox"); + expect(combobox).toBeDisabled(); }); it("should not be disabled when saving is false", () => { @@ -68,7 +69,7 @@ describe("PolicySelect", () => { onChange={vi.fn()} /> ); - expect(screen.getByRole("combobox").closest(".ant-select")).not.toHaveClass("ant-select-disabled"); + expect(screen.getByRole("combobox")).not.toBeDisabled(); }); }); diff --git a/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.tsx b/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.tsx index 1317351931e..a52e5906179 100644 --- a/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.tsx +++ b/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.tsx @@ -1,17 +1,39 @@ "use client"; import React from "react"; -import { Select } from "antd"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { cn } from "@/lib/utils"; export const INPUT_POLICY_OPTIONS = [ - { value: "untrusted", label: "untrusted", color: "#92400e", bg: "#fef3c7", border: "#fcd34d" }, - { value: "trusted", label: "trusted", color: "#065f46", bg: "#d1fae5", border: "#6ee7b7" }, - { value: "blocked", label: "blocked", color: "#991b1b", bg: "#fee2e2", border: "#fca5a5" }, + { + value: "untrusted", + label: "untrusted", + className: "bg-amber-100 text-amber-800 border-amber-300", + dotClass: "bg-amber-800", + }, + { + value: "trusted", + label: "trusted", + className: "bg-emerald-100 text-emerald-800 border-emerald-300", + dotClass: "bg-emerald-800", + }, + { + value: "blocked", + label: "blocked", + className: "bg-red-100 text-red-800 border-red-300", + dotClass: "bg-red-800", + }, ] as const; export const OUTPUT_POLICY_OPTIONS = [ - { value: "untrusted", label: "untrusted", color: "#92400e", bg: "#fef3c7", border: "#fcd34d" }, - { value: "trusted", label: "trusted", color: "#065f46", bg: "#d1fae5", border: "#6ee7b7" }, + INPUT_POLICY_OPTIONS[0], + INPUT_POLICY_OPTIONS[1], ] as const; export const POLICY_OPTIONS = INPUT_POLICY_OPTIONS; @@ -30,6 +52,12 @@ export interface PolicySelectProps { stopPropagation?: boolean; } +// Color pill around the shadcn Select trigger. This is the one place in +// phase 1 where we reach for named Tailwind colors — the policy-badge +// palette (amber/emerald/red) is purposely non-semantic because it +// encodes a categorical (untrusted / trusted / blocked) not a theme state. +// Recorded in DEVIATIONS.md if we decide to force semantic-token +// conformance later. export const PolicySelect: React.FC = ({ value, toolName, @@ -40,53 +68,44 @@ export const PolicySelect: React.FC = ({ minWidth = 110, stopPropagation = true, }) => { - const options = policyType === "output" ? OUTPUT_POLICY_OPTIONS : INPUT_POLICY_OPTIONS; + const options = + policyType === "output" ? OUTPUT_POLICY_OPTIONS : INPUT_POLICY_OPTIONS; const style = policyStyle(value); + const triggerHeight = size === "small" ? "h-7" : "h-8"; + const fontSize = size === "small" ? "text-[11px]" : "text-xs"; return ( - onChange(toolName, v)} + > + + + + + {options.map((o) => ( + + + + {o.label} + + + ))} + + + ); };