import { Link, useSearchParams } from "react-router-dom"; import { useEnforcements } from "@/hooks/useEvents"; import { useEnforcementStream } from "@/hooks/useEnforcementStream"; import { EnforcementStatus } from "@/api"; import { useState, useMemo, memo, useCallback, useEffect } from "react"; import { Search, X } from "lucide-react"; import MultiSelect from "@/components/common/MultiSelect"; // Memoized filter input component to prevent re-render on WebSocket updates const FilterInput = memo( ({ label, value, onChange, placeholder, }: { label: string; value: string; onChange: (value: string) => void; placeholder: string; }) => (
onChange(e.target.value)} placeholder={placeholder} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />
), ); FilterInput.displayName = "FilterInput"; // Status options moved outside component to prevent recreation const STATUS_OPTIONS = [ { value: EnforcementStatus.CREATED, label: "Created" }, { value: EnforcementStatus.PROCESSED, label: "Processed" }, { value: EnforcementStatus.DISABLED, label: "Disabled" }, ]; export default function EnforcementsPage() { const [searchParams] = useSearchParams(); // Initialize filters from URL query parameters const [page, setPage] = useState(1); const pageSize = 50; const [searchFilters, setSearchFilters] = useState({ rule: searchParams.get("rule_ref") || "", trigger: searchParams.get("trigger_ref") || "", event: searchParams.get("event") || "", }); const [selectedStatuses, setSelectedStatuses] = useState(() => { const status = searchParams.get("status"); return status ? [status] : []; }); // Debounced filter state for API calls const [debouncedFilters, setDebouncedFilters] = useState(searchFilters); const [debouncedStatuses, setDebouncedStatuses] = useState(selectedStatuses); // Debounce filter changes (500ms delay) useEffect(() => { const timer = setTimeout(() => { setDebouncedFilters(searchFilters); }, 500); return () => clearTimeout(timer); }, [searchFilters]); // Debounce status changes (300ms delay - shorter since it's a selection) useEffect(() => { const timer = setTimeout(() => { setDebouncedStatuses(selectedStatuses); }, 300); return () => clearTimeout(timer); }, [selectedStatuses]); const queryParams = useMemo(() => { const params: any = { page, pageSize }; if (debouncedFilters.trigger) params.triggerRef = debouncedFilters.trigger; if (debouncedFilters.event) { const eventId = parseInt(debouncedFilters.event, 10); if (!isNaN(eventId)) { params.event = eventId; } } // Include status filter if exactly one status is selected // API only supports single status, so we use the first one for filtering // and show all results if multiple are selected if (debouncedStatuses.length === 1) { params.status = debouncedStatuses[0] as EnforcementStatus; } return params; }, [page, pageSize, debouncedFilters, debouncedStatuses]); const { data, isLoading, error } = useEnforcements(queryParams); // Subscribe to real-time updates for all enforcements const { isConnected } = useEnforcementStream({ enabled: true }); const enforcements = data?.data || []; const total = data?.pagination?.total_items || 0; const totalPages = Math.ceil(total / pageSize); // Client-side filtering for multiple status selection and rule_ref (when > 1 status selected) const filteredEnforcements = useMemo(() => { let filtered = enforcements; // Filter by rule_ref (client-side since API doesn't support it) if (debouncedFilters.rule) { filtered = filtered.filter((enf: any) => enf.rule_ref .toLowerCase() .includes(debouncedFilters.rule.toLowerCase()), ); } // If multiple statuses selected, filter client-side if (debouncedStatuses.length > 1) { filtered = filtered.filter((enf: any) => debouncedStatuses.includes(enf.status), ); } return filtered; }, [enforcements, debouncedFilters.rule, debouncedStatuses]); const handleFilterChange = useCallback((field: string, value: string) => { setSearchFilters((prev) => ({ ...prev, [field]: value })); setPage(1); // Reset to first page on filter change }, []); const clearFilters = useCallback(() => { setSearchFilters({ rule: "", trigger: "", event: "", }); setSelectedStatuses([]); setPage(1); // Reset to first page }, []); const hasActiveFilters = Object.values(searchFilters).some((v) => v !== "") || selectedStatuses.length > 0; if (isLoading) { return (
); } if (error) { return (

Error: {(error as Error).message}

); } const getStatusColor = (status: EnforcementStatus) => { switch (status) { case EnforcementStatus.PROCESSED: return "bg-green-100 text-green-800"; case EnforcementStatus.DISABLED: return "bg-gray-100 text-gray-800"; case EnforcementStatus.CREATED: return "bg-blue-100 text-blue-800"; default: return "bg-gray-100 text-gray-800"; } }; const getConditionBadge = (condition: string) => { const colors = { all: "bg-purple-100 text-purple-800", any: "bg-indigo-100 text-indigo-800", }; return ( colors[condition as keyof typeof colors] || "bg-gray-100 text-gray-800" ); }; const formatTime = (timestamp: string) => { const date = new Date(timestamp); const now = new Date(); const diff = now.getTime() - date.getTime(); if (diff < 60000) return "just now"; if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`; if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`; return date.toLocaleDateString(); }; const formatDate = (dateString: string) => { return new Date(dateString).toLocaleString(); }; return (

Enforcements

{isLoading && hasActiveFilters && (

Searching enforcements...

)}
{isConnected && (
Live Updates
)}
{/* Search Filters */}

Filter Enforcements

{hasActiveFilters && ( )}
handleFilterChange("rule", value)} placeholder="e.g., core.on_timer" /> handleFilterChange("trigger", value)} placeholder="e.g., core.webhook" /> handleFilterChange("event", value)} placeholder="e.g., 123" />
{filteredEnforcements.length === 0 ? (

{enforcements.length === 0 ? "No enforcements found" : "No enforcements match the selected filters"}

{enforcements.length > 0 && hasActiveFilters && ( )}
) : ( <>
{filteredEnforcements.map((enforcement: any) => ( ))}
ID Rule Trigger Event Condition Status Created
#{enforcement.id} {enforcement.rule ? ( {enforcement.rule_ref} ) : ( {enforcement.rule_ref} )} {enforcement.trigger_ref} {enforcement.event ? ( #{enforcement.event} ) : ( - )} {enforcement.condition} {enforcement.status}
{formatTime(enforcement.created)}
{formatDate(enforcement.created)}
{/* Pagination */} {totalPages > 1 && (

Showing{" "} {(page - 1) * pageSize + 1} {" "} to{" "} {Math.min(page * pageSize, total)} {" "} of {total} enforcements

)} )}
); }