972 lines
50 KiB
TypeScript
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>
|
|
)
|
|
}
|