This commit is contained in:
Ishaan Jaffer 2026-01-23 11:28:16 -08:00
parent cbd73ddded
commit 9fe16d4e29
7 changed files with 829 additions and 537 deletions

View file

@ -14,6 +14,7 @@ import LoadingScreen from "@/components/common_components/LoadingScreen";
import { CostTrackingSettings } from "@/components/CostTrackingSettings";
import GeneralSettings from "@/components/general_settings";
import GuardrailsPanel from "@/components/guardrails";
import PoliciesPanel from "@/components/policies";
import { Team } from "@/components/key_team_helpers/key_list";
import { MCPServers } from "@/components/mcp_tools";
import ModelHubTable from "@/components/AIHub/ModelHubTable";
@ -472,6 +473,8 @@ export default function CreateKeyPage() {
<BudgetPanel accessToken={accessToken} />
) : page == "guardrails" ? (
<GuardrailsPanel accessToken={accessToken} userRole={userRole} />
) : page == "policies" ? (
<PoliciesPanel accessToken={accessToken} userRole={userRole} />
) : page == "agents" ? (
<AgentsPanel accessToken={accessToken} userRole={userRole} />
) : page == "prompts" ? (

View file

@ -1,15 +1,11 @@
import React, { useState } from "react";
import {
Modal,
Form,
Input,
Select,
Button,
Space,
Radio,
Divider,
} from "antd";
import { Modal, Form, Select, Radio, Divider, Typography } from "antd";
import { Button } from "@tremor/react";
import { Policy, PolicyAttachmentCreateRequest } from "./types";
import { createPolicyAttachmentCall } from "../networking";
import NotificationsManager from "../molecules/notifications_manager";
const { Text } = Typography;
interface AddAttachmentFormProps {
visible: boolean;
@ -17,7 +13,6 @@ interface AddAttachmentFormProps {
onSuccess: () => void;
accessToken: string | null;
policies: Policy[];
onCreateAttachment: (data: PolicyAttachmentCreateRequest) => Promise<void>;
}
const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
@ -26,17 +21,31 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
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;
const resetForm = () => {
form.resetFields();
setScopeType("global");
};
setIsSubmitting(true);
const handleClose = () => {
resetForm();
onClose();
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
await form.validateFields();
const values = form.getFieldsValue(true);
if (!accessToken) {
throw new Error("No access token available");
}
const data: PolicyAttachmentCreateRequest = {
policy_name: values.policy_name,
};
@ -55,13 +64,17 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
}
}
await onCreateAttachment(data);
await createPolicyAttachmentCall(accessToken, data);
NotificationsManager.success("Attachment created successfully");
resetForm();
onSuccess();
onClose();
form.resetFields();
setScopeType("global");
} catch (error) {
console.error("Error creating attachment:", error);
console.error("Failed to create attachment:", error);
NotificationsManager.fromBackend(
"Failed to create attachment: " + (error instanceof Error ? error.message : String(error))
);
} finally {
setIsSubmitting(false);
}
@ -76,14 +89,13 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
<Modal
title="Create Policy Attachment"
open={visible}
onCancel={onClose}
onCancel={handleClose}
footer={null}
width={600}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
initialValues={{
scope_type: "global",
}}
@ -100,10 +112,13 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
filterOption={(input, option) =>
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
style={{ width: "100%" }}
/>
</Form.Item>
<Divider orientation="left">Scope</Divider>
<Divider orientation="left">
<Text strong>Scope</Text>
</Divider>
<Form.Item label="Scope Type">
<Radio.Group
@ -126,6 +141,7 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
mode="tags"
placeholder="Enter team aliases (e.g., healthcare-team)"
tokenSeparators={[","]}
style={{ width: "100%" }}
/>
</Form.Item>
@ -138,6 +154,7 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
mode="tags"
placeholder="Enter key aliases (e.g., dev-key-*)"
tokenSeparators={[","]}
style={{ width: "100%" }}
/>
</Form.Item>
@ -150,19 +167,20 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
mode="tags"
placeholder="Enter model names (e.g., gpt-4, bedrock/*)"
tokenSeparators={[","]}
style={{ width: "100%" }}
/>
</Form.Item>
</>
)}
<Form.Item>
<Space>
<Button onClick={onClose}>Cancel</Button>
<Button type="primary" htmlType="submit" loading={isSubmitting}>
Create Attachment
</Button>
</Space>
</Form.Item>
<div className="flex justify-end space-x-2 mt-4">
<Button variant="secondary" onClick={handleClose}>
Cancel
</Button>
<Button onClick={handleSubmit} loading={isSubmitting}>
Create Attachment
</Button>
</div>
</Form>
</Modal>
);

