'use client'; import { useState, useEffect, useRef, useCallback } from 'react'; import { parseDeploymentLogs, DeploymentLog, DeploymentStatus } from '@/lib/deployments'; import { usePortal } from '@/lib/PortalContext'; import { Icon } from './Icons'; interface DeploymentLogsProps { deploymentUuid: string; status: DeploymentStatus; initialLogs?: string; } function getLogLineStyle(log: DeploymentLog): string { const output = log.output.toLowerCase(); if (log.type === 'stderr' || output.includes('error') || output.includes('failed')) { return 'text-red-500 dark:text-red-400'; } if (output.includes('warning') || output.includes('warn')) { return 'text-yellow-600 dark:text-yellow-400'; } if (output.includes('success') || output.includes('finished') || output.includes('done') || output.includes('✓')) { return 'text-green-600 dark:text-green-400'; } if (output.startsWith('---') || output.startsWith('===') || output.startsWith('###')) { return 'text-cyan-600 dark:text-cyan-400 font-semibold'; } if (output.startsWith('$') || output.startsWith('>') || output.startsWith('#')) { return 'text-purple-600 dark:text-purple-400'; } return 'text-slate-600 dark:text-stone-400'; } export function DeploymentLogs({ deploymentUuid, status, initialLogs }: DeploymentLogsProps) { const { activeDeployLogs } = usePortal(); const [logs, setLogs] = useState(() => parseDeploymentLogs(initialLogs)); const isActive = status === 'in_progress' || status === 'queued'; const [copied, setCopied] = useState(false); const [isExpanded, setIsExpanded] = useState(false); const containerRef = useRef(null); const logsEndRef = useRef(null); const [autoScroll, setAutoScroll] = useState(true); // Update logs from SSE active deploy logs useEffect(() => { if (!isActive) return; const match = activeDeployLogs.find(l => l.uuid === deploymentUuid); if (match?.logs) { setLogs(parseDeploymentLogs(match.logs)); } }, [activeDeployLogs, deploymentUuid, isActive]); // Fetch logs on first render if none provided const fetchLogs = useCallback(async () => { try { const response = await fetch(`/api/deployments/${deploymentUuid}`); if (response.ok) { const data = await response.json(); setLogs(parseDeploymentLogs(data.logs)); } } catch (error) { console.error('Failed to fetch logs:', error); } }, [deploymentUuid]); useEffect(() => { if (!initialLogs && !isActive) { fetchLogs(); } }, [initialLogs, isActive, fetchLogs]); // Auto-scroll useEffect(() => { if (autoScroll && containerRef.current) { containerRef.current.scrollTop = containerRef.current.scrollHeight; } }, [logs, autoScroll]); const handleScroll = () => { if (!containerRef.current) return; const { scrollTop, scrollHeight, clientHeight } = containerRef.current; setAutoScroll(scrollHeight - scrollTop - clientHeight < 100); }; const copyToClipboard = async () => { const text = logs.map((log) => log.output).join('\n'); try { await navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (error) { console.error('Failed to copy:', error); } }; // Expanded modal view if (isExpanded) { return (
{ e.stopPropagation(); setIsExpanded(false); }} >
e.stopPropagation()} >
Build Logs - {deploymentUuid.substring(0, 12)}
{isActive && ( Live )}
{logs.length === 0 ? (
{isActive ? 'Waiting for logs...' : 'No logs available'}
) : ( logs.map((log, index) => (
{index + 1} {log.timestamp && ( {log.timestamp} )} {log.output}
)) )}
{logs.length} lines
); } // Inline view return (
Build Logs
{isActive && ( Live )}
e.stopPropagation()} className="h-64 overflow-y-auto overflow-x-hidden font-mono text-xs p-4 space-y-0.5" > {logs.length === 0 ? (
{isActive ? 'Waiting for logs...' : 'No logs available'}
) : ( logs.map((log, index) => (
{index + 1} {log.output}
)) )}
{!autoScroll && logs.length > 0 && ( )}
); }