feat: Add history endpoints for resource versioning and implement rollback functionality in server, along with UI integration in ASNs, Domains, and IPRanges managers for enhanced data management
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 4m45s

This commit is contained in:
2025-08-11 17:29:41 +07:00
parent 460c06dfba
commit 6f45015bec
5 changed files with 189 additions and 3 deletions
+104
View File
@@ -0,0 +1,104 @@
import { useEffect, useState } from 'react';
import api from '../lib/api.js';
import { useNotify } from './NotifyProvider.jsx';
import { IconHistory, IconRefresh, IconDeviceFloppy } from '@tabler/icons-react';
export default function HistoryModal({ resource, show, onClose, onRolledBack }) {
const [loading, setLoading] = useState(false);
const [items, setItems] = useState([]);
const notify = useNotify();
const fetchHistory = async () => {
if (!resource) return;
setLoading(true);
try {
const res = await api.get(`/history/${resource}`);
setItems(Array.isArray(res.data?.items) ? res.data.items : []);
} catch (e) {
notify.error('Не удалось загрузить историю версий');
} finally {
setLoading(false);
}
};
useEffect(() => { if (show) fetchHistory(); }, [show, resource]);
const rollback = async (versionId) => {
if (!versionId) return;
if (!confirm('Откатить к выбранной версии? Текущее содержимое будет перезаписано.')) return;
setLoading(true);
try {
const res = await api.post(`/history/${resource}/rollback`, { versionId });
notify.success('Откат выполнен');
onRolledBack && onRolledBack(res.data || {});
onClose && onClose();
} catch (e) {
notify.error('Не удалось выполнить откат');
} finally {
setLoading(false);
}
};
if (!show) return null;
return (
<div className="modal show d-block" tabIndex="-1" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
<div className="modal-dialog modal-lg modal-dialog-centered" role="document">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title d-flex align-items-center">
<IconHistory className="me-2" /> История версий
</h5>
<button type="button" className="btn-close" onClick={onClose}></button>
</div>
<div className="modal-body">
<div className="d-flex justify-content-between align-items-center mb-2">
<div className="text-muted small">Ресурс: <code>{resource}</code></div>
<button className="btn btn-outline-secondary btn-sm" onClick={fetchHistory} disabled={loading}>
<IconRefresh className={loading ? 'spin' : ''} />
<span className="ms-1">Обновить</span>
</button>
</div>
<div className="table-responsive">
<table className="table card-table table-vcenter table-nowrap mb-0">
<thead>
<tr>
<th>Версия</th>
<th>Дата</th>
<th>Размер</th>
<th>ETag</th>
<th className="text-end">Действия</th>
</tr>
</thead>
<tbody>
{items.length === 0 ? (
<tr><td colSpan="5" className="text-muted text-center py-4">Нет данных (возможно, версионирование бакета отключено)</td></tr>
) : items.map((v) => (
<tr key={v.versionId} className={v.isLatest ? 'table-info' : ''}>
<td><code>{v.versionId}</code></td>
<td>{v.lastModified ? new Date(v.lastModified).toLocaleString() : '—'}</td>
<td>{typeof v.size === 'number' ? `${v.size} байт` : '—'}</td>
<td><code>{v.etag || '—'}</code></td>
<td className="text-end">
{!v.isLatest && (
<button className="btn btn-outline-primary btn-sm" onClick={() => rollback(v.versionId)} disabled={loading}>
<IconDeviceFloppy className="me-1" /> Откатить к этой версии
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div className="modal-footer">
<button className="btn" onClick={onClose}>Закрыть</button>
</div>
</div>
</div>
</div>
);
}