feat(ui): policies/policy_templates to shadcn

- antd Card/Button/Spin/Checkbox + @heroicons ShieldCheck/
  ShieldExclamation/Beaker/CurrencyDollar/CheckCircle \u2192 shadcn
  Card/Button/Checkbox + lucide Shield/ShieldAlert/Beaker/
  DollarSign/CheckCircle/Loader2.
- Kept iconMap contract (JSON templates reference 'ShieldCheckIcon'
  etc) but now maps to lucide equivalents.
- Complexity badges keep categorical blue/purple palette; file added
  to .eslintrc 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 17:02:05 +00:00
parent bfc9d8f836
commit 12855c70da
No known key found for this signature in database
2 changed files with 77 additions and 61 deletions

View file

@ -69,6 +69,7 @@
"src/components/model_add/credentials.tsx",
"src/components/model_dashboard/health_check_columns.tsx",
"src/components/UsagePage/components/EntityUsage/TopKeyView.tsx",
"src/components/policies/policy_templates.tsx",
"src/components/permissions/VectorStorePermissions.tsx",
"src/components/permissions/AgentPermissions.tsx",
"src/components/permissions/MCPServerPermissions.tsx",

View file

@ -1,19 +1,23 @@
import React, { useState, useEffect, useMemo } from "react";
import { Card, Button, Spin, Checkbox } from "antd";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils";
import MessageManager from "@/components/molecules/message_manager";
import {
ShieldCheckIcon,
ShieldExclamationIcon,
BeakerIcon,
CurrencyDollarIcon,
CheckCircleIcon,
} from "@heroicons/react/outline";
Beaker,
CheckCircle,
DollarSign,
Loader2,
Shield,
ShieldAlert,
} from "lucide-react";
import { getPolicyTemplates } from "../networking";
interface PolicyTemplateCardProps {
title: string;
description: string;
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
icon: React.ComponentType<{ className?: string }>;
iconColor: string;
iconBg: string;
guardrails: string[];
@ -38,39 +42,41 @@ const PolicyTemplateCard: React.FC<PolicyTemplateCardProps> = ({
const getComplexityStyle = () => {
switch (complexity) {
case "Low":
return "bg-gray-50 text-gray-600 border-gray-200";
return "bg-muted text-muted-foreground border-border";
case "Medium":
return "bg-blue-50 text-blue-600 border-blue-100";
return "bg-blue-50 text-blue-600 border-blue-100 dark:bg-blue-950/30 dark:text-blue-300 dark:border-blue-900";
case "High":
return "bg-purple-50 text-purple-600 border-purple-100";
return "bg-purple-50 text-purple-600 border-purple-100 dark:bg-purple-950/30 dark:text-purple-300 dark:border-purple-900";
}
};
return (
<Card
className="h-full hover:shadow-md transition-shadow"
bodyStyle={{ display: "flex", flexDirection: "column", height: "100%" }}
>
<Card className="h-full hover:shadow-md transition-shadow flex flex-col p-6">
<div className="flex items-start justify-between mb-4">
<div className={`p-2 rounded-lg ${iconBg}`}>
<Icon className={`h-6 w-6 ${iconColor}`} />
<div className={cn("p-2 rounded-lg", iconBg)}>
<Icon className={cn("h-6 w-6", iconColor)} />
</div>
<span
className={`px-2.5 py-0.5 rounded-full text-xs font-medium border ${getComplexityStyle()}`}
className={cn(
"px-2.5 py-0.5 rounded-full text-xs font-medium border",
getComplexityStyle(),
)}
>
{complexity} Complexity
</span>
</div>
<h3 className="text-base font-semibold text-gray-900 mb-2">{title}</h3>
<p className="text-sm text-gray-500 mb-4 flex-grow">{description}</p>
<h3 className="text-base font-semibold text-foreground mb-2">{title}</h3>
<p className="text-sm text-muted-foreground mb-4 flex-grow">
{description}
</p>
{tags.length > 0 && (
<div className="flex flex-wrap gap-1.5 mb-4">
{tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700 border border-blue-100"
className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700 border border-blue-100 dark:bg-blue-950/30 dark:text-blue-300 dark:border-blue-900"
>
{tag}
</span>
@ -80,22 +86,22 @@ const PolicyTemplateCard: React.FC<PolicyTemplateCardProps> = ({
{inherits && (
<div className="mb-4 text-xs">
<span className="text-gray-500">Inherits from: </span>
<span className="font-medium text-gray-700 bg-gray-100 px-2 py-0.5 rounded">
<span className="text-muted-foreground">Inherits from: </span>
<span className="font-medium text-foreground bg-muted px-2 py-0.5 rounded">
{inherits}
</span>
</div>
)}
<div className="mb-6">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wider block mb-2">
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider block mb-2">
Included Guardrails
</span>
<div className="flex flex-wrap gap-2">
{guardrails.map((g) => (
<span
key={g}
className="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-gray-50 text-gray-700 border border-gray-200"
className="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-muted text-foreground border border-border"
>
{g}
</span>
@ -103,12 +109,7 @@ const PolicyTemplateCard: React.FC<PolicyTemplateCardProps> = ({
</div>
</div>
<Button
type="primary"
block
className="mt-auto"
onClick={onUseTemplate}
>
<Button className="mt-auto w-full" onClick={onUseTemplate}>
Use Template
</Button>
</Card>
@ -116,27 +117,34 @@ const PolicyTemplateCard: React.FC<PolicyTemplateCardProps> = ({
};
interface PolicyTemplatesProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onUseTemplate: (templateData: any) => void;
onOpenAiSuggestion: () => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onTemplatesLoaded?: (templates: any[]) => void;
accessToken: string | null;
}
// Map icon names from JSON to actual icon components
const iconMap: Record<string, React.ComponentType<React.SVGProps<SVGSVGElement>>> = {
ShieldCheckIcon: ShieldCheckIcon,
ShieldExclamationIcon: ShieldExclamationIcon,
BeakerIcon: BeakerIcon,
CurrencyDollarIcon: CurrencyDollarIcon,
CheckCircleIcon: CheckCircleIcon,
// Map icon names from JSON to lucide icon components.
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
ShieldCheckIcon: Shield,
ShieldExclamationIcon: ShieldAlert,
BeakerIcon: Beaker,
CurrencyDollarIcon: DollarSign,
CheckCircleIcon: CheckCircle,
};
const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({ onUseTemplate, onOpenAiSuggestion, onTemplatesLoaded, accessToken }) => {
const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({
onUseTemplate,
onOpenAiSuggestion,
onTemplatesLoaded,
accessToken,
}) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [templates, setTemplates] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
// Compute all unique tags with counts
const tagCounts = useMemo(() => {
const counts: Record<string, number> = {};
templates.forEach((t) => {
@ -145,16 +153,16 @@ const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({ onUseTemplate, onOpen
counts[tag] = (counts[tag] || 0) + 1;
});
});
// Sort alphabetically
return Object.entries(counts).sort(([a], [b]) => a.localeCompare(b));
}, [templates]);
// Filter templates: show templates that have ALL selected tags (AND logic)
const filteredTemplates = useMemo(() => {
if (selectedTags.size === 0) return templates;
return templates.filter((t) => {
const tags: string[] = t.tags || [];
return Array.from(selectedTags).every((selectedTag) => tags.includes(selectedTag));
return Array.from(selectedTags).every((selectedTag) =>
tags.includes(selectedTag),
);
});
}, [templates, selectedTags]);
@ -192,12 +200,16 @@ const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({ onUseTemplate, onOpen
};
fetchTemplates();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [accessToken]);
if (isLoading) {
return (
<div className="flex justify-center items-center py-20">
<Spin size="large" tip="Loading policy templates..." />
<div className="flex flex-col items-center justify-center py-20 gap-2">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<span className="text-sm text-muted-foreground">
Loading policy templates...
</span>
</div>
);
}
@ -206,16 +218,16 @@ const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({ onUseTemplate, onOpen
<div className="space-y-6">
<div className="flex justify-between items-end">
<div>
<h2 className="text-lg font-medium text-gray-900">
<h2 className="text-lg font-medium text-foreground">
Policy Templates
</h2>
<p className="text-sm text-gray-500 mt-1">
<p className="text-sm text-muted-foreground mt-1">
Start with a pre-configured policy template to quickly set up
guardrails for your organization.
</p>
</div>
<Button
type="default"
variant="outline"
onClick={onOpenAiSuggestion}
className="flex items-center gap-1.5"
>
@ -232,13 +244,14 @@ const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({ onUseTemplate, onOpen
<div className="w-52 flex-shrink-0">
<div className="sticky top-4">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-semibold text-gray-900">
<span className="text-sm font-semibold text-foreground">
Categories
</span>
{selectedTags.size > 0 && (
<button
type="button"
onClick={handleClearAll}
className="text-xs text-blue-600 hover:text-blue-800"
className="text-xs text-blue-600 hover:text-blue-800 dark:text-blue-300 dark:hover:text-blue-200"
>
Clear all
</button>
@ -248,20 +261,21 @@ const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({ onUseTemplate, onOpen
{tagCounts.map(([tag, count]) => (
<label
key={tag}
className={`flex items-center justify-between px-2 py-1.5 rounded-md cursor-pointer transition-colors ${
className={cn(
"flex items-center justify-between px-2 py-1.5 rounded-md cursor-pointer transition-colors",
selectedTags.has(tag)
? "bg-blue-50"
: "hover:bg-gray-50"
}`}
? "bg-blue-50 dark:bg-blue-950/30"
: "hover:bg-muted",
)}
>
<div className="flex items-center gap-2">
<Checkbox
checked={selectedTags.has(tag)}
onChange={() => handleTagToggle(tag)}
onCheckedChange={() => handleTagToggle(tag)}
/>
<span className="text-sm text-gray-700">{tag}</span>
<span className="text-sm text-foreground">{tag}</span>
</div>
<span className="text-xs text-gray-400 font-medium">
<span className="text-xs text-muted-foreground font-medium">
{count}
</span>
</label>
@ -274,7 +288,7 @@ const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({ onUseTemplate, onOpen
{/* Right content - template cards */}
<div className="flex-1">
{selectedTags.size > 0 && (
<div className="mb-4 text-sm text-gray-500">
<div className="mb-4 text-sm text-muted-foreground">
Showing {filteredTemplates.length} of {templates.length} templates
</div>
)}
@ -284,7 +298,7 @@ const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({ onUseTemplate, onOpen
key={template.id || index}
title={template.title}
description={template.description}
icon={iconMap[template.icon] || ShieldCheckIcon}
icon={iconMap[template.icon] || Shield}
iconColor={template.iconColor}
iconBg={template.iconBg}
guardrails={template.guardrails}
@ -297,11 +311,12 @@ const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({ onUseTemplate, onOpen
</div>
{filteredTemplates.length === 0 && (
<div className="text-center py-12 text-gray-500">
<div className="text-center py-12 text-muted-foreground">
<p>No templates match the selected filters.</p>
<button
type="button"
onClick={handleClearAll}
className="text-blue-600 hover:text-blue-800 mt-2 text-sm"
className="text-blue-600 hover:text-blue-800 dark:text-blue-300 dark:hover:text-blue-200 mt-2 text-sm"
>
Clear all filters
</button>