feat(MikrotikBackupsManager): add side-by-side diff functionality for configuration comparison
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 2m47s

This commit is contained in:
2026-02-11 14:12:07 +07:00
parent ba6c7b9618
commit a2910832be
3 changed files with 513 additions and 12 deletions
+94 -12
View File
@@ -1,4 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
import * as Diff from 'diff';
import api from './lib/api.js';
import { useNotify } from './components/NotifyProvider.jsx';
import PageHeader from './components/PageHeader.jsx';
@@ -17,6 +18,77 @@ import {
IconSearch,
} from '@tabler/icons-react';
/**
* Строит две колонки строк для side-by-side diff (как git diff).
* leftLines/rightLines — массивы { text, type: 'unchanged'|'removed'|'added'|'empty' }.
*/
function getDiffLines(oldStr, newStr) {
const left = [];
const right = [];
const changes = Diff.diffLines(oldStr || '', newStr || '');
for (const part of changes) {
const lines = (part.value || '').split('\n');
if (part.value && part.value.endsWith('\n') && lines.length > 0) lines.pop();
for (const line of lines) {
if (part.added) {
left.push({ text: '', type: 'empty' });
right.push({ text: line, type: 'added' });
} else if (part.removed) {
left.push({ text: line, type: 'removed' });
right.push({ text: '', type: 'empty' });
} else {
left.push({ text: line, type: 'unchanged' });
right.push({ text: line, type: 'unchanged' });
}
}
}
return { left, right };
}
const diffPanelStyle = {
maxHeight: 360,
overflow: 'auto',
fontSize: 12,
fontFamily: 'ui-monospace, monospace',
margin: 0,
padding: '0.5rem 0.75rem',
borderRadius: 4,
backgroundColor: 'var(--tblr-bg-surface-secondary, #1e293b)',
};
const diffLineStyle = {
display: 'block',
minHeight: '1.25em',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
padding: '0 2px',
margin: 0,
};
const diffRemovedBg = { background: 'rgba(248, 81, 73, 0.25)' };
const diffAddedBg = { background: 'rgba(63, 185, 80, 0.25)' };
function DiffPanel({ lines, side }) {
return (
<pre style={diffPanelStyle}>
{lines.length === 0 ? (
<span style={diffLineStyle}># пусто</span>
) : (
lines.map((item, i) => (
<span
key={i}
style={{
...diffLineStyle,
...(item.type === 'removed' && side === 'left' ? diffRemovedBg : null),
...(item.type === 'added' && side === 'right' ? diffAddedBg : null),
}}
>
{item.type === 'empty' ? '\u00A0' : item.text || ''}
</span>
))
)}
</pre>
);
}
function formatDateTime(value) {
if (!value) return '';
try {
@@ -172,6 +244,13 @@ function MikrotikBackupsManager() {
return filteredBackupServers.slice(start, start + backupPageSize);
}, [filteredBackupServers, backupPage, backupTotalPages]);
const diffLines = useMemo(
() => (diffResult?.configA != null && diffResult?.configB != null
? getDiffLines(diffResult.configA, diffResult.configB)
: { left: [], right: [] }),
[diffResult?.configA, diffResult?.configB],
);
const handleBackupSort = (field) => {
setBackupSortField((prevField) => {
if (prevField === field) {
@@ -780,6 +859,13 @@ function MikrotikBackupsManager() {
{diffResult.same
? 'Конфигурации идентичны'
: 'Конфигурации отличаются — проверьте различия перед откатом.'}
{!diffResult.same && (
<span className="ms-2">
<span style={{ background: 'rgba(248, 81, 73, 0.35)', padding: '0 4px', borderRadius: 2 }}> удалено (A)</span>
{' · '}
<span style={{ background: 'rgba(63, 185, 80, 0.35)', padding: '0 4px', borderRadius: 2 }}> добавлено (B)</span>
</span>
)}
</div>
</div>
</div>
@@ -792,23 +878,19 @@ function MikrotikBackupsManager() {
<div className="mb-2 small text-muted text-truncate" title={diffResult.a?.key}>
A: {diffResult.a?.key}
</div>
<pre
className="mb-0"
style={{ maxHeight: 360, overflow: 'auto', fontSize: 12 }}
>
{diffResult.configA || '# пусто'}
</pre>
<DiffPanel
lines={diffLines.left}
side="left"
/>
</div>
<div className="col-md-6">
<div className="mb-2 small text-muted text-truncate" title={diffResult.b?.key}>
B: {diffResult.b?.key}
</div>
<pre
className="mb-0"
style={{ maxHeight: 360, overflow: 'auto', fontSize: 12 }}
>
{diffResult.configB || '# пусто'}
</pre>
<DiffPanel
lines={diffLines.right}
side="right"
/>
</div>
</div>
)}