From 10a86232fc592d1013705e02318be98d7d10f038 Mon Sep 17 00:00:00 2001 From: Denis Shatskiy Date: Tue, 12 Aug 2025 22:48:18 +0700 Subject: [PATCH] feat: Enhance WsUpdateModal with improved message handling, auto-scroll functionality, and log management features for better user experience --- frontend/src/components/WsUpdateModal.jsx | 92 +++++++++++++++++------ 1 file changed, 70 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/WsUpdateModal.jsx b/frontend/src/components/WsUpdateModal.jsx index c085f14..a2c8be6 100644 --- a/frontend/src/components/WsUpdateModal.jsx +++ b/frontend/src/components/WsUpdateModal.jsx @@ -1,11 +1,13 @@ -import { useEffect, useRef, useState } from 'react'; -import { IconX, IconPlayerPlay, IconPlugConnected } from '@tabler/icons-react'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { IconX, IconPlayerPlay, IconPlayerPause, IconPlugConnected, IconCopy, IconTrash, IconClock } from '@tabler/icons-react'; function WsUpdateModal({ show, url, onClose }) { - const [messages, setMessages] = useState([]); + const [rawMessages, setRawMessages] = useState([]); const [status, setStatus] = useState('connecting'); // connecting | open | closed | error const wsRef = useRef(null); const bottomRef = useRef(null); + const [autoScroll, setAutoScroll] = useState(true); + const [startedAt, setStartedAt] = useState(null); useEffect(() => { if (!show) return; @@ -16,7 +18,20 @@ function WsUpdateModal({ show, url, onClose }) { ws.onopen = () => setStatus('open'); ws.onmessage = (evt) => { const text = typeof evt.data === 'string' ? evt.data : '[binary]'; - setMessages((prev) => [...prev, text]); + // Пробуем распарсить JSON после префикса "$ " + let parsed = null; + let clean = text; + if (text.startsWith('$ ')) { + const jsonPart = text.slice(2); + try { + parsed = JSON.parse(jsonPart); + clean = parsed?.line || parsed?.event || jsonPart; + if (parsed?.event === 'start' && parsed?.ts) { + setStartedAt(new Date(parsed.ts)); + } + } catch {} + } + setRawMessages((prev) => [...prev, { text: clean, json: parsed }]); }; ws.onerror = () => setStatus('error'); ws.onclose = () => setStatus('closed'); @@ -30,8 +45,16 @@ function WsUpdateModal({ show, url, onClose }) { }, [show, url]); useEffect(() => { - bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [messages, show]); + if (autoScroll) bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [rawMessages, show, 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; @@ -41,26 +64,51 @@ function WsUpdateModal({ show, url, onClose }) {
- Online-обновление BGP + Логи запуска {status}
- -
-
-
-
- {messages.length === 0 ? ( -
Ожидание сообщений от сервера...
- ) : ( - messages.map((m, i) => ( -
$ {m}
- )) - )} -
-
+
+ + + +
-
+
+
+ {rawMessages.length === 0 ? ( +
Ожидание сообщений от сервера...
+ ) : ( + rawMessages.map((m, i) => { + const isErr = m?.json?.stream === 'stderr' || /\berror\b|\bошиб/i.test(String(m.text)); + return ( +
+ $ {m.text} +
+ ); + }) + )} +
+
+
+
+
+ + + Начало {startedAt ? startedAt.toLocaleTimeString() : '—'} + {elapsedMs != null && ( + <> + , прошло {elapsedMs} ms + + )} + +