"use client" import { useEffect, useState } from "react" import { api, Dataset, SambaShare, NfsShare, Pool } from "@/lib/api" import { Header } from "@/components/Header" import { Button } from "@/components/ui/button" import { Dialog } from "@/components/ui/dialog" import { Plus, Trash2, RefreshCw, ChevronRight, ChevronDown, MoreVertical, Camera } from "lucide-react" import { Snapshot } from "@/lib/api" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" export default function DatasetsPage() { const [tab, setTab] = useState<"datasets" | "shares">("datasets") const [datasets, setDatasets] = useState([]) const [pools, setPools] = useState([]) const [sambaShares, setSambaShares] = useState([]) const [nfsShares, setNfsShares] = useState([]) const [loading, setLoading] = useState(true) const [expandedDatasets, setExpandedDatasets] = useState>(new Set()) const [poolTabs, setPoolTabs] = useState>(new Map()) const [poolSnapshots, setPoolSnapshots] = useState>(new Map()) const [loadingSnaps, setLoadingSnaps] = useState>(new Set()) const [snapContextMenu, setSnapContextMenu] = useState<{ snap: Snapshot; x: number; y: number } | null>(null) const [renameTarget, setRenameTarget] = useState(null) const [renameValue, setRenameValue] = useState("") const [cloneTarget, setCloneTarget] = useState(null) const [cloneValue, setCloneValue] = useState("") // Dialogs const [showCreateDataset, setShowCreateDataset] = useState(false) const [showCreateSambaShare, setShowCreateSambaShare] = useState(false) const [showCreateNfsShare, setShowCreateNfsShare] = useState(false) const [deleteDataset, setDeleteDataset] = useState(null) const [deleteSambaShare, setDeleteSambaShare] = useState(null) const [deleteNfsShare, setDeleteNfsShare] = useState(null) // Form states const [newDatasetName, setNewDatasetName] = useState("") const [newSambaName, setNewSambaName] = useState("") const [newSambaPath, setNewSambaPath] = useState("") const [newSambaComment, setNewSambaComment] = useState("") const [newNfsPath, setNewNfsPath] = useState("") const [newNfsClients, setNewNfsClients] = useState("") const [newNfsOptions, setNewNfsOptions] = useState("ro,sync,no_subtree_check") useEffect(() => { loadData() }, []) const loadData = async () => { setLoading(true) try { const [ds, samba, nfs, poolData] = await Promise.all([ api.getDatasets(), api.getSambaShares(), api.getNfsShares(), api.getPools().catch(() => ({ pools: [] })), ]) setDatasets(ds) setSambaShares(samba) setNfsShares(nfs) setPools((poolData as { pools: Pool[] }).pools || []) } catch (err) { console.error("Failed to load data:", err) } finally { setLoading(false) } } const loadPoolSnapshots = async (poolName: string) => { setLoadingSnaps((s) => new Set(s).add(poolName)) try { const snaps = await api.getSnapshots(poolName, 200) setPoolSnapshots((m) => new Map(m).set(poolName, snaps)) } catch (err) { console.error("Failed to load snapshots:", err) } finally { setLoadingSnaps((s) => { const n = new Set(s); n.delete(poolName); return n }) } } const handleSnapTab = (poolName: string) => { setPoolTabs(new Map(poolTabs).set(poolName, "snapshots")) if (!poolSnapshots.has(poolName)) loadPoolSnapshots(poolName) } const handleSnapAction = async (action: string, snap: Snapshot) => { setSnapContextMenu(null) if (action === "rollback") { if (!confirm(`Roll back ${snap.dataset} to ${snap.name.split("@")[1]}? This destroys data after this snapshot!`)) return await api.rollbackSnapshot(snap.name) loadPoolSnapshots(snap.name.split("@")[0].split("/")[0]) } else if (action === "destroy") { if (!confirm(`Destroy snapshot ${snap.name.split("@")[1]}?`)) return await api.deleteSnapshot(snap.name) const pool = snap.name.split("@")[0].split("/")[0] loadPoolSnapshots(pool) } else if (action === "rename") { setRenameValue(snap.name.split("@")[1]) setRenameTarget(snap) } else if (action === "clone") { setCloneValue(`${snap.dataset}-clone`) setCloneTarget(snap) } } const handleRenameConfirm = async () => { if (!renameTarget || !renameValue.trim()) return await api.renameSnapshot(renameTarget.name, renameValue.trim()) loadPoolSnapshots(renameTarget.name.split("@")[0].split("/")[0]) setRenameTarget(null) } const handleCloneConfirm = async () => { if (!cloneTarget || !cloneValue.trim()) return await api.cloneSnapshot(cloneTarget.name, cloneValue.trim()) loadData() setCloneTarget(null) } const handleCreateDataset = async () => { if (!newDatasetName.trim()) return try { // Create dataset via API await api.createDataset(newDatasetName, {}) setNewDatasetName("") setShowCreateDataset(false) loadData() } catch (err) { console.error("Failed to create dataset:", err) } } const handleDeleteDataset = async (name: string) => { try { await api.deleteDataset(name) setDeleteDataset(null) loadData() } catch (err) { console.error("Failed to delete dataset:", err) } } const handleCreateSambaShare = async () => { if (!newSambaName.trim() || !newSambaPath.trim()) return try { await api.createSambaShare({ name: newSambaName, path: newSambaPath, comment: newSambaComment || undefined, }) setNewSambaName("") setNewSambaPath("") setNewSambaComment("") setShowCreateSambaShare(false) loadData() } catch (err) { console.error("Failed to create Samba share:", err) } } const handleDeleteSambaShare = async (name: string) => { try { await api.deleteSambaShare(name) setDeleteSambaShare(null) loadData() } catch (err) { console.error("Failed to delete Samba share:", err) } } const handleCreateNfsShare = async () => { if (!newNfsPath.trim() || !newNfsClients.trim()) return try { await api.createNfsShare({ path: newNfsPath, clients: newNfsClients, options: newNfsOptions || undefined, }) setNewNfsPath("") setNewNfsClients("") setNewNfsOptions("ro,sync,no_subtree_check") setShowCreateNfsShare(false) loadData() } catch (err) { console.error("Failed to create NFS share:", err) } } const handleDeleteNfsShare = async (path: string) => { try { await api.deleteNfsShare(path) setDeleteNfsShare(null) loadData() } catch (err) { console.error("Failed to delete NFS share:", err) } } const formatBytes = (bytes: number) => { if (bytes === 0) return "0 B" const k = 1024 const sizes = ["B", "KB", "MB", "GB", "TB"] const i = Math.floor(Math.log(bytes) / Math.log(k)) return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i] } const getDatasetDepth = (name: string): number => { return name.split("/").length - 1 } const getTopLevelDatasets = (): Dataset[] => { return datasets.filter((ds) => ds.name.split("/").length === 1) } const getPoolStats = (poolName: string) => { const poolDatasets = datasets.filter((ds) => ds.name === poolName || ds.name.startsWith(poolName + "/")) const totalUsed = poolDatasets.reduce((sum, ds) => sum + (ds.used || 0), 0) const totalAvail = poolDatasets[0]?.avail || 0 const totalSize = totalUsed + totalAvail const usagePercent = totalSize > 0 ? (totalUsed / totalSize) * 100 : 0 return { totalUsed, totalAvail, totalSize, usagePercent } } const getChildDatasets = (parent: string): Dataset[] => { const prefix = parent + "/" return datasets.filter((ds) => ds.name.startsWith(prefix) && ds.name !== parent) } const toggleExpand = (name: string) => { const newExpanded = new Set(expandedDatasets) if (newExpanded.has(name)) { newExpanded.delete(name) } else { newExpanded.add(name) } setExpandedDatasets(newExpanded) } const renderDatasetTree = (parent?: string): React.ReactNode[] => { const items: React.ReactNode[] = [] const datasetList = parent ? getChildDatasets(parent) : getTopLevelDatasets() datasetList.forEach((ds) => { const children = getChildDatasets(ds.name) const isExpanded = expandedDatasets.has(ds.name) const depth = getDatasetDepth(ds.name) items.push(
{children.length > 0 && ( )} {children.length === 0 &&
} {ds.name.split("/").pop()}
{ds.type} {formatBytes(ds.used || 0)} {ds.mountpoint || "—"} {ds.compression || "off"} ) if (isExpanded && children.length > 0) { items.push(...renderDatasetTree(ds.name)) } }) return items } if (loading) { return
Loading...
} return (

Datasets & Shares

{/* Tab Navigation */}
{/* Datasets Tab */} {tab === "datasets" && (
{getTopLevelDatasets().map((pool) => { const stats = getPoolStats(pool.name) const poolInfo = pools.find((p) => p.name === pool.name) const currentPoolTab = poolTabs.get(pool.name) || "filesystems" const childDatasets = getChildDatasets(pool.name) return ( {/* Pool Header */}
{pool.name} ONLINE
{/* Pool Stats Grid */}

Size

{formatBytes(poolInfo?.size || 0)}

Allocated

{formatBytes(poolInfo?.alloc || 0)}

Free

{formatBytes(poolInfo?.free || 0)}

Fragmentation

{poolInfo?.fragmentation || "0%"}

Usage

{stats.usagePercent.toFixed(1)}%

{/* Usage Bar */} {(() => { const pct = poolInfo?.size ? (poolInfo.alloc / poolInfo.size) * 100 : 0 return (

{pct.toFixed(2)}% Allocated • {(100 - pct).toFixed(2)}% Free

) })()} {/* Tabs */}
{/* File Systems Tab */} {currentPoolTab === "filesystems" && (
{childDatasets.map((ds) => ( ))}
Name Type Used Available Mountpoint Compression
{pool.name} {pool.type} {formatBytes(pool.used || 0)} {formatBytes(pool.avail || 0)} {pool.mountpoint || "—"} {pool.compression || "off"}
{ds.name.split("/").pop()} {ds.type} {formatBytes(ds.used || 0)} {formatBytes(ds.avail || 0)} {ds.mountpoint || "—"} {ds.compression || "off"}
)} {/* Snapshots Tab */} {currentPoolTab === "snapshots" && (() => { const snaps = poolSnapshots.get(pool.name) || [] const isLoading = loadingSnaps.has(pool.name) return (
{snaps.length} snapshots
{isLoading ? (
Loading…
) : snaps.length === 0 ? (
No snapshots
) : (() => { // Group snapshots by dataset const groups = new Map() snaps.forEach((s) => { const ds = s.dataset || s.name.split("@")[0] if (!groups.has(ds)) groups.set(ds, []) groups.get(ds)!.push(s) }) const expandKey = `snapgroup-${pool.name}` return (
{Array.from(groups.entries()).map(([ds, dsSnaps]) => { const isGroupExpanded = expandedDatasets.has(`${expandKey}-${ds}`) return [ // Group header row { const next = new Set(expandedDatasets) if (isGroupExpanded) next.delete(`${expandKey}-${ds}`) else next.add(`${expandKey}-${ds}`) setExpandedDatasets(next) }} > , // Snapshot rows (expanded) ...(isGroupExpanded ? dsSnaps.map((snap) => ( )) : []) ] })}
Name Created Used Referenced Clones
{isGroupExpanded ? : } {ds} {dsSnaps.length}
{snap.name.split("@")[1]} {snap.creation_datetime ? new Date(snap.creation_datetime).toLocaleString() : "—"} {formatBytes(snap.used)} {formatBytes(snap.referenced)}
) })()}
) })()} {/* Status Tab */} {currentPoolTab === "status" && (

Health

ONLINE

Mounted

Yes

Record Size

128 KiB

)}
) })} {datasets.length === 0 && (
No datasets found
)}
)} {/* Shares Tab */} {tab === "shares" && (

Samba Shares

{sambaShares.map((share) => ( ))}
Name Path Comment Action
{share.name} {share.path} {share.comment || "—"}
{sambaShares.length === 0 && (
No Samba shares
)}

NFS Shares

{nfsShares.map((share) => ( ))}
Path Clients Options Action
{share.path} {share.clients} {share.options || "—"}
{nfsShares.length === 0 && (
No NFS shares
)}
)}
{/* Create Dataset Dialog */} setShowCreateDataset(false)} title="Create Dataset" > setNewDatasetName(e.target.value)} className="w-full border border-input rounded px-3 py-2 mb-4 bg-background text-foreground" />
{/* Delete Dataset Dialog */} setDeleteDataset(null)} title="Delete Dataset" >

Are you sure you want to delete {deleteDataset}? This cannot be undone.

{/* Create Samba Share Dialog */} setShowCreateSambaShare(false)} title="Create Samba Share" > setNewSambaName(e.target.value)} className="w-full border border-input rounded px-3 py-2 mb-3 bg-background text-foreground text-sm" /> setNewSambaPath(e.target.value)} className="w-full border border-input rounded px-3 py-2 mb-3 bg-background text-foreground text-sm" /> setNewSambaComment(e.target.value)} className="w-full border border-input rounded px-3 py-2 mb-4 bg-background text-foreground text-sm" />
{/* Delete Samba Share Dialog */} setDeleteSambaShare(null)} title="Delete Samba Share" >

