first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,275 @@
/**
* Routing Rules Table
* Displays all routing rules with CRUD actions
*/
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alertDialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { ProviderIconType, RenderProviderIcon } from "@/lib/constants/icons";
import { getProviderLabel } from "@/lib/constants/logs";
import { getErrorMessage } from "@/lib/store";
import { useDeleteRoutingRuleMutation } from "@/lib/store/apis/routingRulesApi";
import { RoutingRule, RoutingTarget } from "@/lib/types/routingRules";
import { getPriorityBadgeClass, getScopeLabel, truncateCELExpression } from "@/lib/utils/routingRules";
import { ChevronLeft, ChevronRight, Edit, Search, Trash2 } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
interface RoutingRulesTableProps {
rules: RoutingRule[] | undefined;
totalCount: number;
isLoading: boolean;
onEdit: (rule: RoutingRule) => void;
onRowClick: (rule: RoutingRule) => void;
/** When false, delete button is hidden and deletion is disabled (e.g. for read-only users). */
canDelete?: boolean;
search: string;
onSearchChange: (value: string) => void;
offset: number;
limit: number;
onOffsetChange: (offset: number) => void;
}
export function RoutingRulesTable({
rules,
totalCount,
isLoading,
onEdit,
onRowClick,
canDelete = false,
search,
onSearchChange,
offset,
limit,
onOffsetChange,
}: RoutingRulesTableProps) {
const [deleteRuleId, setDeleteRuleId] = useState<string | null>(null);
const [deleteRoutingRule, { isLoading: isDeleting }] = useDeleteRoutingRuleMutation();
const handleDelete = async () => {
if (!canDelete || !deleteRuleId) return;
try {
await deleteRoutingRule(deleteRuleId).unwrap();
toast.success("Routing rule deleted successfully");
setDeleteRuleId(null);
} catch (error: any) {
toast.error(getErrorMessage(error));
}
};
if (isLoading) {
return (
<div className="rounded-sm border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Targets</TableHead>
<TableHead>Scope</TableHead>
<TableHead className="text-right">Priority</TableHead>
<TableHead>Expression</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{[...Array(5)].map((_, i) => (
<TableRow key={i}>
<TableCell colSpan={7} className="h-10">
<div className="bg-muted h-2 w-32 animate-pulse rounded" />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
const sortedRules = rules ? [...rules].sort((a, b) => a.priority - b.priority) : [];
const ruleToDelete = sortedRules.find((r) => r.id === deleteRuleId);
return (
<>
{/* Toolbar: Search */}
<div className="flex items-center gap-3">
<div className="relative max-w-sm flex-1">
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
<Input
aria-label="Search routing rules by name"
placeholder="Search by name..."
value={search}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-9"
data-testid="routing-rules-search-input"
/>
</div>
</div>
<div className="overflow-hidden rounded-sm border">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHead className="font-semibold">Name</TableHead>
<TableHead className="font-semibold">Targets</TableHead>
<TableHead className="font-semibold">Scope</TableHead>
<TableHead className="text-right font-semibold">Priority</TableHead>
<TableHead className="font-semibold">Expression</TableHead>
<TableHead className="font-semibold">Status</TableHead>
<TableHead className="text-right font-semibold">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedRules.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="h-24 text-center">
<span className="text-muted-foreground text-sm">No matching routing rules found.</span>
</TableCell>
</TableRow>
) : (
sortedRules.map((rule) => (
<TableRow key={rule.id} className="hover:bg-muted/50 cursor-pointer transition-colors" onClick={() => onRowClick(rule)}>
<TableCell className="font-medium">
<div className="flex flex-col gap-1">
<span className="max-w-xs truncate">{rule.name}</span>
{rule.description && (
<span data-testid="routing-rule-description" className="text-muted-foreground max-w-xs truncate text-xs">
{rule.description}
</span>
)}
</div>
</TableCell>
<TableCell>
<TargetsSummary targets={rule.targets || []} />
</TableCell>
<TableCell>
<Badge variant="secondary">{getScopeLabel(rule.scope)}</Badge>
</TableCell>
<TableCell className="text-right">
<div className={`inline-block rounded px-2.5 py-1 text-xs font-medium ${getPriorityBadgeClass()}`}>{rule.priority}</div>
</TableCell>
<TableCell>
<span className="text-muted-foreground block max-w-xs truncate font-mono text-xs" title={rule.cel_expression}>
{truncateCELExpression(rule.cel_expression)}
</span>
</TableCell>
<TableCell>
<Badge variant={rule.enabled ? "default" : "secondary"}>{rule.enabled ? "Enabled" : "Disabled"}</Badge>
</TableCell>
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => onEdit(rule)}
aria-label="Edit routing rule"
data-testid={`routing-rule-edit-${rule.id}-btn`}
>
<Edit className="h-4 w-4" />
</Button>
{canDelete && (
<Button
variant="ghost"
size="sm"
onClick={() => setDeleteRuleId(rule.id)}
aria-label="Delete routing rule"
data-testid={`routing-rule-delete-${rule.id}-btn`}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* Pagination */}
{totalCount > 0 && (
<div className="flex items-center justify-between px-2">
<p className="text-muted-foreground text-sm">
Showing {offset + 1}-{Math.min(offset + limit, totalCount)} of {totalCount}
</p>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
disabled={offset === 0}
onClick={() => onOffsetChange(Math.max(0, offset - limit))}
data-testid="routing-rules-pagination-prev-btn"
>
<ChevronLeft className="mr-1 h-4 w-4" />
Previous
</Button>
<Button
variant="outline"
size="sm"
disabled={offset + limit >= totalCount}
onClick={() => onOffsetChange(offset + limit)}
data-testid="routing-rules-pagination-next-btn"
>
Next
<ChevronRight className="ml-1 h-4 w-4" />
</Button>
</div>
</div>
)}
<AlertDialog open={!!deleteRuleId} onOpenChange={(open) => !open && setDeleteRuleId(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Routing Rule</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete &quot;{ruleToDelete?.name}&quot;? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete} disabled={isDeleting} className="bg-destructive hover:bg-destructive/90">
{isDeleting ? "Deleting..." : "Delete"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}
function TargetsSummary({ targets }: { targets: RoutingTarget[] }) {
if (!targets || targets.length === 0) {
return <span className="text-muted-foreground text-sm">-</span>;
}
const first = targets[0];
const label = [first.provider ? getProviderLabel(first.provider) : "Any", first.model || "Any model"].join(" / ");
return (
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1.5">
{first.provider && <RenderProviderIcon provider={first.provider as ProviderIconType} size="sm" className="h-4 w-4 shrink-0" />}
<span className="max-w-[160px] truncate text-sm">{label}</span>
</div>
{targets.length > 1 && (
<span className="text-muted-foreground text-xs">
+{targets.length - 1} more target{targets.length > 2 ? "s" : ""}
</span>
)}
</div>
);
}