View file

@ -1,20 +1,13 @@
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 { Form, Select, Modal, Divider, Typography } from "antd";
import { Button, TextInput, Textarea } from "@tremor/react";
import { Policy, PolicyCreateRequest, PolicyUpdateRequest } from "./types";
import { Guardrail } from "../guardrails/types";
import { createPolicyCall, updatePolicyCall } from "../networking";
import NotificationsManager from "../molecules/notifications_manager";
const { TextArea } = Input;
const { Text } = Typography;
const { Option } = Select;
interface AddPolicyFormProps {
visible: boolean;
@ -24,8 +17,6 @@ interface AddPolicyFormProps {
editingPolicy?: Policy | null;
existingPolicies: Policy[];
availableGuardrails: Guardrail[];
onCreatePolicy: (data: PolicyCreateRequest) => Promise<void>;
onUpdatePolicy: (policyId: string, data: PolicyUpdateRequest) => Promise<void>;
}
const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
@ -36,8 +27,6 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
editingPolicy,
existingPolicies,
availableGuardrails,
onCreatePolicy,
onUpdatePolicy,
}) => {
const [form] = Form.useForm();
const [isSubmitting, setIsSubmitting] = useState(false);
@ -59,11 +48,25 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
}
}, [visible, editingPolicy, form]);
const handleSubmit = async (values: any) => {
if (!accessToken) return;
const resetForm = () => {
form.resetFields();
};
setIsSubmitting(true);
const handleClose = () => {
resetForm();
onClose();
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
await form.validateFields();
const values = form.getFieldsValue(true);
if (!accessToken) {
throw new Error("No access token available");
}
const data: PolicyCreateRequest | PolicyUpdateRequest = {
policy_name: values.policy_name,
description: values.description || undefined,
@ -76,16 +79,21 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
};
if (isEditing && editingPolicy) {
await onUpdatePolicy(editingPolicy.policy_id, data as PolicyUpdateRequest);
await updatePolicyCall(accessToken, editingPolicy.policy_id, data as PolicyUpdateRequest);
NotificationsManager.success("Policy updated successfully");
} else {
await onCreatePolicy(data as PolicyCreateRequest);
await createPolicyCall(accessToken, data as PolicyCreateRequest);
NotificationsManager.success("Policy created successfully");
}
resetForm();
onSuccess();
onClose();
form.resetFields();
} catch (error) {
console.error("Error saving policy:", error);
console.error("Failed to save policy:", error);
NotificationsManager.fromBackend(
"Failed to save policy: " + (error instanceof Error ? error.message : String(error))
);
} finally {
setIsSubmitting(false);
}
@ -107,14 +115,13 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
<Modal
title={isEditing ? "Edit Policy" : "Create New Policy"}
open={visible}
onCancel={onClose}
onCancel={handleClose}
footer={null}
width={700}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
initialValues={{
guardrails_add: [],
guardrails_remove: [],
@ -132,20 +139,22 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
},
]}
>
<Input
<TextInput
placeholder="e.g., global-baseline, healthcare-compliance"
disabled={isEditing}
/>
</Form.Item>
<Form.Item name="description" label="Description">
<TextArea
<Textarea
rows={2}
placeholder="Describe what this policy does..."
/>
</Form.Item>
<Divider orientation="left">Inheritance</Divider>
<Divider orientation="left">
<Text strong>Inheritance</Text>
</Divider>
<Form.Item
name="inherit"
@ -156,10 +165,13 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
allowClear
placeholder="Select a parent policy (optional)"
options={policyOptions}
style={{ width: "100%" }}
/>
</Form.Item>
<Divider orientation="left">Guardrails</Divider>
<Divider orientation="left">
<Text strong>Guardrails</Text>
</Divider>
<Form.Item
name="guardrails_add"
@ -171,6 +183,7 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
allowClear
placeholder="Select guardrails to add"
options={guardrailOptions}
style={{ width: "100%" }}
/>
</Form.Item>
@ -184,27 +197,30 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
allowClear
placeholder="Select guardrails to remove (from inherited)"
options={guardrailOptions}
style={{ width: "100%" }}
/>
</Form.Item>
<Divider orientation="left">Conditions (Optional)</Divider>
<Divider orientation="left">
<Text strong>Conditions (Optional)</Text>
</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" />
<TextInput 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>
<div className="flex justify-end space-x-2 mt-4">
<Button variant="secondary" onClick={handleClose}>
Cancel
</Button>
<Button onClick={handleSubmit} loading={isSubmitting}>
{isEditing ? "Update Policy" : "Create Policy"}
</Button>
</div>
</Form>
</Modal>
);

