This commit is contained in:
Ishaan Jaffer 2026-01-23 09:24:36 -08:00
parent ca8c2c3938
commit bfa5d59dd2
11 changed files with 1504 additions and 0 deletions

View file

@ -19,6 +19,7 @@ import {
ExperimentOutlined,
ToolOutlined,
TagsOutlined,
AuditOutlined,
} from "@ant-design/icons";
// import {
// all_admin_roles,
@ -102,6 +103,8 @@ const routeFor = (slug: string): string => {
return "logs";
case "guardrails":
return "guardrails";
case "policies":
return "policies";
// tools
case "mcp-servers":
@ -202,6 +205,13 @@ const menuItems: MenuItemCfg[] = [
icon: <SafetyOutlined style={{ fontSize: 18 }} />,
roles: all_admin_roles,
},
{
key: "28",
page: "policies",
label: "Policies",
icon: <AuditOutlined style={{ fontSize: 18 }} />,
roles: all_admin_roles,
},
{
key: "26",
page: "tools",

View file

@ -0,0 +1,37 @@
"use client";
import PoliciesPanel from "@/components/policies";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import {
getPoliciesList,
createPolicyCall,
updatePolicyCall,
deletePolicyCall,
getPolicyInfo,
getPolicyAttachmentsList,
createPolicyAttachmentCall,
deletePolicyAttachmentCall,
getGuardrailsList,
} from "@/components/networking";
const PoliciesPage = () => {
const { accessToken, userRole } = useAuthorized();
return (
<PoliciesPanel
accessToken={accessToken}
userRole={userRole}
getPoliciesList={getPoliciesList}
createPolicy={createPolicyCall}
updatePolicy={updatePolicyCall}
deletePolicy={deletePolicyCall}
getPolicy={getPolicyInfo}
getAttachmentsList={getPolicyAttachmentsList}
createAttachment={createPolicyAttachmentCall}
deleteAttachment={deletePolicyAttachmentCall}
getGuardrailsList={getGuardrailsList}
/>
);
};
export default PoliciesPage;

View file

@ -3,6 +3,7 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import {
ApiOutlined,
AppstoreOutlined,
AuditOutlined,
BankOutlined,
BarChartOutlined,
BgColorsOutlined,
@ -123,6 +124,13 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
icon: <SafetyOutlined />,
roles: all_admin_roles,
},
{
key: "policies",
page: "policies",
label: "Policies",
icon: <AuditOutlined />,
roles: all_admin_roles,
},
{
key: "tools",
page: "tools",

View file

@ -5347,6 +5347,223 @@ export const getGuardrailsList = async (accessToken: string) => {
}
};
// ─────────────────────────────────────────────────────────────────────────────
// Policy CRUD API Calls
// ─────────────────────────────────────────────────────────────────────────────
export const getPoliciesList = async (accessToken: string) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/list` : `/policies/list`;
const response = await fetch(url, {
method: "GET",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorData = await response.json();
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to get policies list:", error);
throw error;
}
};
export const createPolicyCall = async (accessToken: string, policyData: any) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies` : `/policies`;
const response = await fetch(url, {
method: "POST",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(policyData),
});
if (!response.ok) {
const errorData = await response.json();
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to create policy:", error);
throw error;
}
};
export const updatePolicyCall = async (accessToken: string, policyId: string, policyData: any) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/${policyId}` : `/policies/${policyId}`;
const response = await fetch(url, {
method: "PUT",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(policyData),
});
if (!response.ok) {
const errorData = await response.json();
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to update policy:", error);
throw error;
}
};
export const deletePolicyCall = async (accessToken: string, policyId: string) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/${policyId}` : `/policies/${policyId}`;
const response = await fetch(url, {
method: "DELETE",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorData = await response.json();
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to delete policy:", error);
throw error;
}
};
export const getPolicyInfo = async (accessToken: string, policyId: string) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/${policyId}` : `/policies/${policyId}`;
const response = await fetch(url, {
method: "GET",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorData = await response.json();
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to get policy info:", error);
throw error;
}
};
// Policy Attachments API Calls
export const getPolicyAttachmentsList = async (accessToken: string) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/attachments/list` : `/policies/attachments/list`;
const response = await fetch(url, {
method: "GET",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorData = await response.json();
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to get policy attachments list:", error);
throw error;
}
};
export const createPolicyAttachmentCall = async (accessToken: string, attachmentData: any) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/attachments` : `/policies/attachments`;
const response = await fetch(url, {
method: "POST",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(attachmentData),
});
if (!response.ok) {
const errorData = await response.json();
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to create policy attachment:", error);
throw error;
}
};
export const deletePolicyAttachmentCall = async (accessToken: string, attachmentId: string) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/attachments/${attachmentId}` : `/policies/attachments/${attachmentId}`;
const response = await fetch(url, {
method: "DELETE",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorData = await response.json();
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to delete policy attachment:", error);
throw error;
}
};
export const getPromptsList = async (accessToken: string): Promise<ListPromptsResponse> => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/prompts/list` : `/prompts/list`;

View file

@ -0,0 +1,171 @@
import React, { useState } from "react";
import {
Modal,
Form,
Input,
Select,
Button,
Space,
Radio,
Divider,
} from "antd";
import { Policy, PolicyAttachmentCreateRequest } from "./types";
interface AddAttachmentFormProps {
visible: boolean;
onClose: () => void;
onSuccess: () => void;
accessToken: string | null;
policies: Policy[];
onCreateAttachment: (data: PolicyAttachmentCreateRequest) => Promise<void>;
}
const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
visible,
onClose,
onSuccess,
accessToken,
policies,
onCreateAttachment,
}) => {
const [form] = Form.useForm();
const [isSubmitting, setIsSubmitting] = useState(false);
const [scopeType, setScopeType] = useState<"global" | "specific">("global");
const handleSubmit = async (values: any) => {
if (!accessToken) return;
setIsSubmitting(true);
try {
const data: PolicyAttachmentCreateRequest = {
policy_name: values.policy_name,
};
if (scopeType === "global") {
data.scope = "*";
} else {
if (values.teams && values.teams.length > 0) {
data.teams = values.teams;
}
if (values.keys && values.keys.length > 0) {
data.keys = values.keys;
}
if (values.models && values.models.length > 0) {
data.models = values.models;
}
}
await onCreateAttachment(data);
onSuccess();
onClose();
form.resetFields();
setScopeType("global");
} catch (error) {
console.error("Error creating attachment:", error);
} finally {
setIsSubmitting(false);
}
};
const policyOptions = policies.map((p) => ({
label: p.policy_name,
value: p.policy_name,
}));
return (
<Modal
title="Create Policy Attachment"
open={visible}
onCancel={onClose}
footer={null}
width={600}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
initialValues={{
scope_type: "global",
}}
>
<Form.Item
name="policy_name"
label="Policy"
rules={[{ required: true, message: "Please select a policy" }]}
>
<Select
placeholder="Select a policy to attach"
options={policyOptions}
showSearch
filterOption={(input, option) =>
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
/>
</Form.Item>
<Divider orientation="left">Scope</Divider>
<Form.Item label="Scope Type">
<Radio.Group
value={scopeType}
onChange={(e) => setScopeType(e.target.value)}
>
<Radio value="global">Global (applies to all requests)</Radio>
<Radio value="specific">Specific (teams, keys, or models)</Radio>
</Radio.Group>
</Form.Item>
{scopeType === "specific" && (
<>
<Form.Item
name="teams"
label="Teams"
tooltip="Team aliases this attachment applies to. Supports wildcards (e.g., healthcare-*)"
>
<Select
mode="tags"
placeholder="Enter team aliases (e.g., healthcare-team)"
tokenSeparators={[","]}
/>
</Form.Item>
<Form.Item
name="keys"
label="Keys"
tooltip="Key aliases this attachment applies to. Supports wildcards (e.g., dev-*)"
>
<Select
mode="tags"
placeholder="Enter key aliases (e.g., dev-key-*)"
tokenSeparators={[","]}
/>
</Form.Item>
<Form.Item
name="models"
label="Models"
tooltip="Model names this attachment applies to. Supports wildcards (e.g., gpt-4*)"
>
<Select
mode="tags"
placeholder="Enter model names (e.g., gpt-4, bedrock/*)"
tokenSeparators={[","]}
/>
</Form.Item>
</>
)}
<Form.Item>
<Space>
<Button onClick={onClose}>Cancel</Button>
<Button type="primary" htmlType="submit" loading={isSubmitting}>
Create Attachment
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
);
};
export default AddAttachmentForm;

View file

@ -0,0 +1,213 @@
import React, { useState, useEffect } from "react";
import {
Modal,
Form,
Input,
Select,
Button,
Space,
Typography,
Divider,
} from "antd";
import { PlusOutlined, MinusCircleOutlined } from "@ant-design/icons";
import { Policy, PolicyCreateRequest, PolicyUpdateRequest } from "./types";
import { Guardrail } from "../guardrails/types";
const { TextArea } = Input;
const { Text } = Typography;
interface AddPolicyFormProps {
visible: boolean;
onClose: () => void;
onSuccess: () => void;
accessToken: string | null;
editingPolicy?: Policy | null;
existingPolicies: Policy[];
availableGuardrails: Guardrail[];
onCreatePolicy: (data: PolicyCreateRequest) => Promise<void>;
onUpdatePolicy: (policyId: string, data: PolicyUpdateRequest) => Promise<void>;
}
const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
visible,
onClose,
onSuccess,
accessToken,
editingPolicy,
existingPolicies,
availableGuardrails,
onCreatePolicy,
onUpdatePolicy,
}) => {
const [form] = Form.useForm();
const [isSubmitting, setIsSubmitting] = useState(false);
const isEditing = !!editingPolicy;
useEffect(() => {
if (visible && editingPolicy) {
form.setFieldsValue({
policy_name: editingPolicy.policy_name,
description: editingPolicy.description,
inherit: editingPolicy.inherit,
guardrails_add: editingPolicy.guardrails_add || [],
guardrails_remove: editingPolicy.guardrails_remove || [],
model_condition: editingPolicy.condition?.model,
});
} else if (visible) {
form.resetFields();
}
}, [visible, editingPolicy, form]);
const handleSubmit = async (values: any) => {
if (!accessToken) return;
setIsSubmitting(true);
try {
const data: PolicyCreateRequest | PolicyUpdateRequest = {
policy_name: values.policy_name,
description: values.description || undefined,
inherit: values.inherit || undefined,
guardrails_add: values.guardrails_add || [],
guardrails_remove: values.guardrails_remove || [],
condition: values.model_condition
? { model: values.model_condition }
: undefined,
};
if (isEditing && editingPolicy) {
await onUpdatePolicy(editingPolicy.policy_id, data as PolicyUpdateRequest);
} else {
await onCreatePolicy(data as PolicyCreateRequest);
}
onSuccess();
onClose();
form.resetFields();
} catch (error) {
console.error("Error saving policy:", error);
} finally {
setIsSubmitting(false);
}
};
const guardrailOptions = availableGuardrails.map((g) => ({
label: g.guardrail_name || g.guardrail_id,
value: g.guardrail_name || g.guardrail_id,
}));
const policyOptions = existingPolicies
.filter((p) => !editingPolicy || p.policy_id !== editingPolicy.policy_id)
.map((p) => ({
label: p.policy_name,
value: p.policy_name,
}));
return (
<Modal
title={isEditing ? "Edit Policy" : "Create New Policy"}
open={visible}
onCancel={onClose}
footer={null}
width={700}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
initialValues={{
guardrails_add: [],
guardrails_remove: [],
}}
>
<Form.Item
name="policy_name"
label="Policy Name"
rules={[
{ required: true, message: "Please enter a policy name" },
{
pattern: /^[a-zA-Z0-9_-]+$/,
message:
"Policy name can only contain letters, numbers, hyphens, and underscores",
},
]}
>
<Input
placeholder="e.g., global-baseline, healthcare-compliance"
disabled={isEditing}
/>
</Form.Item>
<Form.Item name="description" label="Description">
<TextArea
rows={2}
placeholder="Describe what this policy does..."
/>
</Form.Item>
<Divider orientation="left">Inheritance</Divider>
<Form.Item
name="inherit"
label="Inherit From"
tooltip="Inherit guardrails from another policy. The child policy will include all guardrails from the parent."
>
<Select
allowClear
placeholder="Select a parent policy (optional)"
options={policyOptions}
/>
</Form.Item>
<Divider orientation="left">Guardrails</Divider>
<Form.Item
name="guardrails_add"
label="Guardrails to Add"
tooltip="These guardrails will be added to requests matching this policy"
>
<Select
mode="multiple"
allowClear
placeholder="Select guardrails to add"
options={guardrailOptions}
/>
</Form.Item>
<Form.Item
name="guardrails_remove"
label="Guardrails to Remove"
tooltip="These guardrails will be removed from inherited guardrails"
>
<Select
mode="multiple"
allowClear
placeholder="Select guardrails to remove (from inherited)"
options={guardrailOptions}
/>
</Form.Item>
<Divider orientation="left">Conditions (Optional)</Divider>
<Form.Item
name="model_condition"
label="Model Condition"
tooltip="Only apply this policy when the model matches this pattern (supports regex)"
>
<Input placeholder="e.g., gpt-4.* or bedrock/claude-3" />
</Form.Item>
<Form.Item>
<Space>
<Button onClick={onClose}>Cancel</Button>
<Button type="primary" htmlType="submit" loading={isSubmitting}>
{isEditing ? "Update Policy" : "Create Policy"}
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
);
};
export default AddPolicyForm;

View file

@ -0,0 +1,153 @@
import React from "react";
import { Table, Tag, Button, Space, Tooltip, Typography } from "antd";
import { DeleteOutlined } from "@ant-design/icons";
import { PolicyAttachment } from "./types";
const { Text } = Typography;
interface AttachmentTableProps {
attachments: PolicyAttachment[];
isLoading: boolean;
onDeleteClick: (attachmentId: string) => void;
isAdmin: boolean;
}
const AttachmentTable: React.FC<AttachmentTableProps> = ({
attachments,
isLoading,
onDeleteClick,
isAdmin,
}) => {
const columns = [
{
title: "Policy",
dataIndex: "policy_name",
key: "policy_name",
render: (text: string) => <Tag color="blue">{text}</Tag>,
},
{
title: "Scope",
dataIndex: "scope",
key: "scope",
render: (scope: string | null) =>
scope === "*" ? (
<Tag color="gold">Global (*)</Tag>
) : scope ? (
<Tag>{scope}</Tag>
) : (
<Text type="secondary">-</Text>
),
},
{
title: "Teams",
dataIndex: "teams",
key: "teams",
render: (teams: string[]) => (
<Space wrap>
{teams && teams.length > 0 ? (
teams.slice(0, 3).map((t) => (
<Tag key={t} color="cyan">
{t}
</Tag>
))
) : (
<Text type="secondary">-</Text>
)}
{teams && teams.length > 3 && (
<Tooltip title={teams.slice(3).join(", ")}>
<Tag>+{teams.length - 3} more</Tag>
</Tooltip>
)}
</Space>
),
},
{
title: "Keys",
dataIndex: "keys",
key: "keys",
render: (keys: string[]) => (
<Space wrap>
{keys && keys.length > 0 ? (
keys.slice(0, 3).map((k) => (
<Tag key={k} color="purple">
{k}
</Tag>
))
) : (
<Text type="secondary">-</Text>
)}
{keys && keys.length > 3 && (
<Tooltip title={keys.slice(3).join(", ")}>
<Tag>+{keys.length - 3} more</Tag>
</Tooltip>
)}
</Space>
),
},
{
title: "Models",
dataIndex: "models",
key: "models",
render: (models: string[]) => (
<Space wrap>
{models && models.length > 0 ? (
models.slice(0, 3).map((m) => (
<Tag key={m} color="green">
{m}
</Tag>
))
) : (
<Text type="secondary">-</Text>
)}
{models && models.length > 3 && (
<Tooltip title={models.slice(3).join(", ")}>
<Tag>+{models.length - 3} more</Tag>
</Tooltip>
)}
</Space>
),
},
{
title: "Created At",
dataIndex: "created_at",
key: "created_at",
render: (date: string) =>
date ? new Date(date).toLocaleDateString() : "-",
},
{
title: "Actions",
key: "actions",
render: (_: any, record: PolicyAttachment) => (
<Space>
{isAdmin && (
<Tooltip title="Delete">
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => onDeleteClick(record.attachment_id)}
/>
</Tooltip>
)}
</Space>
),
},
];
return (
<Table
columns={columns}
dataSource={attachments}
rowKey="attachment_id"
loading={isLoading}
pagination={{
pageSize: 10,
showSizeChanger: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} of ${total} attachments`,
}}
/>
);
};
export default AttachmentTable;

