import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { RefreshCw, Plus, Trash2, CheckCircle, AlertCircle, Download, Server, } from 'lucide-react'; import { devicesApi } from '../../services/api'; import type { IpAddress, Interface } from '../../types'; import clsx from 'clsx'; import { useCanWrite } from '../../hooks/useCanWrite'; interface Props { deviceId: number; } export default function SystemConfigTab({ deviceId }: Props) { const queryClient = useQueryClient(); const canWrite = useCanWrite(); // ─── System config (identity, NTP, DNS) ────────────────────────────────── const { data: sysConfig, isLoading: sysLoading } = useQuery({ queryKey: ['system-config', deviceId], queryFn: () => devicesApi.getSystemConfig(deviceId).then((r) => r.data), }); // ─── Clock ──────────────────────────────────────────────────────────────── const { data: clockData, isLoading: clockLoading } = useQuery({ queryKey: ['device-clock', deviceId], queryFn: () => devicesApi.getClock(deviceId).then((r) => r.data), }); // ─── Combined edit state ────────────────────────────────────────────────── const [editing, setEditing] = useState(false); const [sysForm, setSysForm] = useState({ identity: '', ntp_enabled: true, ntp_primary: '', ntp_secondary: '', dns_servers: '', dns_allow_remote: false, }); const [clockForm, setClockForm] = useState({ date: '', time: '', timezone: '' }); const [saveError, setSaveError] = useState(''); const [saveSuccess, setSaveSuccess] = useState(''); const [saving, setSaving] = useState(false); const startEdit = () => { if (!sysConfig) return; setSysForm({ identity: sysConfig.identity, ntp_enabled: sysConfig.ntp.enabled !== 'no', ntp_primary: sysConfig.ntp['primary-ntp'] || '', ntp_secondary: sysConfig.ntp['secondary-ntp'] || '', dns_servers: sysConfig.dns['servers'] || '', dns_allow_remote: sysConfig.dns['allow-remote-requests'] === 'yes', }); setClockForm({ date: clockData?.date || '', time: clockData?.time || '', timezone: clockData?.timezone || '', }); setSaveError(''); setEditing(true); }; const handleSave = async () => { setSaving(true); setSaveError(''); try { await devicesApi.updateSystemConfig(deviceId, sysForm); await devicesApi.setClock(deviceId, clockForm); queryClient.invalidateQueries({ queryKey: ['system-config', deviceId] }); queryClient.invalidateQueries({ queryKey: ['device-clock', deviceId] }); queryClient.invalidateQueries({ queryKey: ['device', deviceId] }); setEditing(false); setSaveSuccess('System configuration updated'); setTimeout(() => setSaveSuccess(''), 3000); } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; setSaveError(msg || 'Failed to save configuration'); } finally { setSaving(false); } }; // ─── IP Addresses ──────────────────────────────────────────────────────── const { data: ipAddresses = [], isLoading: ipLoading, refetch: refetchIp } = useQuery({ queryKey: ['ip-addresses', deviceId], queryFn: () => devicesApi.getIpAddresses(deviceId).then((r) => r.data as IpAddress[]), }); const { data: interfaces = [] } = useQuery({ queryKey: ['interfaces', deviceId], queryFn: () => devicesApi.getInterfaces(deviceId).then((r) => r.data as Interface[]), }); const [newIp, setNewIp] = useState({ address: '', interface: '' }); const [ipError, setIpError] = useState(''); const addIpMutation = useMutation({ mutationFn: () => devicesApi.addIpAddress(deviceId, newIp), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ip-addresses', deviceId] }); setNewIp({ address: '', interface: '' }); setIpError(''); }, onError: (err: unknown) => { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; setIpError(msg || 'Failed to add IP address'); }, }); const removeIpMutation = useMutation({ mutationFn: (id: string) => devicesApi.removeIpAddress(deviceId, id), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['ip-addresses', deviceId] }), }); // ─── Firmware check / install ───────────────────────────────────────────── const [updateInfo, setUpdateInfo] = useState | null>(null); const [updateChecking, setUpdateChecking] = useState(false); const [updateError, setUpdateError] = useState(''); const [installSuccess, setInstallSuccess] = useState(''); const checkUpdateMutation = useMutation({ mutationFn: () => devicesApi.checkUpdate(deviceId), onMutate: () => { setUpdateChecking(true); setUpdateError(''); setUpdateInfo(null); setInstallSuccess(''); }, onSuccess: (res) => { setUpdateInfo(res.data); setUpdateChecking(false); }, onError: (err: unknown) => { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; setUpdateError(msg || 'Failed to check for updates'); setUpdateChecking(false); }, }); const installUpdateMutation = useMutation({ mutationFn: () => devicesApi.installUpdate(deviceId), onSuccess: () => { setUpdateInfo(null); setInstallSuccess('Update installation initiated. The device will reboot shortly.'); }, onError: (err: unknown) => { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error; setUpdateError(msg || 'Failed to install update'); }, }); // RouterOS 7 uses 'installed-version'; RouterOS 6 uses 'version' const installedVersion = updateInfo?.['installed-version'] || updateInfo?.['version'] || ''; const latestVersion = updateInfo?.['latest-version'] || ''; const statusSaysUpdate = updateInfo?.['status']?.toLowerCase().includes('new version') || updateInfo?.['status']?.toLowerCase().includes('available'); const hasUpdate = updateInfo && latestVersion && ( (installedVersion && latestVersion !== installedVersion) || (!installedVersion && statusSaysUpdate) ); const isLoading = sysLoading || clockLoading; return (
{/* ── System Settings (identity, NTP, DNS, clock) ── */}

System Settings

{canWrite && !editing && ( )}
{isLoading ? (
Loading...
) : !sysConfig ? (
Could not load system configuration
) : editing ? (
{/* Identity */}
setSysForm((f) => ({ ...f, identity: e.target.value }))} placeholder="router" />
{/* Date / Time / Timezone */}

Date & Time

setClockForm((f) => ({ ...f, date: e.target.value }))} />
setClockForm((f) => ({ ...f, time: e.target.value }))} />
setClockForm((f) => ({ ...f, timezone: e.target.value }))} placeholder="America/New_York" />