View file

@ -1,10 +1,17 @@
import React from "react";
import { Table, Tag, Button, Space, Tooltip, Typography } from "antd";
import { DeleteOutlined } from "@ant-design/icons";
import React, { useState } from "react";
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Icon, Badge } from "@tremor/react";
import { TrashIcon, SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline";
import { Tooltip, Tag } from "antd";
import {
ColumnDef,
flexRender,
getCoreRowModel,
getSortedRowModel,
SortingState,
useReactTable,
} from "@tanstack/react-table";
import { PolicyAttachment } from "./types";
const { Text } = Typography;
interface AttachmentTableProps {
attachments: PolicyAttachment[];
isLoading: boolean;
@ -18,135 +25,256 @@ const AttachmentTable: React.FC<AttachmentTableProps> = ({
onDeleteClick,
isAdmin,
}) => {
const columns = [
const [sorting, setSorting] = useState<SortingState>([{ id: "created_at", desc: true }]);
// Format date helper function
const formatDate = (dateString?: string) => {
if (!dateString) return "-";
const date = new Date(dateString);
return date.toLocaleString();
};
const columns: ColumnDef<PolicyAttachment>[] = [
{
title: "Policy",
dataIndex: "policy_name",
key: "policy_name",
render: (text: string) => <Tag color="blue">{text}</Tag>,
header: "Attachment ID",
accessorKey: "attachment_id",
cell: (info: any) => (
<Tooltip title={String(info.getValue() || "")}>
<span className="font-mono text-xs text-gray-600">
{info.getValue() ? `${String(info.getValue()).slice(0, 7)}...` : ""}
</span>
</Tooltip>
),
},
{
title: "Scope",
dataIndex: "scope",
key: "scope",
render: (scope: string | null) =>
scope === "*" ? (
<Tag color="gold">Global (*)</Tag>
) : scope ? (
<Tag>{scope}</Tag>
header: "Policy",
accessorKey: "policy_name",
cell: ({ row }) => {
const attachment = row.original;
return (
<Badge color="blue" size="xs">
{attachment.policy_name}
</Badge>
);
},
},
{
header: "Scope",
accessorKey: "scope",
cell: ({ row }) => {
const attachment = row.original;
if (attachment.scope === "*") {
return (
<Badge color="amber" size="xs">
Global (*)
</Badge>
);
}
return attachment.scope ? (
<span className="text-xs">{attachment.scope}</span>
) : (
<Text type="secondary">-</Text>
),
<span className="text-xs text-gray-400">-</span>
);
},
},
{
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">
header: "Teams",
accessorKey: "teams",
cell: ({ row }) => {
const attachment = row.original;
const teams = attachment.teams || [];
if (teams.length === 0) {
return <span className="text-xs text-gray-400">-</span>;
}
return (
<div className="flex flex-wrap gap-1">
{teams.slice(0, 2).map((t, i) => (
<Tag key={i} color="cyan" className="text-xs">
{t}
</Tag>
))
) : (
<Text type="secondary">-</Text>
)}
{teams && teams.length > 3 && (
<Tooltip title={teams.slice(3).join(", ")}>
<Tag>+{teams.length - 3} more</Tag>
</Tooltip>
)}
</Space>
),
))}
{teams.length > 2 && (
<Tooltip title={teams.slice(2).join(", ")}>
<Tag className="text-xs">+{teams.length - 2}</Tag>
</Tooltip>
)}
</div>
);
},
},
{
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">
header: "Keys",
accessorKey: "keys",
cell: ({ row }) => {
const attachment = row.original;
const keys = attachment.keys || [];
if (keys.length === 0) {
return <span className="text-xs text-gray-400">-</span>;
}
return (
<div className="flex flex-wrap gap-1">
{keys.slice(0, 2).map((k, i) => (
<Tag key={i} color="purple" className="text-xs">
{k}
</Tag>
))
) : (
<Text type="secondary">-</Text>
)}
{keys && keys.length > 3 && (
<Tooltip title={keys.slice(3).join(", ")}>
<Tag>+{keys.length - 3} more</Tag>
</Tooltip>
)}
</Space>
),
))}
{keys.length > 2 && (
<Tooltip title={keys.slice(2).join(", ")}>
<Tag className="text-xs">+{keys.length - 2}</Tag>
</Tooltip>
)}
</div>
);
},
},
{
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">
header: "Models",
accessorKey: "models",
cell: ({ row }) => {
const attachment = row.original;
const models = attachment.models || [];
if (models.length === 0) {
return <span className="text-xs text-gray-400">-</span>;
}
return (
<div className="flex flex-wrap gap-1">
{models.slice(0, 2).map((m, i) => (
<Tag key={i} color="green" className="text-xs">
{m}
</Tag>
))
) : (
<Text type="secondary">-</Text>
)}
{models && models.length > 3 && (
<Tooltip title={models.slice(3).join(", ")}>
<Tag>+{models.length - 3} more</Tag>
</Tooltip>
)}
</Space>
),
))}
{models.length > 2 && (
<Tooltip title={models.slice(2).join(", ")}>
<Tag className="text-xs">+{models.length - 2}</Tag>
</Tooltip>
)}
</div>
);
},
},
{
title: "Created At",
dataIndex: "created_at",
key: "created_at",
render: (date: string) =>
date ? new Date(date).toLocaleDateString() : "-",
header: "Created At",
accessorKey: "created_at",
cell: ({ row }) => {
const attachment = row.original;
return (
<Tooltip title={attachment.created_at}>
<span className="text-xs">{formatDate(attachment.created_at)}</span>
</Tooltip>
);
},
},
{
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>
),
id: "actions",
header: "Actions",
cell: ({ row }) => {
const attachment = row.original;
return (
<div className="flex space-x-2">
{isAdmin && (
<Tooltip title="Delete attachment">
<Icon
icon={TrashIcon}
size="sm"
onClick={() => onDeleteClick(attachment.attachment_id)}
className="cursor-pointer hover:text-red-500"
/>
</Tooltip>
)}
</div>
);
},
},
];
const table = useReactTable({
data: attachments,
columns,
state: {
sorting,
},
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
enableSorting: true,
});
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`,
}}
/>
<div className="rounded-lg custom-border relative">
<div className="overflow-x-auto">
<Table className="[&_td]:py-0.5 [&_th]:py-1">
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHeaderCell
key={header.id}
className={`py-1 h-8 ${
header.id === "actions" ? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]" : ""
}`}
onClick={header.column.getToggleSortingHandler()}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center">
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</div>
{header.id !== "actions" && (
<div className="w-4">
{header.column.getIsSorted() ? (
{
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />,
}[header.column.getIsSorted() as string]
) : (
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
)}
</div>
)}
</div>
</TableHeaderCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>Loading...</p>
</div>
</TableCell>
</TableRow>
) : attachments.length > 0 ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="h-8">
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${
cell.column.id === "actions"
? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]"
: ""
}`}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>No attachments found</p>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
};

