From feda8a9477cbe89fd45751fd14f8f5265d3bd616 Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 4 Jun 2026 23:31:32 +0200 Subject: [PATCH] =?UTF-8?q?Revert=20"Refactor:=20Shares-Funktionen=20in=20?= =?UTF-8?q?Datasets-Seite=20zusammengef=C3=BChrt"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 55a6c38e0242cc63e13898ecfbac67c8fac72311. --- frontend/app/datasets/page.tsx | 925 +++++++++++++++------------------ frontend/app/shares/page.tsx | 515 +++++++++++++++++- frontend/components/Header.tsx | 8 +- 3 files changed, 935 insertions(+), 513 deletions(-) diff --git a/frontend/app/datasets/page.tsx b/frontend/app/datasets/page.tsx index e3b9da2..0a12f24 100644 --- a/frontend/app/datasets/page.tsx +++ b/frontend/app/datasets/page.tsx @@ -1,26 +1,21 @@ "use client" import { useEffect, useState } from "react" -import { useRouter } from "next/navigation" -import { api, Dataset, Pool } from "@/lib/api" +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, AlertCircle } from "lucide-react" +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" -import CreateSambaDialog from "@/components/shares/CreateSambaDialog" -import CreateNfsDialog from "@/components/shares/CreateNfsDialog" -import DeleteConfirmDialog from "@/components/shares/DeleteConfirmDialog" export default function DatasetsPage() { - const router = useRouter() const [tab, setTab] = useState<"datasets" | "shares">("datasets") - - // ── Datasets state ────────────────────────────────────────────────────────── 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()) @@ -32,46 +27,39 @@ export default function DatasetsPage() { const [cloneTarget, setCloneTarget] = useState(null) const [cloneValue, setCloneValue] = useState("") - // Dataset dialogs + // Dialogs const [showCreateDataset, setShowCreateDataset] = useState(false) + const [showCreateSambaShare, setShowCreateSambaShare] = useState(false) + const [showCreateNfsShare, setShowCreateNfsShare] = useState(false) const [deleteDataset, setDeleteDataset] = useState(null) - const [newDatasetName, setNewDatasetName] = useState("") + const [deleteSambaShare, setDeleteSambaShare] = useState(null) + const [deleteNfsShare, setDeleteNfsShare] = useState(null) - // ── Shares state (from shares/page.tsx) ───────────────────────────────────── - const [sharesTab, setSharesTab] = useState<"samba" | "nfs" | "config">("samba") - const [sambaShares, setSambaShares] = useState([]) - const [nfsShares, setNfsShares] = useState([]) - const [sambaConfig, setSambaConfig] = useState([]) - const [sharesLoading, setSharesLoading] = useState(false) - const [sharesError, setSharesError] = useState(null) - const [showSambaDialog, setShowSambaDialog] = useState(false) - const [showNfsDialog, setShowNfsDialog] = useState(false) - const [deleteConfirm, setDeleteConfirm] = useState<{ type: "samba" | "nfs"; name: string } | null>(null) - const [deleting, setDeleting] = useState(false) - const [editMode, setEditMode] = useState(false) - const [rawConfigText, setRawConfigText] = useState("") - const [saving, setSaving] = useState(false) - const [macosEnabled, setMacosEnabled] = useState(false) - const [editingShare, setEditingShare] = 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(() => { - const token = localStorage.getItem("access_token") - if (!token) { - router.push("/login") - return - } loadData() - }, [router]) + }, []) - // ── Load datasets + pools ─────────────────────────────────────────────────── const loadData = async () => { setLoading(true) try { - const [ds, poolData] = await Promise.all([ + const [ds, samba, nfs, poolData] = await Promise.all([ api.getDatasets(), + api.getSambaShares(), + api.getNfsShares(), api.getPools().catch(() => []), ]) setDatasets(ds) + setSambaShares(samba) + setNfsShares(nfs) setPools(Array.isArray(poolData) ? poolData : []) } catch (err) { console.error("Failed to load data:", err) @@ -80,46 +68,6 @@ export default function DatasetsPage() { } } - // ── Load shares ───────────────────────────────────────────────────────────── - const loadShares = async () => { - try { - setSharesLoading(true) - setSharesError(null) - - const [samba, nfs, config] = await Promise.all([ - api.getSambaShares().catch(() => []), - api.getNfsShares().catch(() => []), - api.getSambaConfig().catch(() => ({ parameters: [] })), - ]) - - setSambaShares(samba) - setNfsShares(nfs) - const params: { key: string; value: string }[] = config.parameters || [] - setSambaConfig(params) - const raw = params.map((p) => `${p.key} = ${p.value}`).join("\n") - setRawConfigText(raw) - const hasMacOS = ["fruit:encoding", "fruit:metadata", "fruit:zero_file_id", "fruit:nfs_aces"].every( - (k) => params.some((p) => p.key === k) - ) - setMacosEnabled(hasMacOS) - } catch (err) { - setSharesError(err instanceof Error ? err.message : "Failed to load shares") - } finally { - setSharesLoading(false) - } - } - - // Lazy-load shares when the tab is first activated - const [sharesLoaded, setSharesLoaded] = useState(false) - const handleSharesTab = () => { - setTab("shares") - if (!sharesLoaded) { - setSharesLoaded(true) - loadShares() - } - } - - // ── Snapshot helpers ──────────────────────────────────────────────────────── const loadPoolSnapshots = async (poolName: string) => { setLoadingSnaps((s) => new Set(s).add(poolName)) try { @@ -171,10 +119,10 @@ export default function DatasetsPage() { setCloneTarget(null) } - // ── Dataset CRUD ───────────────────────────────────────────────────────────── const handleCreateDataset = async () => { if (!newDatasetName.trim()) return try { + // Create dataset via API await api.createDataset(newDatasetName, {}) setNewDatasetName("") setShowCreateDataset(false) @@ -194,118 +142,62 @@ export default function DatasetsPage() { } } - // ── Share CRUD ─────────────────────────────────────────────────────────────── - const handleDeleteSamba = async (name: string) => { + 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 { - setDeleting(true) await api.deleteSambaShare(name) - setSambaShares(sambaShares.filter((s) => s.name !== name)) - setDeleteConfirm(null) + setDeleteSambaShare(null) + loadData() } catch (err) { - setSharesError(err instanceof Error ? err.message : "Failed to delete share") - } finally { - setDeleting(false) + console.error("Failed to delete Samba share:", err) } } - const handleDeleteNfs = async (path: string) => { + 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 { - setDeleting(true) await api.deleteNfsShare(path) - setNfsShares(nfsShares.filter((s) => s.path !== path)) - setDeleteConfirm(null) + setDeleteNfsShare(null) + loadData() } catch (err) { - setSharesError(err instanceof Error ? err.message : "Failed to delete share") - } finally { - setDeleting(false) + console.error("Failed to delete NFS share:", err) } } - const handleSambaCreated = (newShare: any) => { - setSambaShares([...sambaShares, newShare]) - setShowSambaDialog(false) - } - - const handleNfsCreated = (newShare: any) => { - setNfsShares([...nfsShares, newShare]) - setShowNfsDialog(false) - } - - const handleSaveShare = async () => { - if (!editingShare) return - try { - setSaving(true) - await api.updateSambaShare(editingShare.oldName, { - name: editingShare.name, - path: editingShare.path, - comment: editingShare.comment, - }) - setSambaShares(sambaShares.map((s) => - s.name === editingShare.oldName - ? { name: editingShare.name, path: editingShare.path, comment: editingShare.comment, ...s } - : s - )) - setEditingShare(null) - setSharesError(null) - } catch (err) { - setSharesError(err instanceof Error ? err.message : "Failed to save share") - } finally { - setSaving(false) - } - } - - const MACOS_PARAMS: { [key: string]: string } = { - "fruit:encoding": "native", - "fruit:metadata": "stream", - "fruit:zero_file_id": "yes", - "fruit:nfs_aces": "no", - } - const MACOS_KEYS = Object.keys(MACOS_PARAMS) - - const handleSaveRaw = async () => { - try { - setSaving(true) - const parsed: { [key: string]: string } = {} - rawConfigText.split("\n").forEach((line) => { - const eq = line.indexOf("=") - if (eq === -1) return - const key = line.slice(0, eq).trim() - const value = line.slice(eq + 1).trim() - if (key) parsed[key] = value - }) - await api.setSambaConfig(parsed) - await loadShares() - setEditMode(false) - setSharesError(null) - } catch (err) { - setSharesError(err instanceof Error ? err.message : "Failed to save configuration") - } finally { - setSaving(false) - } - } - - const handleToggleMacOS = async (enable: boolean) => { - try { - setSaving(true) - const current = sambaConfig.reduce((acc, p) => { acc[p.key] = p.value; return acc }, {} as { [key: string]: string }) - if (enable) { - await api.setSambaConfig({ ...current, ...MACOS_PARAMS } as { [key: string]: string }) - } else { - const filtered: { [key: string]: string } = {} - Object.entries(current).forEach(([k, v]) => { if (!MACOS_KEYS.includes(k)) filtered[k] = v as string }) - await api.setSambaConfig(filtered) - } - setMacosEnabled(enable) - await loadShares() - setSharesError(null) - } catch (err) { - setSharesError(err instanceof Error ? err.message : "Failed to update macOS settings") - } finally { - setSaving(false) - } - } - - // ── Dataset tree helpers ───────────────────────────────────────────────────── const formatBytes = (bytes: number) => { if (bytes === 0) return "0 B" const k = 1024 @@ -314,13 +206,82 @@ export default function DatasetsPage() { return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i] } - const getTopLevelDatasets = (): Dataset[] => datasets.filter((ds) => ds.name.split("/").length === 1) + const getDatasetDepth = (name: string): number => { + return name.split("/").length - 1 + } + + const getTopLevelDatasets = (): Dataset[] => { + return datasets.filter((ds) => ds.name.split("/").length === 1) + } + 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...
} @@ -330,13 +291,13 @@ export default function DatasetsPage() {
-

Datasets & Shares

-
- {/* Top-level Tab Navigation */} + {/* Tab Navigation */}
- {/* ── DATASETS TAB ────────────────────────────────────────────────────── */} + {/* Datasets Tab */} {tab === "datasets" && (
@@ -377,6 +338,7 @@ export default function DatasetsPage() { return ( + {/* Pool Header */}
@@ -391,6 +353,7 @@ export default function DatasetsPage() {
+ {/* Pool Stats Grid */}

Size

@@ -414,6 +377,7 @@ export default function DatasetsPage() {
+ {/* Usage Bar */} {(() => { const pct = poolInfo?.size ? (poolInfo.alloc / poolInfo.size) * 100 : 0 return ( @@ -430,6 +394,7 @@ export default function DatasetsPage() { })()} + {/* Tabs */}
@@ -466,6 +431,7 @@ export default function DatasetsPage() {
+ {/* File Systems Tab */} {currentPoolTab === "filesystems" && (
@@ -505,6 +471,7 @@ export default function DatasetsPage() { )} + {/* Snapshots Tab */} {currentPoolTab === "snapshots" && (() => { const snaps = poolSnapshots.get(pool.name) || [] const isLoading = loadingSnaps.has(pool.name) @@ -528,6 +495,7 @@ export default function DatasetsPage() { ) : snaps.length === 0 ? (
No snapshots
) : (() => { + // Group snapshots by dataset const groups = new Map() snaps.forEach((s) => { const ds = s.dataset || s.name.split("@")[0] @@ -552,6 +520,7 @@ export default function DatasetsPage() { {Array.from(groups.entries()).map(([ds, dsSnaps]) => { const isGroupExpanded = expandedDatasets.has(`${expandKey}-${ds}`) return [ + // Group header row , + // Snapshot rows (expanded) ...(isGroupExpanded ? dsSnaps.map((snap) => ( @@ -601,6 +571,7 @@ export default function DatasetsPage() { ) })()} + {/* Status Tab */} {currentPoolTab === "status" && (
@@ -628,313 +599,110 @@ export default function DatasetsPage() {
)} - {/* ── SHARES & CONFIG TAB ─────────────────────────────────────────────── */} + {/* Shares Tab */} {tab === "shares" && (
- {sharesError && ( - - - -
-

Error

-

{sharesError}

-
-
-
- )} - - {/* Sub-tab navigation */} -
-
- - - +
+

Samba Shares

+
+
+ +
+
{snap.name.split("@")[1]}
+ + + + + + + + + + {sambaShares.map((share) => ( + + + + + + + ))} + +
NamePathCommentAction
{share.name}{share.path}{share.comment || "—"} + +
+
+ + {sambaShares.length === 0 && ( +
+ No Samba shares +
+ )}
- {/* SAMBA SUB-TAB */} - {sharesTab === "samba" && ( - - -
- Samba Shares - -
-
- - {sharesLoading ? ( -

Loading…

- ) : sambaShares.length === 0 ? ( -

- No Samba shares configured. Create one to get started. -

- ) : ( -
- - - - - - - - - - - - - {sambaShares.map((share) => { - const isEditing = editingShare?.oldName === share.name - return ( - - - - - - - - - ) - })} - -
NamePathUsersPermsCommentActions
- {isEditing ? ( - setEditingShare({ ...editingShare, name: e.target.value })} - className="px-2 py-1 rounded border border-border bg-background text-xs font-mono w-full" - disabled={saving} - /> - ) : ( - {share.name} - )} - - {isEditing ? ( - setEditingShare({ ...editingShare, path: e.target.value })} - className="px-2 py-1 rounded border border-border bg-background text-xs font-mono w-full" - disabled={saving} - /> - ) : ( - {share.path} - )} - {share.valid_users || "—"} - - {share.read_only ? "RO" : "RW"} - - - {isEditing ? ( - setEditingShare({ ...editingShare, comment: e.target.value })} - className="px-2 py-1 rounded border border-border bg-background text-xs w-full" - disabled={saving} - placeholder="Optional comment" - /> - ) : ( - share.comment || "—" - )} - - {isEditing ? ( - <> - - - - ) : ( - <> - - - - )} -
-
- )} -
-
- )} - - {/* NFS SUB-TAB */} - {sharesTab === "nfs" && ( - - -
- NFS Shares - -
-
- - {sharesLoading ? ( -

Loading…

- ) : nfsShares.length === 0 ? ( -

- No NFS shares configured. Create one to get started. -

- ) : ( -
- - - - - - - - - - - {nfsShares.map((share) => ( - - - - - - - ))} - -
PathClientsOptionsActions
{share.path}{share.clients}{share.options || "—"} - -
-
- )} -
-
- )} - - {/* SAMBA CONFIG SUB-TAB */} - {sharesTab === "config" && ( -
- - -
-
-

Global MacOS Shares

-

Optimize all shares for MacOS

- {macosEnabled && ( -
- {Object.entries(MACOS_PARAMS).map(([k, v]) => ( - - {k} = {v} - - ))} -
- )} -
- -
-
-
- - - -
- Advanced - {!editMode ? ( - - ) : ( -
- - -
- )} -
-
- -