Kraft102's picture
fix: sql.js Docker/Alpine compatibility layer for PatternMemory and FailureMemory
5a81b95
/**
* ╔═══════════════════════════════════════════════════════════════════════════╗
* β•‘ 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<string, unknown>;
}
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<HTMLDivElement>(null);
const [autoScroll, setAutoScroll] = useState(initialAutoScroll);
const [isPaused, setIsPaused] = useState(false);
const [filter, setFilter] = useState('');
const [levelFilter, setLevelFilter] = useState<Set<string>>(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<string, number>);
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 (
<div className="rounded-lg border border-border/30 bg-background/50 overflow-hidden flex flex-col">
{/* Header */}
<div className="flex items-center justify-between px-4 py-2 bg-muted/30 border-b border-border/30">
<div className="flex items-center gap-3">
<span className="text-sm font-medium">{title}</span>
<Badge variant="outline" className="text-[10px] font-mono">
{filteredLogs.length}/{logs.length}
</Badge>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => setIsPaused(!isPaused)}
className={cn("h-7 px-2", isPaused && "text-yellow-500")}
>
{isPaused ? <Play className="w-3 h-3" /> : <Pause className="w-3 h-3" />}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setShowFilters(!showFilters)}
className={cn("h-7 px-2", showFilters && "bg-muted")}
>
<Filter className="w-3 h-3" />
</Button>
{onClear && (
<Button
variant="ghost"
size="sm"
onClick={onClear}
className="h-7 px-2 text-red-400 hover:text-red-300"
>
<Trash2 className="w-3 h-3" />
</Button>
)}
</div>
</div>
{/* Filter bar */}
{showFilters && (
<div className="px-4 py-2 bg-muted/20 border-b border-border/30 space-y-2">
<div className="flex items-center gap-2">
<Search className="w-3 h-3 text-muted-foreground" />
<Input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter logs..."
className="h-7 text-xs bg-background/50"
/>
</div>
<div className="flex items-center gap-2 flex-wrap">
{Object.entries(levelConfig).map(([level, config]) => (
<button
key={level}
onClick={() => toggleLevel(level)}
className={cn(
'px-2 py-0.5 rounded text-[10px] font-mono uppercase transition-all',
levelFilter.has(level) ? config.badge : 'bg-muted/30 text-muted-foreground opacity-50'
)}
>
{level} ({levelCounts[level] || 0})
</button>
))}
</div>
</div>
)}
{/* Log entries */}
<ScrollArea ref={scrollRef} className="flex-1 h-[300px]">
<div className="font-mono text-xs">
{filteredLogs.map((log) => {
const config = levelConfig[log.level];
const Icon = config.icon;
return (
<div
key={log.id}
className={cn(
'flex items-start gap-2 px-3 py-1.5 border-b border-border/10 hover:bg-muted/20 transition-colors',
config.bg
)}
>
<Icon className={cn('w-3 h-3 mt-0.5 flex-shrink-0', config.color)} />
{showTimestamps && (
<span className="text-muted-foreground flex-shrink-0 w-24">
{formatTimestamp(log.timestamp)}
</span>
)}
{showSource && (
<span className="text-primary/70 flex-shrink-0 w-20 truncate">
[{log.source}]
</span>
)}
<span className={cn('flex-1', config.color)}>
{log.message}
</span>
</div>
);
})}
{filteredLogs.length === 0 && (
<div className="flex items-center justify-center py-8 text-muted-foreground">
No logs to display
</div>
)}
</div>
</ScrollArea>
{/* Footer */}
<div className="px-4 py-2 bg-muted/20 border-t border-border/30 flex items-center justify-between text-[10px] text-muted-foreground">
<div className="flex items-center gap-2">
<button
onClick={() => setAutoScroll(!autoScroll)}
className={cn(
'flex items-center gap-1 px-2 py-0.5 rounded',
autoScroll ? 'bg-green-500/20 text-green-400' : 'bg-muted/30'
)}
>
<ChevronDown className="w-3 h-3" />
Auto-scroll
</button>
{isPaused && (
<span className="text-yellow-500 animate-pulse">PAUSED</span>
)}
</div>
<span>Max: {maxLines} lines</span>
</div>
</div>
);
}
export default LogStreamViewer;