View file

@ -0,0 +1,301 @@
import React, { useState, useEffect, useCallback } from "react";
import { Button, Tabs, Modal, message } from "antd";
import { PlusOutlined, ExclamationCircleOutlined } from "@ant-design/icons";
import { isAdminRole } from "@/utils/roles";
import PolicyTable from "./policy_table";
import PolicyInfoView from "./policy_info";
import AddPolicyForm from "./add_policy_form";
import AttachmentTable from "./attachment_table";
import AddAttachmentForm from "./add_attachment_form";
import {
Policy,
PolicyAttachment,
PolicyCreateRequest,
PolicyUpdateRequest,
PolicyAttachmentCreateRequest,
} from "./types";
import { Guardrail } from "../guardrails/types";
interface PoliciesPanelProps {
accessToken: string | null;
userRole?: string;
// API functions passed from parent
getPoliciesList: (accessToken: string) => Promise<{ policies: Policy[]; total_count: number }>;
createPolicy: (accessToken: string, data: PolicyCreateRequest) => Promise<Policy>;
updatePolicy: (accessToken: string, policyId: string, data: PolicyUpdateRequest) => Promise<Policy>;
deletePolicy: (accessToken: string, policyId: string) => Promise<void>;
getPolicy: (accessToken: string, policyId: string) => Promise<Policy>;
getAttachmentsList: (accessToken: string) => Promise<{ attachments: PolicyAttachment[]; total_count: number }>;
createAttachment: (accessToken: string, data: PolicyAttachmentCreateRequest) => Promise<PolicyAttachment>;
deleteAttachment: (accessToken: string, attachmentId: string) => Promise<void>;
getGuardrailsList: (accessToken: string) => Promise<{ guardrails: Guardrail[] }>;
}
const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
accessToken,
userRole,
getPoliciesList,
createPolicy,
updatePolicy,
deletePolicy,
getPolicy,
getAttachmentsList,
createAttachment,
deleteAttachment,
getGuardrailsList,
}) => {
const [policiesList, setPoliciesList] = useState<Policy[]>([]);
const [attachmentsList, setAttachmentsList] = useState<PolicyAttachment[]>([]);
const [guardrailsList, setGuardrailsList] = useState<Guardrail[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isAttachmentsLoading, setIsAttachmentsLoading] = useState(false);
const [isAddPolicyModalVisible, setIsAddPolicyModalVisible] = useState(false);
const [isAddAttachmentModalVisible, setIsAddAttachmentModalVisible] = useState(false);
const [editingPolicy, setEditingPolicy] = useState<Policy | null>(null);
const [selectedPolicyId, setSelectedPolicyId] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<string>("policies");
const isAdmin = userRole ? isAdminRole(userRole) : false;
const fetchPolicies = useCallback(async () => {
if (!accessToken) return;
setIsLoading(true);
try {
const response = await getPoliciesList(accessToken);
setPoliciesList(response.policies);
} catch (error) {
console.error("Error fetching policies:", error);
message.error("Failed to fetch policies");
} finally {
setIsLoading(false);
}
}, [accessToken, getPoliciesList]);
const fetchAttachments = useCallback(async () => {
if (!accessToken) return;
setIsAttachmentsLoading(true);
try {
const response = await getAttachmentsList(accessToken);
setAttachmentsList(response.attachments);
} catch (error) {
console.error("Error fetching attachments:", error);
message.error("Failed to fetch attachments");
} finally {
setIsAttachmentsLoading(false);
}
}, [accessToken, getAttachmentsList]);
const fetchGuardrails = useCallback(async () => {
if (!accessToken) return;
try {
const response = await getGuardrailsList(accessToken);
setGuardrailsList(response.guardrails);
} catch (error) {
console.error("Error fetching guardrails:", error);
}
}, [accessToken, getGuardrailsList]);
useEffect(() => {
fetchPolicies();
fetchAttachments();
fetchGuardrails();
}, [fetchPolicies, fetchAttachments, fetchGuardrails]);
const handleCreatePolicy = async (data: PolicyCreateRequest) => {
if (!accessToken) return;
await createPolicy(accessToken, data);
message.success("Policy created successfully");
};
const handleUpdatePolicy = async (policyId: string, data: PolicyUpdateRequest) => {
if (!accessToken) return;
await updatePolicy(accessToken, policyId, data);
message.success("Policy updated successfully");
};
const handleDeletePolicy = (policyId: string, policyName: string) => {
Modal.confirm({
title: "Delete Policy",
icon: <ExclamationCircleOutlined />,
content: `Are you sure you want to delete the policy "${policyName}"? This action cannot be undone.`,
okText: "Delete",
okType: "danger",
cancelText: "Cancel",
onOk: async () => {
if (!accessToken) return;
try {
await deletePolicy(accessToken, policyId);
message.success("Policy deleted successfully");
fetchPolicies();
} catch (error) {
console.error("Error deleting policy:", error);
message.error("Failed to delete policy");
}
},
});
};
const handleCreateAttachment = async (data: PolicyAttachmentCreateRequest) => {
if (!accessToken) return;
await createAttachment(accessToken, data);
message.success("Attachment created successfully");
};
const handleDeleteAttachment = (attachmentId: string) => {
Modal.confirm({
title: "Delete Attachment",
icon: <ExclamationCircleOutlined />,
content: "Are you sure you want to delete this attachment? This action cannot be undone.",
okText: "Delete",
okType: "danger",
cancelText: "Cancel",
onOk: async () => {
if (!accessToken) return;
try {
await deleteAttachment(accessToken, attachmentId);
message.success("Attachment deleted successfully");
fetchAttachments();
} catch (error) {
console.error("Error deleting attachment:", error);
message.error("Failed to delete attachment");
}
},
});
};
const fetchPolicyById = useCallback(
async (policyId: string): Promise<Policy | null> => {
if (!accessToken) return null;
try {
return await getPolicy(accessToken, policyId);
} catch (error) {
console.error("Error fetching policy:", error);
return null;
}
},
[accessToken, getPolicy]
);
const handlePolicySuccess = () => {
fetchPolicies();
setEditingPolicy(null);
};
const handleAttachmentSuccess = () => {
fetchAttachments();
};
const tabItems = [
{
key: "policies",
label: "Policies",
children: selectedPolicyId ? (
<PolicyInfoView
policyId={selectedPolicyId}
onClose={() => setSelectedPolicyId(null)}
onEdit={(policy) => {
setEditingPolicy(policy);
setIsAddPolicyModalVisible(true);
setSelectedPolicyId(null);
}}
accessToken={accessToken}
isAdmin={isAdmin}
fetchPolicy={fetchPolicyById}
/>
) : (
<>
<div style={{ marginBottom: 16 }}>
{isAdmin && (
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
setEditingPolicy(null);
setIsAddPolicyModalVisible(true);
}}
>
Add New Policy
</Button>
)}
</div>
<PolicyTable
policies={policiesList}
isLoading={isLoading}
onDeleteClick={handleDeletePolicy}
onEditClick={(policy) => {
setEditingPolicy(policy);
setIsAddPolicyModalVisible(true);
}}
onViewClick={(policyId) => setSelectedPolicyId(policyId)}
isAdmin={isAdmin}
/>
</>
),
},
{
key: "attachments",
label: "Attachments",
children: (
<>
<div style={{ marginBottom: 16 }}>
{isAdmin && (
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setIsAddAttachmentModalVisible(true)}
disabled={policiesList.length === 0}
>
Add New Attachment
</Button>
)}
</div>
<AttachmentTable
attachments={attachmentsList}
isLoading={isAttachmentsLoading}
onDeleteClick={handleDeleteAttachment}
isAdmin={isAdmin}
/>
</>
),
},
];
return (
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={tabItems}
/>
<AddPolicyForm
visible={isAddPolicyModalVisible}
onClose={() => {
setIsAddPolicyModalVisible(false);
setEditingPolicy(null);
}}
onSuccess={handlePolicySuccess}
accessToken={accessToken}
editingPolicy={editingPolicy}
existingPolicies={policiesList}
availableGuardrails={guardrailsList}
onCreatePolicy={handleCreatePolicy}
onUpdatePolicy={handleUpdatePolicy}
/>
<AddAttachmentForm
visible={isAddAttachmentModalVisible}
onClose={() => setIsAddAttachmentModalVisible(false)}
onSuccess={handleAttachmentSuccess}
accessToken={accessToken}
policies={policiesList}
onCreateAttachment={handleCreateAttachment}
/>
</div>
);
};
export default PoliciesPanel;

