fix mostly bugs on system management, and user roles and group assignment

This commit is contained in:
Warp Agent
2025-12-30 01:49:19 +07:00
parent cb923704db
commit ebaf718424
12 changed files with 1178 additions and 282 deletions

View File

@@ -31,6 +31,28 @@ export interface NTPSettings {
ntp_servers: string[]
}
export interface ServiceStatus {
name: string
active_state: string // "active", "inactive", "activating", "deactivating", "failed"
sub_state: string
load_state: string
description: string
since?: string
}
export interface SystemLogEntry {
time: string
level: string
source: string
message: string
}
export interface NetworkDataPoint {
time: string
inbound: number // Mbps
outbound: number // Mbps
}
export const systemAPI = {
listNetworkInterfaces: async (): Promise<NetworkInterface[]> => {
const response = await apiClient.get<{ interfaces: NetworkInterface[] | null }>('/system/interfaces')
@@ -47,5 +69,20 @@ export const systemAPI = {
saveNTPSettings: async (data: SaveNTPSettingsRequest): Promise<void> => {
await apiClient.post('/system/ntp', data)
},
listServices: async (): Promise<ServiceStatus[]> => {
const response = await apiClient.get<{ services: ServiceStatus[] }>('/system/services')
return response.data.services || []
},
restartService: async (name: string): Promise<void> => {
await apiClient.post(`/system/services/${name}/restart`)
},
getSystemLogs: async (limit: number = 30): Promise<SystemLogEntry[]> => {
const response = await apiClient.get<{ logs: SystemLogEntry[] }>(`/system/logs?limit=${limit}`)
return response.data.logs || []
},
getNetworkThroughput: async (duration: string = '5m'): Promise<NetworkDataPoint[]> => {
const response = await apiClient.get<{ data: NetworkDataPoint[] }>(`/system/network/throughput?duration=${duration}`)
return response.data.data || []
},
}

View File

@@ -3,6 +3,7 @@ import { useState, useMemo, useEffect } from 'react'
import apiClient from '@/api/client'
import { monitoringApi } from '@/api/monitoring'
import { storageApi } from '@/api/storage'
import { systemAPI } from '@/api/system'
import { formatBytes } from '@/lib/format'
import {
Cpu,
@@ -46,17 +47,18 @@ const MOCK_ACTIVE_JOBS = [
},
]
const MOCK_SYSTEM_LOGS = [
{ time: '10:45:22', level: 'INFO', source: 'systemd', message: 'Started User Manager for UID 1000.' },
{ time: '10:45:15', level: 'WARN', source: 'smartd', message: 'Device: /dev/ada5, SMART Usage Attribute: 194 Temperature_Celsius changed from 38 to 41' },
{ time: '10:44:58', level: 'INFO', source: 'kernel', message: 'ix0: link state changed to UP' },
{ time: '10:42:10', level: 'INFO', source: 'zfs', message: 'zfs_arc_reclaim_thread: reclaiming 157286400 bytes ...' },
]
export default function Dashboard() {
const [activeTab, setActiveTab] = useState<'jobs' | 'logs' | 'alerts'>('jobs')
const [networkDataPoints, setNetworkDataPoints] = useState<Array<{ time: string; inbound: number; outbound: number }>>([])
const refreshInterval = 5
// Fetch system logs with auto-refresh every 10 minutes
const { data: systemLogs = [], isLoading: logsLoading, refetch: refetchLogs } = useQuery({
queryKey: ['system-logs'],
queryFn: () => systemAPI.getSystemLogs(30),
refetchInterval: 10 * 60 * 1000, // 10 minutes
})
const { data: health } = useQuery({
queryKey: ['health'],
@@ -143,51 +145,25 @@ export default function Dashboard() {
return { totalStorage: total, usedStorage: used, storagePercent: percent }
}, [repositories])
// Initialize network data
// Fetch network throughput data from RRD
const { data: networkThroughput = [] } = useQuery({
queryKey: ['network-throughput'],
queryFn: () => systemAPI.getNetworkThroughput('5m'),
refetchInterval: 5 * 1000, // Refresh every 5 seconds
})
// Update network data points when new data arrives
useEffect(() => {
// Generate initial 30 data points
const initialData = []
const now = Date.now()
for (let i = 29; i >= 0; i--) {
const time = new Date(now - i * 5000)
const minutes = time.getMinutes().toString().padStart(2, '0')
const seconds = time.getSeconds().toString().padStart(2, '0')
const baseInbound = 800 + Math.random() * 400
const baseOutbound = 400 + Math.random() * 200
initialData.push({
time: `${minutes}:${seconds}`,
inbound: Math.round(baseInbound),
outbound: Math.round(baseOutbound),
})
if (networkThroughput.length > 0) {
// Take last 30 points
const points = networkThroughput.slice(-30).map((point) => ({
time: point.time,
inbound: Math.round(point.inbound),
outbound: Math.round(point.outbound),
}))
setNetworkDataPoints(points)
}
setNetworkDataPoints(initialData)
// Update data every 5 seconds
const interval = setInterval(() => {
setNetworkDataPoints((prev) => {
const now = new Date()
const minutes = now.getMinutes().toString().padStart(2, '0')
const seconds = now.getSeconds().toString().padStart(2, '0')
const baseInbound = 800 + Math.random() * 400
const baseOutbound = 400 + Math.random() * 200
const newPoint = {
time: `${minutes}:${seconds}`,
inbound: Math.round(baseInbound),
outbound: Math.round(baseOutbound),
}
// Keep only last 30 points
const updated = [...prev.slice(1), newPoint]
return updated
})
}, 5000)
return () => clearInterval(interval)
}, [])
}, [networkThroughput])
// Calculate current and peak throughput
const currentThroughput = useMemo(() => {
@@ -564,39 +540,59 @@ export default function Dashboard() {
<h4 className="text-xs uppercase text-text-secondary font-bold tracking-wider">
Recent System Events
</h4>
<button className="text-xs text-primary hover:text-white transition-colors">
View All Logs
</button>
<div className="flex items-center gap-3">
<button
onClick={() => refetchLogs()}
disabled={logsLoading}
className="text-xs text-primary hover:text-white transition-colors flex items-center gap-1 disabled:opacity-50"
>
<RefreshCw size={14} className={logsLoading ? 'animate-spin' : ''} />
Refresh
</button>
<button className="text-xs text-primary hover:text-white transition-colors">
View All Logs
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto custom-scrollbar bg-[#111a22]">
<table className="w-full text-left border-collapse">
<tbody className="text-sm font-mono divide-y divide-border-dark/50">
{MOCK_SYSTEM_LOGS.map((log, idx) => (
<tr key={idx} className="group hover:bg-[#233648] transition-colors">
<td className="px-6 py-2 text-text-secondary w-32 whitespace-nowrap">
{log.time}
</td>
<td className="px-6 py-2 w-24">
<span
className={
log.level === 'INFO'
? 'text-emerald-500'
: log.level === 'WARN'
? 'text-yellow-500'
: 'text-red-500'
}
>
{log.level}
</span>
</td>
<td className="px-6 py-2 w-32 text-white">{log.source}</td>
<td className="px-6 py-2 text-text-secondary truncate max-w-lg">
{log.message}
</td>
</tr>
))}
</tbody>
</table>
{logsLoading ? (
<div className="flex items-center justify-center py-8">
<span className="text-text-secondary">Loading logs...</span>
</div>
) : systemLogs.length === 0 ? (
<div className="flex items-center justify-center py-8">
<span className="text-text-secondary">No logs available</span>
</div>
) : (
<table className="w-full text-left border-collapse">
<tbody className="text-sm font-mono divide-y divide-border-dark/50">
{systemLogs.map((log, idx) => (
<tr key={idx} className="group hover:bg-[#233648] transition-colors">
<td className="px-6 py-2 text-text-secondary w-32 whitespace-nowrap">
{log.time}
</td>
<td className="px-6 py-2 w-24">
<span
className={
log.level === 'INFO' || log.level === 'NOTICE' || log.level === 'DEBUG'
? 'text-emerald-500'
: log.level === 'WARN'
? 'text-yellow-500'
: 'text-red-500'
}
>
{log.level}
</span>
</td>
<td className="px-6 py-2 w-32 text-white">{log.source}</td>
<td className="px-6 py-2 text-text-secondary truncate max-w-lg">
{log.message}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</>
)}

View File

@@ -696,10 +696,15 @@ function EditUserForm({ user, onClose, onSuccess }: EditUserFormProps) {
iamApi.updateUser(user.id, data),
onSuccess: async () => {
onSuccess()
// Invalidate all related queries to refresh counts
queryClient.invalidateQueries({ queryKey: ['iam-users'] })
await queryClient.refetchQueries({ queryKey: ['iam-users'] })
queryClient.invalidateQueries({ queryKey: ['iam-user', user.id] })
queryClient.invalidateQueries({ queryKey: ['iam-roles'] }) // Refresh role user counts
queryClient.invalidateQueries({ queryKey: ['iam-groups'] }) // Refresh group user counts
await queryClient.refetchQueries({ queryKey: ['iam-users'] })
await queryClient.refetchQueries({ queryKey: ['iam-user', user.id] })
await queryClient.refetchQueries({ queryKey: ['iam-roles'] })
await queryClient.refetchQueries({ queryKey: ['iam-groups'] })
},
onError: (error: any) => {
console.error('Failed to update user:', error)
@@ -725,9 +730,11 @@ function EditUserForm({ user, onClose, onSuccess }: EditUserFormProps) {
},
onSuccess: async (_, roleName: string) => {
// Don't overwrite state with server data - keep optimistic update
// Just invalidate queries for other components
// Invalidate queries to refresh counts
queryClient.invalidateQueries({ queryKey: ['iam-users'] })
queryClient.invalidateQueries({ queryKey: ['iam-user', user.id] })
queryClient.invalidateQueries({ queryKey: ['iam-roles'] }) // Refresh role user count
await queryClient.refetchQueries({ queryKey: ['iam-roles'] })
// Use functional update to get current state
setUserRoles(current => {
console.log('assignRoleMutation onSuccess - roleName:', roleName, 'current userRoles:', current)
@@ -753,9 +760,11 @@ function EditUserForm({ user, onClose, onSuccess }: EditUserFormProps) {
},
onSuccess: async (_, roleName: string) => {
// Don't overwrite state with server data - keep optimistic update
// Just invalidate queries for other components
// Invalidate queries to refresh counts
queryClient.invalidateQueries({ queryKey: ['iam-users'] })
queryClient.invalidateQueries({ queryKey: ['iam-user', user.id] })
queryClient.invalidateQueries({ queryKey: ['iam-roles'] }) // Refresh role user count
await queryClient.refetchQueries({ queryKey: ['iam-roles'] })
console.log('Role removed successfully:', roleName, 'Current userRoles:', userRoles)
},
onError: (error: any, _roleName: string, context: any) => {
@@ -785,9 +794,11 @@ function EditUserForm({ user, onClose, onSuccess }: EditUserFormProps) {
},
onSuccess: async (_, groupName: string) => {
// Don't overwrite state with server data - keep optimistic update
// Just invalidate queries for other components
// Invalidate queries to refresh counts
queryClient.invalidateQueries({ queryKey: ['iam-users'] })
queryClient.invalidateQueries({ queryKey: ['iam-user', user.id] })
queryClient.invalidateQueries({ queryKey: ['iam-groups'] }) // Refresh group user count
await queryClient.refetchQueries({ queryKey: ['iam-groups'] })
// Use functional update to get current state
setUserGroups(current => {
console.log('assignGroupMutation onSuccess - groupName:', groupName, 'current userGroups:', current)
@@ -813,9 +824,11 @@ function EditUserForm({ user, onClose, onSuccess }: EditUserFormProps) {
},
onSuccess: async (_, groupName: string) => {
// Don't overwrite state with server data - keep optimistic update
// Just invalidate queries for other components
// Invalidate queries to refresh counts
queryClient.invalidateQueries({ queryKey: ['iam-users'] })
queryClient.invalidateQueries({ queryKey: ['iam-user', user.id] })
queryClient.invalidateQueries({ queryKey: ['iam-groups'] }) // Refresh group user count
await queryClient.refetchQueries({ queryKey: ['iam-groups'] })
console.log('Group removed successfully:', groupName, 'Current userGroups:', userGroups)
},
onError: (error: any, _groupName: string, context: any) => {

View File

@@ -7,8 +7,11 @@ export default function System() {
const [snmpEnabled, setSnmpEnabled] = useState(false)
const [openMenu, setOpenMenu] = useState<string | null>(null)
const [editingInterface, setEditingInterface] = useState<NetworkInterface | null>(null)
const [viewingInterface, setViewingInterface] = useState<NetworkInterface | null>(null)
const [timezone, setTimezone] = useState('Etc/UTC')
const [ntpServers, setNtpServers] = useState<string[]>(['pool.ntp.org', 'time.google.com'])
const [showAddNtpServer, setShowAddNtpServer] = useState(false)
const [newNtpServer, setNewNtpServer] = useState('')
const menuRef = useRef<HTMLDivElement>(null)
const queryClient = useQueryClient()
@@ -34,6 +37,14 @@ export default function System() {
refetchInterval: 5000, // Refresh every 5 seconds
})
// Fetch services
const { data: services = [], isLoading: servicesLoading } = useQuery({
queryKey: ['system', 'services'],
queryFn: () => systemAPI.listServices(),
refetchInterval: 5000, // Refresh every 5 seconds
})
// Fetch NTP settings on mount
const { data: ntpSettings } = useQuery({
queryKey: ['system', 'ntp'],
@@ -200,7 +211,7 @@ export default function System() {
</button>
<button
onClick={() => {
// TODO: Implement view details
setViewingInterface(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"
@@ -245,145 +256,65 @@ export default function System() {
</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>
{servicesLoading ? (
<div className="flex items-center justify-center py-8">
<span className="text-text-secondary">Loading services...</span>
</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>
) : (
// Service configs to display - map backend service names to display configs
[
{ key: 'ssh', serviceNames: ['ssh', 'sshd'], displayName: 'SSH Service', description: 'Remote command line access', icon: 'terminal' },
{ key: 'smb', serviceNames: ['smbd', 'samba', 'smb'], displayName: 'SMB / CIFS', description: 'Windows file sharing', icon: 'folder_shared' },
{ key: 'iscsi', serviceNames: ['iscsi-scst', 'iscsi', 'scst'], displayName: 'iSCSI Target', description: 'Block storage sharing', icon: 'storage' },
{ key: 'nfs', serviceNames: ['nfs-server', 'nfs', 'nfsd'], displayName: 'NFS Service', description: 'Unix file sharing', icon: 'share' },
{ key: 'vtl', serviceNames: ['mhvtl', 'vtl'], displayName: 'VTL Service', description: 'Virtual tape library emulation', icon: 'album' },
].map((config) => {
const service = services.find(s => {
const serviceNameLower = s.name.toLowerCase()
return config.serviceNames.some(name => serviceNameLower.includes(name.toLowerCase()) || name.toLowerCase().includes(serviceNameLower))
})
const isActive = service?.active_state === 'active'
const status = isActive ? 'RUNNING' : 'STOPPED'
const statusColor = isActive ? 'bg-green-500/20 text-green-500 border-green-500/20' : 'bg-yellow-500/20 text-yellow-500 border-yellow-500/20'
return (
<div key={config.key} 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]">{config.icon}</span>
</div>
<div>
<p className="text-sm font-bold text-white">{config.displayName}</p>
<p className="text-xs text-text-secondary">{config.description}</p>
</div>
</div>
<div className="flex items-center gap-4">
<span className={`px-2 py-0.5 rounded text-[10px] font-bold border ${statusColor}`}>{status}</span>
<label className="relative inline-block w-10 h-5 mr-2 align-middle select-none cursor-pointer">
<input
checked={isActive}
onChange={() => {
if (service) {
systemAPI.restartService(service.name).then(() => {
queryClient.invalidateQueries({ queryKey: ['system', 'services'] })
}).catch((err) => {
alert(`Failed to ${isActive ? 'stop' : 'start'} service: ${err.message || 'Unknown error'}`)
})
}
}}
className="sr-only peer"
id={`${config.key}-toggle`}
name="toggle"
type="checkbox"
/>
<span className="absolute inset-0 rounded-full bg-border-dark transition-colors duration-300 peer-checked:bg-primary/20"></span>
<span className="absolute left-0.5 top-0.5 h-4 w-4 rounded-full bg-white transition-transform duration-300 peer-checked:translate-x-5"></span>
</label>
</div>
</div>
)
})
)}
</div>
</div>
@@ -440,11 +371,60 @@ export default function System() {
<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">
<button
onClick={() => setShowAddNtpServer(true)}
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">
{showAddNtpServer && (
<div className="flex items-center gap-2 rounded-lg bg-[#111a22] p-3 border border-border-dark">
<input
type="text"
value={newNtpServer}
onChange={(e) => setNewNtpServer(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && newNtpServer.trim()) {
if (!ntpServers.includes(newNtpServer.trim())) {
setNtpServers([...ntpServers, newNtpServer.trim()])
setNewNtpServer('')
setShowAddNtpServer(false)
}
}
if (e.key === 'Escape') {
setNewNtpServer('')
setShowAddNtpServer(false)
}
}}
placeholder="Enter NTP server address (e.g., 0.pool.ntp.org)"
className="flex-1 bg-transparent text-sm text-white placeholder-gray-500 focus:outline-none"
autoFocus
/>
<button
onClick={() => {
if (newNtpServer.trim() && !ntpServers.includes(newNtpServer.trim())) {
setNtpServers([...ntpServers, newNtpServer.trim()])
setNewNtpServer('')
setShowAddNtpServer(false)
}
}}
className="text-green-500 hover:text-green-400"
>
<span className="material-symbols-outlined text-[16px]">check</span>
</button>
<button
onClick={() => {
setNewNtpServer('')
setShowAddNtpServer(false)
}}
className="text-red-500 hover:text-red-400"
>
<span className="material-symbols-outlined text-[16px]">close</span>
</button>
</div>
)}
{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">
@@ -484,20 +464,18 @@ export default function System() {
<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">
<label className="relative inline-block w-10 h-5 align-middle select-none cursor-pointer">
<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"
className="sr-only peer"
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>
<span className="absolute inset-0 rounded-full bg-border-dark transition-colors duration-300 peer-checked:bg-primary/20"></span>
<span className="absolute left-0.5 top-0.5 h-4 w-4 rounded-full bg-white transition-transform duration-300 peer-checked:translate-x-5"></span>
</label>
</div>
<div className={`grid grid-cols-1 gap-4 transition-opacity ${snmpEnabled ? 'opacity-100' : 'opacity-50 pointer-events-none'}`}>
<div>
@@ -550,6 +528,14 @@ export default function System() {
onClose={() => setEditingInterface(null)}
/>
)}
{/* View Details Modal */}
{viewingInterface && (
<ViewDetailsModal
interface={viewingInterface}
onClose={() => setViewingInterface(null)}
/>
)}
</div>
)
}
@@ -727,3 +713,128 @@ function EditConnectionModal({ interface: iface, onClose }: EditConnectionModalP
)
}
// View Details Modal Component
interface ViewDetailsModalProps {
interface: NetworkInterface
onClose: () => void
}
function ViewDetailsModal({ interface: iface, onClose }: ViewDetailsModalProps) {
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">info</span>
<h2 className="text-lg font-bold text-white">Interface Details - {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>
<div className="p-6">
<div className="space-y-4">
{/* Status */}
<div className="flex items-center justify-between p-4 rounded-lg bg-[#111a22] border border-border-dark">
<div className="flex items-center gap-3">
<div className={`h-3 w-3 rounded-full ${iface.status === 'Connected' ? 'bg-green-500' : 'bg-red-500'}`}></div>
<span className="text-sm font-medium text-text-secondary">Status</span>
</div>
<span className={`text-sm font-bold ${iface.status === 'Connected' ? 'text-green-500' : 'text-red-500'}`}>
{iface.status}
</span>
</div>
{/* Network Configuration Grid */}
<div className="grid grid-cols-2 gap-4">
{/* IP Address */}
<div className="p-4 rounded-lg bg-[#111a22] border border-border-dark">
<label className="mb-2 block text-xs font-medium text-text-secondary uppercase">IP Address</label>
<p className="text-sm font-mono text-white">{iface.ip_address || 'Not configured'}</p>
</div>
{/* Subnet */}
<div className="p-4 rounded-lg bg-[#111a22] border border-border-dark">
<label className="mb-2 block text-xs font-medium text-text-secondary uppercase">Subnet Mask (CIDR)</label>
<p className="text-sm font-mono text-white">/{iface.subnet || 'N/A'}</p>
</div>
{/* Gateway */}
<div className="p-4 rounded-lg bg-[#111a22] border border-border-dark">
<label className="mb-2 block text-xs font-medium text-text-secondary uppercase">Default Gateway</label>
<p className="text-sm font-mono text-white">{iface.gateway || 'Not configured'}</p>
</div>
{/* Speed */}
<div className="p-4 rounded-lg bg-[#111a22] border border-border-dark">
<label className="mb-2 block text-xs font-medium text-text-secondary uppercase">Link Speed</label>
<p className="text-sm font-mono text-white">{iface.speed || 'Unknown'}</p>
</div>
</div>
{/* DNS Servers */}
<div className="p-4 rounded-lg bg-[#111a22] border border-border-dark">
<label className="mb-3 block text-xs font-medium text-text-secondary uppercase">DNS Servers</label>
<div className="space-y-2">
{iface.dns1 ? (
<div className="flex items-center gap-2">
<span className="text-xs text-text-secondary">Primary:</span>
<span className="text-sm font-mono text-white">{iface.dns1}</span>
</div>
) : (
<p className="text-xs text-text-secondary">Primary DNS: Not configured</p>
)}
{iface.dns2 ? (
<div className="flex items-center gap-2">
<span className="text-xs text-text-secondary">Secondary:</span>
<span className="text-sm font-mono text-white">{iface.dns2}</span>
</div>
) : (
<p className="text-xs text-text-secondary">Secondary DNS: Not configured</p>
)}
</div>
</div>
{/* Interface Role */}
{iface.role && (
<div className="p-4 rounded-lg bg-[#111a22] border border-border-dark">
<label className="mb-2 block text-xs font-medium text-text-secondary uppercase">Interface Role</label>
<span className={`inline-block px-3 py-1 rounded text-xs font-bold uppercase ${
iface.role === 'ISCSI'
? 'bg-purple-500/20 text-purple-400 border border-purple-500/20'
: 'bg-primary/20 text-primary border border-primary/20'
}`}>
{iface.role}
</span>
</div>
)}
{/* Full Network Address */}
{iface.ip_address && iface.subnet && (
<div className="p-4 rounded-lg bg-[#111a22] border border-border-dark">
<label className="mb-2 block text-xs font-medium text-text-secondary uppercase">Full Network Address</label>
<p className="text-sm font-mono text-white">{iface.ip_address}/{iface.subnet}</p>
</div>
)}
</div>
{/* Close Button */}
<div className="mt-6 flex justify-end">
<button
onClick={onClose}
className="flex items-center justify-center gap-2 rounded-lg bg-primary px-6 py-2.5 text-sm font-bold text-white hover:bg-blue-600 transition-all"
>
<span className="material-symbols-outlined text-[18px]">close</span>
Close
</button>
</div>
</div>
</div>
</div>
)
}