Skip to main content
Glama
Events.tsx6.68 kB
import { useState } from 'react'; import { useSSE } from '../hooks/useSSE'; import { AlertTriangle, Filter, Download, Trash2 } from 'lucide-react'; import { formatDistanceToNow, format } from 'date-fns'; export default function Events() { const [filter, setFilter] = useState<{ severity?: string[] }>({}); const { events, connected, clearEvents } = useSSE('/sse/events', filter); const [selectedSeverity, setSelectedSeverity] = useState<string[]>([]); const toggleSeverity = (severity: string) => { const newSelected = selectedSeverity.includes(severity) ? selectedSeverity.filter(s => s !== severity) : [...selectedSeverity, severity]; setSelectedSeverity(newSelected); setFilter({ severity: newSelected.length > 0 ? newSelected : undefined }); }; const exportEvents = () => { const csv = [ ['Timestamp', 'Type', 'Plugin', 'Severity', 'Source', 'Data'].join(','), ...events.map(e => [ format(e.timestamp, 'yyyy-MM-dd HH:mm:ss'), e.type, e.pluginId, e.severity, e.source || '', JSON.stringify(e.data), ].join(',')), ].join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `events-${Date.now()}.csv`; a.click(); }; return ( <div className="space-y-6"> <div className="flex items-center justify-between"> <div> <h1 className="text-3xl font-bold text-gray-900 dark:text-white">Events</h1> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400"> Real-time event stream from all plugins </p> </div> <div className="flex items-center space-x-2"> <div className={`flex items-center space-x-2 px-3 py-1 rounded-full ${ connected ? 'bg-green-100 dark:bg-green-900/20' : 'bg-red-100 dark:bg-red-900/20' }`}> <div className={`w-2 h-2 rounded-full ${connected ? 'bg-green-500' : 'bg-red-500'}`} /> <span className="text-sm font-medium">{connected ? 'Connected' : 'Disconnected'}</span> </div> </div> </div> {/* Filters */} <div className="card"> <div className="flex items-center justify-between mb-4"> <div className="flex items-center space-x-2"> <Filter className="w-5 h-5 text-gray-500" /> <span className="font-medium text-gray-900 dark:text-white">Filters</span> </div> <div className="flex items-center space-x-2"> <button onClick={exportEvents} className="btn btn-secondary flex items-center space-x-2" > <Download className="w-4 h-4" /> <span>Export</span> </button> <button onClick={clearEvents} className="btn btn-secondary flex items-center space-x-2" > <Trash2 className="w-4 h-4" /> <span>Clear</span> </button> </div> </div> <div className="flex flex-wrap gap-2"> {['debug', 'info', 'warning', 'error', 'critical'].map(severity => ( <button key={severity} onClick={() => toggleSeverity(severity)} className={`px-3 py-1 rounded-full text-sm font-medium transition-colors ${ selectedSeverity.includes(severity) ? 'bg-blue-600 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300' }`} > {severity} </button> ))} </div> </div> {/* Events List */} <div className="card"> <div className="flex items-center justify-between mb-4"> <h2 className="text-xl font-semibold text-gray-900 dark:text-white"> Event Stream ({events.length} events) </h2> </div> <div className="space-y-2 max-h-[calc(100vh-400px)] overflow-y-auto"> {events.map((event) => ( <div key={event.id} className="flex items-start space-x-4 p-4 rounded-lg bg-gray-50 dark:bg-gray-700/50 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors" > <div className={`mt-1 ${getSeverityColor(event.severity)}`}> <AlertTriangle className="w-5 h-5" /> </div> <div className="flex-1 min-w-0"> <div className="flex items-center justify-between mb-1"> <p className="text-sm font-medium text-gray-900 dark:text-white"> {event.type} </p> <span className={`badge ${getSeverityBadge(event.severity)}`}> {event.severity} </span> </div> <p className="text-xs text-gray-500 dark:text-gray-400 mb-2"> {event.pluginId} • {event.source} • {formatDistanceToNow(event.timestamp, { addSuffix: true })} </p> {Object.keys(event.data).length > 0 && ( <details className="text-xs"> <summary className="cursor-pointer text-blue-600 dark:text-blue-400 hover:underline"> View data </summary> <pre className="mt-2 p-2 bg-gray-100 dark:bg-gray-800 rounded overflow-x-auto"> {JSON.stringify(event.data, null, 2)} </pre> </details> )} </div> </div> ))} {events.length === 0 && ( <div className="text-center py-12"> <AlertTriangle className="w-12 h-12 text-gray-400 mx-auto mb-4" /> <p className="text-gray-500 dark:text-gray-400"> No events yet. Waiting for activity... </p> </div> )} </div> </div> </div> ); } function getSeverityColor(severity: string): string { const colors: Record<string, string> = { debug: 'text-gray-400', info: 'text-blue-500', warning: 'text-yellow-500', error: 'text-red-500', critical: 'text-red-600', }; return colors[severity] || 'text-gray-400'; } function getSeverityBadge(severity: string): string { const badges: Record<string, string> = { debug: 'badge-info', info: 'badge-info', warning: 'badge-warning', error: 'badge-error', critical: 'badge-error', }; return badges[severity] || 'badge-info'; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/vespo92/OPNSenseMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server