730 lines
38 KiB
TypeScript
730 lines
38 KiB
TypeScript
import { useState, useRef, useEffect } from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { systemAPI, NetworkInterface } from '@/api/system'
|
|
|
|
export default function System() {
|
|
const [snmpEnabled, setSnmpEnabled] = useState(false)
|
|
const [openMenu, setOpenMenu] = useState<string | null>(null)
|
|
const [editingInterface, setEditingInterface] = useState<NetworkInterface | null>(null)
|
|
const [timezone, setTimezone] = useState('Etc/UTC')
|
|
const [ntpServers, setNtpServers] = useState<string[]>(['pool.ntp.org', 'time.google.com'])
|
|
const menuRef = useRef<HTMLDivElement>(null)
|
|
|
|
const queryClient = useQueryClient()
|
|
|
|
// Save NTP settings mutation
|
|
const saveNTPSettingsMutation = useMutation({
|
|
mutationFn: (data: { timezone: string; ntp_servers: string[] }) => systemAPI.saveNTPSettings(data),
|
|
onSuccess: () => {
|
|
// Refetch NTP settings to get the updated values
|
|
queryClient.invalidateQueries({ queryKey: ['system', 'ntp'] })
|
|
// Show success message (you can add a toast notification here)
|
|
alert('NTP settings saved successfully!')
|
|
},
|
|
onError: (error: any) => {
|
|
alert(`Failed to save NTP settings: ${error.message || 'Unknown error'}`)
|
|
},
|
|
})
|
|
|
|
// Fetch network interfaces
|
|
const { data: interfaces = [], isLoading: interfacesLoading } = useQuery({
|
|
queryKey: ['system', 'interfaces'],
|
|
queryFn: () => systemAPI.listNetworkInterfaces(),
|
|
refetchInterval: 5000, // Refresh every 5 seconds
|
|
})
|
|
|
|
// Fetch NTP settings on mount
|
|
const { data: ntpSettings } = useQuery({
|
|
queryKey: ['system', 'ntp'],
|
|
queryFn: () => systemAPI.getNTPSettings(),
|
|
})
|
|
|
|
// Update state when NTP settings are loaded
|
|
useEffect(() => {
|
|
if (ntpSettings) {
|
|
setTimezone(ntpSettings.timezone)
|
|
setNtpServers(ntpSettings.ntp_servers)
|
|
}
|
|
}, [ntpSettings])
|
|
|
|
// Close menu when clicking outside
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
|
setOpenMenu(null)
|
|
}
|
|
}
|
|
document.addEventListener('mousedown', handleClickOutside)
|
|
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
}, [])
|
|
|
|
return (
|
|
<div className="flex-1 flex flex-col h-full overflow-hidden relative bg-background-dark">
|
|
{/* Top Navigation */}
|
|
<header className="flex h-16 items-center justify-between border-b border-border-dark bg-background-dark px-6 lg:px-10 shrink-0 z-10">
|
|
<div className="flex items-center gap-4">
|
|
{/* Breadcrumbs */}
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Link to="/" className="text-text-secondary hover:text-white transition-colors">
|
|
System
|
|
</Link>
|
|
<span className="text-text-secondary">/</span>
|
|
<span className="text-white font-medium">Configuration</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<div className="hidden md:flex items-center gap-2 px-3 py-1.5 rounded-full bg-green-500/10 border border-green-500/20">
|
|
<div className="h-2 w-2 rounded-full bg-green-500 animate-pulse"></div>
|
|
<span className="text-xs font-medium text-green-500">System Healthy</span>
|
|
</div>
|
|
<div className="h-6 w-px bg-border-dark mx-2"></div>
|
|
<button className="flex items-center justify-center gap-2 rounded-lg bg-border-dark px-4 py-2 text-sm font-bold text-white hover:bg-[#2f455a] transition-colors">
|
|
<span className="material-symbols-outlined text-[18px]">restart_alt</span>
|
|
<span className="hidden sm:inline">Reboot</span>
|
|
</button>
|
|
<button className="flex items-center justify-center gap-2 rounded-lg bg-red-500/10 px-4 py-2 text-sm font-bold text-red-500 hover:bg-red-500/20 transition-colors border border-red-500/20">
|
|
<span className="material-symbols-outlined text-[18px]">power_settings_new</span>
|
|
<span className="hidden sm:inline">Shutdown</span>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Scrollable Content */}
|
|
<div className="flex-1 overflow-y-auto p-4 md:p-8 lg:px-12 scroll-smooth">
|
|
<div className="mx-auto max-w-7xl">
|
|
{/* Page Header */}
|
|
<div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight text-white mb-2">System Configuration</h1>
|
|
<p className="text-text-secondary text-sm max-w-2xl">
|
|
Manage network interfaces, time synchronization, service states, and remote management protocols.
|
|
</p>
|
|
</div>
|
|
<button className="flex items-center justify-center gap-2 rounded-lg bg-primary px-5 py-2.5 text-sm font-bold text-white hover:bg-blue-600 transition-all shadow-lg shadow-blue-500/20">
|
|
<span className="material-symbols-outlined text-[20px]">save</span>
|
|
Save Changes
|
|
</button>
|
|
</div>
|
|
|
|
{/* Grid Layout */}
|
|
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
|
|
{/* Network Card */}
|
|
<div className="flex flex-col rounded-xl border border-border-dark bg-card-dark shadow-sm">
|
|
<div className="flex items-center justify-between border-b border-border-dark px-6 py-4">
|
|
<div className="flex items-center gap-3">
|
|
<span className="material-symbols-outlined text-primary">lan</span>
|
|
<h2 className="text-lg font-bold text-white">Network Interfaces</h2>
|
|
</div>
|
|
<button className="text-xs font-bold text-primary hover:text-blue-400">CONFIGURE DNS</button>
|
|
</div>
|
|
<div className="p-2">
|
|
{interfacesLoading ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<span className="text-text-secondary">Loading interfaces...</span>
|
|
</div>
|
|
) : interfaces.length === 0 ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<span className="text-text-secondary">No network interfaces found</span>
|
|
</div>
|
|
) : (
|
|
interfaces.map((iface: NetworkInterface) => {
|
|
const isConnected = iface.status === 'Connected'
|
|
const roleBgColor = iface.role === 'ISCSI' ? 'bg-purple-500/20' : 'bg-primary/20'
|
|
const roleTextColor = iface.role === 'ISCSI' ? 'text-purple-400' : 'text-primary'
|
|
|
|
return (
|
|
<div
|
|
key={iface.name}
|
|
className={`group flex items-center justify-between rounded-lg p-3 hover:bg-border-dark/50 transition-colors ${!isConnected ? 'opacity-70' : ''}`}
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<div className={`flex h-10 w-10 items-center justify-center rounded-lg bg-border-dark ${isConnected ? 'text-white' : 'text-text-secondary'}`}>
|
|
<span className="material-symbols-outlined">settings_ethernet</span>
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<p className={`font-bold ${isConnected ? 'text-white' : 'text-text-secondary'}`}>{iface.name}</p>
|
|
{iface.role && (
|
|
<span className={`rounded ${roleBgColor} px-1.5 py-0.5 text-[10px] font-bold ${roleTextColor} uppercase`}>
|
|
{iface.role}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{iface.ip_address ? (
|
|
<p className="font-mono text-xs text-text-secondary">
|
|
{iface.ip_address} <span className="opacity-50 mx-1">/</span> {iface.subnet}
|
|
</p>
|
|
) : (
|
|
<p className="font-mono text-xs text-text-secondary">No Carrier</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<div className="hidden sm:flex flex-col items-end">
|
|
{isConnected ? (
|
|
<>
|
|
<div className="flex items-center gap-1.5">
|
|
<div className="h-2 w-2 rounded-full bg-green-500"></div>
|
|
<span className="text-xs font-medium text-white">Connected</span>
|
|
</div>
|
|
{iface.speed && iface.speed !== 'Unknown' && (
|
|
<span className="text-xs text-text-secondary">{iface.speed}</span>
|
|
)}
|
|
</>
|
|
) : (
|
|
<div className="flex items-center gap-1.5">
|
|
<div className="h-2 w-2 rounded-full bg-red-500"></div>
|
|
<span className="text-xs font-medium text-red-500">Down</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="relative" ref={menuRef}>
|
|
<button
|
|
onClick={() => setOpenMenu(openMenu === iface.name ? null : iface.name)}
|
|
className="h-8 w-8 rounded-full hover:bg-border-dark flex items-center justify-center text-text-secondary hover:text-white transition-colors"
|
|
>
|
|
<span className="material-symbols-outlined">more_vert</span>
|
|
</button>
|
|
{openMenu === iface.name && (
|
|
<div className="absolute right-0 mt-1 w-48 rounded-lg border border-border-dark bg-card-dark shadow-lg z-50">
|
|
<button
|
|
onClick={() => {
|
|
setEditingInterface(iface)
|
|
setOpenMenu(null)
|
|
}}
|
|
className="w-full flex items-center gap-2 px-4 py-2.5 text-sm text-white hover:bg-border-dark transition-colors first:rounded-t-lg"
|
|
>
|
|
<span className="material-symbols-outlined text-[18px]">edit</span>
|
|
<span>Edit Connection</span>
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
// TODO: Implement view details
|
|
setOpenMenu(null)
|
|
}}
|
|
className="w-full flex items-center gap-2 px-4 py-2.5 text-sm text-white hover:bg-border-dark transition-colors"
|
|
>
|
|
<span className="material-symbols-outlined text-[18px]">info</span>
|
|
<span>View Details</span>
|
|
</button>
|
|
<div className="border-t border-border-dark"></div>
|
|
<button
|
|
onClick={() => {
|
|
// TODO: Implement disable/enable
|
|
setOpenMenu(null)
|
|
}}
|
|
className="w-full flex items-center gap-2 px-4 py-2.5 text-sm text-white hover:bg-border-dark transition-colors last:rounded-b-lg"
|
|
>
|
|
<span className="material-symbols-outlined text-[18px]">
|
|
{isConnected ? 'toggle_on' : 'toggle_off'}
|
|
</span>
|
|
<span>{isConnected ? 'Disable' : 'Enable'}</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
})
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Services Card */}
|
|
<div className="flex flex-col rounded-xl border border-border-dark bg-card-dark shadow-sm">
|
|
<div className="flex items-center justify-between border-b border-border-dark px-6 py-4">
|
|
<div className="flex items-center gap-3">
|
|
<span className="material-symbols-outlined text-primary">memory</span>
|
|
<h2 className="text-lg font-bold text-white">Service Control</h2>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="h-2 w-2 rounded-full bg-green-500"></span>
|
|
<span className="text-xs text-text-secondary">All Systems Normal</span>
|
|
</div>
|
|
</div>
|
|
<div className="p-4 flex flex-col gap-1">
|
|
{/* Service Row */}
|
|
<div className="flex items-center justify-between rounded-lg bg-[#111a22] p-3 border border-transparent hover:border-border-dark transition-colors">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 rounded bg-border-dark/50 text-white">
|
|
<span className="material-symbols-outlined text-[20px]">terminal</span>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-bold text-white">SSH Service</p>
|
|
<p className="text-xs text-text-secondary">Remote command line access</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-green-500/20 text-green-500 border border-green-500/20">RUNNING</span>
|
|
<div className="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
|
|
<input
|
|
defaultChecked
|
|
className="toggle-checkbox absolute block w-5 h-5 rounded-full bg-white border-4 appearance-none cursor-pointer checked:right-0 checked:border-primary transition-all duration-300"
|
|
id="ssh-toggle"
|
|
name="toggle"
|
|
type="checkbox"
|
|
/>
|
|
<label
|
|
className="toggle-label block overflow-hidden h-5 rounded-full bg-border-dark cursor-pointer transition-colors duration-300"
|
|
htmlFor="ssh-toggle"
|
|
></label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* Service Row */}
|
|
<div className="flex items-center justify-between rounded-lg bg-[#111a22] p-3 border border-transparent hover:border-border-dark transition-colors">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 rounded bg-border-dark/50 text-white">
|
|
<span className="material-symbols-outlined text-[20px]">folder_shared</span>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-bold text-white">SMB / CIFS</p>
|
|
<p className="text-xs text-text-secondary">Windows file sharing</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-green-500/20 text-green-500 border border-green-500/20">RUNNING</span>
|
|
<div className="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
|
|
<input
|
|
defaultChecked
|
|
className="toggle-checkbox absolute block w-5 h-5 rounded-full bg-white border-4 appearance-none cursor-pointer checked:right-0 checked:border-primary transition-all duration-300"
|
|
id="smb-toggle"
|
|
name="toggle"
|
|
type="checkbox"
|
|
/>
|
|
<label
|
|
className="toggle-label block overflow-hidden h-5 rounded-full bg-border-dark cursor-pointer transition-colors duration-300"
|
|
htmlFor="smb-toggle"
|
|
></label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* Service Row */}
|
|
<div className="flex items-center justify-between rounded-lg bg-[#111a22] p-3 border border-transparent hover:border-border-dark transition-colors">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 rounded bg-border-dark/50 text-white">
|
|
<span className="material-symbols-outlined text-[20px]">storage</span>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-bold text-white">iSCSI Target</p>
|
|
<p className="text-xs text-text-secondary">Block storage sharing</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-yellow-500/20 text-yellow-500 border border-yellow-500/20">STOPPED</span>
|
|
<div className="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
|
|
<input
|
|
className="toggle-checkbox absolute block w-5 h-5 rounded-full bg-white border-4 appearance-none cursor-pointer checked:right-0 checked:border-primary transition-all duration-300"
|
|
id="iscsi-toggle"
|
|
name="toggle"
|
|
type="checkbox"
|
|
/>
|
|
<label
|
|
className="toggle-label block overflow-hidden h-5 rounded-full bg-border-dark cursor-pointer transition-colors duration-300"
|
|
htmlFor="iscsi-toggle"
|
|
></label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* Service Row */}
|
|
<div className="flex items-center justify-between rounded-lg bg-[#111a22] p-3 border border-transparent hover:border-border-dark transition-colors">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 rounded bg-border-dark/50 text-white">
|
|
<span className="material-symbols-outlined text-[20px]">share</span>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-bold text-white">NFS Service</p>
|
|
<p className="text-xs text-text-secondary">Unix file sharing</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-green-500/20 text-green-500 border border-green-500/20">RUNNING</span>
|
|
<div className="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
|
|
<input
|
|
defaultChecked
|
|
className="toggle-checkbox absolute block w-5 h-5 rounded-full bg-white border-4 appearance-none cursor-pointer checked:right-0 checked:border-primary transition-all duration-300"
|
|
id="nfs-toggle"
|
|
name="toggle"
|
|
type="checkbox"
|
|
/>
|
|
<label
|
|
className="toggle-label block overflow-hidden h-5 rounded-full bg-border-dark cursor-pointer transition-colors duration-300"
|
|
htmlFor="nfs-toggle"
|
|
></label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* Service Row - VTL (MHVTL) */}
|
|
<div className="flex items-center justify-between rounded-lg bg-[#111a22] p-3 border border-transparent hover:border-border-dark transition-colors">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 rounded bg-border-dark/50 text-white">
|
|
<span className="material-symbols-outlined text-[20px]">album</span>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-bold text-white">VTL Service</p>
|
|
<p className="text-xs text-text-secondary">Virtual tape library emulation</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-green-500/20 text-green-500 border border-green-500/20">RUNNING</span>
|
|
<div className="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
|
|
<input
|
|
defaultChecked
|
|
className="toggle-checkbox absolute block w-5 h-5 rounded-full bg-white border-4 appearance-none cursor-pointer checked:right-0 checked:border-primary transition-all duration-300"
|
|
id="mhvtl-toggle"
|
|
name="toggle"
|
|
type="checkbox"
|
|
/>
|
|
<label
|
|
className="toggle-label block overflow-hidden h-5 rounded-full bg-border-dark cursor-pointer transition-colors duration-300"
|
|
htmlFor="mhvtl-toggle"
|
|
></label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Date & Time Card */}
|
|
<div className="flex flex-col rounded-xl border border-border-dark bg-card-dark shadow-sm">
|
|
<div className="flex items-center justify-between border-b border-border-dark px-6 py-4">
|
|
<div className="flex items-center gap-3">
|
|
<span className="material-symbols-outlined text-primary">schedule</span>
|
|
<h2 className="text-lg font-bold text-white">Date & Time</h2>
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
saveNTPSettingsMutation.mutate({
|
|
timezone,
|
|
ntp_servers: ntpServers,
|
|
})
|
|
}}
|
|
disabled={saveNTPSettingsMutation.isPending}
|
|
className="flex items-center justify-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-bold text-white hover:bg-blue-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<span className="material-symbols-outlined text-[16px]">save</span>
|
|
{saveNTPSettingsMutation.isPending ? 'Saving...' : 'Save'}
|
|
</button>
|
|
</div>
|
|
<div className="p-6 flex flex-col gap-6">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="col-span-2">
|
|
<label className="mb-2 block text-xs font-medium text-text-secondary uppercase">System Timezone</label>
|
|
<div className="relative">
|
|
<select
|
|
value={timezone}
|
|
onChange={(e) => setTimezone(e.target.value)}
|
|
className="block w-full rounded-lg border-border-dark bg-[#111a22] py-2.5 pl-3 pr-10 text-sm text-white focus:border-primary focus:ring-1 focus:ring-primary appearance-none"
|
|
>
|
|
<option>Etc/UTC</option>
|
|
<option>Asia/Jakarta</option>
|
|
<option>Asia/Singapore</option>
|
|
<option>Asia/Bangkok</option>
|
|
<option>Asia/Manila</option>
|
|
<option>Asia/Tokyo</option>
|
|
<option>Asia/Shanghai</option>
|
|
<option>Asia/Hong_Kong</option>
|
|
<option>Europe/London</option>
|
|
<option>Europe/Paris</option>
|
|
<option>America/New_York</option>
|
|
<option>America/Los_Angeles</option>
|
|
</select>
|
|
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-white">
|
|
<span className="material-symbols-outlined text-sm">expand_more</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<label className="block text-xs font-medium text-text-secondary uppercase">NTP Servers</label>
|
|
<button className="text-xs text-primary font-bold hover:text-white flex items-center gap-1">
|
|
<span className="material-symbols-outlined text-[14px]">add</span> Add Server
|
|
</button>
|
|
</div>
|
|
<div className="flex flex-col gap-2">
|
|
{ntpServers.map((server, index) => (
|
|
<div key={index} className="flex items-center justify-between rounded-lg bg-[#111a22] p-3 border border-border-dark">
|
|
<div className="flex items-center gap-3">
|
|
<div className="h-2 w-2 rounded-full bg-green-500 animate-pulse"></div>
|
|
<span className="text-sm font-mono text-white">{server}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs text-text-secondary">Stratum 2 • 12ms</span>
|
|
<button
|
|
onClick={() => {
|
|
setNtpServers(ntpServers.filter((_, i) => i !== index))
|
|
}}
|
|
className="text-red-500 hover:text-red-400"
|
|
>
|
|
<span className="material-symbols-outlined text-[16px]">delete</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Management & SNMP Card */}
|
|
<div className="flex flex-col rounded-xl border border-border-dark bg-card-dark shadow-sm">
|
|
<div className="flex items-center justify-between border-b border-border-dark px-6 py-4">
|
|
<div className="flex items-center gap-3">
|
|
<span className="material-symbols-outlined text-primary">hub</span>
|
|
<h2 className="text-lg font-bold text-white">Management</h2>
|
|
</div>
|
|
</div>
|
|
<div className="p-6 flex flex-col gap-6">
|
|
<div>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div>
|
|
<h3 className="text-sm font-bold text-white">SNMP Monitoring</h3>
|
|
<p className="text-xs text-text-secondary">Enable Simple Network Management Protocol</p>
|
|
</div>
|
|
<div className="relative inline-block w-10 align-middle select-none transition duration-200 ease-in">
|
|
<input
|
|
checked={snmpEnabled}
|
|
onChange={(e) => setSnmpEnabled(e.target.checked)}
|
|
className="toggle-checkbox absolute block w-5 h-5 rounded-full bg-white border-4 appearance-none cursor-pointer checked:right-0 checked:border-primary transition-all duration-300"
|
|
id="snmp-toggle"
|
|
name="toggle"
|
|
type="checkbox"
|
|
/>
|
|
<label
|
|
className="toggle-label block overflow-hidden h-5 rounded-full bg-border-dark cursor-pointer transition-colors duration-300"
|
|
htmlFor="snmp-toggle"
|
|
></label>
|
|
</div>
|
|
</div>
|
|
<div className={`grid grid-cols-1 gap-4 transition-opacity ${snmpEnabled ? 'opacity-100' : 'opacity-50 pointer-events-none'}`}>
|
|
<div>
|
|
<label className="mb-1.5 block text-xs font-medium text-text-secondary uppercase">Community String</label>
|
|
<input
|
|
className="block w-full rounded-lg border-border-dark bg-[#111a22] p-2.5 text-sm text-white placeholder-gray-500 focus:border-primary focus:ring-1 focus:ring-primary"
|
|
placeholder="e.g. public"
|
|
type="text"
|
|
defaultValue="public"
|
|
disabled={!snmpEnabled}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="mb-1.5 block text-xs font-medium text-text-secondary uppercase">Trap Receiver IP</label>
|
|
<input
|
|
className="block w-full rounded-lg border-border-dark bg-[#111a22] p-2.5 text-sm text-white placeholder-gray-500 focus:border-primary focus:ring-1 focus:ring-primary"
|
|
placeholder="e.g. 192.168.1.100"
|
|
type="text"
|
|
disabled={!snmpEnabled}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="border-t border-border-dark pt-4">
|
|
<h3 className="text-sm font-bold text-white mb-3">Syslog Forwarding</h3>
|
|
<div className="flex gap-2">
|
|
<input
|
|
className="flex-1 rounded-lg border-border-dark bg-[#111a22] p-2.5 text-sm text-white placeholder-gray-500 focus:border-primary focus:ring-1 focus:ring-primary"
|
|
placeholder="Syslog Server Address (UDP:514)"
|
|
type="text"
|
|
/>
|
|
<button className="rounded-lg bg-border-dark px-4 py-2 text-sm font-bold text-white hover:bg-[#2f455a] transition-colors">
|
|
Test
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bottom Spacer */}
|
|
<div className="h-10"></div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Edit Connection Modal */}
|
|
{editingInterface && (
|
|
<EditConnectionModal
|
|
interface={editingInterface}
|
|
onClose={() => setEditingInterface(null)}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Edit Connection Modal Component
|
|
interface EditConnectionModalProps {
|
|
interface: NetworkInterface
|
|
onClose: () => void
|
|
}
|
|
|
|
function EditConnectionModal({ interface: iface, onClose }: EditConnectionModalProps) {
|
|
const queryClient = useQueryClient()
|
|
const [formData, setFormData] = useState({
|
|
ip_address: iface.ip_address || '',
|
|
subnet: iface.subnet || '24',
|
|
gateway: iface.gateway || '',
|
|
dns1: iface.dns1 || '',
|
|
dns2: iface.dns2 || '',
|
|
role: iface.role || '',
|
|
})
|
|
|
|
const updateMutation = useMutation({
|
|
mutationFn: (data: { ip_address: string; subnet: string; gateway?: string; dns1?: string; dns2?: string; role?: string }) =>
|
|
systemAPI.updateNetworkInterface(iface.name, data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['system', 'interfaces'] })
|
|
onClose()
|
|
},
|
|
onError: (error: any) => {
|
|
alert(`Failed to update interface: ${error.response?.data?.error || error.message}`)
|
|
},
|
|
})
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
updateMutation.mutate({
|
|
ip_address: formData.ip_address,
|
|
subnet: formData.subnet,
|
|
gateway: formData.gateway || undefined,
|
|
dns1: formData.dns1 || undefined,
|
|
dns2: formData.dns2 || undefined,
|
|
role: formData.role || undefined,
|
|
})
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
|
<div className="w-full max-w-2xl rounded-xl border border-border-dark bg-card-dark shadow-xl">
|
|
<div className="flex items-center justify-between border-b border-border-dark px-6 py-4">
|
|
<div className="flex items-center gap-3">
|
|
<span className="material-symbols-outlined text-primary">settings_ethernet</span>
|
|
<h2 className="text-lg font-bold text-white">Edit Connection - {iface.name}</h2>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="h-8 w-8 rounded-full hover:bg-border-dark flex items-center justify-center text-text-secondary hover:text-white transition-colors"
|
|
>
|
|
<span className="material-symbols-outlined text-[20px]">close</span>
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="p-6">
|
|
<div className="space-y-4">
|
|
{/* IP Address */}
|
|
<div>
|
|
<label className="mb-2 block text-xs font-medium text-text-secondary uppercase">
|
|
IP Address
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.ip_address}
|
|
onChange={(e) => setFormData({ ...formData, ip_address: e.target.value })}
|
|
className="block w-full rounded-lg border-border-dark bg-[#111a22] p-2.5 text-sm text-white placeholder-gray-500 focus:border-primary focus:ring-1 focus:ring-primary"
|
|
placeholder="192.168.1.100"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{/* Subnet Mask */}
|
|
<div>
|
|
<label className="mb-2 block text-xs font-medium text-text-secondary uppercase">
|
|
Subnet Mask (CIDR)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.subnet}
|
|
onChange={(e) => setFormData({ ...formData, subnet: e.target.value })}
|
|
className="block w-full rounded-lg border-border-dark bg-[#111a22] p-2.5 text-sm text-white placeholder-gray-500 focus:border-primary focus:ring-1 focus:ring-primary"
|
|
placeholder="24"
|
|
required
|
|
/>
|
|
<p className="mt-1 text-xs text-text-secondary">Enter CIDR notation (e.g., 24 for 255.255.255.0)</p>
|
|
</div>
|
|
|
|
{/* Gateway */}
|
|
<div>
|
|
<label className="mb-2 block text-xs font-medium text-text-secondary uppercase">
|
|
Default Gateway
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.gateway}
|
|
onChange={(e) => setFormData({ ...formData, gateway: e.target.value })}
|
|
className="block w-full rounded-lg border-border-dark bg-[#111a22] p-2.5 text-sm text-white placeholder-gray-500 focus:border-primary focus:ring-1 focus:ring-primary"
|
|
placeholder="192.168.1.1"
|
|
/>
|
|
</div>
|
|
|
|
{/* DNS Servers */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="mb-2 block text-xs font-medium text-text-secondary uppercase">
|
|
Primary DNS
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.dns1}
|
|
onChange={(e) => setFormData({ ...formData, dns1: e.target.value })}
|
|
className="block w-full rounded-lg border-border-dark bg-[#111a22] p-2.5 text-sm text-white placeholder-gray-500 focus:border-primary focus:ring-1 focus:ring-primary"
|
|
placeholder="8.8.8.8"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="mb-2 block text-xs font-medium text-text-secondary uppercase">
|
|
Secondary DNS
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.dns2}
|
|
onChange={(e) => setFormData({ ...formData, dns2: e.target.value })}
|
|
className="block w-full rounded-lg border-border-dark bg-[#111a22] p-2.5 text-sm text-white placeholder-gray-500 focus:border-primary focus:ring-1 focus:ring-primary"
|
|
placeholder="8.8.4.4"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Role */}
|
|
<div>
|
|
<label className="mb-2 block text-xs font-medium text-text-secondary uppercase">
|
|
Interface Role
|
|
</label>
|
|
<select
|
|
value={formData.role}
|
|
onChange={(e) => setFormData({ ...formData, role: e.target.value })}
|
|
className="block w-full rounded-lg border-border-dark bg-[#111a22] py-2.5 pl-3 pr-10 text-sm text-white focus:border-primary focus:ring-1 focus:ring-primary appearance-none"
|
|
>
|
|
<option value="">None</option>
|
|
<option value="Management">Management</option>
|
|
<option value="ISCSI">iSCSI</option>
|
|
<option value="Storage">Storage</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6 flex items-center justify-end gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="px-4 py-2 text-sm font-bold text-text-secondary hover:text-white transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={updateMutation.isPending}
|
|
className="flex items-center gap-2 rounded-lg bg-primary px-5 py-2.5 text-sm font-bold text-white hover:bg-blue-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<span className="material-symbols-outlined text-[18px]">save</span>
|
|
{updateMutation.isPending ? 'Saving...' : 'Save Changes'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|