feat(ui): guardrails/pii_components to shadcn

- antd Typography/Select/Button/Checkbox/Tooltip/Tag + @ant-design
  Close/EyeInvisible/Stop/Filter \u2192 shadcn Badge/Button/Checkbox/
  Select/Tooltip + lucide Ban/EyeOff/Filter/Info/X.
- CategoryFilter keeps antd Select mode='multiple' \u2014 shadcn Select
  doesn't support multiple. Blue category badge in PiiEntityList
  preserved as categorical (file added to .eslintrc.json overrides).
- Quick Action buttons use destructive/primary outline variants for
  Mask/Block/Unselect respectively.

Gates: TS \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 15:08:02 +00:00
parent aa0ab33ac7
commit 1f4656bc4c
No known key found for this signature in database
2 changed files with 133 additions and 92 deletions

View file

@ -70,6 +70,7 @@
"src/components/mcp_tools/mcp_server_cost_config.tsx",
"src/components/view_logs/GuardrailViewer/CompliancePanel.tsx",
"src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx",
"src/components/guardrails/pii_components.tsx",
"src/components/prompts/prompt_editor_view/PromptEditorHeader.tsx",
"src/components/prompts/prompt_editor_view/VersionHistorySidePanel.tsx",
"src/app/(dashboard)/teams/components/TeamsTable/ModelsCell.tsx",

View file

