Files
zmb-webui/frontend/app/shares/page.tsx
T
patrick a187b625bc Fix: Identities Group Management - bessere Fehlermeldungen
- add_user_to_group: Exception werfen mit stderr Nachricht
- remove_user_from_group: Exception werfen mit stderr Nachricht
- text=True für subprocess für besseres Error Handling
- Router aktualisiert um Fehlermeldungen an Frontend weiterzugeben
- Benutzer sehen jetzt detaillierte Fehlermeldungen beim Gruppe-Entfernen

Behebt: 'Failed to remove user from group' verschluckt die echte Fehlermeldung

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-04 14:58:50 +02:00

475 lines
20 KiB
TypeScript

"use client"
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import { api } from "@/lib/api"
import { Header } from "@/components/Header"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { RefreshCw, Plus, Trash2, AlertCircle } from "lucide-react"
import CreateSambaDialog from "@/components/shares/CreateSambaDialog"
import CreateNfsDialog from "@/components/shares/CreateNfsDialog"
import DeleteConfirmDialog from "@/components/shares/DeleteConfirmDialog"
export default function SharesPage() {
const router = useRouter()
const [activeTab, setActiveTab] = useState<"samba" | "nfs" | "config">("samba")
const [sambaShares, setSambaShares] = useState<any[]>([])
const [nfsShares, setNfsShares] = useState<any[]>([])
const [sambaConfig, setSambaConfig] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(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 [editedConfig, setEditedConfig] = useState<{ [key: string]: string }>({})
const [saving, setSaving] = useState(false)
const [editingShare, setEditingShare] = useState<any | null>(null)
useEffect(() => {
const token = localStorage.getItem("access_token")
if (!token) {
router.push("/login")
return
}
loadShares()
}, [router])
const loadShares = async () => {
try {
setLoading(true)
setError(null)
const [samba, nfs, config] = await Promise.all([
api.getSambaShares().catch(() => []),
api.getNfsShares().catch(() => []),
api.getSambaConfig().catch(() => ({ parameters: [] })),
])
setSambaShares(samba)
setNfsShares(nfs)
setSambaConfig(config.parameters || [])
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load shares")
} finally {
setLoading(false)
}
}
const handleDeleteSamba = async (name: string) => {
try {
setDeleting(true)
await api.deleteSambaShare(name)
setSambaShares(sambaShares.filter((s) => s.name !== name))
setDeleteConfirm(null)
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to delete share")
} finally {
setDeleting(false)
}
}
const handleDeleteNfs = async (path: string) => {
try {
setDeleting(true)
await api.deleteNfsShare(path)
setNfsShares(nfsShares.filter((s) => s.path !== path))
setDeleteConfirm(null)
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to delete share")
} finally {
setDeleting(false)
}
}
const handleSambaCreated = (newShare: any) => {
setSambaShares([...sambaShares, newShare])
setShowSambaDialog(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)
setError(null)
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to save share")
} finally {
setSaving(false)
}
}
const handleNfsCreated = (newShare: any) => {
setNfsShares([...nfsShares, newShare])
setShowNfsDialog(false)
}
const handleEditMode = () => {
const configMap = sambaConfig.reduce((acc, param) => {
acc[param.key] = param.value
return acc
}, {} as { [key: string]: string })
setEditedConfig(configMap)
setEditMode(true)
}
const handleSaveConfig = async () => {
try {
setSaving(true)
await api.setSambaConfig(editedConfig)
setEditMode(false)
await loadShares()
setError(null)
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to save configuration")
} finally {
setSaving(false)
}
}
return (
<div className="min-h-screen bg-background">
<Header />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold">File Sharing</h1>
<p className="text-muted-foreground mt-1">Manage Samba (SMB) and NFS network shares</p>
</div>
<Button onClick={loadShares} disabled={loading}>
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? "animate-spin" : ""}`} />
Refresh
</Button>
</div>
{error && (
<Card className="mb-6 border-red-200 bg-red-50">
<CardContent className="flex items-center gap-3 pt-6">
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0" />
<div>
<p className="font-medium text-red-900">Error</p>
<p className="text-sm text-red-800">{error}</p>
</div>
</CardContent>
</Card>
)}
{/* Tabs */}
<div className="border-b border-border mb-6">
<div className="flex gap-4">
<button
onClick={() => setActiveTab("samba")}
className={`px-4 py-2 font-medium transition-colors ${
activeTab === "samba"
? "border-b-2 border-primary text-primary"
: "text-muted-foreground hover:text-foreground"
}`}
>
SMB/Samba
</button>
<button
onClick={() => setActiveTab("nfs")}
className={`px-4 py-2 font-medium transition-colors ${
activeTab === "nfs"
? "border-b-2 border-primary text-primary"
: "text-muted-foreground hover:text-foreground"
}`}
>
NFS
</button>
<button
onClick={() => setActiveTab("config")}
className={`px-4 py-2 font-medium transition-colors ${
activeTab === "config"
? "border-b-2 border-primary text-primary"
: "text-muted-foreground hover:text-foreground"
}`}
>
Samba Config
</button>
</div>
</div>
{/* SAMBA TAB */}
{activeTab === "samba" && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Samba Shares</CardTitle>
<Button size="sm" onClick={() => setShowSambaDialog(true)}>
<Plus className="w-4 h-4 mr-2" />
New Share
</Button>
</div>
</CardHeader>
<CardContent>
{sambaShares.length === 0 ? (
<p className="text-muted-foreground text-center py-12">
No Samba shares configured. Create one to get started.
</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="border-b border-border bg-muted/30">
<tr>
<th className="text-left py-3 px-4 font-medium">Name</th>
<th className="text-left py-3 px-4 font-medium">Path</th>
<th className="text-left py-3 px-4 font-medium">Users</th>
<th className="text-left py-3 px-4 font-medium">Perms</th>
<th className="text-left py-3 px-4 font-medium">Comment</th>
<th className="text-right py-3 px-4 font-medium">Actions</th>
</tr>
</thead>
<tbody>
{sambaShares.map((share) => {
const isEditing = editingShare?.oldName === share.name
return (
<tr key={share.name} className="border-b border-border/50 hover:bg-muted/30">
<td className="py-3 px-4 text-xs">
{isEditing ? (
<input
type="text"
value={editingShare.name}
onChange={(e) => 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}
/>
) : (
<span className="font-mono">{share.name}</span>
)}
</td>
<td className="py-3 px-4 text-xs">
{isEditing ? (
<input
type="text"
value={editingShare.path}
onChange={(e) => 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}
/>
) : (
<span className="font-mono">{share.path}</span>
)}
</td>
<td className="py-3 px-4 text-xs">{share.valid_users || "—"}</td>
<td className="py-3 px-4 text-xs">
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-700">
{share.read_only ? "RO" : "RW"}
</span>
</td>
<td className="py-3 px-4 text-xs">
{isEditing ? (
<input
type="text"
value={editingShare.comment || ""}
onChange={(e) => 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 || "—"
)}
</td>
<td className="py-3 px-4 text-right space-x-2">
{isEditing ? (
<>
<button
onClick={handleSaveShare}
className="text-green-600 hover:text-green-700 transition-colors text-xs font-medium"
disabled={saving}
title="Save"
>
Save
</button>
<button
onClick={() => setEditingShare(null)}
className="text-gray-600 hover:text-gray-700 transition-colors text-xs font-medium"
disabled={saving}
title="Cancel"
>
Cancel
</button>
</>
) : (
<>
<button
onClick={() => setEditingShare({ ...share, oldName: share.name })}
className="text-blue-600 hover:text-blue-700 transition-colors"
title="Edit"
>
Edit
</button>
<button
onClick={() => setDeleteConfirm({ type: "samba", name: share.name })}
className="text-red-600 hover:text-red-700 transition-colors"
title="Delete"
>
<Trash2 className="w-4 h-4 inline" />
</button>
</>
)}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
)}
{/* NFS TAB */}
{activeTab === "nfs" && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>NFS Shares</CardTitle>
<Button size="sm" onClick={() => setShowNfsDialog(true)}>
<Plus className="w-4 h-4 mr-2" />
New Share
</Button>
</div>
</CardHeader>
<CardContent>
{nfsShares.length === 0 ? (
<p className="text-muted-foreground text-center py-12">
No NFS shares configured. Create one to get started.
</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="border-b border-border bg-muted/30">
<tr>
<th className="text-left py-3 px-4 font-medium">Path</th>
<th className="text-left py-3 px-4 font-medium">Clients</th>
<th className="text-left py-3 px-4 font-medium">Options</th>
<th className="text-right py-3 px-4 font-medium">Actions</th>
</tr>
</thead>
<tbody>
{nfsShares.map((share) => (
<tr key={share.path} className="border-b border-border/50 hover:bg-muted/30">
<td className="py-3 px-4 font-mono text-xs">{share.path}</td>
<td className="py-3 px-4 text-xs">{share.clients}</td>
<td className="py-3 px-4 text-xs font-mono text-xs">{share.options || "—"}</td>
<td className="py-3 px-4 text-right space-x-2">
<button
onClick={() => setDeleteConfirm({ type: "nfs", name: share.path })}
className="text-red-600 hover:text-red-700 transition-colors"
title="Delete"
>
<Trash2 className="w-4 h-4 inline" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
)}
{/* SAMBA CONFIG TAB */}
{activeTab === "config" && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Samba Global Configuration</CardTitle>
{!editMode && sambaConfig.length > 0 && (
<Button size="sm" onClick={handleEditMode} variant="outline">
Edit Config
</Button>
)}
{editMode && (
<div className="flex gap-2">
<Button size="sm" onClick={handleSaveConfig} disabled={saving}>
{saving ? "Saving..." : "Save Changes"}
</Button>
<Button size="sm" onClick={() => setEditMode(false)} variant="outline" disabled={saving}>
Cancel
</Button>
</div>
)}
</div>
</CardHeader>
<CardContent>
{sambaConfig.length === 0 ? (
<p className="text-muted-foreground text-center py-12">
No global configuration parameters found.
</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="border-b border-border bg-muted/30">
<tr>
<th className="text-left py-3 px-4 font-medium w-40">Parameter</th>
<th className="text-left py-3 px-4 font-medium">Value</th>
</tr>
</thead>
<tbody>
{sambaConfig.map((param, idx) => (
<tr key={idx} className="border-b border-border/50 hover:bg-muted/30">
<td className="py-3 px-4 font-mono text-xs font-medium text-blue-600">{param.key}</td>
<td className="py-3 px-4 text-xs">
{editMode ? (
<input
type="text"
value={editedConfig[param.key] || ""}
onChange={(e) => setEditedConfig({ ...editedConfig, [param.key]: e.target.value })}
className="w-full px-2 py-1 rounded border border-border bg-background text-xs font-mono"
/>
) : (
<span className="font-mono break-all">{param.value}</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
)}
</main>
{/* Dialogs */}
<CreateSambaDialog open={showSambaDialog} onOpenChange={setShowSambaDialog} onCreated={handleSambaCreated} />
<CreateNfsDialog open={showNfsDialog} onOpenChange={setShowNfsDialog} onCreated={handleNfsCreated} />
<DeleteConfirmDialog
open={!!deleteConfirm}
onOpenChange={(open) => !open && setDeleteConfirm(null)}
type={deleteConfirm?.type === "samba" ? "Samba Share" : "NFS Share"}
name={deleteConfirm?.name || ""}
onConfirm={() => {
if (deleteConfirm?.type === "samba") {
handleDeleteSamba(deleteConfirm.name)
} else if (deleteConfirm?.type === "nfs") {
handleDeleteNfs(deleteConfirm.name)
}
}}
loading={deleting}
/>
</div>
)
}