Files
calypso/frontend/src/pages/Shares.tsx
2026-01-04 12:54:25 +07:00

972 lines
50 KiB
TypeScript

import { useState, useEffect } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { sharesAPI, type Share, type CreateShareRequest, type UpdateShareRequest } from '@/api/shares'
import { zfsApi, type ZFSDataset } from '@/api/storage'
import { Button } from '@/components/ui/button'
import {
Plus, Search, FolderOpen, Share as ShareIcon, Cloud, Settings, X, ChevronRight,
FolderSymlink, Network, Lock, History, Save, Gauge, Server,
ChevronDown, Ban
} from 'lucide-react'
export default function SharesPage() {
const queryClient = useQueryClient()
const [selectedShare, setSelectedShare] = useState<Share | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const [showCreateForm, setShowCreateForm] = useState(false)
const [activeTab, setActiveTab] = useState<'configuration' | 'permissions' | 'clients'>('configuration')
const [showAdvanced, setShowAdvanced] = useState(false)
const [formData, setFormData] = useState<CreateShareRequest>({
dataset_id: '',
nfs_enabled: false,
nfs_options: 'rw,sync,no_subtree_check',
nfs_clients: [],
smb_enabled: false,
smb_share_name: '',
smb_comment: '',
smb_guest_ok: false,
smb_read_only: false,
smb_browseable: true,
})
const [nfsClientInput, setNfsClientInput] = useState('')
const { data: shares = [], isLoading } = useQuery<Share[]>({
queryKey: ['shares'],
queryFn: sharesAPI.listShares,
refetchInterval: 5000,
staleTime: 0,
})
// Get all datasets for create form
const { data: pools = [] } = useQuery({
queryKey: ['storage', 'zfs', 'pools'],
queryFn: zfsApi.listPools,
})
const [datasets, setDatasets] = useState<ZFSDataset[]>([])
useEffect(() => {
const fetchDatasets = async () => {
const allDatasets: ZFSDataset[] = []
for (const pool of pools) {
try {
const poolDatasets = await zfsApi.listDatasets(pool.id)
// Filter only filesystem datasets
const filesystems = poolDatasets.filter(ds => ds.type === 'filesystem')
allDatasets.push(...filesystems)
} catch (err) {
console.error(`Failed to fetch datasets for pool ${pool.id}:`, err)
}
}
setDatasets(allDatasets)
}
if (pools.length > 0) {
fetchDatasets()
}
}, [pools])
const createMutation = useMutation({
mutationFn: sharesAPI.createShare,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['shares'] })
setShowCreateForm(false)
setFormData({
dataset_id: '',
nfs_enabled: false,
nfs_options: 'rw,sync,no_subtree_check',
nfs_clients: [],
smb_enabled: false,
smb_share_name: '',
smb_comment: '',
smb_guest_ok: false,
smb_read_only: false,
smb_browseable: true,
})
alert('Share created successfully!')
},
onError: (error: any) => {
const errorMessage = error?.response?.data?.error || error?.message || 'Failed to create share'
alert(`Error: ${errorMessage}`)
console.error('Failed to create share:', error)
},
})
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdateShareRequest }) =>
sharesAPI.updateShare(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['shares'] })
},
})
const filteredShares = shares.filter(share =>
share.dataset_name.toLowerCase().includes(searchQuery.toLowerCase()) ||
share.mount_point.toLowerCase().includes(searchQuery.toLowerCase())
)
const handleCreateShare = (e?: React.MouseEvent) => {
e?.preventDefault()
e?.stopPropagation()
console.log('Creating share with data:', formData)
if (!formData.dataset_id) {
alert('Please select a dataset')
return
}
if (!formData.nfs_enabled && !formData.smb_enabled) {
alert('At least one protocol (NFS or SMB) must be enabled')
return
}
// Prepare the data to send
const submitData: CreateShareRequest = {
dataset_id: formData.dataset_id,
nfs_enabled: formData.nfs_enabled,
smb_enabled: formData.smb_enabled,
}
if (formData.nfs_enabled) {
submitData.nfs_options = formData.nfs_options || 'rw,sync,no_subtree_check'
submitData.nfs_clients = formData.nfs_clients || []
}
if (formData.smb_enabled) {
submitData.smb_share_name = formData.smb_share_name || ''
submitData.smb_comment = formData.smb_comment || ''
submitData.smb_guest_ok = formData.smb_guest_ok || false
submitData.smb_read_only = formData.smb_read_only || false
submitData.smb_browseable = formData.smb_browseable !== undefined ? formData.smb_browseable : true
}
console.log('Submitting share data:', submitData)
createMutation.mutate(submitData)
}
const handleToggleNFS = (share: Share) => {
updateMutation.mutate({
id: share.id,
data: { nfs_enabled: !share.nfs_enabled },
})
}
const handleToggleSMB = (share: Share) => {
updateMutation.mutate({
id: share.id,
data: { smb_enabled: !share.smb_enabled },
})
}
const handleAddNFSClient = (share: Share) => {
if (!nfsClientInput.trim()) return
const newClients = [...(share.nfs_clients || []), nfsClientInput.trim()]
updateMutation.mutate({
id: share.id,
data: { nfs_clients: newClients },
})
setNfsClientInput('')
}
const handleRemoveNFSClient = (share: Share, client: string) => {
const newClients = (share.nfs_clients || []).filter(c => c !== client)
updateMutation.mutate({
id: share.id,
data: { nfs_clients: newClients },
})
}
return (
<div className="flex-1 overflow-hidden flex flex-col bg-background-dark">
{/* Header */}
<div className="flex-shrink-0 border-b border-border-dark bg-background-dark/95 backdrop-blur z-10">
<div className="flex flex-col gap-4 p-6 pb-4">
<div className="flex flex-wrap justify-between gap-3 items-center">
<div className="flex flex-col gap-1">
<h2 className="text-white text-3xl font-black leading-tight tracking-[-0.033em]">Shares Management</h2>
<div className="flex items-center gap-2 text-text-secondary text-sm">
<span>Storage</span>
<ChevronRight size={14} />
<span>Shares</span>
<ChevronRight size={14} />
<span className="text-white">Overview</span>
</div>
</div>
<Button
onClick={() => setShowCreateForm(true)}
className="flex items-center gap-2 px-4 h-10 rounded-lg bg-primary hover:bg-blue-600 text-white text-sm font-bold"
>
<Plus size={20} />
<span>Create New Share</span>
</Button>
</div>
{/* Status Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-2">
<div className="flex flex-col gap-1 rounded-lg p-4 border border-border-dark bg-surface-dark/50">
<div className="flex justify-between items-start">
<p className="text-text-secondary text-xs font-bold uppercase tracking-wider">SMB Service</p>
<div className="size-2 rounded-full bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]"></div>
</div>
<p className="text-white text-xl font-bold leading-tight">Running</p>
<p className="text-emerald-500 text-xs mt-1">Port 445 Active</p>
</div>
<div className="flex flex-col gap-1 rounded-lg p-4 border border-border-dark bg-surface-dark/50">
<div className="flex justify-between items-start">
<p className="text-text-secondary text-xs font-bold uppercase tracking-wider">NFS Service</p>
<div className="size-2 rounded-full bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]"></div>
</div>
<p className="text-white text-xl font-bold leading-tight">Running</p>
<p className="text-emerald-500 text-xs mt-1">Port 2049 Active</p>
</div>
<div className="flex flex-col gap-1 rounded-lg p-4 border border-border-dark bg-surface-dark/50">
<div className="flex justify-between items-start">
<p className="text-text-secondary text-xs font-bold uppercase tracking-wider">Throughput</p>
<Gauge className="text-text-secondary" size={20} />
</div>
<p className="text-white text-xl font-bold leading-tight">565 MB/s</p>
<p className="text-text-secondary text-xs mt-1">14 Clients Connected</p>
</div>
</div>
</div>
</div>
{/* Master-Detail Layout */}
<div className="flex flex-1 overflow-hidden">
{/* Left Panel: Shares List */}
<div className="w-full lg:w-[400px] flex flex-col border-r border-border-dark bg-background-dark flex-shrink-0">
{/* Search */}
<div className="p-4 border-b border-border-dark bg-background-dark sticky top-0 z-10">
<div className="relative">
<Search className="absolute left-3 top-2.5 text-text-secondary" size={18} />
<input
className="w-full bg-surface-dark border border-border-dark rounded-lg pl-10 pr-4 py-2.5 text-sm text-white focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary placeholder-text-secondary"
placeholder="Filter shares..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
{/* List Items */}
<div className="flex-1 overflow-y-auto">
{isLoading ? (
<div className="p-4 text-center text-text-secondary text-sm">Loading shares...</div>
) : filteredShares.length === 0 ? (
<div className="p-4 text-center text-text-secondary text-sm">No shares found</div>
) : (
filteredShares.map((share) => (
<div
key={share.id}
onClick={() => setSelectedShare(share)}
className={`group flex flex-col border-b border-border-dark/50 cursor-pointer transition-colors ${
selectedShare?.id === share.id
? 'bg-primary/10 border-l-4 border-l-primary'
: 'hover:bg-surface-dark'
}`}
>
<div className={`px-4 py-3 flex items-start gap-3 ${selectedShare?.id === share.id ? 'pl-3' : ''}`}>
{selectedShare?.id === share.id ? (
<Server className="text-primary mt-1" size={20} />
) : (
<FolderOpen className="text-text-secondary mt-1" size={20} />
)}
<div className="flex-1 min-w-0">
<div className="flex justify-between items-center mb-1">
<h3 className={`text-sm truncate ${selectedShare?.id === share.id ? 'font-bold text-white' : 'font-medium text-white'}`}>
{share.dataset_name}
</h3>
</div>
<p className={`text-xs truncate mb-2 ${selectedShare?.id === share.id ? 'text-primary/80' : 'text-text-secondary'}`}>
{share.mount_point || 'No mount point'}
</p>
<div className="flex gap-2">
{share.smb_enabled ? (
<span className={`px-1.5 py-0.5 rounded text-[10px] font-bold border ${
selectedShare?.id === share.id
? 'bg-surface-dark text-text-secondary border-border-dark'
: 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20'
}`}>
SMB
</span>
) : null}
{share.nfs_enabled && (
<span className="px-1.5 py-0.5 rounded text-[10px] font-bold bg-emerald-500/10 text-emerald-500 border border-emerald-500/20">
NFS
</span>
)}
</div>
</div>
{selectedShare?.id !== share.id && (
<ChevronRight className="text-text-secondary text-[18px]" size={18} />
)}
</div>
</div>
))
)}
</div>
<div className="p-4 border-t border-border-dark bg-background-dark text-center">
<p className="text-xs text-text-secondary">
Showing {filteredShares.length} of {shares.length} shares
</p>
</div>
</div>
{/* Right Panel: Share Details */}
<div className="flex-1 flex flex-col overflow-hidden bg-background-light dark:bg-[#0d141c]">
{selectedShare ? (
<>
{/* Detail Header */}
<div className="p-6 pb-0 flex flex-col gap-6">
<div className="flex justify-between items-start">
<div>
<div className="flex items-center gap-3 mb-2">
<div className="bg-primary p-2 rounded-lg text-white">
<Server size={20} />
</div>
<div>
<h2 className="text-2xl font-bold text-white">
{selectedShare.dataset_name.split('/').pop() || selectedShare.dataset_name}
</h2>
<p className="text-text-secondary text-sm font-mono">{selectedShare.dataset_name}</p>
</div>
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
className="flex items-center justify-center rounded-lg h-9 px-4 border border-border-dark text-white text-sm font-medium hover:bg-surface-dark transition-colors"
>
<History size={18} className="mr-2" />
Revert
</Button>
<Button
className="flex items-center justify-center rounded-lg h-9 px-4 bg-primary text-white text-sm font-bold shadow-lg shadow-primary/20 hover:bg-blue-600 transition-colors"
>
<Save size={18} className="mr-2" />
Save Changes
</Button>
</div>
</div>
{/* Protocol Toggles */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* SMB Toggle */}
<div className={`flex items-center justify-between p-4 rounded-xl border ${
selectedShare.smb_enabled
? 'border-primary/50 bg-primary/5'
: 'border-border-dark bg-surface-dark/40'
}`}>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${
selectedShare.smb_enabled
? 'bg-primary/20 text-primary'
: 'bg-surface-dark text-text-secondary'
}`}>
<ShareIcon size={20} />
</div>
<div className="flex flex-col">
<span className="text-sm font-bold text-white">SMB Protocol</span>
<span className="text-xs text-text-secondary">Windows File Sharing</span>
</div>
</div>
<button
onClick={() => handleToggleSMB(selectedShare)}
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-background-dark ${
selectedShare.smb_enabled ? 'bg-primary' : 'bg-slate-700'
}`}
>
<span className="sr-only">Use setting</span>
<span
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
selectedShare.smb_enabled ? 'translate-x-5' : 'translate-x-0'
}`}
></span>
</button>
</div>
{/* NFS Toggle */}
<div className={`flex items-center justify-between p-4 rounded-xl border ${
selectedShare.nfs_enabled
? 'border-primary/50 bg-primary/5'
: 'border-border-dark bg-surface-dark/40'
}`}>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${
selectedShare.nfs_enabled
? 'bg-primary/20 text-primary'
: 'bg-surface-dark text-text-secondary'
}`}>
<Cloud size={20} />
</div>
<div className="flex flex-col">
<span className="text-sm font-bold text-white">NFS Protocol</span>
<span className="text-xs text-text-secondary">Unix/Linux File Sharing</span>
</div>
</div>
<button
onClick={() => handleToggleNFS(selectedShare)}
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-background-dark ${
selectedShare.nfs_enabled ? 'bg-primary' : 'bg-slate-700'
}`}
>
<span className="sr-only">Use setting</span>
<span
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
selectedShare.nfs_enabled ? 'translate-x-5' : 'translate-x-0'
}`}
></span>
</button>
</div>
</div>
{/* Tabs */}
<div className="border-b border-border-dark mt-2">
<div className="flex gap-6">
<button
onClick={() => setActiveTab('configuration')}
className={`pb-3 border-b-2 text-sm flex items-center gap-2 transition-colors ${
activeTab === 'configuration'
? 'border-primary text-primary font-bold'
: 'border-transparent text-text-secondary hover:text-white font-medium'
}`}
>
<Settings size={18} />
Configuration
</button>
<button
onClick={() => setActiveTab('permissions')}
className={`pb-3 border-b-2 text-sm flex items-center gap-2 transition-colors ${
activeTab === 'permissions'
? 'border-primary text-primary font-bold'
: 'border-transparent text-text-secondary hover:text-white font-medium'
}`}
>
<Lock size={18} />
Permissions (ACL)
</button>
<button
onClick={() => setActiveTab('clients')}
className={`pb-3 border-b-2 text-sm flex items-center gap-2 transition-colors ${
activeTab === 'clients'
? 'border-primary text-primary font-bold'
: 'border-transparent text-text-secondary hover:text-white font-medium'
}`}
>
<Network size={18} />
Connected Clients
<span className="bg-surface-dark text-white text-[10px] px-1.5 py-0.5 rounded-full ml-1">8</span>
</button>
</div>
</div>
</div>
{/* Tab Content */}
<div className="flex-1 overflow-y-auto p-6">
<div className="max-w-4xl flex flex-col gap-6">
{activeTab === 'configuration' && (
<>
{/* NFS Settings Card */}
{selectedShare.nfs_enabled && (
<div className="rounded-xl border border-border-dark bg-surface-dark overflow-hidden">
<div className="px-5 py-4 border-b border-border-dark flex justify-between items-center bg-[#1c2a39]">
<h3 className="text-sm font-bold text-white flex items-center gap-2">
<Network className="text-primary" size={20} />
NFS Configuration
</h3>
<span className="text-xs text-emerald-500 font-medium px-2 py-1 bg-emerald-500/10 rounded border border-emerald-500/20">
Active
</span>
</div>
<div className="p-5 flex flex-col gap-5">
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="flex flex-col gap-2">
<label className="text-xs font-semibold text-text-secondary uppercase">Allowed Subnets / IPs</label>
<div className="flex gap-2">
<input
className="flex-1 bg-background-dark border border-border-dark rounded-lg px-3 py-2 text-sm text-white focus:border-primary focus:ring-1 focus:ring-primary font-mono"
type="text"
placeholder="192.168.10.0/24"
value={nfsClientInput}
onChange={(e) => setNfsClientInput(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleAddNFSClient(selectedShare)
}
}}
/>
<button
onClick={() => handleAddNFSClient(selectedShare)}
className="p-2 bg-surface-dark hover:bg-border-dark border border-border-dark rounded-lg text-white"
>
<Plus size={18} />
</button>
</div>
<p className="text-xs text-text-secondary">CIDR notation supported. Use comma for multiple entries.</p>
{selectedShare.nfs_clients && selectedShare.nfs_clients.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{selectedShare.nfs_clients.map((client) => (
<span
key={client}
className="inline-flex items-center gap-1 px-2 py-1 bg-primary/20 text-primary text-xs rounded border border-primary/30"
>
{client}
<button
onClick={() => handleRemoveNFSClient(selectedShare, client)}
className="hover:text-red-400"
>
<X size={14} />
</button>
</span>
))}
</div>
)}
</div>
<div className="flex flex-col gap-2">
<label className="text-xs font-semibold text-text-secondary uppercase">Map Root User</label>
<div className="relative">
<select className="w-full bg-background-dark border border-border-dark rounded-lg px-3 py-2 text-sm text-white focus:border-primary focus:ring-1 focus:ring-primary appearance-none">
<option>root (User ID 0)</option>
<option>admin</option>
<option>nobody</option>
</select>
<ChevronDown className="absolute right-3 top-2.5 text-text-secondary pointer-events-none" size={18} />
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="flex flex-col gap-2">
<label className="text-xs font-semibold text-text-secondary uppercase">Security Profile</label>
<div className="flex gap-2">
<label className="flex items-center gap-2 px-3 py-2 rounded-lg border border-border-dark bg-background-dark cursor-pointer flex-1">
<input
checked
className="text-primary focus:ring-primary bg-surface-dark border-border-dark"
name="sec"
type="radio"
/>
<span className="text-sm text-white">sys (Default)</span>
</label>
<label className="flex items-center gap-2 px-3 py-2 rounded-lg border border-border-dark bg-background-dark cursor-pointer flex-1">
<input
className="text-primary focus:ring-primary bg-surface-dark border-border-dark"
name="sec"
type="radio"
/>
<span className="text-sm text-white">krb5</span>
</label>
</div>
</div>
<div className="flex flex-col gap-2">
<label className="text-xs font-semibold text-text-secondary uppercase">Sync Mode</label>
<div className="relative">
<select className="w-full bg-background-dark border border-border-dark rounded-lg px-3 py-2 text-sm text-white focus:border-primary focus:ring-1 focus:ring-primary appearance-none">
<option>Standard</option>
<option>Always Sync</option>
<option>Disabled (Async)</option>
</select>
<ChevronDown className="absolute right-3 top-2.5 text-text-secondary pointer-events-none" size={18} />
</div>
</div>
</div>
</div>
</div>
)}
{/* SMB Settings Card */}
{selectedShare.smb_enabled && (
<div className="rounded-xl border border-border-dark bg-surface-dark overflow-hidden">
<div className="px-5 py-4 border-b border-border-dark flex justify-between items-center bg-[#1c2a39]">
<h3 className="text-sm font-bold text-white flex items-center gap-2">
<ShareIcon className="text-primary" size={20} />
SMB Configuration
</h3>
<span className="text-xs text-emerald-500 font-medium px-2 py-1 bg-emerald-500/10 rounded border border-emerald-500/20">
Active
</span>
</div>
<div className="p-5 flex flex-col gap-5">
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="flex flex-col gap-2">
<label className="text-xs font-semibold text-text-secondary uppercase">Share Name</label>
<input
className="w-full bg-background-dark border border-border-dark rounded-lg px-3 py-2 text-sm text-white focus:border-primary focus:ring-1 focus:ring-primary"
type="text"
value={selectedShare.smb_share_name || ''}
onChange={(e) => {
updateMutation.mutate({
id: selectedShare.id,
data: { smb_share_name: e.target.value },
})
}}
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-xs font-semibold text-text-secondary uppercase">Path</label>
<input
className="w-full bg-background-dark border border-border-dark rounded-lg px-3 py-2 text-sm text-white focus:border-primary focus:ring-1 focus:ring-primary font-mono"
type="text"
value={selectedShare.smb_path || selectedShare.mount_point || ''}
readOnly
/>
</div>
</div>
<div className="flex flex-col gap-2">
<label className="text-xs font-semibold text-text-secondary uppercase">Comment</label>
<input
className="w-full bg-background-dark border border-border-dark rounded-lg px-3 py-2 text-sm text-white focus:border-primary focus:ring-1 focus:ring-primary"
type="text"
value={selectedShare.smb_comment || ''}
onChange={(e) => {
updateMutation.mutate({
id: selectedShare.id,
data: { smb_comment: e.target.value },
})
}}
/>
</div>
<div className="flex flex-col gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={selectedShare.smb_guest_ok}
onChange={(e) => {
updateMutation.mutate({
id: selectedShare.id,
data: { smb_guest_ok: e.target.checked },
})
}}
className="rounded border-border-dark bg-background-dark text-primary focus:ring-primary h-4 w-4"
/>
<span className="text-sm text-white">Allow Guest Access</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={selectedShare.smb_read_only}
onChange={(e) => {
updateMutation.mutate({
id: selectedShare.id,
data: { smb_read_only: e.target.checked },
})
}}
className="rounded border-border-dark bg-background-dark text-primary focus:ring-primary h-4 w-4"
/>
<span className="text-sm text-white">Read Only</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={selectedShare.smb_browseable}
onChange={(e) => {
updateMutation.mutate({
id: selectedShare.id,
data: { smb_browseable: e.target.checked },
})
}}
className="rounded border-border-dark bg-background-dark text-primary focus:ring-primary h-4 w-4"
/>
<span className="text-sm text-white">Browseable</span>
</label>
</div>
</div>
</div>
)}
{/* Advanced Attributes */}
<div className="rounded-xl border border-border-dark bg-surface-dark overflow-hidden">
<button
onClick={() => setShowAdvanced(!showAdvanced)}
className="w-full px-5 py-4 flex justify-between items-center hover:bg-[#1c2a39] transition-colors text-left"
>
<h3 className="text-sm font-bold text-white flex items-center gap-2">
<Settings className="text-text-secondary" size={20} />
Advanced Attributes
</h3>
<ChevronDown
className={`text-text-secondary transition-transform ${showAdvanced ? 'rotate-180' : ''}`}
size={20}
/>
</button>
{showAdvanced && (
<div className="p-5 border-t border-border-dark flex flex-wrap gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
className="rounded border-border-dark bg-background-dark text-primary focus:ring-primary h-4 w-4"
/>
<span className="text-sm text-white">Read Only</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
checked
type="checkbox"
className="rounded border-border-dark bg-background-dark text-primary focus:ring-primary h-4 w-4"
/>
<span className="text-sm text-white">Enable Compression (LZ4)</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
className="rounded border-border-dark bg-background-dark text-primary focus:ring-primary h-4 w-4"
/>
<span className="text-sm text-white">Enable Deduplication</span>
</label>
</div>
)}
</div>
{/* Connected Clients Preview */}
<div className="flex flex-col gap-3">
<div className="flex justify-between items-end">
<h3 className="text-base font-bold text-white">Top Active Clients</h3>
<a className="text-sm text-primary hover:text-blue-400 font-medium cursor-pointer" href="#">
View all clients
</a>
</div>
<div className="rounded-lg border border-border-dark overflow-hidden bg-surface-dark">
<table className="w-full text-sm text-left">
<thead className="bg-background-dark text-text-secondary font-medium border-b border-border-dark">
<tr>
<th className="px-4 py-3">IP Address</th>
<th className="px-4 py-3">User</th>
<th className="px-4 py-3">Protocol</th>
<th className="px-4 py-3 text-right">Throughput</th>
<th className="px-4 py-3 text-right">Action</th>
</tr>
</thead>
<tbody className="text-white divide-y divide-border-dark">
<tr>
<td className="px-4 py-3 font-mono">192.168.10.105</td>
<td className="px-4 py-3">esxi-host-01</td>
<td className="px-4 py-3">
<span className="bg-primary/20 text-primary px-1.5 py-0.5 rounded text-xs font-bold">NFS</span>
</td>
<td className="px-4 py-3 text-right font-mono text-text-secondary">420 MB/s</td>
<td className="px-4 py-3 text-right">
<button className="text-text-secondary hover:text-red-400" title="Disconnect">
<Ban size={18} />
</button>
</td>
</tr>
<tr>
<td className="px-4 py-3 font-mono">192.168.10.106</td>
<td className="px-4 py-3">esxi-host-02</td>
<td className="px-4 py-3">
<span className="bg-primary/20 text-primary px-1.5 py-0.5 rounded text-xs font-bold">NFS</span>
</td>
<td className="px-4 py-3 text-right font-mono text-text-secondary">105 MB/s</td>
<td className="px-4 py-3 text-right">
<button className="text-text-secondary hover:text-red-400" title="Disconnect">
<Ban size={18} />
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</>
)}
{activeTab === 'permissions' && (
<div className="rounded-xl border border-border-dark bg-surface-dark p-8 text-center">
<Lock className="mx-auto mb-4 text-text-secondary" size={48} />
<p className="text-text-secondary text-sm">Permissions (ACL) configuration coming soon</p>
</div>
)}
{activeTab === 'clients' && (
<div className="rounded-xl border border-border-dark bg-surface-dark overflow-hidden">
<div className="px-5 py-4 border-b border-border-dark flex justify-between items-center bg-[#1c2a39]">
<h3 className="text-sm font-bold text-white flex items-center gap-2">
<Network className="text-primary" size={20} />
Connected Clients
</h3>
</div>
<div className="p-5">
<div className="rounded-lg border border-border-dark overflow-hidden bg-surface-dark">
<table className="w-full text-sm text-left">
<thead className="bg-background-dark text-text-secondary font-medium border-b border-border-dark">
<tr>
<th className="px-4 py-3">IP Address</th>
<th className="px-4 py-3">User</th>
<th className="px-4 py-3">Protocol</th>
<th className="px-4 py-3 text-right">Throughput</th>
<th className="px-4 py-3 text-right">Action</th>
</tr>
</thead>
<tbody className="text-white divide-y divide-border-dark">
<tr>
<td className="px-4 py-3 font-mono">192.168.10.105</td>
<td className="px-4 py-3">esxi-host-01</td>
<td className="px-4 py-3">
<span className="bg-primary/20 text-primary px-1.5 py-0.5 rounded text-xs font-bold">NFS</span>
</td>
<td className="px-4 py-3 text-right font-mono text-text-secondary">420 MB/s</td>
<td className="px-4 py-3 text-right">
<button className="text-text-secondary hover:text-red-400" title="Disconnect">
<Ban size={18} />
</button>
</td>
</tr>
<tr>
<td className="px-4 py-3 font-mono">192.168.10.106</td>
<td className="px-4 py-3">esxi-host-02</td>
<td className="px-4 py-3">
<span className="bg-primary/20 text-primary px-1.5 py-0.5 rounded text-xs font-bold">NFS</span>
</td>
<td className="px-4 py-3 text-right font-mono text-text-secondary">105 MB/s</td>
<td className="px-4 py-3 text-right">
<button className="text-text-secondary hover:text-red-400" title="Disconnect">
<Ban size={18} />
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
)}
</div>
</div>
</>
) : (
<div className="flex-1 flex items-center justify-center text-text-secondary">
<div className="text-center">
<FolderOpen className="mx-auto mb-4 text-text-secondary" size={48} />
<p className="text-sm">Select a share to view details</p>
</div>
</div>
)}
</div>
</div>
{/* Create Share Modal */}
{showCreateForm && (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
onClick={() => setShowCreateForm(false)}
>
<div
className="bg-surface-dark rounded-xl border border-border-dark max-w-2xl w-full max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="p-6 border-b border-border-dark flex justify-between items-center">
<h3 className="text-lg font-bold text-white">Create New Share</h3>
<button
onClick={() => setShowCreateForm(false)}
className="text-text-secondary hover:text-white"
>
<X size={20} />
</button>
</div>
<div className="p-6 flex flex-col gap-4">
<div className="flex flex-col gap-2">
<label className="text-sm font-semibold text-white">Dataset</label>
<select
className="w-full bg-background-dark border border-border-dark rounded-lg px-3 py-2 text-sm text-white focus:border-primary focus:ring-1 focus:ring-primary"
value={formData.dataset_id}
onChange={(e) => setFormData({ ...formData, dataset_id: e.target.value })}
>
<option value="">Select a dataset</option>
{datasets.map((ds) => (
<option key={ds.id} value={ds.id}>
{ds.name} {ds.mount_point && ds.mount_point !== 'none' ? `(${ds.mount_point})` : ''}
</option>
))}
</select>
</div>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between p-4 rounded-xl border border-border-dark bg-surface-dark/40">
<div className="flex items-center gap-3">
<Network className="text-text-secondary" size={20} />
<div className="flex flex-col">
<span className="text-sm font-bold text-white">Enable NFS</span>
</div>
</div>
<input
type="checkbox"
checked={formData.nfs_enabled}
onChange={(e) => setFormData({ ...formData, nfs_enabled: e.target.checked })}
className="h-4 w-4 rounded border-border-dark bg-background-dark text-primary focus:ring-primary"
/>
</div>
<div className="flex items-center justify-between p-4 rounded-xl border border-border-dark bg-surface-dark/40">
<div className="flex items-center gap-3">
<FolderSymlink className="text-text-secondary" size={20} />
<div className="flex flex-col">
<span className="text-sm font-bold text-white">Enable SMB</span>
</div>
</div>
<input
type="checkbox"
checked={formData.smb_enabled}
onChange={(e) => setFormData({ ...formData, smb_enabled: e.target.checked })}
className="h-4 w-4 rounded border-border-dark bg-background-dark text-primary focus:ring-primary"
/>
</div>
</div>
{formData.nfs_enabled && (
<div className="flex flex-col gap-2">
<label className="text-sm font-semibold text-white">NFS Options</label>
<input
className="w-full bg-background-dark border border-border-dark rounded-lg px-3 py-2 text-sm text-white focus:border-primary focus:ring-1 focus:ring-primary font-mono"
type="text"
value={formData.nfs_options}
onChange={(e) => setFormData({ ...formData, nfs_options: e.target.value })}
/>
</div>
)}
{formData.smb_enabled && (
<>
<div className="flex flex-col gap-2">
<label className="text-sm font-semibold text-white">SMB Share Name</label>
<input
className="w-full bg-background-dark border border-border-dark rounded-lg px-3 py-2 text-sm text-white focus:border-primary focus:ring-1 focus:ring-primary"
type="text"
value={formData.smb_share_name}
onChange={(e) => setFormData({ ...formData, smb_share_name: e.target.value })}
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-semibold text-white">Comment</label>
<input
className="w-full bg-background-dark border border-border-dark rounded-lg px-3 py-2 text-sm text-white focus:border-primary focus:ring-1 focus:ring-primary"
type="text"
value={formData.smb_comment}
onChange={(e) => setFormData({ ...formData, smb_comment: e.target.value })}
/>
</div>
</>
)}
<div className="flex justify-end gap-3 pt-4">
<Button
onClick={() => setShowCreateForm(false)}
variant="outline"
className="px-4 h-10"
>
Cancel
</Button>
<Button
type="button"
onClick={handleCreateShare}
disabled={createMutation.isPending}
className="px-4 h-10 bg-primary hover:bg-blue-600"
>
{createMutation.isPending ? 'Creating...' : 'Create Share'}
</Button>
</div>
</div>
</div>
</div>
)}
</div>
)
}