Refactor: Shares-Funktionen in Datasets-Seite zusammengeführt
- datasets/page.tsx: neuer Tab 'Shares & Config' mit vollem Inhalt aus shares/page.tsx (Samba, NFS, Samba Config mit MacOS-Toggle + Raw-Editor) - shares/page.tsx: redirect -> /datasets - Header.tsx: 'Shares'-Link zeigt auf /datasets Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+475
-386
File diff suppressed because it is too large
Load Diff
@@ -1,516 +1,5 @@
|
|||||||
"use client"
|
import { redirect } from "next/navigation"
|
||||||
|
|
||||||
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() {
|
export default function SharesPage() {
|
||||||
const router = useRouter()
|
redirect("/datasets")
|
||||||
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 [rawConfigText, setRawConfigText] = useState("")
|
|
||||||
const [saving, setSaving] = useState(false)
|
|
||||||
const [macosEnabled, setMacosEnabled] = 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)
|
|
||||||
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) {
|
|
||||||
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 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)
|
|
||||||
setError(null)
|
|
||||||
} catch (err) {
|
|
||||||
setError(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()
|
|
||||||
setError(null)
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : "Failed to update macOS settings")
|
|
||||||
} 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" && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* MacOS Toggle */}
|
|
||||||
<Card>
|
|
||||||
<CardContent className="py-5">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">Global MacOS Shares</p>
|
|
||||||
<p className="text-sm text-muted-foreground mt-0.5">Optimize all shares for MacOS</p>
|
|
||||||
{macosEnabled && (
|
|
||||||
<div className="mt-2 flex flex-wrap gap-2">
|
|
||||||
{Object.entries(MACOS_PARAMS).map(([k, v]) => (
|
|
||||||
<span key={k} className="text-xs font-mono bg-muted px-2 py-0.5 rounded">
|
|
||||||
{k} = {v}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
role="switch"
|
|
||||||
aria-checked={macosEnabled}
|
|
||||||
disabled={saving}
|
|
||||||
onClick={() => handleToggleMacOS(!macosEnabled)}
|
|
||||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none ${macosEnabled ? "bg-red-500" : "bg-muted-foreground/30"} disabled:opacity-50`}
|
|
||||||
>
|
|
||||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${macosEnabled ? "translate-x-6" : "translate-x-1"}`} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Advanced raw config */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<CardTitle>Advanced</CardTitle>
|
|
||||||
{!editMode ? (
|
|
||||||
<Button size="sm" onClick={() => setEditMode(true)} variant="outline">
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button size="sm" onClick={handleSaveRaw} disabled={saving}>
|
|
||||||
{saving ? "Saving..." : "Apply"}
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" onClick={() => setEditMode(false)} variant="outline" disabled={saving}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<textarea
|
|
||||||
readOnly={!editMode}
|
|
||||||
value={rawConfigText}
|
|
||||||
onChange={(e) => setRawConfigText(e.target.value)}
|
|
||||||
rows={20}
|
|
||||||
spellCheck={false}
|
|
||||||
className={`w-full font-mono text-xs rounded border px-3 py-2 bg-background resize-y focus:outline-none focus:ring-2 focus:ring-ring ${editMode ? "border-border" : "border-transparent text-muted-foreground"}`}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,9 +85,9 @@ export function Header() {
|
|||||||
Navigator
|
Navigator
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/shares"
|
href="/datasets"
|
||||||
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||||
isActive("/shares")
|
isActive("/datasets")
|
||||||
? "bg-accent text-accent-foreground"
|
? "bg-accent text-accent-foreground"
|
||||||
: "text-muted-foreground hover:text-foreground"
|
: "text-muted-foreground hover:text-foreground"
|
||||||
}`}
|
}`}
|
||||||
@@ -205,9 +205,9 @@ export function Header() {
|
|||||||
Files
|
Files
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/shares"
|
href="/datasets"
|
||||||
className={`block px-3 py-2 rounded-md text-sm font-medium ${
|
className={`block px-3 py-2 rounded-md text-sm font-medium ${
|
||||||
isActive("/shares")
|
isActive("/datasets")
|
||||||
? "bg-accent text-accent-foreground"
|
? "bg-accent text-accent-foreground"
|
||||||
: "text-muted-foreground hover:text-foreground"
|
: "text-muted-foreground hover:text-foreground"
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
Reference in New Issue
Block a user