209 lines
6.8 KiB
TypeScript
209 lines
6.8 KiB
TypeScript
import { useState } from "react";
|
|
import { Plus, Pencil, Trash2, Download } from "lucide-react";
|
|
import {
|
|
useListPolicies,
|
|
useCreatePolicy,
|
|
useUpdatePolicy,
|
|
useDeletePolicy,
|
|
useSeedTemplate,
|
|
type PolicyPayload,
|
|
} from "@/api/policies";
|
|
import { PolicyForm } from "@/components/PolicyForm";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import type { RoutingRule } from "@/types/api";
|
|
|
|
const TEMPLATES = ["hr", "finance", "engineering", "catchall"];
|
|
|
|
export function PoliciesPage() {
|
|
const [formOpen, setFormOpen] = useState(false);
|
|
const [editingRule, setEditingRule] = useState<RoutingRule | undefined>();
|
|
|
|
const { data: policies, isLoading } = useListPolicies();
|
|
const createMutation = useCreatePolicy();
|
|
const updateMutation = useUpdatePolicy();
|
|
const deleteMutation = useDeletePolicy();
|
|
const seedMutation = useSeedTemplate();
|
|
|
|
const handleSubmit = async (payload: PolicyPayload) => {
|
|
if (editingRule) {
|
|
await updateMutation.mutateAsync({ id: editingRule.id, payload });
|
|
} else {
|
|
await createMutation.mutateAsync(payload);
|
|
}
|
|
setFormOpen(false);
|
|
setEditingRule(undefined);
|
|
};
|
|
|
|
const handleEdit = (rule: RoutingRule) => {
|
|
setEditingRule(rule);
|
|
setFormOpen(true);
|
|
};
|
|
|
|
const handleDelete = async (id: string) => {
|
|
if (confirm("Supprimer cette politique ?")) {
|
|
await deleteMutation.mutateAsync(id);
|
|
}
|
|
};
|
|
|
|
const handleOpenCreate = () => {
|
|
setEditingRule(undefined);
|
|
setFormOpen(true);
|
|
};
|
|
|
|
const isPending =
|
|
createMutation.isPending || updateMutation.isPending;
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-xl font-bold">Politiques de routage</h2>
|
|
<p className="text-sm text-muted-foreground">
|
|
Gérez les règles de routage intelligentes par tenant
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
{/* Seed template */}
|
|
<Select
|
|
onValueChange={(template) => seedMutation.mutate(template)}
|
|
value=""
|
|
>
|
|
<SelectTrigger className="w-44">
|
|
<Download className="h-4 w-4 mr-2" />
|
|
<SelectValue placeholder="Importer template" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{TEMPLATES.map((t) => (
|
|
<SelectItem key={t} value={t} className="capitalize">
|
|
{t}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Button onClick={handleOpenCreate}>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Nouvelle politique
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="space-y-2">
|
|
{Array.from({ length: 4 }).map((_, i) => (
|
|
<Skeleton key={i} className="h-14 w-full" />
|
|
))}
|
|
</div>
|
|
) : !policies || policies.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center h-64 text-center border rounded-lg">
|
|
<p className="text-muted-foreground font-medium">Aucune politique configurée</p>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
Créez votre première politique ou importez un template.
|
|
</p>
|
|
<Button onClick={handleOpenCreate} className="mt-4">
|
|
<Plus className="h-4 w-4 mr-2" /> Créer une politique
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<div className="rounded-lg border">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Nom</TableHead>
|
|
<TableHead>Priorité</TableHead>
|
|
<TableHead>Statut</TableHead>
|
|
<TableHead>Conditions</TableHead>
|
|
<TableHead>Action</TableHead>
|
|
<TableHead className="text-right">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{policies.map((rule) => (
|
|
<TableRow key={rule.id}>
|
|
<TableCell>
|
|
<div>
|
|
<p className="font-medium text-sm">{rule.name}</p>
|
|
{rule.description && (
|
|
<p className="text-xs text-muted-foreground truncate max-w-xs">
|
|
{rule.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant="outline">{rule.priority}</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant={rule.is_enabled ? "success" : "secondary"}>
|
|
{rule.is_enabled ? "Actif" : "Désactivé"}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<span className="text-sm text-muted-foreground">
|
|
{rule.conditions.length} condition{rule.conditions.length !== 1 ? "s" : ""}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell>
|
|
<span className="text-sm font-mono">
|
|
{rule.action.provider}/{rule.action.model}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
<div className="flex justify-end gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => handleEdit(rule)}
|
|
title="Modifier"
|
|
>
|
|
<Pencil className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => handleDelete(rule.id)}
|
|
title="Supprimer"
|
|
disabled={deleteMutation.isPending}
|
|
>
|
|
<Trash2 className="h-4 w-4 text-destructive" />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)}
|
|
|
|
<PolicyForm
|
|
open={formOpen}
|
|
onOpenChange={(open) => {
|
|
setFormOpen(open);
|
|
if (!open) setEditingRule(undefined);
|
|
}}
|
|
rule={editingRule}
|
|
onSubmit={handleSubmit}
|
|
isPending={isPending}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|