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 && (
)}
{/* 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 && (
)}
) : (
<>
|
ID
|
Rule
|
Trigger
|
Event
|
Condition
|
Status
|
Created
|
{filteredEnforcements.map((enforcement: any) => (
|
#{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
)}
>
)}
);
}