refactor(MikrotikBackupsManager): optimize diff computation and synchronize scrolling between panels
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 3m25s

This commit is contained in:
2026-02-11 15:51:05 +07:00
parent 562f09753b
commit bdc7dd2bb3
+65 -17
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import * as Diff from 'diff'; import * as Diff from 'diff';
import api from './lib/api.js'; import api from './lib/api.js';
import { useNotify } from './components/NotifyProvider.jsx'; import { useNotify } from './components/NotifyProvider.jsx';
@@ -75,9 +75,9 @@ const diffAddedBg = {
borderLeft: '3px solid rgba(63, 185, 80, 0.9)', borderLeft: '3px solid rgba(63, 185, 80, 0.9)',
}; };
function DiffPanel({ lines, side }) { function DiffPanel({ lines, side, innerRef, onScroll }) {
return ( return (
<pre style={diffPanelStyle}> <pre style={diffPanelStyle} ref={innerRef} onScroll={onScroll}>
{lines.length === 0 ? ( {lines.length === 0 ? (
<span style={diffLineStyle}># пусто</span> <span style={diffLineStyle}># пусто</span>
) : ( ) : (
@@ -141,6 +141,9 @@ function MikrotikBackupsManager() {
const [diffResult, setDiffResult] = useState(null); const [diffResult, setDiffResult] = useState(null);
const [diffLoading, setDiffLoading] = useState(false); const [diffLoading, setDiffLoading] = useState(false);
const [diffModalOpen, setDiffModalOpen] = useState(false); const [diffModalOpen, setDiffModalOpen] = useState(false);
const leftDiffRef = useRef(null);
const rightDiffRef = useRef(null);
const isSyncingScrollRef = useRef(false);
// UI-состояние выбора сервера для просмотра бэкапов (в стиле /filters) // UI-состояние выбора сервера для просмотра бэкапов (в стиле /filters)
const [serverSearch, setServerSearch] = useState(''); const [serverSearch, setServerSearch] = useState('');
@@ -254,12 +257,41 @@ function MikrotikBackupsManager() {
return filteredBackupServers.slice(start, start + backupPageSize); return filteredBackupServers.slice(start, start + backupPageSize);
}, [filteredBackupServers, backupPage, backupTotalPages]); }, [filteredBackupServers, backupPage, backupTotalPages]);
const diffLines = useMemo( const diffComputed = useMemo(() => {
() => (diffResult?.configA != null && diffResult?.configB != null if (diffResult?.configA == null || diffResult?.configB == null) {
? getDiffLines(diffResult.configA, diffResult.configB) return {
: { left: [], right: [] }), left: [],
[diffResult?.configA, diffResult?.configB], right: [],
); stats: { removed: 0, added: 0, total: 0 },
};
}
const { left, right } = getDiffLines(diffResult.configA, diffResult.configB);
const removed = left.filter((l) => l.type === 'removed').length;
const added = right.filter((l) => l.type === 'added').length;
return {
left,
right,
stats: { removed, added, total: removed + added },
};
}, [diffResult?.configA, diffResult?.configB]);
const syncScroll = (source) => {
const leftEl = leftDiffRef.current;
const rightEl = rightDiffRef.current;
if (!leftEl || !rightEl) return;
const current = source === 'left' ? leftEl : rightEl;
const other = source === 'left' ? rightEl : leftEl;
if (isSyncingScrollRef.current) return;
isSyncingScrollRef.current = true;
const maxCurrent = current.scrollHeight - current.clientHeight;
const ratio = maxCurrent > 0 ? current.scrollTop / maxCurrent : 0;
const maxOther = other.scrollHeight - other.clientHeight;
other.scrollTop = ratio * (maxOther > 0 ? maxOther : 0);
isSyncingScrollRef.current = false;
};
const handleBackupSort = (field) => { const handleBackupSort = (field) => {
setBackupSortField((prevField) => { setBackupSortField((prevField) => {
@@ -891,20 +923,34 @@ function MikrotikBackupsManager() {
</div> </div>
) : ( ) : (
<> <>
{!diffResult.same && ( <div className="alert alert-info mb-3 small d-flex justify-content-between align-items-center">
<div className="alert alert-info mb-3 small"> <span>
<IconAlertTriangle size={16} className="me-1 text-warning" /> {!diffResult.same ? (
Конфигурации отличаются слева A, справа B. Проверьте различия перед откатом. <>
</div> <IconAlertTriangle size={16} className="me-1 text-warning" />
)} Конфигурации отличаются слева A, справа B. Проверьте различия перед откатом.
</>
) : (
'Конфигурации идентичны — изменений нет.'
)}
</span>
<span>
Всего изменений:{' '}
<span className="fw-semibold">{diffComputed.stats.total}</span>
<span className="text-success ms-2">+{diffComputed.stats.added}</span>
<span className="text-danger ms-2">{diffComputed.stats.removed}</span>
</span>
</div>
<div className="row"> <div className="row">
<div className="col-md-6 mb-3 mb-md-0"> <div className="col-md-6 mb-3 mb-md-0">
<div className="mb-2 small text-muted text-truncate" title={diffResult.a?.key}> <div className="mb-2 small text-muted text-truncate" title={diffResult.a?.key}>
A: {diffResult.a?.key} A: {diffResult.a?.key}
</div> </div>
<DiffPanel <DiffPanel
lines={diffLines.left} lines={diffComputed.left}
side="left" side="left"
innerRef={leftDiffRef}
onScroll={() => syncScroll('left')}
/> />
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
@@ -912,8 +958,10 @@ function MikrotikBackupsManager() {
B: {diffResult.b?.key} B: {diffResult.b?.key}
</div> </div>
<DiffPanel <DiffPanel
lines={diffLines.right} lines={diffComputed.right}
side="right" side="right"
innerRef={rightDiffRef}
onScroll={() => syncScroll('right')}
/> />
</div> </div>
</div> </div>