Files
zmb-webui/frontend/components/shares/CreateNfsDialog.tsx
T
Claude Code 6d74d874b6 ZMB Webui: Complete Project – Rebrand & Initial Clean Commit
ARCHITECTURE
============
Backend: FastAPI + uvicorn (port 8000)
  - JWT authentication with PAM system users
  - ZFS CLI wrapper with caching (30-60s TTL)
  - WebSocket pool status broadcaster (30s interval)
  - Services: auth, zfs_runner, file_manager, shares, identities, system_info
  - Routers: pools, datasets, snapshots, shares, identities, navigator, system

Frontend: Next.js 15 + TypeScript (static export)
  - Incremental Static Regeneration (ISR) for weak hardware
  - Type-safe API client (lib/api.ts)
  - Dark mode + custom Tailwind theme
  - Pages: Dashboard, Login, Snapshots, Datasets, Shares, etc.

DEPLOYMENT
==========
Test Target: 192.168.1.179:8090 (Debian LXC)
Production: 10.66.120.3:9090 (Raspberry Pi 4GB ARM64)
Updater: Automated Gitea-based deployment (update-test.sh, update-pi.sh)

FEATURES COMPLETED
==================
Phase 3a: Dashboard Quick Stats (System, CPU, Memory, Storage)
  - Real-time stats with color-coded progress bars
  - Responsive grid layout (mobile: 1, tablet: 2, desktop: 4 columns)
  - ISR-optimized for fast loads on weak hardware

REBRANDING
==========
Renamed throughout:
  - Project: 'ZFS Manager' → 'ZMB Webui'
  - Services: 'zfs-manager' → 'zmb-webui'
  - Systemd units: zfs-manager-backend → zmb-webui-backend
  - Configuration files and documentation

Co-Authored-By: Patrick <patrick@perlbach24.de>
2026-04-22 00:43:05 +02:00

167 lines
5.3 KiB
TypeScript

"use client"
import { useState } from "react"
import { api } from "@/lib/api"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { AlertCircle } from "lucide-react"
interface CreateNfsDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onCreated: (share: any) => void
}
export default function CreateNfsDialog({
open,
onOpenChange,
onCreated,
}: CreateNfsDialogProps) {
const [path, setPath] = useState("")
const [clients, setClients] = useState("")
const [readonly, setReadonly] = useState(false)
const [sync, setSync] = useState(true)
const [loading, setLoading] = useState(false)
const [error, setError] = useState("")
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError("")
if (!path.trim()) {
setError("Path is required")
return
}
if (!clients.trim()) {
setError("Clients are required")
return
}
// Build options
const opts = []
opts.push(readonly ? "ro" : "rw")
opts.push(sync ? "sync" : "async")
opts.push("no_subtree_check")
const options = opts.join(",")
try {
setLoading(true)
await api.createNfsShare({ path, clients, options })
onCreated({
path,
clients,
options,
})
setPath("")
setClients("")
setReadonly(false)
setSync(true)
onOpenChange(false)
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create share")
} finally {
setLoading(false)
}
}
if (!open) return null
return (
<div className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Create NFS Share</CardTitle>
</CardHeader>
<form onSubmit={handleSubmit}>
<CardContent className="space-y-4">
{error && (
<div className="bg-red-50 border border-red-200 rounded p-3 flex gap-2">
<AlertCircle className="w-4 h-4 text-red-600 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-800">{error}</p>
</div>
)}
<div>
<label className="block text-sm font-medium mb-1">Path</label>
<input
type="text"
value={path}
onChange={(e) => setPath(e.target.value)}
placeholder="e.g., /tank/share"
className="w-full px-3 py-2 border border-border rounded bg-background text-sm"
disabled={loading}
/>
<p className="text-xs text-muted-foreground mt-1">
Must be an existing filesystem path
</p>
</div>
<div>
<label className="block text-sm font-medium mb-1">Clients</label>
<textarea
value={clients}
onChange={(e) => setClients(e.target.value)}
placeholder="e.g., 192.168.1.0/24 10.0.0.0/8"
className="w-full px-3 py-2 border border-border rounded bg-background text-sm min-h-[80px] font-mono text-xs"
disabled={loading}
/>
<p className="text-xs text-muted-foreground mt-1">
Space-separated CIDR ranges or IPs (e.g., 192.168.1.0/24, 10.0.0.5)
</p>
</div>
<div className="space-y-3 border-t border-border pt-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={readonly}
onChange={(e) => setReadonly(e.target.checked)}
disabled={loading}
className="rounded border-border"
/>
<span className="text-sm font-medium">Read-Only</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={sync}
onChange={(e) => setSync(e.target.checked)}
disabled={loading}
className="rounded border-border"
/>
<span className="text-sm font-medium">Sync Mode</span>
<span className="text-xs text-muted-foreground">(safer than async)</span>
</label>
</div>
<div className="bg-blue-50 border border-blue-200 rounded p-3">
<p className="text-xs text-blue-800">
<strong>Generated options:</strong>
<br />
{readonly ? "ro" : "rw"},{sync ? "sync" : "async"},no_subtree_check
</p>
</div>
<div className="flex gap-3 justify-end pt-4">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={loading}
>
Cancel
</Button>
<Button type="submit" disabled={loading}>
{loading ? "Creating..." : "Create Share"}
</Button>
</div>
</CardContent>
</form>
</Card>
</div>
)
}