View file

@ -1,12 +1,24 @@
import React, { useState, useEffect, useCallback } from "react";
import { Button, Tabs, Modal, message } from "antd";
import { PlusOutlined, ExclamationCircleOutlined } from "@ant-design/icons";
import { Button, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
import { Modal, message } from "antd";
import { 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 {
getPoliciesList,
createPolicyCall,
updatePolicyCall,
deletePolicyCall,
getPolicyInfo,
getPolicyAttachmentsList,
createPolicyAttachmentCall,
deletePolicyAttachmentCall,
getGuardrailsList,
} from "../networking";
import {
Policy,
PolicyAttachment,
@ -15,34 +27,16 @@ import {
PolicyAttachmentCreateRequest,
} from "./types";
import { Guardrail } from "../guardrails/types";
import DeleteResourceModal from "../common_components/DeleteResourceModal";
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[]>([]);
@ -53,7 +47,10 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
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 [activeTab, setActiveTab] = useState<number>(0);
const [isDeleting, setIsDeleting] = useState(false);
const [policyToDelete, setPolicyToDelete] = useState<Policy | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const isAdmin = userRole ? isAdminRole(userRole) : false;
@ -63,40 +60,40 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
setIsLoading(true);
try {
const response = await getPoliciesList(accessToken);
setPoliciesList(response.policies);
setPoliciesList(response.policies || []);
} catch (error) {
console.error("Error fetching policies:", error);
message.error("Failed to fetch policies");
} finally {
setIsLoading(false);
}
}, [accessToken, getPoliciesList]);
}, [accessToken]);
const fetchAttachments = useCallback(async () => {
if (!accessToken) return;
setIsAttachmentsLoading(true);
try {
const response = await getAttachmentsList(accessToken);
setAttachmentsList(response.attachments);
const response = await getPolicyAttachmentsList(accessToken);
setAttachmentsList(response.attachments || []);
} catch (error) {
console.error("Error fetching attachments:", error);
message.error("Failed to fetch attachments");
} finally {
setIsAttachmentsLoading(false);
}
}, [accessToken, getAttachmentsList]);
}, [accessToken]);
const fetchGuardrails = useCallback(async () => {
if (!accessToken) return;
try {
const response = await getGuardrailsList(accessToken);
setGuardrailsList(response.guardrails);
setGuardrailsList(response.guardrails || []);
} catch (error) {
console.error("Error fetching guardrails:", error);
}
}, [accessToken, getGuardrailsList]);
}, [accessToken]);
useEffect(() => {
fetchPolicies();
@ -104,44 +101,51 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
fetchGuardrails();
}, [fetchPolicies, fetchAttachments, fetchGuardrails]);
const handleCreatePolicy = async (data: PolicyCreateRequest) => {
if (!accessToken) return;
await createPolicy(accessToken, data);
message.success("Policy created successfully");
const handleAddPolicy = () => {
if (selectedPolicyId) {
setSelectedPolicyId(null);
}
setEditingPolicy(null);
setIsAddPolicyModalVisible(true);
};
const handleUpdatePolicy = async (policyId: string, data: PolicyUpdateRequest) => {
if (!accessToken) return;
await updatePolicy(accessToken, policyId, data);
message.success("Policy updated successfully");
const handleCloseModal = () => {
setIsAddPolicyModalVisible(false);
setEditingPolicy(null);
};
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 handleSuccess = () => {
fetchPolicies();
setEditingPolicy(null);
};
const handleCreateAttachment = async (data: PolicyAttachmentCreateRequest) => {
if (!accessToken) return;
await createAttachment(accessToken, data);
message.success("Attachment created successfully");
const handleDeleteClick = (policyId: string, policyName: string) => {
const policy = policiesList.find((p) => p.policy_id === policyId) || null;
setPolicyToDelete(policy);
setIsDeleteModalOpen(true);
};
const handleDeleteConfirm = async () => {
if (!policyToDelete || !accessToken) return;
setIsDeleting(true);
try {
await deletePolicyCall(accessToken, policyToDelete.policy_id);
message.success(`Policy "${policyToDelete.policy_name}" deleted successfully`);
await fetchPolicies();
} catch (error) {
console.error("Error deleting policy:", error);
message.error("Failed to delete policy");
} finally {
setIsDeleting(false);
setIsDeleteModalOpen(false);
setPolicyToDelete(null);
}
};
const handleDeleteCancel = () => {
setIsDeleteModalOpen(false);
setPolicyToDelete(null);
};
const handleDeleteAttachment = (attachmentId: string) => {
@ -155,7 +159,7 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
onOk: async () => {
if (!accessToken) return;
try {
await deleteAttachment(accessToken, attachmentId);
await deletePolicyAttachmentCall(accessToken, attachmentId);
message.success("Attachment deleted successfully");
fetchAttachments();
} catch (error) {
@ -166,134 +170,106 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
});
};
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}
/>
<TabGroup index={activeTab} onIndexChange={setActiveTab}>
<TabList className="mb-4">
<Tab>Policies</Tab>
<Tab>Attachments</Tab>
</TabList>
<AddPolicyForm
visible={isAddPolicyModalVisible}
onClose={() => {
setIsAddPolicyModalVisible(false);
setEditingPolicy(null);
}}
onSuccess={handlePolicySuccess}
accessToken={accessToken}
editingPolicy={editingPolicy}
existingPolicies={policiesList}
availableGuardrails={guardrailsList}
onCreatePolicy={handleCreatePolicy}
onUpdatePolicy={handleUpdatePolicy}
/>
<TabPanels>
<TabPanel>
<div className="flex justify-between items-center mb-4">
<Button onClick={handleAddPolicy} disabled={!accessToken}>
+ Add New Policy
</Button>
</div>
<AddAttachmentForm
visible={isAddAttachmentModalVisible}
onClose={() => setIsAddAttachmentModalVisible(false)}
onSuccess={handleAttachmentSuccess}
accessToken={accessToken}
policies={policiesList}
onCreateAttachment={handleCreateAttachment}
/>
{selectedPolicyId ? (
<PolicyInfoView
policyId={selectedPolicyId}
onClose={() => setSelectedPolicyId(null)}
onEdit={(policy) => {
setEditingPolicy(policy);
setIsAddPolicyModalVisible(true);
setSelectedPolicyId(null);
}}
accessToken={accessToken}
isAdmin={isAdmin}
/>
) : (
<PolicyTable
policies={policiesList}
isLoading={isLoading}
onDeleteClick={handleDeleteClick}
onEditClick={(policy) => {
setEditingPolicy(policy);
setIsAddPolicyModalVisible(true);
}}
onViewClick={(policyId) => setSelectedPolicyId(policyId)}
isAdmin={isAdmin}
/>
)}
<AddPolicyForm
visible={isAddPolicyModalVisible}
onClose={handleCloseModal}
onSuccess={handleSuccess}
accessToken={accessToken}
editingPolicy={editingPolicy}
existingPolicies={policiesList}
availableGuardrails={guardrailsList}
/>
<DeleteResourceModal
isOpen={isDeleteModalOpen}
title="Delete Policy"
message={`Are you sure you want to delete policy: ${policyToDelete?.policy_name}? This action cannot be undone.`}
resourceInformationTitle="Policy Information"
resourceInformation={[
{ label: "Name", value: policyToDelete?.policy_name },
{ label: "ID", value: policyToDelete?.policy_id, code: true },
{ label: "Description", value: policyToDelete?.description || "-" },
{ label: "Inherits From", value: policyToDelete?.inherit || "-" },
]}
onCancel={handleDeleteCancel}
onOk={handleDeleteConfirm}
confirmLoading={isDeleting}
/>
</TabPanel>
<TabPanel>
<div className="flex justify-between items-center mb-4">
<Button
onClick={() => setIsAddAttachmentModalVisible(true)}
disabled={!accessToken || policiesList.length === 0}
>
+ Add New Attachment
</Button>
</div>
<AttachmentTable
attachments={attachmentsList}
isLoading={isAttachmentsLoading}
onDeleteClick={handleDeleteAttachment}
isAdmin={isAdmin}
/>
<AddAttachmentForm
visible={isAddAttachmentModalVisible}
onClose={() => setIsAddAttachmentModalVisible(false)}
onSuccess={handleAttachmentSuccess}
accessToken={accessToken}
policies={policiesList}
/>
</TabPanel>
</TabPanels>
</TabGroup>
</div>
);
};