Are you sure you want to delete {deleteSambaShare}?

{/* Create NFS Share Dialog */} setShowCreateNfsShare(false)} title="Create NFS Share" > setNewNfsPath(e.target.value)} className="w-full border border-input rounded px-3 py-2 mb-3 bg-background text-foreground text-sm" /> setNewNfsClients(e.target.value)} className="w-full border border-input rounded px-3 py-2 mb-3 bg-background text-foreground text-sm" /> setNewNfsOptions(e.target.value)} className="w-full border border-input rounded px-3 py-2 mb-4 bg-background text-foreground text-sm" />
{/* Delete NFS Share Dialog */} setDeleteNfsShare(null)} title="Delete NFS Share" >

Are you sure you want to delete {deleteNfsShare}?

{/* Snapshot Context Menu */} {snapContextMenu && ( <>
setSnapContextMenu(null)} />
{[ { action: "clone", label: "Clone Snapshot" }, { action: "rename", label: "Rename Snapshot" }, { action: "rollback", label: "Roll Back Snapshot" }, { action: "destroy", label: "Destroy Snapshot", danger: true }, ].map(({ action, label, danger }) => ( ))}
)} {/* Rename Snapshot Dialog */} setRenameTarget(null)} title="Rename Snapshot">

New name for {renameTarget?.name.split("@")[1]}

setRenameValue(e.target.value)} className="w-full border border-input rounded px-3 py-2 mb-4 bg-background text-foreground font-mono" onKeyDown={(e) => e.key === "Enter" && handleRenameConfirm()} />
{/* Clone Snapshot Dialog */} setCloneTarget(null)} title="Clone Snapshot">

Clone {cloneTarget?.name.split("@")[1]} to:

setCloneValue(e.target.value)} placeholder="target dataset name" className="w-full border border-input rounded px-3 py-2 mb-4 bg-background text-foreground font-mono" onKeyDown={(e) => e.key === "Enter" && handleCloneConfirm()} />
) }