import { useEffect, useMemo, useRef, useState } from 'react'; import { IconX, IconPlayerPlay, IconPlayerPause, IconPlugConnected, IconCopy, IconTrash, IconClock } from '@tabler/icons-react'; /** * WsUpdateModal - модальное окно с логами WebSocket * Простой подход со встроенным backdrop (как в HistoryModal) */ function WsUpdateModal({ show, url, onClose }) { const [rawMessages, setRawMessages] = useState([]); const [status, setStatus] = useState('connecting'); const wsRef = useRef(null); const bottomRef = useRef(null); const [autoScroll, setAutoScroll] = useState(true); const [startedAt, setStartedAt] = useState(null); useEffect(() => { if (!show) return; try { const ws = new WebSocket(url); wsRef.current = ws; setStatus('connecting'); ws.onopen = () => setStatus('open'); ws.onmessage = (evt) => { const text = typeof evt.data === 'string' ? evt.data : ''; let parsed = null; let jsonStart = text.indexOf('{'); if (jsonStart >= 0) { const candidate = text.slice(jsonStart).trim(); try { parsed = JSON.parse(candidate); } catch { parsed = null; } } if (parsed) { if (parsed.event === 'start' && parsed.ts) { setStartedAt(new Date(parsed.ts)); return; } if (typeof parsed.line === 'string' && parsed.line.trim().length > 0) { setRawMessages((prev) => [...prev, { text: parsed.line, json: parsed }]); return; } return; } const clean = text.startsWith('$ ') ? text.slice(2) : text; if (clean.trim().length === 0) return; setRawMessages((prev) => [...prev, { text: clean, json: null }]); }; ws.onerror = () => setStatus('error'); ws.onclose = () => setStatus('closed'); } catch (_e) { setStatus('error'); } return () => { try { wsRef.current?.close(); } catch { /* no-op */ } wsRef.current = null; }; }, [show, url]); useEffect(() => { if (autoScroll) { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); } }, [rawMessages, autoScroll]); const plainLog = useMemo( () => rawMessages.map(m => (typeof m.text === 'string' ? m.text : '')).join('\n'), [rawMessages] ); const elapsedMs = useMemo(() => { if (!startedAt || rawMessages.length === 0) return null; const lastTs = Date.now(); return lastTs - startedAt.getTime(); }, [startedAt, rawMessages]); if (!show) return null; const copyLog = async () => { try { await navigator.clipboard.writeText(plainLog); } catch { /* no-op */ } }; const clearLog = () => { setRawMessages([]); }; const getStatusBadge = () => { const variants = { open: 'bg-green-lt text-green', connecting: 'bg-orange-lt text-orange', error: 'bg-red-lt text-red', closed: 'bg-secondary-lt text-secondary' }; return ( {status} ); }; const handleBackdropClick = (e) => { if (e.target === e.currentTarget) { onClose?.(); } }; return ( <> {/* Modal Backdrop */}
{/* Modal */}