import { useState } from 'react'; import api from '../lib/api.js'; import { useQuery } from '@tanstack/react-query'; import ConfirmDialog from './ConfirmDialog.jsx'; import { useNotify } from './NotifyProvider.jsx'; import { IconHistory, IconRefresh, IconInfoCircle, IconRotate2, IconClock } from '@tabler/icons-react'; import { formatDateTimeWithRelative } from '../lib/datetime.js'; /** * HistoryModal - модальное окно истории версий * Простой подход со встроенным backdrop */ export default function HistoryModal({ resource, show, onClose, onRolledBack }) { const [loading, setLoading] = useState(false); const [items, setItems] = useState([]); const notify = useNotify(); const { refetch } = useQuery({ queryKey: ['history', resource], enabled: false, queryFn: async () => { const res = await api.get(`/history/${resource}`); const data = Array.isArray(res.data?.items) ? res.data.items : []; setItems(data); return data; }, }); const [confirmState, setConfirmState] = useState({ open: false, onConfirm: null }); const rollback = async (versionId) => { if (!versionId) return; setConfirmState({ open: true, onConfirm: async () => { setConfirmState({ open: false, onConfirm: null }); setLoading(true); try { const res = await api.post(`/history/${resource}/rollback`, { versionId }); notify.success('Откат выполнен успешно'); onRolledBack?.(res.data || {}); onClose?.(); } catch (_e) { notify.error('Не удалось выполнить откат'); } finally { setLoading(false); } } }); }; if (!show) return null; if (show && items.length === 0) { refetch().catch(() => notify.error('Не удалось загрузить историю версий')); } const handleBackdropClick = (e) => { if (e.target === e.currentTarget) { onClose?.(); } }; return ( <> {/* Modal Backdrop */}
{/* Modal */}
{ if (e.key === 'Escape') onClose?.(); }} >
{ if (e.key === 'Tab') { const c = e.currentTarget; const focusable = c.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); if (!focusable || focusable.length === 0) return; const first = focusable[0]; const last = focusable[focusable.length - 1]; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } } }} >
История версий
{/* Info bar */}
Ресурс: {resource}
{/* Table */}
{items.length === 0 ? ( ) : ( items.map((v) => ( )) )}
Версия Дата Размер ETag Действия

Нет данных

Версионирование бакета может быть отключено

{v.versionId} {v.isLatest && ( Текущая )} {v.lastModified ? formatDateTimeWithRelative(v.lastModified) : '—'} {typeof v.size === 'number' ? `${v.size} байт` : '—'} {v.etag || '—'} {!v.isLatest && ( )}
setConfirmState({ open: false, onConfirm: null })} onConfirm={confirmState.onConfirm} /> ); }