feat(ui): guardrails/content_filter \u2014 PatternTable + PatternModal + ContentFilterDisplay

- PatternTable: antd Table/Select/Tag/Typography/Button + @ant-design
  DeleteOutlined \u2192 shadcn Table/Select/Badge/Button + lucide Trash2.
  Categorical blue (prebuilt) / emerald (custom) type badges added to
  overrides.
- PatternModal: antd Modal/Select.OptGroup/Space/Button \u2192 shadcn
  Dialog/Select (with SelectGroup/SelectLabel for categories) + Button.
- ContentFilterDisplay: @tremor Card/Text/Badge \u2192 shadcn Card/Badge
  + plain span. Blue category counter badge preserved and file added
  to overrides.

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 12:06:27 +00:00
parent 6d50a85b98
commit 80a2c25e5a
No known key found for this signature in database
4 changed files with 201 additions and 161 deletions

View file

@ -56,6 +56,8 @@
"src/components/permissions/MCPServerPermissions.tsx",
"src/components/survey/ClaudeCodeModal.tsx",
"src/components/view_logs/ToolsSection/ToolItem.tsx",
"src/components/guardrails/content_filter/PatternTable.tsx",
"src/components/guardrails/content_filter/ContentFilterDisplay.tsx",
"src/components/policies/impact_preview_alert.tsx",
"src/components/policies/impact_popover.tsx",
"src/components/DeletedKeysPage/DeletedKeysPage.tsx",

View file