View file

@ -0,0 +1,168 @@
import React, { useState, useEffect } from "react";
import {
Card,
Descriptions,
Tag,
Button,
Space,
Typography,
Spin,
Divider,
} from "antd";
import { ArrowLeftOutlined, EditOutlined } from "@ant-design/icons";
import { Policy } from "./types";
const { Title, Text } = Typography;
interface PolicyInfoViewProps {
policyId: string;
onClose: () => void;
onEdit: (policy: Policy) => void;
accessToken: string | null;
isAdmin: boolean;
fetchPolicy: (policyId: string) => Promise<Policy | null>;
}
const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
policyId,
onClose,
onEdit,
accessToken,
isAdmin,
fetchPolicy,
}) => {
const [policy, setPolicy] = useState<Policy | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const loadPolicy = async () => {
if (!accessToken || !policyId) return;
setIsLoading(true);
try {
const data = await fetchPolicy(policyId);
setPolicy(data);
} catch (error) {
console.error("Error fetching policy:", error);
} finally {
setIsLoading(false);
}
};
loadPolicy();
}, [policyId, accessToken, fetchPolicy]);
if (isLoading) {
return (
<div style={{ textAlign: "center", padding: "50px" }}>
<Spin size="large" />
</div>
);
}
if (!policy) {
return (
<Card>
<Text type="danger">Policy not found</Text>
<br />
<Button onClick={onClose} style={{ marginTop: 16 }}>
Go Back
</Button>
</Card>
);
}
return (
<Card>
<Space direction="vertical" style={{ width: "100%" }} size="large">
<Space style={{ justifyContent: "space-between", width: "100%" }}>
<Button icon={<ArrowLeftOutlined />} onClick={onClose}>
Back to Policies
</Button>
{isAdmin && (
<Button
type="primary"
icon={<EditOutlined />}
onClick={() => onEdit(policy)}
>
Edit Policy
</Button>
)}
</Space>
<Title level={4}>{policy.policy_name}</Title>
<Descriptions bordered column={1}>
<Descriptions.Item label="Policy ID">
<Text code>{policy.policy_id}</Text>
</Descriptions.Item>
<Descriptions.Item label="Description">
{policy.description || <Text type="secondary">No description</Text>}
</Descriptions.Item>
<Descriptions.Item label="Inherits From">
{policy.inherit ? (
<Tag color="blue">{policy.inherit}</Tag>
) : (
<Text type="secondary">None</Text>
)}
</Descriptions.Item>
<Descriptions.Item label="Created At">
{policy.created_at
? new Date(policy.created_at).toLocaleString()
: "-"}
</Descriptions.Item>
<Descriptions.Item label="Updated At">
{policy.updated_at
? new Date(policy.updated_at).toLocaleString()
: "-"}
</Descriptions.Item>
</Descriptions>
<Divider orientation="left">Guardrails Configuration</Divider>
<Descriptions bordered column={1}>
<Descriptions.Item label="Guardrails to Add">
<Space wrap>
{policy.guardrails_add && policy.guardrails_add.length > 0 ? (
policy.guardrails_add.map((g) => (
<Tag key={g} color="green">
{g}
</Tag>
))
) : (
<Text type="secondary">None</Text>
)}
</Space>
</Descriptions.Item>
<Descriptions.Item label="Guardrails to Remove">
<Space wrap>
{policy.guardrails_remove && policy.guardrails_remove.length > 0 ? (
policy.guardrails_remove.map((g) => (
<Tag key={g} color="red">
{g}
</Tag>
))
) : (
<Text type="secondary">None</Text>
)}
</Space>
</Descriptions.Item>
</Descriptions>
<Divider orientation="left">Conditions</Divider>
<Descriptions bordered column={1}>
<Descriptions.Item label="Model Condition">
{policy.condition?.model ? (
<Tag color="purple">{policy.condition.model}</Tag>
) : (
<Text type="secondary">No model condition (applies to all models)</Text>
)}
</Descriptions.Item>
</Descriptions>
</Space>
</Card>
);
};
export default PolicyInfoView;

