fix network interface information fetch from OS

This commit is contained in:
Warp Agent
2025-12-29 20:43:34 +07:00
parent 5fdb56e498
commit cb923704db
6 changed files with 595 additions and 22 deletions

View File

@@ -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)
},
}

View File

@@ -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>
)
}