/** * ╔═══════════════════════════════════════════════════════════════════════════╗ * ║ LOG STREAM VIEWER ║ * ║═══════════════════════════════════════════════════════════════════════════║ * ║ Real-time log streaming with filtering and syntax highlighting ║ * ║ Part of the Liquid UI Arsenal ║ * ╚═══════════════════════════════════════════════════════════════════════════╝ */ import { useEffect, useRef, useState } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { ScrollArea } from '@/components/ui/scroll-area'; import { cn } from '@/lib/utils'; import { Play, Pause, Trash2, Search, Filter, ChevronDown, AlertTriangle, Info, Bug, Zap } from 'lucide-react'; export interface LogEntry { id: string; timestamp: string; level: 'debug' | 'info' | 'warn' | 'error' | 'fatal'; source: string; message: string; metadata?: Record; } export interface LogStreamViewerProps { logs: LogEntry[]; maxLines?: number; autoScroll?: boolean; showTimestamps?: boolean; showSource?: boolean; title?: string; onClear?: () => void; } const levelConfig = { debug: { icon: Bug, color: 'text-gray-400', bg: 'bg-gray-500/10', badge: 'bg-gray-500/20' }, info: { icon: Info, color: 'text-blue-400', bg: 'bg-blue-500/10', badge: 'bg-blue-500/20' }, warn: { icon: AlertTriangle, color: 'text-yellow-400', bg: 'bg-yellow-500/10', badge: 'bg-yellow-500/20' }, error: { icon: Zap, color: 'text-red-400', bg: 'bg-red-500/10', badge: 'bg-red-500/20' }, fatal: { icon: Zap, color: 'text-red-500', bg: 'bg-red-600/20', badge: 'bg-red-600/30' }, }; export function LogStreamViewer({ logs, maxLines = 500, autoScroll: initialAutoScroll = true, showTimestamps = true, showSource = true, title = 'System Logs', onClear, }: LogStreamViewerProps) { const scrollRef = useRef(null); const [autoScroll, setAutoScroll] = useState(initialAutoScroll); const [isPaused, setIsPaused] = useState(false); const [filter, setFilter] = useState(''); const [levelFilter, setLevelFilter] = useState>(new Set(['debug', 'info', 'warn', 'error', 'fatal'])); const [showFilters, setShowFilters] = useState(false); // Auto-scroll to bottom useEffect(() => { if (autoScroll && !isPaused && scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [logs, autoScroll, isPaused]); // Filter logs const filteredLogs = logs .filter(log => levelFilter.has(log.level)) .filter(log => !filter || log.message.toLowerCase().includes(filter.toLowerCase()) || log.source.toLowerCase().includes(filter.toLowerCase()) ) .slice(-maxLines); // Level counts const levelCounts = logs.reduce((acc, log) => { acc[log.level] = (acc[log.level] || 0) + 1; return acc; }, {} as Record); const toggleLevel = (level: string) => { const newFilter = new Set(levelFilter); if (newFilter.has(level)) { newFilter.delete(level); } else { newFilter.add(level); } setLevelFilter(newFilter); }; const formatTimestamp = (ts: string) => { try { return new Date(ts).toLocaleTimeString('da-DK', { hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 }); } catch { return ts; } }; return (
{/* Header */}
{title} {filteredLogs.length}/{logs.length}
{onClear && ( )}
{/* Filter bar */} {showFilters && (
setFilter(e.target.value)} placeholder="Filter logs..." className="h-7 text-xs bg-background/50" />
{Object.entries(levelConfig).map(([level, config]) => ( ))}
)} {/* Log entries */}
{filteredLogs.map((log) => { const config = levelConfig[log.level]; const Icon = config.icon; return (
{showTimestamps && ( {formatTimestamp(log.timestamp)} )} {showSource && ( [{log.source}] )} {log.message}
); })} {filteredLogs.length === 0 && (
No logs to display
)}
{/* Footer */}
{isPaused && ( PAUSED )}
Max: {maxLines} lines
); } export default LogStreamViewer;