mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
UI fixes
This commit is contained in:
parent
cbd73ddded
commit
9fe16d4e29
7 changed files with 829 additions and 537 deletions
|
|
@ -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" ? (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue