import { useState, useEffect, useRef, useMemo } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Play, Square, Trash2, RefreshCw, Search, X, ChevronDown, ChevronUp, AlertCircle, AlertTriangle, Info, Bug, } from 'lucide-react'; import { supportApi, type LogEntry } from '../api/client'; const LOG_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR'] as const; type LogLevel = (typeof LOG_LEVELS)[number]; const levelColors: Record = { DEBUG: 'text-gray-400', INFO: 'text-blue-400', WARNING: 'text-yellow-400', ERROR: 'text-red-400', }; const levelIcons: Record = { DEBUG: Bug, INFO: Info, WARNING: AlertTriangle, ERROR: AlertCircle, }; export function LogViewer() { const queryClient = useQueryClient(); const [autoScroll, setAutoScroll] = useState(true); const [expandedLogs, setExpandedLogs] = useState>(new Set()); const [searchQuery, setSearchQuery] = useState(''); const [levelFilter, setLevelFilter] = useState('ALL'); const [isExpanded, setIsExpanded] = useState(false); const [isStreaming, setIsStreaming] = useState(false); const logContainerRef = useRef(null); // Fetch logs with polling when streaming is enabled const { data, isLoading, refetch } = useQuery({ queryKey: ['application-logs', levelFilter, searchQuery], queryFn: () => supportApi.getLogs({ limit: 200, level: levelFilter === 'ALL' ? undefined : levelFilter, search: searchQuery || undefined, }), refetchInterval: isStreaming ? 2000 : false, // Poll every 2 seconds when streaming enabled: isExpanded, // Only fetch when viewer is expanded }); // Stop streaming when viewer is collapsed useEffect(() => { if (!isExpanded) { setIsStreaming(false); } }, [isExpanded]); const clearMutation = useMutation({ mutationFn: () => supportApi.clearLogs(), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['application-logs'] }); }, }); // Auto-scroll to bottom when new logs arrive useEffect(() => { if (autoScroll && logContainerRef.current && data?.entries) { logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; } }, [data?.entries, autoScroll]); const toggleExpand = (index: number) => { setExpandedLogs((prev) => { const newSet = new Set(prev); if (newSet.has(index)) { newSet.delete(index); } else { newSet.add(index); } return newSet; }); }; const formatTimestamp = (timestamp: string) => { // Input format: "2024-01-15 10:30:45,123" const parts = timestamp.split(' '); if (parts.length >= 2) { return parts[1]; // Return just the time part } return timestamp; }; const entries = useMemo(() => data?.entries ?? [], [data?.entries]); // Reverse to show newest at bottom (better for auto-scroll UX) const displayEntries = useMemo(() => [...entries].reverse(), [entries]); const LevelIcon = ({ level }: { level: string }) => { const Icon = levelIcons[level as LogLevel] || Info; return ; }; return (
{/* Header - always visible */} {/* Expanded content */} {isExpanded && (
{/* Controls */}
{/* Start/Stop streaming button */} {isStreaming ? ( ) : ( )} {/* Clear button */} {/* Refresh button */}
{/* Auto-scroll toggle */} {/* Entry count */} {data?.filtered_count ?? 0}/{data?.total_in_file ?? 0}
{/* Search and Filter Row */}
{/* Search input */}
setSearchQuery(e.target.value)} className="w-full pl-8 pr-8 py-1.5 text-sm bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none" /> {searchQuery && ( )}
{/* Level filter */}
{LOG_LEVELS.map((level, idx) => ( ))}
{/* Log Content */}
{entries.length === 0 ? (

No log entries found

Log file may be empty or cleared

) : (
{displayEntries.map((log: LogEntry, index: number) => { const isEntryExpanded = expandedLogs.has(index); const hasMultiLine = log.message.includes('\n'); return (
hasMultiLine && toggleExpand(index)} >
{formatTimestamp(log.timestamp)} [{log.logger_name}] {isEntryExpanded ? (
{log.message}
) : ( log.message.split('\n')[0] )}
{hasMultiLine && ( {isEntryExpanded ? ( ) : ( )} )}
); })}
)}
{/* Footer */}
{isStreaming ? ( Auto-refreshing every 2 seconds ) : ( Click Start to enable live log streaming )}
)}
); }