"use client" import { useMemo, useRef, useState, type ComponentProps } from "react" import { HistoryIcon, LoaderCircleIcon, RotateCcwIcon } from "lucide-react" import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from "@/components/ui/sheet" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogMedia, AlertDialogTitle, } from "@/components/ui/alert-dialog" import { Button } from "@/components/ui/button" import { Badge } from "@/components/reui/badge" import { Frame, FramePanel } from "@/components/reui/frame" import { Timeline, TimelineContent, TimelineDate, TimelineHeader, TimelineIndicator, TimelineItem, TimelineSeparator, TimelineTitle, } from "@/components/reui/timeline" import { EmptyState } from "@/components/empty-state" import { Alert, AlertDescription, AlertTitle } from "@/components/reui/alert" import { TriangleAlertIcon } from "lucide-react" import type { ConfigRevisionDto } from "@/lib/config-revisions" const SOURCE_LABEL: Record = { apply: "Изменение", rollback: "Откат", observed: "С роутера", copy: "Копирование", } const SOURCE_BADGE: Record["variant"]> = { apply: "primary-light", rollback: "warning-light", observed: "secondary", copy: "success-light", } function formatWhen(iso: string): string { const d = new Date(iso) if (Number.isNaN(d.getTime())) return iso return d.toLocaleString("ru-RU", { dateStyle: "short", timeStyle: "short" }) } export function ConfigHistorySheet({ open, onOpenChange, title, itemLabel, revisions, loading, loadError, onRetry, restoring, onRestore, restoreDisabled = true, }: { open: boolean onOpenChange: (open: boolean) => void title: string itemLabel: string revisions: ConfigRevisionDto[] loading: boolean loadError?: string | null onRetry?: () => void restoring: boolean restoreDisabled: boolean onRestore: (id: string) => Promise | void }) { const [pending, setPending] = useState(null) const restoreLock = useRef(false) const [submitting, setSubmitting] = useState(false) const [restoreError, setRestoreError] = useState(null) const busy = restoring || submitting async function confirmRestore() { if (!pending || restoreLock.current || restoring || restoreDisabled) return restoreLock.current = true setSubmitting(true) setRestoreError(null) try { await onRestore(pending.id) setPending(null) } catch (error) { setRestoreError(error instanceof Error ? error.message : "Не удалось восстановить версию") } finally { restoreLock.current = false setSubmitting(false) } } const newestFirst = useMemo(() => revisions, [revisions]) return ( <> { if (restoreLock.current || restoring) return if (!v) { setPending(null) setRestoreError(null) } onOpenChange(v) }}> {title} Снапшоты managed-объектов. Откат применяет выбранную версию на CHR.
{restoreDisabled && !busy &&

Откат недоступен. Проверьте права на изменение и состояние выбранного устройства.

} {loading ? (
Загрузка истории…
) : loadError ? (

Не удалось загрузить историю: {loadError}

{onRetry && }
) : newestFirst.length === 0 ? ( } title="Истории пока нет" description="Снапшот появится после первого чтения или изменения на роутере" className="py-12" /> ) : ( {newestFirst.map((rev, idx) => ( {formatWhen(rev.createdAt)} {SOURCE_LABEL[rev.source]} {rev.itemCount} {itemLabel} ))} )}
{ if (!v && !restoreLock.current && !restoring) { setPending(null); setRestoreError(null) } }}> Откатить на эту версию? На CHR будут применены {pending?.itemCount ?? 0} {itemLabel} от{" "} {pending ? formatWhen(pending.createdAt) : ""}. Изменятся только объекты MikrotikManager Чужие правила и маршруты на роутере не удаляются. {busy &&

Ожидание результата восстановления…

} {restoreError &&

{restoreError}

} setPending(null)}>Отмена void confirmRestore()} > {busy ? : null} Откатить
) }