fix network interface information fetch from OS
This commit is contained in:
@@ -7,6 +7,28 @@ export interface NetworkInterface {
|
||||
status: string // "Connected" or "Down"
|
||||
speed: string // e.g., "10 Gbps", "1 Gbps"
|
||||
role: string // "Management", "ISCSI", or empty
|
||||
gateway?: string
|
||||
dns1?: string
|
||||
dns2?: string
|
||||
}
|
||||
|
||||
export interface UpdateNetworkInterfaceRequest {
|
||||
ip_address: string
|
||||
subnet: string
|
||||
gateway?: string
|
||||
dns1?: string
|
||||
dns2?: string
|
||||
role?: string
|
||||
}
|
||||
|
||||
export interface SaveNTPSettingsRequest {
|
||||
timezone: string
|
||||
ntp_servers: string[]
|
||||
}
|
||||
|
||||
export interface NTPSettings {
|
||||
timezone: string
|
||||
ntp_servers: string[]
|
||||
}
|
||||
|
||||
export const systemAPI = {
|
||||
@@ -14,5 +36,16 @@ export const systemAPI = {
|
||||
const response = await apiClient.get<{ interfaces: NetworkInterface[] | null }>('/system/interfaces')
|
||||
return response.data.interfaces || []
|
||||
},
|
||||
updateNetworkInterface: async (name: string, data: UpdateNetworkInterfaceRequest): Promise<NetworkInterface> => {
|
||||
const response = await apiClient.put<{ interface: NetworkInterface }>(`/system/interfaces/${name}`, data)
|
||||
return response.data.interface
|
||||
},
|
||||
getNTPSettings: async (): Promise<NTPSettings> => {
|
||||
const response = await apiClient.get<{ settings: NTPSettings }>('/system/ntp')
|
||||
return response.data.settings
|
||||
},
|
||||
saveNTPSettings: async (data: SaveNTPSettingsRequest): Promise<void> => {
|
||||
await apiClient.post('/system/ntp', data)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
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({
|
||||
@@ -13,6 +34,31 @@ export default function System() {
|
||||
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 */}
|
||||
@@ -133,9 +179,51 @@ export default function System() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button 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>
|
||||
<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>
|
||||
)
|
||||
@@ -306,18 +394,42 @@ export default function System() {
|
||||
<span className="material-symbols-outlined text-primary">schedule</span>
|
||||
<h2 className="text-lg font-bold text-white">Date & Time</h2>
|
||||
</div>
|
||||
<span className="text-xs font-mono text-text-secondary bg-border-dark px-2 py-1 rounded">UTC</span>
|
||||
<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 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">
|
||||
<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>America/New_York</option>
|
||||
<option>Europe/London</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>
|
||||
@@ -333,20 +445,25 @@ export default function System() {
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div 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">pool.ntp.org</span>
|
||||
{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>
|
||||
<span className="text-xs text-text-secondary">Stratum 2 • 12ms</span>
|
||||
</div>
|
||||
<div 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"></div>
|
||||
<span className="text-sm font-mono text-white">time.google.com</span>
|
||||
</div>
|
||||
<span className="text-xs text-text-secondary">Stratum 1 • 45ms</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -425,6 +542,187 @@ export default function System() {
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user