feat(ui): policies/policy_test_panel to shadcn

- antd Alert/Tag/Empty/Typography + @tremor Button \u2192 shadcn Badge/
  Button + plain inline bg-red-50 error callout + lucide
  AlertCircle/Info. antd Form/Form.Item/Select preserved (mode=tags
  + filterOption).
- loadOptions wrapped in useCallback with full deps. Categorical
  emerald guardrail / blue matched-via badge; 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 19:11:11 +00:00
parent 8b0dfae348
commit 3705fb5ef5
No known key found for this signature in database
2 changed files with 114 additions and 53 deletions

View file

@ -85,6 +85,7 @@
"src/components/AIHub/forms/MakeAgentPublicForm.tsx",
"src/components/AIHub/forms/MakeMCPPublicForm.tsx",
"src/components/AIHub/forms/MakeModelPublicForm.tsx",
"src/components/policies/policy_test_panel.tsx",
"src/components/permissions/VectorStorePermissions.tsx",
"src/components/permissions/AgentPermissions.tsx",
"src/components/permissions/MCPServerPermissions.tsx",

View file

@ -1,11 +1,17 @@
import React, { useState, useEffect } from "react";
import { Form, Select, Alert, Tag, Empty, Typography } from "antd";
import { Button } from "@tremor/react";
import { resolvePoliciesCall, teamListCall, keyListCall, modelAvailableCall } from "../networking";
import React, { useCallback, useState, useEffect } from "react";
import { Form, Select } from "antd";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { AlertCircle, Info } from "lucide-react";
import {
resolvePoliciesCall,
teamListCall,
keyListCall,
modelAvailableCall,
} from "../networking";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
const { Text } = Typography;
interface PolicyTestPanelProps {
accessToken: string | null;
}
@ -31,46 +37,65 @@ const PolicyTestPanel: React.FC<PolicyTestPanelProps> = ({ accessToken }) => {
const [availableModels, setAvailableModels] = useState<string[]>([]);
const { userId, userRole } = useAuthorized();
useEffect(() => {
if (accessToken) {
loadOptions();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [accessToken]);
const loadOptions = async () => {
const loadOptions = useCallback(async () => {
if (!accessToken) return;
try {
const teamsResponse = await teamListCall(accessToken, null, userId);
const teamsArray = Array.isArray(teamsResponse) ? teamsResponse : (teamsResponse?.data || []);
const teamsArray = Array.isArray(teamsResponse)
? teamsResponse
: teamsResponse?.data || [];
setAvailableTeams(
teamsArray.map((t: any) => t.team_alias).filter(Boolean)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
teamsArray.map((t: any) => t.team_alias).filter(Boolean),
);
} catch (error) {
console.error("Failed to load teams:", error);
}
try {
const keysResponse = await keyListCall(accessToken, null, null, null, null, null, 1, 100);
const keysResponse = await keyListCall(
accessToken,
null,
null,
null,
null,
null,
1,
100,
);
const keysArray = keysResponse?.keys || keysResponse?.data || [];
setAvailableKeys(
keysArray.map((k: any) => k.key_alias).filter(Boolean)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
keysArray.map((k: any) => k.key_alias).filter(Boolean),
);
} catch (error) {
console.error("Failed to load keys:", error);
}
try {
const modelsResponse = await modelAvailableCall(accessToken, userId || "", userRole || "");
const modelsArray = modelsResponse?.data || (Array.isArray(modelsResponse) ? modelsResponse : []);
const modelsResponse = await modelAvailableCall(
accessToken,
userId || "",
userRole || "",
);
const modelsArray =
modelsResponse?.data ||
(Array.isArray(modelsResponse) ? modelsResponse : []);
setAvailableModels(
modelsArray.map((m: any) => m.id || m.model_name).filter(Boolean)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
modelsArray.map((m: any) => m.id || m.model_name).filter(Boolean),
);
} catch (error) {
console.error("Failed to load models:", error);
}
};
}, [accessToken, userId, userRole]);
useEffect(() => {
if (accessToken) {
loadOptions();
}
}, [accessToken, loadOptions]);
const handleTest = async () => {
if (!accessToken) return;
@ -79,6 +104,7 @@ const PolicyTestPanel: React.FC<PolicyTestPanelProps> = ({ accessToken }) => {
setHasSearched(true);
try {
const values = form.getFieldsValue(true);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const context: any = {};
if (values.team_alias) context.team_alias = values.team_alias;
if (values.key_alias) context.key_alias = values.key_alias;
@ -103,12 +129,14 @@ const PolicyTestPanel: React.FC<PolicyTestPanelProps> = ({ accessToken }) => {
return (
<div>
<div className="bg-white border rounded-lg p-6 mb-6">
<div className="bg-background border border-border rounded-lg p-6 mb-6">
<div className="mb-5">
<h3 className="text-base font-semibold mb-1">Policy Simulator</h3>
<Text type="secondary">
Simulate a request to see which policies and guardrails would apply. Select a team, key, model, or tags below and click &quot;Simulate&quot; to see the results.
</Text>
<p className="text-muted-foreground text-sm">
Simulate a request to see which policies and guardrails would
apply. Select a team, key, model, or tags below and click
&quot;Simulate&quot; to see the results.
</p>
</div>
<Form form={form} layout="vertical">
@ -158,8 +186,11 @@ const PolicyTestPanel: React.FC<PolicyTestPanelProps> = ({ accessToken }) => {
</Form.Item>
</div>
<div className="flex space-x-2">
<Button onClick={handleTest} loading={isLoading} disabled={!accessToken}>
Simulate
<Button
onClick={handleTest}
disabled={!accessToken || isLoading}
>
{isLoading ? "Simulating..." : "Simulate"}
</Button>
<Button variant="secondary" onClick={handleReset}>
Reset
@ -169,34 +200,44 @@ const PolicyTestPanel: React.FC<PolicyTestPanelProps> = ({ accessToken }) => {
</div>
{!hasSearched && (
<div className="bg-white border rounded-lg p-8 text-center">
<div className="text-gray-400 mb-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-10 w-10 mx-auto mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
</svg>
<div className="bg-background border border-border rounded-lg p-8 text-center">
<div className="text-muted-foreground/80 mb-2">
<Info className="h-10 w-10 mx-auto mb-3" />
</div>
<p className="text-sm font-medium text-gray-600 mb-1">No simulation run yet</p>
<p className="text-xs text-gray-400">
Fill in one or more fields above and click &quot;Simulate&quot; to see which policies and guardrails would apply to that request.
<p className="text-sm font-medium text-muted-foreground mb-1">
No simulation run yet
</p>
<p className="text-xs text-muted-foreground/80">
Fill in one or more fields above and click &quot;Simulate&quot; to
see which policies and guardrails would apply to that request.
</p>
</div>
)}
{hasSearched && result && (
<div className="bg-white border rounded-lg p-6">
<div className="bg-background border border-border rounded-lg p-6">
{result.matched_policies.length === 0 ? (
<Empty description="No policies matched this context" />
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground">
<p>No policies matched this context</p>
</div>
) : (
<>
<div className="mb-4">
<p className="text-sm font-semibold mb-2">Effective Guardrails</p>
<p className="text-sm font-semibold mb-2">
Effective Guardrails
</p>
<div className="flex flex-wrap gap-1">
{result.effective_guardrails.length > 0 ? (
result.effective_guardrails.map((g) => (
<Tag key={g} color="green">{g}</Tag>
<Badge
key={g}
className="bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300"
>
{g}
</Badge>
))
) : (
<span className="text-gray-400 text-sm">None</span>
<span className="text-muted-foreground text-sm">None</span>
)}
</div>
</div>
@ -205,7 +246,7 @@ const PolicyTestPanel: React.FC<PolicyTestPanelProps> = ({ accessToken }) => {
<p className="text-sm font-semibold mb-2">Matched Policies</p>
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<tr className="border-b border-border">
<th className="text-left py-2 pr-4">Policy</th>
<th className="text-left py-2 pr-4">Matched Via</th>
<th className="text-left py-2">Guardrails Added</th>
@ -213,20 +254,32 @@ const PolicyTestPanel: React.FC<PolicyTestPanelProps> = ({ accessToken }) => {
</thead>
<tbody>
{result.matched_policies.map((p) => (
<tr key={p.policy_name} className="border-b last:border-0">
<td className="py-2 pr-4 font-medium">{p.policy_name}</td>
<tr
key={p.policy_name}
className="border-b border-border last:border-0"
>
<td className="py-2 pr-4 font-medium">
{p.policy_name}
</td>
<td className="py-2 pr-4">
<Tag color="blue">{p.matched_via}</Tag>
<Badge className="bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300">
{p.matched_via}
</Badge>
</td>
<td className="py-2">
{p.guardrails_added.length > 0 ? (
<div className="flex flex-wrap gap-1">
{p.guardrails_added.map((g) => (
<Tag key={g} color="green">{g}</Tag>
<Badge
key={g}
className="bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300"
>
{g}
</Badge>
))}
</div>
) : (
<span className="text-gray-400">None</span>
<span className="text-muted-foreground">None</span>
)}
</td>
</tr>
@ -240,12 +293,19 @@ const PolicyTestPanel: React.FC<PolicyTestPanelProps> = ({ accessToken }) => {
)}
{hasSearched && !result && !isLoading && (
<Alert
message="Error"
description="Failed to resolve policies. Check the proxy logs."
type="error"
showIcon
/>
<div
className={cn(
"flex gap-2 items-start p-3 rounded-md bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900 text-red-800 dark:text-red-200",
)}
>
<AlertCircle className="h-4 w-4 mt-0.5 shrink-0" />
<div>
<div className="font-semibold">Error</div>
<div className="text-sm">
Failed to resolve policies. Check the proxy logs.
</div>
</div>
</div>
)}
</div>
);