feat: Рефакторинг модальных окон с заменой на простой подход со встроенным backdrop для улучшения UX. Обновление компонентов CommandPalette, ConfirmDialog, ConfirmDiffModal, ConfirmModal, FormModal, ImportModal, SettingsModal и WsUpdateModal для повышения удобства использования и унификации интерфейса.
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 1m9s
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 1m9s
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import Modal from './Modal';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
IconX,
|
||||
@@ -12,11 +11,11 @@ import {
|
||||
|
||||
/**
|
||||
* WsUpdateModal - модальное окно с логами WebSocket
|
||||
* Рефакторинг: использует базовый Modal, улучшенный UX
|
||||
* Простой подход со встроенным backdrop (как в HistoryModal)
|
||||
*/
|
||||
function WsUpdateModal({ show, url, onClose }) {
|
||||
const [rawMessages, setRawMessages] = useState([]);
|
||||
const [status, setStatus] = useState('connecting'); // connecting | open | closed | error
|
||||
const [status, setStatus] = useState('connecting');
|
||||
const wsRef = useRef(null);
|
||||
const bottomRef = useRef(null);
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
@@ -92,6 +91,8 @@ function WsUpdateModal({ show, url, onClose }) {
|
||||
return lastTs - startedAt.getTime();
|
||||
}, [startedAt, rawMessages]);
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
const copyLog = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(plainLog);
|
||||
@@ -117,87 +118,97 @@ function WsUpdateModal({ show, url, onClose }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={show}
|
||||
onClose={onClose}
|
||||
title={
|
||||
<div className="d-flex align-items-center">
|
||||
<IconPlugConnected className="me-2" size={24} />
|
||||
Логи запуска
|
||||
{getStatusBadge()}
|
||||
</div>
|
||||
}
|
||||
size="lg"
|
||||
scrollable={false}
|
||||
footer={
|
||||
<div className="d-flex justify-content-between align-items-center w-100">
|
||||
<div className="text-muted small d-flex align-items-center">
|
||||
<IconClock size={16} className="me-2" />
|
||||
<span>
|
||||
Начало {startedAt ? startedAt.toLocaleTimeString() : '—'}
|
||||
{elapsedMs != null && <> • Прошло {elapsedMs} ms</>}
|
||||
</span>
|
||||
</div>
|
||||
<button className="btn btn-secondary" onClick={onClose}>
|
||||
<IconX size={16} className="me-1" />
|
||||
Закрыть
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
<div
|
||||
className="modal show d-block"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}
|
||||
onKeyDown={(e) => { if (e.key === 'Escape') onClose?.(); }}
|
||||
>
|
||||
{/* Controls */}
|
||||
<div className="d-flex gap-2 mb-3">
|
||||
<button
|
||||
className="btn btn-outline-secondary btn-sm"
|
||||
onClick={() => setAutoScroll(!autoScroll)}
|
||||
title={autoScroll ? 'Отключить автопрокрутку' : 'Включить автопрокрутку'}
|
||||
>
|
||||
{autoScroll ? <IconPlayerPause size={16} /> : <IconPlayerPlay size={16} />}
|
||||
<span className="ms-1">Автопрокрутка</span>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
onClick={copyLog}
|
||||
title="Скопировать лог"
|
||||
>
|
||||
<IconCopy size={16} />
|
||||
<span className="ms-1">Копировать</span>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-danger btn-sm"
|
||||
onClick={clearLog}
|
||||
title="Очистить лог"
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
<span className="ms-1">Очистить</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-dialog modal-lg modal-dialog-centered" role="document">
|
||||
<div className="modal-content" tabIndex={-1}>
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title d-flex align-items-center">
|
||||
<IconPlugConnected className="me-2" size={24} />
|
||||
Логи запуска
|
||||
{getStatusBadge()}
|
||||
</h5>
|
||||
<button type="button" className="btn-close" onClick={onClose} />
|
||||
</div>
|
||||
|
||||
<div className="modal-body">
|
||||
{/* Controls */}
|
||||
<div className="d-flex gap-2 mb-3">
|
||||
<button
|
||||
className="btn btn-outline-secondary btn-sm"
|
||||
onClick={() => setAutoScroll(!autoScroll)}
|
||||
title={autoScroll ? 'Отключить автопрокрутку' : 'Включить автопрокрутку'}
|
||||
>
|
||||
{autoScroll ? <IconPlayerPause size={16} /> : <IconPlayerPlay size={16} />}
|
||||
<span className="ms-1">Автопрокрутка</span>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
onClick={copyLog}
|
||||
title="Скопировать лог"
|
||||
>
|
||||
<IconCopy size={16} />
|
||||
<span className="ms-1">Копировать</span>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-danger btn-sm"
|
||||
onClick={clearLog}
|
||||
title="Очистить лог"
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
<span className="ms-1">Очистить</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Log output */}
|
||||
<div
|
||||
className="bg-dark text-light p-3 rounded font-monospace"
|
||||
style={{
|
||||
maxHeight: '60vh',
|
||||
overflowY: 'auto',
|
||||
fontSize: '0.875rem',
|
||||
border: '1px solid rgba(255,255,255,0.08)'
|
||||
}}
|
||||
>
|
||||
{rawMessages.length === 0 ? (
|
||||
<div className="text-muted">Ожидание сообщений от сервера...</div>
|
||||
) : (
|
||||
rawMessages.map((m, i) => {
|
||||
const isErr = m?.json?.stream === 'stderr' || /\berror\b|\bошиб/i.test(String(m.text));
|
||||
return (
|
||||
<div key={i} className={`text-break ${isErr ? 'text-danger' : ''}`}>
|
||||
<span className="text-secondary">$</span> {m.text}
|
||||
{/* Log output */}
|
||||
<div
|
||||
className="bg-dark text-light p-3 rounded font-monospace"
|
||||
style={{
|
||||
maxHeight: '60vh',
|
||||
overflowY: 'auto',
|
||||
fontSize: '0.875rem',
|
||||
border: '1px solid rgba(255,255,255,0.08)'
|
||||
}}
|
||||
>
|
||||
{rawMessages.length === 0 ? (
|
||||
<div className="text-muted">Ожидание сообщений от сервера...</div>
|
||||
) : (
|
||||
rawMessages.map((m, i) => {
|
||||
const isErr = m?.json?.stream === 'stderr' || /\berror\b|\bошиб/i.test(String(m.text));
|
||||
return (
|
||||
<div key={i} className={`text-break ${isErr ? 'text-danger' : ''}`}>
|
||||
<span className="text-secondary">$</span> {m.text}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<div className="d-flex justify-content-between align-items-center w-100">
|
||||
<div className="text-muted small d-flex align-items-center">
|
||||
<IconClock size={16} className="me-2" />
|
||||
<span>
|
||||
Начало {startedAt ? startedAt.toLocaleTimeString() : '—'}
|
||||
{elapsedMs != null && <> • Прошло {elapsedMs} ms</>}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
<div ref={bottomRef} />
|
||||
<button className="btn btn-secondary" onClick={onClose}>
|
||||
<IconX size={16} className="me-1" />
|
||||
Закрыть
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user