View file

@ -0,0 +1,160 @@
import React from "react";
import { Table, Tag, Button, Space, Tooltip, Typography } from "antd";
import { DeleteOutlined, EditOutlined, EyeOutlined } from "@ant-design/icons";
import { Policy } from "./types";
const { Text } = Typography;
interface PolicyTableProps {
policies: Policy[];
isLoading: boolean;
onDeleteClick: (policyId: string, policyName: string) => void;
onEditClick: (policy: Policy) => void;
onViewClick: (policyId: string) => void;
isAdmin: boolean;
}
const PolicyTable: React.FC<PolicyTableProps> = ({
policies,
isLoading,
onDeleteClick,
onEditClick,
onViewClick,
isAdmin,
}) => {
const columns = [
{
title: "Policy Name",
dataIndex: "policy_name",
key: "policy_name",
render: (text: string, record: Policy) => (
<Button type="link" onClick={() => onViewClick(record.policy_id)}>
{text}
</Button>
),
},
{
title: "Description",
dataIndex: "description",
key: "description",
render: (text: string | null) => text || <Text type="secondary">-</Text>,
},
{
title: "Inherits From",
dataIndex: "inherit",
key: "inherit",
render: (text: string | null) =>
text ? <Tag color="blue">{text}</Tag> : <Text type="secondary">-</Text>,
},
{
title: "Guardrails (Add)",
dataIndex: "guardrails_add",
key: "guardrails_add",
render: (guardrails: string[]) => (
<Space wrap>
{guardrails && guardrails.length > 0 ? (
guardrails.slice(0, 3).map((g) => (
<Tag key={g} color="green">
{g}
</Tag>
))
) : (
<Text type="secondary">-</Text>
)}
{guardrails && guardrails.length > 3 && (
<Tooltip title={guardrails.slice(3).join(", ")}>
<Tag>+{guardrails.length - 3} more</Tag>
</Tooltip>
)}
</Space>
),
},
{
title: "Guardrails (Remove)",
dataIndex: "guardrails_remove",
key: "guardrails_remove",
render: (guardrails: string[]) => (
<Space wrap>
{guardrails && guardrails.length > 0 ? (
guardrails.slice(0, 3).map((g) => (
<Tag key={g} color="red">
{g}
</Tag>
))
) : (
<Text type="secondary">-</Text>
)}
{guardrails && guardrails.length > 3 && (
<Tooltip title={guardrails.slice(3).join(", ")}>
<Tag>+{guardrails.length - 3} more</Tag>
</Tooltip>
)}
</Space>
),
},
{
title: "Model Condition",
dataIndex: "condition",
key: "condition",
render: (condition: { model?: string } | null) =>
condition?.model ? (
<Tag color="purple">{condition.model}</Tag>
) : (
<Text type="secondary">-</Text>
),
},
{
title: "Actions",
key: "actions",
render: (_: any, record: Policy) => (
<Space>
<Tooltip title="View">
<Button
type="text"
icon={<EyeOutlined />}
onClick={() => onViewClick(record.policy_id)}
/>
</Tooltip>
{isAdmin && (
<>
<Tooltip title="Edit">
<Button
type="text"
icon={<EditOutlined />}
onClick={() => onEditClick(record)}
/>
</Tooltip>
<Tooltip title="Delete">
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() =>
onDeleteClick(record.policy_id, record.policy_name)
}
/>
</Tooltip>
</>
)}
</Space>
),
},
];
return (
<Table
columns={columns}
dataSource={policies}
rowKey="policy_id"
loading={isLoading}
pagination={{
pageSize: 10,
showSizeChanger: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} of ${total} policies`,
}}
/>
);
};
export default PolicyTable;

View file

@ -0,0 +1,66 @@
export interface Policy {
policy_id: string;
policy_name: string;
inherit: string | null;
description: string | null;
guardrails_add: string[];
guardrails_remove: string[];
condition: PolicyCondition | null;
created_at?: string;
updated_at?: string;
created_by?: string;
updated_by?: string;
}
export interface PolicyCondition {
model?: string;
}
export interface PolicyAttachment {
attachment_id: string;
policy_name: string;
scope: string | null;
teams: string[];
keys: string[];
models: string[];
created_at?: string;
updated_at?: string;
created_by?: string;
updated_by?: string;
}
export interface PolicyCreateRequest {
policy_name: string;
inherit?: string;
description?: string;
guardrails_add?: string[];
guardrails_remove?: string[];
condition?: PolicyCondition;
}
export interface PolicyUpdateRequest {
policy_name?: string;
inherit?: string;
description?: string;
guardrails_add?: string[];
guardrails_remove?: string[];
condition?: PolicyCondition;
}
export interface PolicyAttachmentCreateRequest {
policy_name: string;
scope?: string;
teams?: string[];
keys?: string[];
models?: string[];
}
export interface PolicyListResponse {
policies: Policy[];
total_count: number;
}
export interface PolicyAttachmentListResponse {
attachments: PolicyAttachment[];
total_count: number;
}