import { useEffect, useRef, useState } from 'react'; import { connectLiveLogs, LiveLogEntry, LiveLogFilter } from '../api/logs'; import { Button } from './ui/Button'; import { Pause, Play, Trash2, Filter } from 'lucide-react'; interface LiveLogViewerProps { filters?: LiveLogFilter; maxLogs?: number; className?: string; } export function LiveLogViewer({ filters = {}, maxLogs = 500, className = '' }: LiveLogViewerProps) { const [logs, setLogs] = useState([]); const [isPaused, setIsPaused] = useState(false); const [isConnected, setIsConnected] = useState(false); const [textFilter, setTextFilter] = useState(''); const [levelFilter, setLevelFilter] = useState(''); const logContainerRef = useRef(null); const closeConnectionRef = useRef<(() => void) | null>(null); // Auto-scroll when new logs arrive (only if not paused and user hasn't scrolled up) const shouldAutoScroll = useRef(true); useEffect(() => { // Connect to WebSocket const closeConnection = connectLiveLogs( filters, (log: LiveLogEntry) => { if (!isPaused) { setLogs((prev) => { const updated = [...prev, log]; // Keep only last maxLogs entries if (updated.length > maxLogs) { return updated.slice(updated.length - maxLogs); } return updated; }); } }, () => { // onOpen callback - connection established console.log('Live log viewer connected'); setIsConnected(true); }, (error) => { console.error('WebSocket error:', error); setIsConnected(false); }, () => { console.log('Live log viewer disconnected'); setIsConnected(false); } ); closeConnectionRef.current = closeConnection; // Don't set isConnected here - wait for onOpen callback return () => { closeConnection(); setIsConnected(false); }; }, [filters, isPaused, maxLogs]); // Handle auto-scroll useEffect(() => { if (shouldAutoScroll.current && logContainerRef.current) { logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; } }, [logs]); // Track if user has manually scrolled const handleScroll = () => { if (logContainerRef.current) { const { scrollTop, scrollHeight, clientHeight } = logContainerRef.current; // If scrolled to bottom (within 50px), enable auto-scroll shouldAutoScroll.current = scrollHeight - scrollTop - clientHeight < 50; } }; const handleClear = () => { setLogs([]); }; const handleTogglePause = () => { setIsPaused(!isPaused); }; // Filter logs based on text and level const filteredLogs = logs.filter((log) => { if (textFilter && !log.message.toLowerCase().includes(textFilter.toLowerCase())) { return false; } if (levelFilter && log.level.toLowerCase() !== levelFilter.toLowerCase()) { return false; } return true; }); // Color coding based on log level const getLevelColor = (level: string) => { const normalized = level.toLowerCase(); if (normalized.includes('error') || normalized.includes('fatal')) return 'text-red-400'; if (normalized.includes('warn')) return 'text-yellow-400'; if (normalized.includes('info')) return 'text-blue-400'; if (normalized.includes('debug')) return 'text-gray-400'; return 'text-gray-300'; }; const formatTimestamp = (timestamp: string) => { try { const date = new Date(timestamp); return date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }); } catch { return timestamp; } }; return (
{/* Header with controls */}

Live Security Logs

{isConnected ? 'Connected' : 'Disconnected'}
{/* Filters */}
setTextFilter(e.target.value)} className="flex-1 px-2 py-1 text-sm bg-gray-700 border border-gray-600 rounded text-white placeholder-gray-400 focus:outline-none focus:border-blue-500" />
{/* Log display */}
{filteredLogs.length === 0 && (
{logs.length === 0 ? 'No logs yet. Waiting for events...' : 'No logs match the current filters.'}
)} {filteredLogs.map((log, index) => (
{formatTimestamp(log.timestamp)} {log.level.toUpperCase()} {log.source && [{log.source}]} {log.message} {log.data && Object.keys(log.data).length > 0 && (
{JSON.stringify(log.data, null, 2)}
)}
))}
{/* Footer with log count */}
Showing {filteredLogs.length} of {logs.length} logs {isPaused && ⏸ Paused}
); }