"use client" import { useEffect, useState, useCallback } from "react" import { useRouter } from "next/navigation" import { api, Snapshot, Dataset } from "@/lib/api" import { Header } from "@/components/Header" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Dialog } from "@/components/ui/dialog" import { RefreshCw, AlertCircle, Trash2, Plus, RotateCcw, ChevronRight, ChevronDown } from "lucide-react" import { formatBytes } from "@/lib/utils" function formatUnix(ts: number) { return new Date(ts * 1000).toLocaleString() } export default function SnapshotsPage() { const router = useRouter() const [snapshots, setSnapshots] = useState([]) const [datasets, setDatasets] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [deleting, setDeleting] = useState(null) const [filterDataset, setFilterDataset] = useState("") // Create dialog const [createOpen, setCreateOpen] = useState(false) const [createDataset, setCreateDataset] = useState("") const [createName, setCreateName] = useState("") const [creating, setCreating] = useState(false) // Rollback dialog const [rollbackTarget, setRollbackTarget] = useState(null) const [rollingBack, setRollingBack] = useState(false) const [expandedDatasets, setExpandedDatasets] = useState>(new Set()) const fetchSnapshots = useCallback(async (dataset?: string) => { try { setLoading(true) setError(null) const data = await api.getSnapshots(dataset || undefined) setSnapshots(data) } catch (err) { setError(err instanceof Error ? err.message : "Failed to fetch snapshots") } finally { setLoading(false) } }, []) useEffect(() => { const token = localStorage.getItem("access_token") if (!token) { router.push("/login"); return } fetchSnapshots() api.getDatasets().then(setDatasets).catch(() => {}) const iv = setInterval(() => fetchSnapshots(filterDataset || undefined), 60000) return () => clearInterval(iv) }, [router, fetchSnapshots, filterDataset]) const handleFilterChange = (ds: string) => { setFilterDataset(ds) fetchSnapshots(ds || undefined) } const handleDelete = async (name: string) => { if (!confirm(`Delete snapshot "${name}"?\nThis cannot be undone.`)) return try { setDeleting(name) await api.deleteSnapshot(name) setSnapshots((prev) => prev.filter((s) => s.name !== name)) } catch (err) { setError(err instanceof Error ? err.message : "Failed to delete snapshot") } finally { setDeleting(null) } } const handleCreate = async () => { if (!createDataset) return setCreating(true) try { await api.createSnapshot(createDataset, createName || undefined) setCreateOpen(false) setCreateDataset("") setCreateName("") fetchSnapshots(filterDataset || undefined) } catch (err) { setError(err instanceof Error ? err.message : "Failed to create snapshot") } finally { setCreating(false) } } const handleRollback = async () => { if (!rollbackTarget) return setRollingBack(true) try { await api.rollbackSnapshot(rollbackTarget) setRollbackTarget(null) fetchSnapshots(filterDataset || undefined) } catch (err) { setError(err instanceof Error ? err.message : "Rollback failed") } finally { setRollingBack(false) } } // Unique dataset names for filter dropdown const datasetNames = Array.from( new Set(snapshots.map((s) => s.name.split("@")[0]).filter(Boolean)) ).sort() const getDatasetDepth = (name: string): number => { return name.split("/").length - 1 } const getSnapshotsByDataset = (dsName: string): Snapshot[] => { return snapshots.filter((s) => s.name.split("@")[0] === dsName) } const getTopLevelDatasets = (): string[] => { const topLevel = new Set() snapshots.forEach((snap) => { const dsName = snap.name.split("@")[0] const topDsName = dsName.split("/")[0] topLevel.add(topDsName) }) return Array.from(topLevel).sort() } const getAllDatasetsByPrefix = (prefix?: string): string[] => { const allDs = new Set() snapshots.forEach((snap) => { const dsName = snap.name.split("@")[0] if (!prefix) { if (dsName.split("/").length === 1) allDs.add(dsName) } else { const dsPrefix = prefix + "/" if (dsName.startsWith(dsPrefix) && dsName !== prefix) { const remaining = dsName.slice(dsPrefix.length) if (remaining.split("/").length === 1) { allDs.add(dsName) } } } }) return Array.from(allDs).sort() } const toggleExpand = (name: string) => { const newExpanded = new Set(expandedDatasets) if (newExpanded.has(name)) { newExpanded.delete(name) } else { newExpanded.add(name) } setExpandedDatasets(newExpanded) } const renderSnapshotTree = (parentDs?: string): React.ReactNode[] => { const items: React.ReactNode[] = [] const datasets = parentDs ? getAllDatasetsByPrefix(parentDs) : getTopLevelDatasets() datasets.forEach((dsName) => { const childDatasets = getAllDatasetsByPrefix(dsName) const isExpanded = expandedDatasets.has(dsName) const depth = getDatasetDepth(dsName) const snapshotsForDs = getSnapshotsByDataset(dsName) snapshotsForDs.forEach((snap, idx) => { const [, tag] = snap.name.split("@") items.push(
{idx === 0 && childDatasets.length > 0 && ( )} {(idx > 0 || childDatasets.length === 0) &&
} {idx === 0 ? dsName.split("/").pop() : ""}
{tag} {formatUnix(snap.created)} {formatBytes(snap.used)} {formatBytes(snap.referenced)}
) }) if (isExpanded && childDatasets.length > 0) { items.push(...renderSnapshotTree(dsName)) } }) return items } return (
{/* Page Header */}

Snapshots

Manage ZFS snapshots

{/* Filter */}
{snapshots.length} snapshots
{/* Error */} {error && (
{error}
)} {/* Loading */} {loading && snapshots.length === 0 && (

Loading snapshots…

)} {/* Table */} {snapshots.length > 0 && ( Snapshots
{renderSnapshotTree()}
Dataset Snapshot Created Used Referenced Actions
)} {!loading && snapshots.length === 0 && !error && ( No snapshots found. )}
{/* Create Snapshot Dialog */} setCreateOpen(false)} title="Create Snapshot">
setCreateName(e.target.value)} placeholder="e.g. before-upgrade" className="w-full text-sm bg-background border border-border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-ring" />
{/* Rollback Dialog */} setRollbackTarget(null)} title="Rollback to Snapshot" >
Warning: Rollback will permanently destroy all data written after this snapshot. This cannot be undone.

Roll back to: {rollbackTarget}?

) }