import React from 'react'; import { LogProps } from './LogTypes'; import { AutoScroll } from '../AutoScroll'; import { formatUnixTimestamp } from '../../../utils/time'; import styles from './Log.module.css'; import { useLanguage } from '../../../providers/language/LanguageContext'; // Helper to get status badge class const getStatusBadgeClass = (status?: string | null): string => { if (!status) return styles.statusBadge; switch (status.toLowerCase()) { case 'completed': return `${styles.statusBadge} ${styles.statusCompleted}`; case 'failed': case 'error': return `${styles.statusBadge} ${styles.statusFailed}`; case 'running': return `${styles.statusBadge} ${styles.statusRunning}`; default: return styles.statusBadge; } }; const Log: React.FC = ({ className = '', emptyMessage = 'Keine Log-Informationen verfügbar', dashboardTree, onToggleOperationExpanded, getChildOperations }) => { const { t } = useLanguage(); const resolvedEmptyMessage = typeof emptyMessage === 'string' ? t(emptyMessage, emptyMessage) : emptyMessage; const formatLogTimestamp = (timestamp: number): string => { try { const formatted = formatUnixTimestamp(timestamp, undefined, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); return formatted.time; } catch { return new Date(timestamp * 1000).toLocaleString(); } }; // Render operation node recursively const renderOperationNode = (operationId: string, depth: number = 0): React.ReactNode => { if (!dashboardTree || !getChildOperations) { return null; } const operation = dashboardTree.operations.get(operationId); if (!operation) { return null; } // Get logs for this operation, sorted by timestamp const logsArray = Array.from(operation.logs.values()).sort((a, b) => { const tsA = a.timestamp || 0; const tsB = b.timestamp || 0; return tsA - tsB; // Ascending order (oldest first) }); // Get latest log for timestamp const latestLog = logsArray.length > 0 ? logsArray[logsArray.length - 1] : null; // Skip rendering if no logs yet if (logsArray.length === 0) { return null; } // Get child operations const childOperations = getChildOperations(operationId); const hasChildren = childOperations.length > 0; const hasLogs = logsArray.length > 0; const hasContentToExpand = hasChildren || hasLogs; // Calculate progress percentage let progressPercentage = 0; if (operation.latestProgress !== null && operation.latestProgress !== undefined) { progressPercentage = Math.min(Math.max(operation.latestProgress * 100, 0), 100); } // Force 100% progress when status is 'completed' if (operation.latestStatus === 'completed') { progressPercentage = 100; } // Use stable operation name (from first log) or fallback to operationId const operationName = operation.operationName || `${t('Operation')} ${operationId}`; // Use latest message as status tag (updates with each poll) const latestMessage = operation.latestMessage || ''; const operationStatus = operation.latestStatus || 'running'; const operationTimestamp = latestLog?.timestamp; // Calculate consistent indentation per level (24px per level) const hasIndent = depth > 0; // Calculate log entry indentation to align with operation name // Operation name starts at: header padding-left (12px) + button/spacer (20px) + gap (8px) = 40px from operationContent // operationLogsList has margin-left: 12px (for border), so log entries are at: container marginLeft + 12px // We want log entries at 40px from operationContent, so: container marginLeft + 12px = 40px // Therefore: container marginLeft = 28px from operationContent // But operationNode has paddingLeft: 12px for indented nodes, 0 for root // So from operationNode: container marginLeft = 28px - operationNode.paddingLeft // Root: 28px - 0 = 28px // Indented: 28px - 12px = 16px const logIndentPx = hasIndent ? 16 : 28; // Calculate header indentation to match message indentation // Headers are inside operationNode which has paddingLeft: 12px (for indented) // Messages container has marginLeft: logIndentPx from operationNode, and list has margin-left: 12px // So messages start at: logIndentPx + 12px from operationNode's left edge // Headers start at: operationNode marginLeft + paddingLeft = headerIndentPx + 12px // To align: headerIndentPx + 12px = logIndentPx + 12px, so headerIndentPx = logIndentPx // But headers have their own padding (12px), so header content starts at headerIndentPx + 12px + 12px // Messages start at logIndentPx + 12px, so we need headerIndentPx = logIndentPx - 12px to align content // Actually, we want the header box to align with messages, so headerIndentPx should account for header padding const headerIndentPx = logIndentPx; // Headers and messages both use logIndentPx, paddingLeft handles alignment return (
{/* Operation content */}
{hasContentToExpand && ( )} {!hasContentToExpand && } {operationName} {/* Latest status message tag (updates with each poll) */} {latestMessage && ( {latestMessage} )} {operationTimestamp && ( {formatLogTimestamp(operationTimestamp)} )} {operationStatus} {progressPercentage > 0 && ( {Math.round(progressPercentage)}% )}
{progressPercentage > 0 && (
= 100 ? styles.progressCompleted : ''}`} style={{ width: `${progressPercentage}%` }} />
)}
{/* Show logs and children when expanded */} {operation.expanded && ( <> {/* Log messages for this operation - show only latest log */} {latestLog && (
{formatLogTimestamp(latestLog.timestamp)} {latestLog.message} {latestLog.status && ( {latestLog.status} )} {latestLog.progress !== undefined && latestLog.progress !== null && ( {Math.round(latestLog.progress * 100)}% )}
)} {/* Child operations */} {hasChildren && (
{childOperations.map((childOpId) => renderOperationNode(childOpId, depth + 1))}
)} )}
); }; // Render dashboard tree const renderDashboard = (): React.ReactNode => { if (!dashboardTree || !getChildOperations) { return null; } if (dashboardTree.rootOperations.length === 0) { return (
{resolvedEmptyMessage}
); } return (
{dashboardTree.rootOperations.map((rootOpId) => renderOperationNode(rootOpId, 0))}
); }; // Check if we have dashboard logs to display const hasDashboardLogs = dashboardTree && dashboardTree.rootOperations.length > 0; if (!hasDashboardLogs) { return (
{resolvedEmptyMessage}
); } return (
{renderDashboard()}
); }; export default Log;