@ -1,5 +1,6 @@
import React from "react";
import { Card, Text, Badge } from "@tremor/react";
import { Badge } from "@/components/ui/badge";
import { Card } from "@/components/ui/card";
import PatternTable from "./PatternTable";
import KeywordTable from "./KeywordTable";
import CategoryTable from "./CategoryTable";
@ -35,10 +36,14 @@ interface ContentFilterDisplayProps {
readOnly?: boolean;
onPatternActionChange?: (id: string, action: "BLOCK" | "MASK") => void;
onPatternRemove?: (id: string) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onBlockedWordUpdate?: (id: string, field: string, value: any) => void;
onBlockedWordRemove?: (id: string) => void;
onCategoryActionChange?: (id: string, action: "BLOCK" | "MASK") => void;
onCategorySeverityChange?: (id: string, severity: "high" | "medium" | "low") => void;
onCategorySeverityChange?: (
id: string,
severity: "high" | "medium" | "low",
) => void;
onCategoryRemove?: (id: string) => void;
}
@ -55,20 +60,25 @@ const ContentFilterDisplay: React.FC<ContentFilterDisplayProps> = ({
onCategorySeverityChange,
onCategoryRemove,
}) => {
if (patterns.length === 0 && blockedWords.length === 0 && categories.length === 0) {
if (
patterns.length === 0 &&
blockedWords.length === 0 &&
categories.length === 0
) {
return null;
}
// No-op handlers for read-only mode
const noOp = () => {};
return (
<>
{categories.length > 0 && (
<Card className="mt-6">
<Card className="mt-6 p-6">
<div className="flex justify-between items-center mb-4">
<Text className="text-lg font-semibold">Content Categories</Text>
<Badge color="blue">{categories.length} categories configured</Badge>
<span className="text-lg font-semibold">Content Categories</span>
<Badge className="bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300">
{categories.length} categories configured
</Badge>
</div>
<CategoryTable
categories={categories}
@ -81,29 +91,33 @@ const ContentFilterDisplay: React.FC<ContentFilterDisplayProps> = ({
)}
{patterns.length > 0 && (
<Card className="mt-6">
<Card className="mt-6 p-6">
<div className="flex justify-between items-center mb-4">
<Text className="text-lg font-semibold">Pattern Detection</Text>
<Badge color="blue">{patterns.length} patterns configured</Badge>
<span className="text-lg font-semibold">Pattern Detection</span>
<Badge className="bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300">
{patterns.length} patterns configured
</Badge>
</div>
<PatternTable
patterns={patterns}
onActionChange={readOnly ? noOp : (onPatternActionChange || noOp)}
onRemove={readOnly ? noOp : (onPatternRemove || noOp)}
onActionChange={readOnly ? noOp : onPatternActionChange || noOp}
onRemove={readOnly ? noOp : onPatternRemove || noOp}
/>
</Card>
)}
{blockedWords.length > 0 && (
<Card className="mt-6">
<Card className="mt-6 p-6">
<div className="flex justify-between items-center mb-4">
<Text className="text-lg font-semibold">Blocked Keywords</Text>
<Badge color="blue">{blockedWords.length} keywords configured</Badge>
<span className="text-lg font-semibold">Blocked Keywords</span>
<Badge className="bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300">
{blockedWords.length} keywords configured
</Badge>
</div>
<KeywordTable
keywords={blockedWords}
onActionChange={readOnly ? noOp : (onBlockedWordUpdate || noOp)}
onRemove={readOnly ? noOp : (onBlockedWordRemove || noOp)}
onActionChange={readOnly ? noOp : onBlockedWordUpdate || noOp}
onRemove={readOnly ? noOp : onBlockedWordRemove || noOp}
/>
</Card>
)}
@ -112,4 +126,3 @@ const ContentFilterDisplay: React.FC<ContentFilterDisplayProps> = ({
};
export default ContentFilterDisplay;

View file

@ -1,8 +1,22 @@
import React from "react";
import { Typography, Select, Modal, Space, Button } from "antd";
const { Text } = Typography;
const { Option } = Select;
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface PrebuiltPattern {
name: string;
@ -35,77 +49,75 @@ const PatternModal: React.FC<PatternModalProps> = ({
onCancel,
}) => {
return (
<Modal
title="Add prebuilt pattern"
<Dialog
open={visible}
onCancel={onCancel}
footer={null}
width={800}
onOpenChange={(o) => (!o ? onCancel() : undefined)}
>
<Space direction="vertical" style={{ width: "100%" }} size="large">
<div>
<Text strong>Pattern type</Text>
<Select
placeholder="Choose pattern type"
value={selectedPatternName}
onChange={onPatternNameChange}
style={{ width: "100%", marginTop: 8 }}
showSearch
filterOption={(input, option) => {
const pattern = prebuiltPatterns.find((p) => p.name === option?.value);
if (pattern) {
return (
pattern.display_name.toLowerCase().includes(input.toLowerCase()) ||
pattern.name.toLowerCase().includes(input.toLowerCase())
);
}
return false;
}}
>
{categories.map((category) => {
const categoryPatterns = prebuiltPatterns.filter((p) => p.category === category);
if (categoryPatterns.length === 0) return null;
return (
<Select.OptGroup key={category} label={category}>
{categoryPatterns.map((pattern) => (
<Option key={pattern.name} value={pattern.name}>
{pattern.display_name}
</Option>
))}
</Select.OptGroup>
);
})}
</Select>
</div>
<DialogContent className="max-w-[800px]">
<DialogHeader>
<DialogTitle>Add prebuilt pattern</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-6">
<div>
<Label className="font-bold">Pattern type</Label>
<Select
value={selectedPatternName}
onValueChange={onPatternNameChange}
>
<SelectTrigger className="w-full mt-2">
<SelectValue placeholder="Choose pattern type" />
</SelectTrigger>
<SelectContent>
{categories.map((category) => {
const categoryPatterns = prebuiltPatterns.filter(
(p) => p.category === category,
);
if (categoryPatterns.length === 0) return null;
<div>
<Text strong>Action</Text>
<Text type="secondary" style={{ display: "block", marginTop: 4, marginBottom: 8 }}>
Choose what action the guardrail should take when this pattern is detected
</Text>
<Select
value={patternAction}
onChange={onActionChange}
style={{ width: "100%" }}
>
<Option value="BLOCK">Block</Option>
<Option value="MASK">Mask</Option>
</Select>
</div>
</Space>
return (
<SelectGroup key={category}>
<SelectLabel>{category}</SelectLabel>
{categoryPatterns.map((pattern) => (
<SelectItem key={pattern.name} value={pattern.name}>
{pattern.display_name}
</SelectItem>
))}
</SelectGroup>
);
})}
</SelectContent>
</Select>
</div>
<div style={{ display: "flex", justifyContent: "flex-end", gap: "8px", marginTop: "24px" }}>
<Button onClick={onCancel}>
Cancel
</Button>
<Button type="primary" onClick={onAdd}>
Add
</Button>
</div>
</Modal>
<div>
<Label className="font-bold">Action</Label>
<p className="text-sm text-muted-foreground mt-1 mb-2">
Choose what action the guardrail should take when this pattern is
detected
</p>
<Select
value={patternAction}
onValueChange={(v) => onActionChange(v as "BLOCK" | "MASK")}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="BLOCK">Block</SelectItem>
<SelectItem value="MASK">Mask</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={onCancel}>
Cancel
</Button>
<Button onClick={onAdd}>Add</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default PatternModal;

View file

@ -1,9 +1,22 @@
import { Trash2 } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import React from "react";
import { Typography, Select, Table, Tag, Button } from "antd";
import { DeleteOutlined } from "@ant-design/icons";
const { Text } = Typography;
const { Option } = Select;
interface Pattern {
id: string;
@ -25,83 +38,83 @@ const PatternTable: React.FC<PatternTableProps> = ({
onActionChange,
onRemove,
}) => {
const columns = [
{
title: "Type",
dataIndex: "type",
key: "type",
width: 100,
render: (type: string) => (
<Tag color={type === "prebuilt" ? "blue" : "green"}>
{type === "prebuilt" ? "Prebuilt" : "Custom"}
</Tag>
),
},
{
title: "Pattern name",
dataIndex: "name",
key: "name",
render: (_: string, record: Pattern) => record.display_name || record.name,
},
{
title: "Regex pattern",
dataIndex: "pattern",
key: "pattern",
render: (pattern: string) => (pattern ? <Text code style={{ fontSize: 12 }}>{pattern.substring(0, 40)}...</Text> : "-"),
},
{
title: "Action",
dataIndex: "action",
key: "action",
width: 150,
render: (action: string, record: Pattern) => (
<Select
value={action}
onChange={(value) => onActionChange(record.id, value as "BLOCK" | "MASK")}
style={{ width: 120 }}
size="small"
>
<Option value="BLOCK">Block</Option>
<Option value="MASK">Mask</Option>
</Select>
),
},
{
title: "",
key: "actions",
width: 100,
render: (_: any, record: Pattern) => (
<Button
type="text"
danger
size="small"
icon={<DeleteOutlined />}
onClick={() => onRemove(record.id)}
>
Delete
</Button>
),
},
];
if (patterns.length === 0) {
return (
<div style={{ textAlign: "center", padding: "40px 0", color: "#999" }}>
<div className="text-center py-10 text-muted-foreground">
No patterns added.
</div>
);
}
return (
<Table
dataSource={patterns}
columns={columns}
rowKey="id"
pagination={false}
size="small"
/>
<div className="border border-border rounded-md overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Type</TableHead>
<TableHead>Pattern name</TableHead>
<TableHead>Regex pattern</TableHead>
<TableHead className="w-[150px]">Action</TableHead>
<TableHead className="w-[100px]" />
</TableRow>
</TableHeader>
<TableBody>
{patterns.map((record) => (
<TableRow key={record.id}>
<TableCell>
<Badge
className={
record.type === "prebuilt"
? "bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300"
: "bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300"
}
>
{record.type === "prebuilt" ? "Prebuilt" : "Custom"}
</Badge>
</TableCell>
<TableCell>{record.display_name || record.name}</TableCell>
<TableCell>
{record.pattern ? (
<code className="text-xs bg-muted px-1 py-0.5 rounded">
{record.pattern.substring(0, 40)}...
</code>
) : (
"-"
)}
</TableCell>
<TableCell>
<Select
value={record.action}
onValueChange={(v) =>
onActionChange(record.id, v as "BLOCK" | "MASK")
}
>
<SelectTrigger className="w-[120px] h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="BLOCK">Block</SelectItem>
<SelectItem value="MASK">Mask</SelectItem>
</SelectContent>
</Select>
</TableCell>
<TableCell>
<Button
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => onRemove(record.id)}
>
<Trash2 className="h-3.5 w-3.5" />
Delete
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
};
export default PatternTable;