import { IconAlertCircle as AlertCircle, IconChevronDown as ChevronDown, IconExternalLink as ExternalLink, IconLoader2 as Loader2, IconRefresh as RefreshCw, IconTerminal2 as Terminal, } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; import { useEffect, useRef, useState } from 'react'; import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { adminServices, getServiceUrl } from '@/lib/admin-services'; import { getAuthHeaders } from '@/lib/api'; import { cn } from '@/lib/utils'; interface LogsResponse { success: boolean; logs: string[]; error?: string; meta?: { tail: number; since: string; container: string; lineCount: number; timestamp: string; }; } interface ContainersResponse { containers: Array<{ name: string; container: string; }>; } const TIME_OPTIONS = [ { label: 'Last 5 minutes', value: '5m' }, { label: 'Last 10 minutes', value: '10m' }, { label: 'Last 30 minutes', value: '30m' }, { label: 'Last 1 hour', value: '1h' }, { label: 'Last 6 hours', value: '6h' }, { label: 'Last 24 hours', value: '24h' }, ]; const TAIL_OPTIONS = [ { label: '100 lines', value: 100 }, { label: '200 lines', value: 200 }, { label: '500 lines', value: 500 }, { label: '1000 lines', value: 1000 }, ]; async function fetchLogs(container: string, since: string, tail: number): Promise { const params = new URLSearchParams({ since, tail: String(tail), }); if (container !== 'all') { params.set('container', container); } const headers = await getAuthHeaders(); const response = await fetch(`/api/_internal/services/status/logs?${params}`, { headers }); return response.json(); } async function fetchContainers(): Promise { const headers = await getAuthHeaders(); const response = await fetch('/api/_internal/services/status/logs/containers', { headers }); return response.json(); } // Color code log lines based on content function getLogLineClass(line: string): string { const lowerLine = line.toLowerCase(); if (lowerLine.includes('error') || lowerLine.includes('fatal') || lowerLine.includes('panic')) { return 'text-destructive'; } if (lowerLine.includes('warn')) { return 'text-warning'; } if (lowerLine.includes('debug') || lowerLine.includes('trace')) { return 'text-gray-500'; } return ''; } // Extract container name from log line (format: "container-name | timestamp log...") function parseLogLine(line: string): { container?: string; content: string } { const match = line.match(/^([a-z0-9_-]+)\s+\|\s+(.*)$/i); if (match) { return { container: match[1], content: match[2] }; } return { content: line }; } const projectName = import.meta.env.VITE_PROJECT_NAME ?? '{{PROJECT_NAME}}'; function getGrafanaLogsUrl(): string | null { const monitoring = adminServices.find((s) => s.id === 'monitoring'); const base = monitoring ? getServiceUrl(monitoring) : null; return base && `${base}/d/${projectName}-logs/logs`; } interface DockerLogsProps { showGrafanaLink?: boolean; } export function DockerLogs({ showGrafanaLink = false }: DockerLogsProps) { const [selectedContainer, setSelectedContainer] = useState('all'); const [since, setSince] = useState('10m'); const [tail, setTail] = useState(200); const [autoScroll, setAutoScroll] = useState(false); const logsEndRef = useRef(null); const { data: containersData } = useQuery({ queryKey: ['docker-containers'], queryFn: fetchContainers, }); const { data: logsData, isLoading, isFetching, error, refetch, } = useQuery({ queryKey: ['docker-logs', selectedContainer, since, tail], queryFn: () => fetchLogs(selectedContainer, since, tail), refetchInterval: 10000, // Auto-refresh every 10 seconds staleTime: 5000, }); // Auto-scroll to bottom when new logs arrive useEffect(() => { if (autoScroll && logsEndRef.current && logsData?.logs) { logsEndRef.current.scrollIntoView({ behavior: 'smooth' }); } }, [logsData?.logs, autoScroll]); const containers = containersData?.containers || []; const logs = logsData?.logs || []; const selectedTimeOption = TIME_OPTIONS.find((o) => o.value === since); const selectedTailOption = TAIL_OPTIONS.find((o) => o.value === tail); const selectedContainerName = selectedContainer === 'all' ? 'All Containers' : containers.find((c) => c.container === selectedContainer)?.name || selectedContainer; const handleOpenGrafana = () => { const url = getGrafanaLogsUrl(); if (url) window.open(url, '_blank', 'noopener,noreferrer'); }; return ( <> {/* Header with filters and actions */}
{/* Filters */}
{/* Container filter */} {selectedContainerName} setSelectedContainer('all')}> All Containers {containers.map((c) => ( setSelectedContainer(c.container)} > {c.name} ))} {/* Time filter */} {selectedTimeOption?.label} {TIME_OPTIONS.map((option) => ( setSince(option.value)}> {option.label} ))} {/* Tail filter */} {selectedTailOption?.label} {TAIL_OPTIONS.map((option) => ( setTail(option.value)}> {option.label} ))} {/* Auto-scroll toggle */} {/* Refresh button */}
{/* Grafana link */} {showGrafanaLink && ( )}
{/* Terminal section - fills remaining space */} {isLoading ? (
) : error || (logsData && !logsData.success) ? (

Failed to load logs

{logsData?.error || 'Could not connect to Docker'}

) : logs.length === 0 ? (

No logs found for the selected filters

) : (
{logs.map((line, index) => { const { container, content } = parseLogLine(line); const lineKey = `${index}-${line.slice(0, 50)}`; return (
{container && [{container}]} {content}
); })}
)} ); }