@ -1,12 +1,27 @@
import React from "react";
import { Typography, Select, Button, Checkbox, Tooltip, Tag } from "antd";
import { CloseOutlined, EyeInvisibleOutlined, StopOutlined, FilterOutlined } from "@ant-design/icons";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { Ban, Eye, EyeOff, Filter, Info, X } from "lucide-react";
import { Select as AntSelect } from "antd";
import { PiiEntityCategory } from "./types";
const { Text } = Typography;
const { Option } = Select;
const { Option } = AntSelect;
// Helper functions
export const formatEntityName = (name: string) => {
return name.replace(/_/g, " ");
};
@ -14,29 +29,35 @@ export const formatEntityName = (name: string) => {
export const getActionIcon = (action: string) => {
switch (action) {
case "MASK":
return <EyeInvisibleOutlined style={{ marginRight: 4 }} />;
return <EyeOff className="h-3.5 w-3.5 mr-1" />;
case "BLOCK":
return <StopOutlined style={{ marginRight: 4 }} />;
return <Ban className="h-3.5 w-3.5 mr-1" />;
default:
return null;
}
};
// CategoryFilter component
// CategoryFilter
export interface CategoryFilterProps {
categories: PiiEntityCategory[];
selectedCategories: string[];
onChange: (categories: string[]) => void;
}
export const CategoryFilter: React.FC<CategoryFilterProps> = ({ categories, selectedCategories, onChange }) => {
export const CategoryFilter: React.FC<CategoryFilterProps> = ({
categories,
selectedCategories,
onChange,
}) => {
return (
<div>
<div className="flex items-center mb-2">
<FilterOutlined className="text-gray-500 mr-1" />
<Text className="text-gray-500 font-medium">Filter by category</Text>
<Filter className="h-3.5 w-3.5 text-muted-foreground mr-1" />
<span className="text-muted-foreground font-medium">
Filter by category
</span>
</div>
<Select
<AntSelect
mode="multiple"
placeholder="Select categories to filter by"
style={{ width: "100%" }}
@ -46,78 +67,80 @@ export const CategoryFilter: React.FC<CategoryFilterProps> = ({ categories, sele
showSearch
optionFilterProp="children"
className="mb-4"
tagRender={(props) => (
<Tag color="blue" closable={props.closable} onClose={props.onClose} className="mr-2 mb-2">
{props.label}
</Tag>
)}
>
{categories.map((cat) => (
<Option key={cat.category} value={cat.category}>
{cat.category}
</Option>
))}
</Select>
</AntSelect>
</div>
);
};
// QuickActions component
// QuickActions
export interface QuickActionsProps {
onSelectAll: (action: string) => void;
onUnselectAll: () => void;
hasSelectedEntities: boolean;
}
export const QuickActions: React.FC<QuickActionsProps> = ({ onSelectAll, onUnselectAll, hasSelectedEntities }) => {
export const QuickActions: React.FC<QuickActionsProps> = ({
onSelectAll,
onUnselectAll,
hasSelectedEntities,
}) => {
return (
<div className="bg-gray-50 p-5 rounded-lg mb-6 border border-gray-200 shadow-sm">
<div className="bg-muted p-5 rounded-lg mb-6 border border-border shadow-sm">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center">
<Text strong className="text-gray-700 text-base">
<span className="font-bold text-foreground text-base">
Quick Actions
</Text>
<Tooltip title="Apply action to all PII types at once">
<div className="ml-2 text-gray-400 cursor-help text-xs">ⓘ</div>
</Tooltip>
</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="ml-2 h-3 w-3 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
Apply action to all PII types at once
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Button
color="danger"
variant="outlined"
variant="outline"
onClick={onUnselectAll}
disabled={!hasSelectedEntities}
icon={<CloseOutlined />}
className="text-destructive border-destructive/30 hover:bg-destructive/10"
>
<X className="h-4 w-4" />
Unselect All
</Button>
</div>
<div className="grid grid-cols-2 gap-4">
<Button
color="primary"
variant="outlined"
variant="outline"
onClick={() => onSelectAll("MASK")}
className="h-10"
block
icon={<EyeInvisibleOutlined />}
className="h-10 text-primary border-primary/30 hover:bg-primary/10 w-full"
>
Select All & Mask
<EyeOff className="h-4 w-4" />
Select All &amp; Mask
</Button>
<Button
color="danger"
variant="outlined"
variant="outline"
onClick={() => onSelectAll("BLOCK")}
className="h-10 hover:bg-red-100"
block
icon={<StopOutlined />}
className="h-10 text-destructive border-destructive/30 hover:bg-destructive/10 w-full"
>
Select All & Block
<Ban className="h-4 w-4" />
Select All &amp; Block
</Button>
</div>
</div>
);
};
// PiiEntityList component
// PiiEntityList
export interface PiiEntityListProps {
entities: string[];
selectedEntities: string[];
@ -138,62 +161,79 @@ export const PiiEntityList: React.FC<PiiEntityListProps> = ({
entityToCategoryMap,
}) => {
return (
<div className="border rounded-lg overflow-hidden shadow-sm">
<div className="bg-gray-50 px-5 py-3 border-b flex">
<Text strong className="flex-1 text-gray-700">
PII Type
</Text>
<Text strong className="w-32 text-right text-gray-700">
Action
</Text>
<div className="border border-border rounded-lg overflow-hidden shadow-sm">
<div className="bg-muted px-5 py-3 border-b border-border flex">
<span className="flex-1 text-foreground font-bold">PII Type</span>
<span className="w-32 text-right text-foreground font-bold">Action</span>
</div>
<div className="max-h-[400px] overflow-y-auto">
{entities.length === 0 ? (
<div className="py-10 text-center text-gray-500">No PII types match your filter criteria</div>
<div className="py-10 text-center text-muted-foreground">
No PII types match your filter criteria
</div>
) : (
entities.map((entity) => (
<div
key={entity}
className={`px-5 py-3 flex items-center justify-between hover:bg-gray-50 border-b ${
selectedEntities.includes(entity) ? "bg-blue-50" : ""
}`}
>
<div className="flex items-center flex-1">
<Checkbox
checked={selectedEntities.includes(entity)}
onChange={() => onEntitySelect(entity)}
className="mr-3"
/>
<Text className={selectedEntities.includes(entity) ? "font-medium text-gray-900" : "text-gray-700"}>
{formatEntityName(entity)}
</Text>
{entityToCategoryMap.get(entity) && (
<Tag className="ml-2 text-xs" color="blue">
{entityToCategoryMap.get(entity)}
</Tag>
entities.map((entity) => {
const isSelected = selectedEntities.includes(entity);
return (
<div
key={entity}
className={cn(
"px-5 py-3 flex items-center justify-between hover:bg-muted border-b border-border",
isSelected && "bg-blue-50 dark:bg-blue-950/30",
)}
>
<label className="flex items-center flex-1 cursor-pointer">
<Checkbox
checked={isSelected}
onCheckedChange={() => onEntitySelect(entity)}
className="mr-3"
/>
<span
className={cn(
isSelected
? "font-medium text-foreground"
: "text-foreground/80",
)}
>
{formatEntityName(entity)}
</span>
{entityToCategoryMap.get(entity) && (
<Badge className="ml-2 text-xs bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300">
{entityToCategoryMap.get(entity)}
</Badge>
)}
</label>
<div className="w-32">
<Select
value={
isSelected ? selectedActions[entity] || "MASK" : "MASK"
}
onValueChange={(value) => onActionSelect(entity, value)}
disabled={!isSelected}
>
<SelectTrigger
className={cn(
"w-[120px] h-8",
!isSelected && "opacity-50",
)}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
{actions.map((action) => (
<SelectItem key={action} value={action}>
<div className="flex items-center">
{getActionIcon(action)}
{action}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="w-32">
<Select
value={selectedEntities.includes(entity) ? selectedActions[entity] || "MASK" : "MASK"}
onChange={(value) => onActionSelect(entity, value)}
style={{ width: 120 }}
disabled={!selectedEntities.includes(entity)}
className={`${!selectedEntities.includes(entity) ? "opacity-50" : ""}`}
dropdownMatchSelectWidth={false}
>
{actions.map((action) => (
<Option key={action} value={action}>
<div className="flex items-center">
{getActionIcon(action)}
{action}
</div>
</Option>
))}
</Select>
</div>
</div>
))
);
})
)}
</div>
</div>