/
h0tnanny
/
IotPlatform
Обзор
Документация
Войти
/
h0tnanny
/
IotPlatform
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
client/src/components/WorkflowEditor/ConsolePanel.tsx
225 строк
8 KB
h0tnanny
pop-up окно для логов, а также скрытие окна состояние выполнения
09 мар 2026, 22:59
09 мар 2026, 22:59
1a449ca
Код
Авторство
О чём код?
import { useEffect, useRef, useState, useCallback } from 'react'; import { Terminal, Trash2, Maximize2, X, Download } from 'lucide-react'; import styles from './ConsolePanel.module.scss'; interface LogEntry { timestamp: number; level: 'info' | 'success' | 'warning' | 'error'; message: string; nodeId?: string; } interface ConsolePanelProps { logs: LogEntry[]; isRunning: boolean; onClear: () => void; } export function ConsolePanel({ logs, isRunning, onClear }: ConsolePanelProps) { const logContainerRef = useRef<HTMLDivElement>(null); const popupLogRef = useRef<HTMLDivElement>(null); const [isPopupOpen, setIsPopupOpen] = useState(false); const [filterLevel, setFilterLevel] = useState<string>('all'); useEffect(() => { if (logContainerRef.current) { logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; } }, [logs]); useEffect(() => { if (popupLogRef.current) { popupLogRef.current.scrollTop = popupLogRef.current.scrollHeight; } }, [logs, isPopupOpen]); // Закрытие popup по Escape useEffect(() => { if (!isPopupOpen) return; const handleEscape = (e: KeyboardEvent) => { if (e.key === 'Escape') setIsPopupOpen(false); }; document.addEventListener('keydown', handleEscape); document.body.style.overflow = 'hidden'; return () => { document.removeEventListener('keydown', handleEscape); document.body.style.overflow = ''; }; }, [isPopupOpen]); const formatTimestamp = (timestamp: number) => { const date = new Date(timestamp); const timeString = date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit', }); const ms = date.getMilliseconds().toString().padStart(3, '0'); return `${timeString}.${ms}`; }; const getStatusClass = () => { if (isRunning) return 'running'; if (logs.some(log => log.level === 'error')) return 'error'; return 'idle'; }; const getStatusText = () => { if (isRunning) return 'Выполняется...'; if (logs.length === 0) return 'Ожидание'; return 'Готов'; }; const filteredLogs = filterLevel === 'all' ? logs : logs.filter(log => log.level === filterLevel); const exportLogs = useCallback(() => { const text = logs .map(log => `[${formatTimestamp(log.timestamp)}] [${log.level.toUpperCase()}] ${log.message}`) .join('\n'); const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `workflow-logs-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.txt`; a.click(); URL.revokeObjectURL(url); }, [logs]); const levelCounts = { info: logs.filter(l => l.level === 'info').length, success: logs.filter(l => l.level === 'success').length, warning: logs.filter(l => l.level === 'warning').length, error: logs.filter(l => l.level === 'error').length, }; const renderLogEntries = (entries: LogEntry[], refProp?: React.RefObject<HTMLDivElement | null>) => ( <div className={styles.logContainer} ref={refProp}> {entries.length === 0 ? ( <div className={styles.emptyState}> <Terminal /> <p>{filterLevel === 'all' ? 'Логи выполнения workflow появятся здесь' : 'Нет записей с выбранным уровнем'}</p> </div> ) : ( entries.map((log, index) => ( <div key={index} className={`${styles.logEntry} ${styles[log.level]}`}> <span className={styles.timestamp}>{formatTimestamp(log.timestamp)}</span> <span className={styles.levelBadge} data-level={log.level}> {log.level.toUpperCase()} </span> <span className={styles.message}>{log.message}</span> </div> )) )} </div> ); return ( <> <div className={styles.consolePanel}> <div className={styles.header}> <h3> <Terminal /> Консоль вывода </h3> <div className={styles.actions}> <button onClick={() => setIsPopupOpen(true)} disabled={logs.length === 0} title="Развернуть логи" > <Maximize2 size={14} /> Развернуть </button> <button onClick={onClear} disabled={logs.length === 0}> <Trash2 size={14} /> Очистить </button> </div> </div> <div className={styles.logContainer} ref={logContainerRef}> {logs.length === 0 ? ( <div className={styles.emptyState}> <Terminal /> <p>Логи выполнения workflow появятся здесь</p> </div> ) : ( logs.map((log, index) => ( <div key={index} className={`${styles.logEntry} ${styles[log.level]}`}> <span className={styles.timestamp}>{formatTimestamp(log.timestamp)}</span> <span className={styles.message}>{log.message}</span> </div> )) )} </div> <div className={styles.statusBar}> <div className={styles.status}> <div className={`${styles.indicator} ${styles[getStatusClass()]}`} /> <span>{getStatusText()}</span> </div> <div className={styles.count}> {logs.length} {logs.length === 1 ? 'запись' : 'записей'} </div> </div> </div> {isPopupOpen && ( <div className={styles.popupOverlay} onClick={() => setIsPopupOpen(false)}> <div className={styles.popupModal} onClick={(e) => e.stopPropagation()}> <div className={styles.popupHeader}> <div className={styles.popupTitle}> <Terminal size={20} /> <h2>Журнал выполнения</h2> <div className={styles.popupStats}> {levelCounts.error > 0 && ( <span className={styles.statError}>{levelCounts.error} ошибок</span> )} {levelCounts.warning > 0 && ( <span className={styles.statWarning}>{levelCounts.warning} предупр.</span> )} <span className={styles.statTotal}>{logs.length} записей</span> </div> </div> <div className={styles.popupActions}> <div className={styles.filterGroup}> {(['all', 'info', 'success', 'warning', 'error'] as const).map(level => ( <button key={level} className={`${styles.filterBtn} ${filterLevel === level ? styles.filterActive : ''}`} data-level={level} onClick={() => setFilterLevel(level)} > {level === 'all' ? 'Все' : level === 'info' ? 'Инфо' : level === 'success' ? 'Успех' : level === 'warning' ? 'Предупр.' : 'Ошибки'} </button> ))} </div> <button className={styles.popupBtn} onClick={exportLogs} title="Экспорт в файл"> <Download size={16} /> </button> <button className={styles.popupCloseBtn} onClick={() => setIsPopupOpen(false)} title="Закрыть"> <X size={20} /> </button> </div> </div> {renderLogEntries(filteredLogs, popupLogRef)} <div className={styles.popupFooter}> <div className={styles.status}> <div className={`${styles.indicator} ${styles[getStatusClass()]}`} /> <span>{getStatusText()}</span> </div> <span> {filterLevel !== 'all' && `${filteredLogs.length} из `}{logs.length} записей </span> </div> </div> </div> )} </> ); }