re-uploading work

This commit is contained in:
2026-02-04 17:46:30 -06:00
commit 3b14c65998
1388 changed files with 381262 additions and 0 deletions

View File

@@ -0,0 +1,368 @@
import { useState, useCallback } from "react";
import { Link } from "react-router-dom";
import { useQueryClient } from "@tanstack/react-query";
import { useEvents } from "@/hooks/useEvents";
import {
useEntityNotifications,
Notification,
} from "@/contexts/WebSocketContext";
import type { EventSummary } from "@/api";
export default function EventsPage() {
const queryClient = useQueryClient();
const [page, setPage] = useState(1);
const [triggerFilter, setTriggerFilter] = useState<string>("");
const pageSize = 50;
// Set up WebSocket for real-time event updates with stable callback
const handleEventNotification = useCallback(
(notification: Notification) => {
// Extract event data from notification payload
if (
notification.notification_type === "event_created" &&
notification.payload
) {
const eventData = (notification.payload as any).data;
if (eventData) {
// Create EventSummary from notification data
const newEvent: EventSummary = {
id: eventData.id,
trigger: eventData.trigger,
trigger_ref: eventData.trigger_ref,
rule: eventData.rule,
rule_ref: eventData.rule_ref,
source: eventData.source,
source_ref: eventData.source_ref,
has_payload:
eventData.payload !== null && eventData.payload !== undefined,
created: eventData.created,
};
// Update the query cache directly instead of invalidating
queryClient.setQueryData(
[
"events",
{ page, pageSize, triggerRef: triggerFilter || undefined },
],
(oldData: any) => {
if (!oldData) return oldData;
// Check if filtering and event matches filter
if (triggerFilter && newEvent.trigger_ref !== triggerFilter) {
return oldData;
}
// Add new event to the beginning of the list if on first page
if (page === 1) {
return {
...oldData,
data: [newEvent, ...oldData.data].slice(0, pageSize),
pagination: {
...oldData.pagination,
total_items: (oldData.pagination?.total_items || 0) + 1,
},
};
}
// For other pages, just update the total count
return {
...oldData,
pagination: {
...oldData.pagination,
total_items: (oldData.pagination?.total_items || 0) + 1,
},
};
},
);
}
}
},
[queryClient, page, pageSize, triggerFilter],
);
const { connected: wsConnected } = useEntityNotifications(
"event",
handleEventNotification,
);
const { data, isLoading, error } = useEvents({
page,
pageSize,
triggerRef: triggerFilter || undefined,
});
const events = data?.data || [];
const total = data?.pagination?.total_items || 0;
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString();
};
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 totalPages = total ? Math.ceil(total / pageSize) : 0;
return (
<div className="p-6">
{/* Header */}
<div className="mb-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Events</h1>
<p className="mt-2 text-gray-600">
Event instances generated by sensors and triggers
</p>
</div>
{wsConnected && (
<div className="flex items-center gap-2 text-sm text-green-600">
<div className="w-2 h-2 bg-green-600 rounded-full animate-pulse"></div>
<span>Live updates</span>
</div>
)}
</div>
</div>
{/* Filters */}
<div className="bg-white rounded-lg shadow mb-6 p-4">
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
<div className="flex-1 max-w-md">
<label
htmlFor="trigger-filter"
className="block text-sm font-medium text-gray-700 mb-1"
>
Filter by Trigger
</label>
<input
id="trigger-filter"
type="text"
value={triggerFilter}
onChange={(e) => {
setTriggerFilter(e.target.value);
setPage(1); // Reset to first page on filter change
}}
placeholder="e.g., core.webhook"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{triggerFilter && (
<button
onClick={() => {
setTriggerFilter("");
setPage(1);
}}
className="px-4 py-2 text-sm text-gray-600 hover:text-gray-900"
>
Clear Filter
</button>
)}
</div>
{data && (
<div className="mt-3 text-sm text-gray-600">
Showing {events.length} of {total} events
{triggerFilter && ` (filtered by "${triggerFilter}")`}
</div>
)}
</div>
{/* Events List */}
<div className="bg-white rounded-lg shadow overflow-hidden">
{isLoading ? (
<div className="p-12 text-center">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<p className="mt-4 text-gray-600">Loading events...</p>
</div>
) : error ? (
<div className="p-12 text-center">
<p className="text-red-600">Failed to load events</p>
<p className="text-sm text-gray-600 mt-2">
{error instanceof Error ? error.message : "Unknown error"}
</p>
</div>
) : !events || events.length === 0 ? (
<div className="p-12 text-center">
<svg
className="mx-auto h-12 w-12 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
<p className="mt-4 text-gray-600">No events found</p>
<p className="text-sm text-gray-500 mt-1">
{triggerFilter
? "Try adjusting your filter"
: "Events will appear here when triggers fire"}
</p>
</div>
) : (
<>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
ID
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Trigger
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Rule
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Source
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Created
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{events.map((event) => (
<tr key={event.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<span className="text-sm font-mono text-gray-900">
{event.id}
</span>
</td>
<td className="px-6 py-4">
<div className="text-sm">
<div className="font-medium text-gray-900">
{event.trigger_ref}
</div>
<div className="text-gray-500 text-xs">
ID: {event.trigger || "N/A"}
</div>
</div>
</td>
<td className="px-6 py-4">
{event.rule_ref ? (
<div className="text-sm">
<Link
to={`/rules/${event.rule}`}
className="font-medium text-blue-600 hover:text-blue-900"
>
{event.rule_ref}
</Link>
<div className="text-gray-500 text-xs">
ID: {event.rule}
</div>
</div>
) : (
<span className="text-sm text-gray-400 italic">
No rule
</span>
)}
</td>
<td className="px-6 py-4">
{event.source_ref ? (
<div className="text-sm">
<div className="font-medium text-gray-900">
{event.source_ref}
</div>
<div className="text-gray-500 text-xs">
ID: {event.source || "N/A"}
</div>
</div>
) : (
<span className="text-sm text-gray-400 italic">
No source
</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{formatTime(event.created)}
</div>
<div className="text-xs text-gray-500">
{formatDate(event.created)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<Link
to={`/events/${event.id}`}
className="text-blue-600 hover:text-blue-900"
>
View Details
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="bg-gray-50 px-6 py-4 flex items-center justify-between border-t border-gray-200">
<div className="flex-1 flex justify-between sm:hidden">
<button
onClick={() => setPage(page - 1)}
disabled={page === 1}
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
<button
onClick={() => setPage(page + 1)}
disabled={page === totalPages}
className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Page <span className="font-medium">{page}</span> of{" "}
<span className="font-medium">{totalPages}</span>
</p>
</div>
<div>
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
<button
onClick={() => setPage(page - 1)}
disabled={page === 1}
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
<button
onClick={() => setPage(page + 1)}
disabled={page === totalPages}
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</nav>
</div>
</div>
</div>
)}
</>
)}
</div>
</div>
);
}