import { useState, useEffect, useRef, useCallback } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { servicesApi } from '@/services/api'; import { Copy, Download, RefreshCw, X } from 'lucide-react'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; interface ServiceLogViewerProps { instanceName: string; serviceName: string; containers?: string[]; onClose: () => void; } export function ServiceLogViewer({ instanceName, serviceName, containers = [], onClose, }: ServiceLogViewerProps) { const [logs, setLogs] = useState([]); const [follow, setFollow] = useState(false); const [tail, setTail] = useState(100); const [container, setContainer] = useState(containers[0]); const [autoScroll, setAutoScroll] = useState(true); const logsEndRef = useRef(null); const logsContainerRef = useRef(null); const eventSourceRef = useRef(null); // Scroll to bottom when logs change and autoScroll is enabled useEffect(() => { if (autoScroll && logsEndRef.current) { logsEndRef.current.scrollIntoView({ behavior: 'smooth' }); } }, [logs, autoScroll]); // Fetch initial buffered logs const fetchLogs = useCallback(async () => { try { const url = servicesApi.getLogsUrl(instanceName, serviceName, tail, false, container); const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch logs: ${response.statusText}`); } const data = await response.json(); // API returns { lines: string[] } if (data.lines && Array.isArray(data.lines)) { setLogs(data.lines); } else { setLogs([]); } } catch (error) { console.error('Error fetching logs:', error); setLogs([`Error: ${error instanceof Error ? error.message : 'Failed to fetch logs'}`]); } }, [instanceName, serviceName, tail, container]); // Set up SSE streaming when follow is enabled useEffect(() => { if (follow) { const url = servicesApi.getLogsUrl(instanceName, serviceName, tail, true, container); const eventSource = new EventSource(url); eventSourceRef.current = eventSource; eventSource.onmessage = (event) => { const line = event.data; if (line && line.trim() !== '') { setLogs((prev) => [...prev, line]); } }; eventSource.onerror = (error) => { console.error('SSE error:', error); eventSource.close(); setFollow(false); }; return () => { eventSource.close(); }; } else { if (eventSourceRef.current) { eventSourceRef.current.close(); eventSourceRef.current = null; } } }, [follow, instanceName, serviceName, tail, container]); // Fetch initial logs on mount and when parameters change useEffect(() => { if (!follow) { fetchLogs(); } }, [fetchLogs, follow]); const handleCopyLogs = () => { const text = logs.join('\n'); navigator.clipboard.writeText(text); }; const handleDownloadLogs = () => { const text = logs.join('\n'); const blob = new Blob([text], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${serviceName}-logs.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; const handleClearLogs = () => { setLogs([]); }; const handleRefresh = () => { setLogs([]); fetchLogs(); }; return (
Service Logs: {serviceName}
{containers.length > 1 && (
)}
setFollow(e.target.checked)} className="rounded" />
setAutoScroll(e.target.checked)} className="rounded" />
{logs.length === 0 ? (
No logs available
) : ( logs.map((line, index) => (
{line}
)) )}
); }