IANA name (e.g. UTC, Europe/London)

{/* NTP */}

NTP Client

setSysForm((f) => ({ ...f, ntp_enabled: e.target.checked }))} className="w-4 h-4" />
setSysForm((f) => ({ ...f, ntp_primary: e.target.value }))} placeholder="0.pool.ntp.org" />
setSysForm((f) => ({ ...f, ntp_secondary: e.target.value }))} placeholder="1.pool.ntp.org" />
{/* DNS */}

DNS

setSysForm((f) => ({ ...f, dns_servers: e.target.value }))} placeholder="8.8.8.8,8.8.4.4" />
setSysForm((f) => ({ ...f, dns_allow_remote: e.target.checked }))} className="w-4 h-4" />
{saveError && (

{saveError}

)}
) : (
{[ ['Identity', sysConfig.identity || '—'], ['Date', clockData?.date || '—'], ['Time', clockData?.time || '—'], ['Timezone', clockData?.timezone || '—'], ['NTP Enabled', sysConfig.ntp.enabled !== 'no' ? 'Yes' : 'No'], ['Primary NTP', sysConfig.ntp['primary-ntp'] || '—'], ['Secondary NTP', sysConfig.ntp['secondary-ntp'] || '—'], ['DNS Servers', sysConfig.dns.servers || '—'], ['Allow Remote DNS', sysConfig.dns['allow-remote-requests'] === 'yes' ? 'Yes' : 'No'], ].map(([k, v]) => (
{k} {v}
))}
)} {saveSuccess && (
{saveSuccess}
)}
{/* ── IP Addresses ── */}

IP Addresses

{ipLoading ? ( ) : (ipAddresses as IpAddress[]).length === 0 ? ( ) : ( (ipAddresses as IpAddress[]).map((ip) => ( )) )}
Address / Prefix Network Interface Status
Loading...
No IP addresses
{ip.address} {ip.network || '—'} {ip.interface} {ip.dynamic === 'true' ? 'dynamic' : ip.disabled === 'true' ? 'disabled' : 'active'} {canWrite && ip.dynamic !== 'true' && ( )}
{canWrite &&
setNewIp((f) => ({ ...f, address: e.target.value }))} placeholder="192.168.1.1/24" />
{ipError && (
{ipError}
)}
}
{/* ── Firmware Updates ── */}

Firmware Updates

{updateError && (
{updateError}
)} {installSuccess && (
{installSuccess}
)} {updateInfo && (
{[ ['Status', updateInfo['status'] || '—'], ['Installed Version', installedVersion || '—'], ['Latest Version', latestVersion || '—'], ['Channel', updateInfo['channel'] || '—'], ].map(([k, v]) => (
{k} {v}
))}
{hasUpdate ? (

Update available: {latestVersion}

The device will reboot after installing.

{canWrite && ( )}
) : (
RouterOS is up to date
)}
)} {!updateInfo && !updateChecking && !installSuccess && (

Click "Check for Updates" to query the MikroTik update server.

)}
); }