View file

@ -1,16 +1,9 @@
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 React, { useState, useEffect, useCallback } from "react";
import { Card, Badge, Button } from "@tremor/react";
import { ArrowLeftIcon, PencilIcon } from "@heroicons/react/outline";
import { Descriptions, Tag, Spin, Divider, Typography } from "antd";
import { Policy } from "./types";
import { getPolicyInfo } from "../networking";
const { Title, Text } = Typography;
@ -20,7 +13,6 @@ interface PolicyInfoViewProps {
onEdit: (policy: Policy) => void;
accessToken: string | null;
isAdmin: boolean;
fetchPolicy: (policyId: string) => Promise<Policy | null>;
}
const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
@ -29,32 +21,31 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
onEdit,
accessToken,
isAdmin,
fetchPolicy,
}) => {
const [policy, setPolicy] = useState<Policy | null>(null);
const [isLoading, setIsLoading] = useState(true);
const fetchPolicy = useCallback(async () => {
if (!accessToken || !policyId) return;
setIsLoading(true);
try {
const data = await getPolicyInfo(accessToken, policyId);
setPolicy(data);
} catch (error) {
console.error("Error fetching policy:", error);
} finally {
setIsLoading(false);
}
}, [policyId, accessToken]);
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]);
fetchPolicy();
}, [fetchPolicy]);
if (isLoading) {
return (
<div style={{ textAlign: "center", padding: "50px" }}>
<div className="flex justify-center items-center p-12">
<Spin size="large" />
</div>
);
@ -65,7 +56,7 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
<Card>
<Text type="danger">Policy not found</Text>
<br />
<Button onClick={onClose} style={{ marginTop: 16 }}>
<Button onClick={onClose} className="mt-4">
Go Back
</Button>
</Card>
@ -74,34 +65,37 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
return (
<Card>
<Space direction="vertical" style={{ width: "100%" }} size="large">
<Space style={{ justifyContent: "space-between", width: "100%" }}>
<Button icon={<ArrowLeftOutlined />} onClick={onClose}>
<div className="space-y-6">
<div className="flex justify-between items-center">
<Button
variant="secondary"
icon={ArrowLeftIcon}
onClick={onClose}
>
Back to Policies
</Button>
{isAdmin && (
<Button
type="primary"
icon={<EditOutlined />}
icon={PencilIcon}
onClick={() => onEdit(policy)}
>
Edit Policy
</Button>
)}
</Space>
</div>
<Title level={4}>{policy.policy_name}</Title>
<Descriptions bordered column={1}>
<Descriptions.Item label="Policy ID">
<Text code>{policy.policy_id}</Text>
<code className="text-xs bg-gray-100 px-2 py-1 rounded">{policy.policy_id}</code>
</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>
<Badge color="blue" size="sm">{policy.inherit}</Badge>
) : (
<Text type="secondary">None</Text>
)}
@ -118,11 +112,13 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
</Descriptions.Item>
</Descriptions>
<Divider orientation="left">Guardrails Configuration</Divider>
<Divider orientation="left">
<Text strong>Guardrails Configuration</Text>
</Divider>
<Descriptions bordered column={1}>
<Descriptions.Item label="Guardrails to Add">
<Space wrap>
<div className="flex flex-wrap gap-1">
{policy.guardrails_add && policy.guardrails_add.length > 0 ? (
policy.guardrails_add.map((g) => (
<Tag key={g} color="green">
@ -132,10 +128,10 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
) : (
<Text type="secondary">None</Text>
)}
</Space>
</div>
</Descriptions.Item>
<Descriptions.Item label="Guardrails to Remove">
<Space wrap>
<div className="flex flex-wrap gap-1">
{policy.guardrails_remove && policy.guardrails_remove.length > 0 ? (
policy.guardrails_remove.map((g) => (
<Tag key={g} color="red">
@ -145,22 +141,28 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
) : (
<Text type="secondary">None</Text>
)}
</Space>
</div>
</Descriptions.Item>
</Descriptions>
<Divider orientation="left">Conditions</Divider>
<Divider orientation="left">
<Text strong>Conditions</Text>
</Divider>
<Descriptions bordered column={1}>
<Descriptions.Item label="Model Condition">
{policy.condition?.model ? (
<Tag color="purple">{policy.condition.model}</Tag>
<Tag color="purple">
{typeof policy.condition.model === "string"
? policy.condition.model
: JSON.stringify(policy.condition.model)}
</Tag>
) : (
<Text type="secondary">No model condition (applies to all models)</Text>
)}
</Descriptions.Item>
</Descriptions>
</Space>
</div>
</Card>
);
};

View file

@ -1,17 +1,24 @@
import React from "react";
import { Table, Tag, Button, Space, Tooltip, Typography } from "antd";
import { DeleteOutlined, EditOutlined, EyeOutlined } from "@ant-design/icons";
import React, { useState } from "react";
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Icon, Button, Badge } from "@tremor/react";
import { TrashIcon, PencilIcon, SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline";
import { Tooltip, Tag } from "antd";
import {
ColumnDef,
flexRender,
getCoreRowModel,
getSortedRowModel,
SortingState,
useReactTable,
} from "@tanstack/react-table";
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;
isAdmin?: boolean;
}
const PolicyTable: React.FC<PolicyTableProps> = ({
@ -20,140 +27,282 @@ const PolicyTable: React.FC<PolicyTableProps> = ({
onDeleteClick,
onEditClick,
onViewClick,
isAdmin,
isAdmin = false,
}) => {
const columns = [
const [sorting, setSorting] = useState<SortingState>([{ id: "created_at", desc: true }]);
// Format date helper function
const formatDate = (dateString?: string) => {
if (!dateString) return "-";
const date = new Date(dateString);
return date.toLocaleString();
};
const columns: ColumnDef<Policy>[] = [
{
title: "Policy Name",
dataIndex: "policy_name",
key: "policy_name",
render: (text: string, record: Policy) => (
<Button type="link" onClick={() => onViewClick(record.policy_id)}>
{text}
</Button>
header: "Policy ID",
accessorKey: "policy_id",
cell: (info: any) => (
<Tooltip title={String(info.getValue() || "")}>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
onClick={() => info.getValue() && onViewClick(info.getValue())}
>
{info.getValue() ? `${String(info.getValue()).slice(0, 7)}...` : ""}
</Button>
</Tooltip>
),
},
{
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)}
/>
header: "Name",
accessorKey: "policy_name",
cell: ({ row }) => {
const policy = row.original;
return (
<Tooltip title={policy.policy_name}>
<span className="text-xs font-medium">{policy.policy_name || "-"}</span>
</Tooltip>
{isAdmin && (
<>
<Tooltip title="Edit">
<Button
type="text"
icon={<EditOutlined />}
onClick={() => onEditClick(record)}
/>
);
},
},
{
header: "Description",
accessorKey: "description",
cell: ({ row }) => {
const policy = row.original;
return (
<Tooltip title={policy.description}>
<span className="text-xs truncate max-w-[200px] block">
{policy.description || "-"}
</span>
</Tooltip>
);
},
},
{
header: "Inherits From",
accessorKey: "inherit",
cell: ({ row }) => {
const policy = row.original;
return policy.inherit ? (
<Badge color="blue" size="xs">
{policy.inherit}
</Badge>
) : (
<span className="text-xs text-gray-400">-</span>
);
},
},
{
header: "Guardrails (Add)",
accessorKey: "guardrails_add",
cell: ({ row }) => {
const policy = row.original;
const guardrails = policy.guardrails_add || [];
if (guardrails.length === 0) {
return <span className="text-xs text-gray-400">-</span>;
}
return (
<div className="flex flex-wrap gap-1">
{guardrails.slice(0, 2).map((g, i) => (
<Tag key={i} color="green" className="text-xs">
{g}
</Tag>
))}
{guardrails.length > 2 && (
<Tooltip title={guardrails.slice(2).join(", ")}>
<Tag className="text-xs">+{guardrails.length - 2}</Tag>
</Tooltip>
<Tooltip title="Delete">
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() =>
onDeleteClick(record.policy_id, record.policy_name)
}
/>
)}
</div>
);
},
},
{
header: "Guardrails (Remove)",
accessorKey: "guardrails_remove",
cell: ({ row }) => {
const policy = row.original;
const guardrails = policy.guardrails_remove || [];
if (guardrails.length === 0) {
return <span className="text-xs text-gray-400">-</span>;
}
return (
<div className="flex flex-wrap gap-1">
{guardrails.slice(0, 2).map((g, i) => (
<Tag key={i} color="red" className="text-xs">
{g}
</Tag>
))}
{guardrails.length > 2 && (
<Tooltip title={guardrails.slice(2).join(", ")}>
<Tag className="text-xs">+{guardrails.length - 2}</Tag>
</Tooltip>
</>
)}
</Space>
),
)}
</div>
);
},
},
{
header: "Model Condition",
accessorKey: "condition",
cell: ({ row }) => {
const policy = row.original;
const modelCondition = policy.condition?.model;
if (!modelCondition) {
return <span className="text-xs text-gray-400">-</span>;
}
return (
<Tooltip title={typeof modelCondition === "string" ? modelCondition : JSON.stringify(modelCondition)}>
<code className="text-xs bg-gray-100 px-1 py-0.5 rounded">
{typeof modelCondition === "string"
? modelCondition.length > 20
? modelCondition.slice(0, 20) + "..."
: modelCondition
: "Multiple"}
</code>
</Tooltip>
);
},
},
{
header: "Created At",
accessorKey: "created_at",
cell: ({ row }) => {
const policy = row.original;
return (
<Tooltip title={policy.created_at}>
<span className="text-xs">{formatDate(policy.created_at)}</span>
</Tooltip>
);
},
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
const policy = row.original;
return (
<div className="flex space-x-2">
{isAdmin && (
<>
<Tooltip title="Edit policy">
<Icon
icon={PencilIcon}
size="sm"
onClick={() => onEditClick(policy)}
className="cursor-pointer hover:text-blue-500"
/>
</Tooltip>
<Tooltip title="Delete policy">
<Icon
icon={TrashIcon}
size="sm"
onClick={() =>
policy.policy_id &&
onDeleteClick(policy.policy_id, policy.policy_name || "Unnamed Policy")
}
className="cursor-pointer hover:text-red-500"
/>
</Tooltip>
</>
)}
</div>
);
},
},
];
const table = useReactTable({
data: policies,
columns,
state: {
sorting,
},
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
enableSorting: true,
});
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`,
}}
/>
<div className="rounded-lg custom-border relative">
<div className="overflow-x-auto">
<Table className="[&_td]:py-0.5 [&_th]:py-1">
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHeaderCell
key={header.id}
className={`py-1 h-8 ${
header.id === "actions" ? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]" : ""
}`}
onClick={header.column.getToggleSortingHandler()}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center">
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</div>
{header.id !== "actions" && (
<div className="w-4">
{header.column.getIsSorted() ? (
{
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />,
}[header.column.getIsSorted() as string]
) : (
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
)}
</div>
)}
</div>
</TableHeaderCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>Loading...</p>
</div>
</TableCell>
</TableRow>
) : policies.length > 0 ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="h-8">
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${
cell.column.id === "actions"
? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]"
: ""
}`}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>No policies found</p>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
};