From b97c7392198a46ca63fab378066a1002fe2c2bdb Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:06:05 +0200 Subject: [PATCH] Added missed configs to network config Fixed some layouts issues --- webui-src/app/config/config_network.js | 519 +++++++++++++++---------- 1 file changed, 321 insertions(+), 198 deletions(-) diff --git a/webui-src/app/config/config_network.js b/webui-src/app/config/config_network.js index 7045256..1af47d6 100644 --- a/webui-src/app/config/config_network.js +++ b/webui-src/app/config/config_network.js @@ -76,13 +76,15 @@ const SetNwMode = () => { }, view: (vnode) => { const isHiddenMode = vnode.attrs && vnode.attrs.isHiddenMode; + const hideLabel = vnode.attrs && vnode.attrs.hideLabel; const modes = isHiddenMode ? hiddenModes : networkModes; return [ - m('p', isHiddenMode ? 'Discovery:' : 'Network mode:'), + !hideLabel && m('p', isHiddenMode ? 'Discovery:' : 'Network mode:'), m( 'select', { + style: 'flex: 1; max-width: 320px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px;', value: selectedMode, onchange: (e) => { const idx = e.target.selectedIndex; @@ -130,52 +132,6 @@ const SetNwMode = () => { }; }; -const SetNAT = () => { - let sslId; - let netMode; - - return { - oninit: () => { - rs.rsJsonApiRequest('/rsAccounts/getCurrentAccountId').then((res) => { - if (res.body.retval) { - sslId = res.body.id; - rs.rsJsonApiRequest('/rsPeers/getPeerDetails', { - sslId, - }).then((res) => { - if (res.body.retval) { - netMode = res.body.det.netMode; - } - }); - } - }); - }, - view: () => [ - m('p', 'NAT:'), - m( - 'select', - { - value: netMode, - onchange: (e) => { - rs.rsJsonApiRequest('/rsPeers/setNetworkMode', { - sslId, - netMode, - }).then((res) => { - if (res.body.retval) { - netMode = e.target.value; - } - }); - }, - }, - [ - m('option', { value: util.RS_NETMODE_UPNP }, 'Automatic (UPnP)'), - m('option', { value: util.RS_NETMODE_UDP }, 'FireWalled'), - m('option', { value: util.RS_NETMODE_EXT }, 'Manually Forwarded Port'), - ] - ), - ], - }; -}; - const SetLimits = () => { let dlim = undefined; let ulim = undefined; @@ -191,37 +147,41 @@ const SetLimits = () => { ulim = data.outKb; }), view: () => [ - m( - 'p', - util.tooltip( - 'The download limit covers the whole application. ' + - 'However, in some situations, such as when transfering ' + - 'many files at once, the estimated bandwidth becomes ' + - 'unreliable and the total value reported by Retroshare ' + - 'might exceed that limit.' - ), - 'Download limit(KB/s):' - ), - m('input[type=number][name=download]', { - value: dlim, - oninput: (e) => (dlim = Number(e.target.value)), - onchange: setMaxRates, - }), - m( - 'p', - util.tooltip( - 'The upload limit covers the entire software. ' + - 'Too small an upload limit may eventually block ' + - 'low priority services(forums, channels). ' + - 'A minimum recommended value is 50KB/s.' - ), - 'Upload limit(KB/s):' - ), - m('input[type=number][name=upload]', { - value: ulim, - oninput: (e) => (ulim = Number(e.target.value)), - onchange: setMaxRates, - }), + m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center; margin-bottom: 0.75rem;' }, [ + m('p', { style: 'font-weight: 600; color: #475569;' }, [ + util.tooltip( + 'The download limit covers the whole application. ' + + 'However, in some situations, such as when transfering ' + + 'many files at once, the estimated bandwidth becomes ' + + 'unreliable and the total value reported by Retroshare ' + + 'might exceed that limit.' + ), + 'Download limit(KB/s):' + ]), + m('input[type=number][name=download]', { + style: 'padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px; max-width: 320px; width: 100%;', + value: dlim, + oninput: (e) => (dlim = Number(e.target.value)), + onchange: setMaxRates, + }), + ]), + m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center; margin-bottom: 0.75rem;' }, [ + m('p', { style: 'font-weight: 600; color: #475569;' }, [ + util.tooltip( + 'The upload limit covers the entire software. ' + + 'Too small an upload limit may eventually block ' + + 'low priority services(forums, channels). ' + + 'A minimum recommended value is 50KB/s.' + ), + 'Upload limit(KB/s):' + ]), + m('input[type=number][name=upload]', { + style: 'padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px; max-width: 320px; width: 100%;', + value: ulim, + oninput: (e) => (ulim = Number(e.target.value)), + onchange: setMaxRates, + }), + ]), ], }; }; @@ -236,87 +196,241 @@ const SetOpMode = () => { oninit: () => rs.rsJsonApiRequest('/rsConfig/getOperatingMode', {}, (data) => (opmode = data.retval)), view: () => [ - m( - 'p', - 'Operating mode:', - util.tooltip( - `No Anon D/L: Switches off file forwarding\n - Gaming Mode: 25% standard traffic and TODO: Reduced popups\n - Low traffic: 10% standard traffic and TODO: pause all file transfers\n` - ) - ), - m( - 'select', - { - oninput: (e) => (opmode = e.target.value), - value: opmode, - onchange: setmode, - }, - ['Normal', 'No Anon D/L', 'Gaming', 'Low traffic'].map((val, i) => - m(`option[value=${i + 1}]`, val) - ) - ), + m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center; margin-bottom: 0.75rem;' }, [ + m('p', { style: 'font-weight: 600; color: #475569;' }, [ + 'Operating mode: ', + util.tooltip( + `No Anon D/L: Switches off file forwarding\n + Gaming Mode: 25% standard traffic and TODO: Reduced popups\n + Low traffic: 10% standard traffic and TODO: pause all file transfers\n` + ) + ]), + m( + 'select', + { + style: 'padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px; max-width: 320px; width: 100%;', + oninput: (e) => (opmode = e.target.value), + value: opmode, + onchange: setmode, + }, + ['Normal', 'No Anon D/L', 'Gaming', 'Low traffic'].map((val, i) => + m(`option[value=${i + 1}]`, val) + ) + ), + ]), ], }; }; -const displayLocalIPAddress = () => { - return { - view: ({ attrs: { details } }) => - details && [m('p', 'Local Address: '), m('p', details.localAddr)], - }; -}; -const displayExternalIPAddress = () => { - return { - view: ({ attrs: { details, isHiddenMode } }) => - details && [m('p', 'External Address: '), m('p', isHiddenMode ? 'Hidden - See Config' : details.extAddr)], - }; -}; - const displayIPAddresses = () => { return { view: ({ attrs: { details } }) => - details && [ - m('p', 'External Address: '), + details && m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: flex-start; margin-bottom: 0.75rem;' }, [ + m('p', { style: 'font-weight: 600; color: #475569;' }, 'External Address: '), m( 'ul.external-address', details.ipAddressList.map((ip) => m('li', ip)) ), - ], + ]), }; }; -const SetDynamicDNS = () => { - let addr = ''; +const NetworkConfigForm = () => { let sslId = ''; + let details = {}; + let netStatus = {}; + let localAddr = ''; + let localPort = 0; + let extAddr = ''; + let extPort = 0; + let dyndns = ''; + let netMode = util.RS_NETMODE_EXT; + + const loadData = () => { + rs.rsJsonApiRequest('/rsAccounts/getCurrentAccountId').then((res) => { + if (res.body.retval) { + sslId = res.body.id; + rs.rsJsonApiRequest('/rsPeers/getPeerDetails', { sslId }).then((pRes) => { + if (pRes.body.retval) { + details = pRes.body.det; + localAddr = details.localAddr || ''; + localPort = details.localPort || 0; + extAddr = details.extAddr || ''; + extPort = details.extPort || 0; + dyndns = details.dyndns || ''; + netMode = details.netMode || util.RS_NETMODE_EXT; + m.redraw(); + } + }); + rs.rsJsonApiRequest('/rsConfig/getConfigNetStatus', {}).then((nRes) => { + if (nRes.body) { + netStatus = nRes.body; + if (netStatus.localPort) localPort = netStatus.localPort; + if (netStatus.extPort) extPort = netStatus.extPort; + m.redraw(); + } + }); + } + }); + }; + + const saveLocalAddress = () => { + if (!sslId) return; + rs.rsJsonApiRequest('/rsPeers/setLocalAddress', { + sslId, + addr: localAddr, + port: parseInt(localPort) || 0, + }).then(() => loadData()); + }; + + const saveExtAddress = () => { + if (!sslId) return; + rs.rsJsonApiRequest('/rsPeers/setExtAddress', { + sslId, + addr: extAddr, + port: parseInt(extPort) || 0, + }).then(() => loadData()); + }; + + const saveDynDNS = () => { + if (!sslId) return; + rs.rsJsonApiRequest('/rsPeers/setDynDNS', { + sslId, + addr: dyndns, + }); + }; + + const saveNetMode = (newMode) => { + if (!sslId) return; + netMode = newMode; + rs.rsJsonApiRequest('/rsPeers/setNetworkMode', { + sslId, + netMode: parseInt(newMode), + }).then(() => loadData()); + }; + return { oninit: () => { - rs.rsJsonApiRequest('/rsAccounts/getCurrentAccountId').then((res) => { - if (res.body.retval) { - sslId = res.body.id; - rs.rsJsonApiRequest('/rsPeers/getPeerDetails', { - sslId, - }).then((res) => { - if (res.body.retval) { - addr = res.body.det.dyndns; - } - }); - } - }); + loadData(); }, - view: () => [ - m('p', 'Set Dynamic DNS:'), - m('input[type=text]', { - value: addr, - oninput: (e) => (addr = e.target.value), - onchange: () => { - rs.rsJsonApiRequest('/rsPeers/setDynDNS', { - sslId, - addr, - }); - }, - }), - ], + view: ({ attrs: { isHiddenMode } }) => { + const isUpnpOk = Boolean(netStatus.netUpnpOk || netStatus.uPnPActive); + const isLocalOk = Boolean(netStatus.netLocalOk !== false); + const isExtOk = Boolean(netStatus.netExtAddressOk); + + return m('.network-config-form', { + style: { + display: 'flex', + flexDirection: 'column', + gap: '0.75rem', + width: '100%', + } + }, [ + // Network Mode row + m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center;' }, [ + m('label', { style: 'font-weight: 600; color: #475569;' }, 'Network Mode'), + m('.nw-mode-group', { style: 'display: flex; align-items: center; gap: 1rem;' }, [ + m(SetNwMode, { isHiddenMode, hideLabel: true }), + isHiddenMode && m('.status-indicator', { style: 'display: flex; align-items: center; gap: 0.4rem;' }, [ + m('.bullet', { + style: 'width: 10px; height: 10px; border-radius: 50%; background-color: #22c55e;' + }), + m('span', { style: 'font-size: 0.85rem; font-weight: 700; color: #000000;' }, '[Hidden mode]'), + ]), + ]), + ]), + + // NAT row + UPnP status bullet + !isHiddenMode && m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center;' }, [ + m('label', { style: 'font-weight: 600; color: #475569;' }, 'NAT'), + m('.nat-control-group', { style: 'display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;' }, [ + m('select', { + style: 'flex: 1; max-width: 320px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px;', + value: netMode, + onchange: (e) => saveNetMode(e.target.value), + }, [ + m('option', { value: util.RS_NETMODE_UPNP }, 'Automatic (UPnP)'), + m('option', { value: util.RS_NETMODE_UDP }, 'FireWalled'), + m('option', { value: util.RS_NETMODE_EXT }, 'Manually Forwarded Port'), + ]), + m('.status-indicator', { style: 'display: flex; align-items: center; gap: 0.4rem;' }, [ + m('.bullet', { + style: `width: 10px; height: 10px; border-radius: 50%; background-color: ${isUpnpOk ? '#22c55e' : '#475569'};` + }), + m('span', { style: 'font-size: 0.85rem; font-weight: 600; color: #334155;' }, 'UPnP'), + ]), + ]), + ]), + + // Local Address + Port + Local network status bullet + m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center;' }, [ + m('label', { style: 'font-weight: 600; color: #475569;' }, 'Local Address'), + m('.addr-control-group', { style: 'display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;' }, [ + m('input[type=text]', { + style: 'flex: 1; max-width: 320px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px;', + value: localAddr, + oninput: (e) => (localAddr = e.target.value), + onchange: saveLocalAddress, + }), + m('.port-group', { style: 'display: flex; align-items: center; gap: 0.4rem;' }, [ + m('span', { style: 'font-size: 0.85rem; font-weight: 600; color: #475569;' }, 'Port:'), + m('input[type=number]', { + style: 'width: 90px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px;', + value: localPort, + oninput: (e) => (localPort = parseInt(e.target.value) || 0), + onchange: saveLocalAddress, + }), + ]), + !isHiddenMode && m('.status-indicator', { style: 'display: flex; align-items: center; gap: 0.4rem; margin-left: 0.5rem;' }, [ + m('.bullet', { + style: `width: 10px; height: 10px; border-radius: 50%; background-color: ${isLocalOk ? '#22c55e' : '#ef4444'};` + }), + m('span', { style: 'font-size: 0.85rem; font-weight: 600; color: #334155;' }, 'Local network'), + ]), + ]), + ]), + + // External Address + Port + External ip address finder status bullet + m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center;' }, [ + m('label', { style: 'font-weight: 600; color: #475569;' }, 'External Address'), + m('.addr-control-group', { style: 'display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;' }, [ + m('input[type=text]', { + style: 'flex: 1; max-width: 320px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px;', + value: isHiddenMode ? 'Hidden' : extAddr, + disabled: isHiddenMode, + oninput: (e) => (extAddr = e.target.value), + onchange: saveExtAddress, + }), + !isHiddenMode && m('.port-group', { style: 'display: flex; align-items: center; gap: 0.4rem;' }, [ + m('span', { style: 'font-size: 0.85rem; font-weight: 600; color: #475569;' }, 'Port:'), + m('input[type=number]', { + style: 'width: 90px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px;', + value: extPort, + oninput: (e) => (extPort = parseInt(e.target.value) || 0), + onchange: saveExtAddress, + }), + ]), + !isHiddenMode && m('.status-indicator', { style: 'display: flex; align-items: center; gap: 0.4rem; margin-left: 0.5rem;' }, [ + m('.bullet', { + style: `width: 10px; height: 10px; border-radius: 50%; background-color: ${isExtOk ? '#22c55e' : '#808080'};` + }), + m('span', { style: 'font-size: 0.85rem; font-weight: 600; color: #334155;' }, 'External ip address finder'), + ]), + ]), + ]), + + // Dynamic DNS row + !isHiddenMode && m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center;' }, [ + m('label', { style: 'font-weight: 600; color: #475569;' }, 'Dynamic DNS'), + m('input[type=text]', { + style: 'flex: 1; max-width: 320px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px;', + value: dyndns, + oninput: (e) => (dyndns = e.target.value), + onchange: saveDynDNS, + }), + ]), + ]); + } }; }; @@ -416,46 +530,45 @@ const SetSocksProxy = () => { }); }, view: () => - m('.proxy-server', [ - m( - 'p', + m('.proxy-server-form', { style: 'display: flex; flex-direction: column; gap: 0.75rem; width: 100%;' }, [ + m('p.proxy-description', { style: 'margin-bottom: 0.5rem; color: #475569;' }, 'Configure your TOR and I2P SOCKS proxy here. It will allow you to also connect to hidden nodes.' ), - m('.proxy-rows-container', - Object.keys(socksProxyObj).map((proxyItem) => { - const isTor = proxyItem === 'tor'; - const labelText = isTor ? 'TOR Socks Proxy:' : 'I2P Socks Proxy:'; - const outgoingText = isTor ? 'TOR outgoing' : 'I2P outgoing'; - const notEnabledText = isTor ? 'Tor proxy is not enabled' : 'I2P proxy is not enabled'; - const isOutgoing = socksProxyObj[proxyItem].outgoing; - return m('.proxy-row', [ - m('label.proxy-label', labelText), - m('input[type=text].proxy-addr-input', { - value: socksProxyObj[proxyItem].addr, + Object.keys(socksProxyObj).map((proxyItem) => { + const isTor = proxyItem === 'tor'; + const labelText = isTor ? 'TOR Socks Proxy:' : 'I2P Socks Proxy:'; + const outgoingText = isTor ? 'TOR outgoing' : 'I2P outgoing'; + const notEnabledText = isTor ? 'Tor proxy is not enabled' : 'I2P proxy is not enabled'; + const isOutgoing = socksProxyObj[proxyItem].outgoing; + + return m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center;' }, [ + m('label', { style: 'font-weight: 600; color: #475569;' }, labelText), + m('.proxy-control-group', { style: 'display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap;' }, [ + m('input[type=text]', { + style: 'flex: 1; max-width: 480px; min-width: 320px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px;', + value: socksProxyObj[proxyItem].addr || '', oninput: (e) => (socksProxyObj[proxyItem].addr = e.target.value), onchange: () => handleProxyChange(proxyItem), }), - m('input[type=number].proxy-port-input', { - value: socksProxyObj[proxyItem].port, - oninput: (e) => (socksProxyObj[proxyItem].port = parseInt(e.target.value)), + m('input[type=number]', { + style: 'width: 90px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px;', + value: socksProxyObj[proxyItem].port || 0, + oninput: (e) => (socksProxyObj[proxyItem].port = parseInt(e.target.value) || 0), onchange: () => handleProxyChange(proxyItem), }), socksProxyObj[proxyItem].outgoing !== undefined && - m('.proxy-status-container', [ - m('.proxy-status-bullet', { - style: { - backgroundColor: isOutgoing ? '#22c55e' : '#808080', - }, + m('.status-indicator', { style: 'display: flex; align-items: center; gap: 0.4rem; margin-left: 0.5rem;' }, [ + m('.bullet', { + style: `width: 10px; height: 10px; border-radius: 50%; background-color: ${isOutgoing ? '#22c55e' : '#808080'};`, title: isOutgoing ? 'Proxy seems to work.' : notEnabledText, }), - m( - 'span.proxy-status-text', + m('span', { style: 'font-size: 0.85rem; font-weight: 600; color: #334155;' }, `${outgoingText} ${isOutgoing ? 'on' : 'off'}` ), ]), - ]); - }) - ), + ]), + ]); + }), ]), }; }; @@ -464,27 +577,42 @@ const displayHiddenServiceInfo = () => { return { view: ({ attrs: { details } }) => details && details.hiddenNodeAddress && - m('.proxy-server', [ - m('p.proxy-description', details.hiddenType === 4 - ? 'I2P has been automatically configured by Retroshare. You shouldn\'t need to change anything here.' - : 'Tor has been automatically configured by Retroshare. You shouldn\'t need to change anything here.' + m('.hidden-service-info', { style: 'display: flex; flex-direction: column; gap: 0.75rem; width: 100%;' }, [ + m('p.proxy-description', { style: 'margin-bottom: 0.5rem; color: #475569;' }, details.hiddenType === 4 + ? "I2P has been automatically configured by Retroshare. You shouldn't need to change anything here." + : "Tor has been automatically configured by Retroshare. You shouldn't need to change anything here." ), - m('hr'), - m('.proxy-row', [ - m('label.proxy-label', 'Local Address:'), - m('span', details.localAddr || '127.0.0.1'), + // Local Address + Local Port row + m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center;' }, [ + m('label', { style: 'font-weight: 600; color: #475569;' }, 'Local Address:'), + m('.addr-port-group', { style: 'display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap;' }, [ + m('input[type=text]', { + style: 'flex: 1; max-width: 480px; min-width: 320px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px; background-color: #f8fafc; color: #334155;', + readOnly: true, + value: details.localAddr || '127.0.0.1', + }), + m('input[type=number]', { + style: 'width: 90px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px; background-color: #f8fafc; color: #334155;', + readOnly: true, + value: details.localPort || 0, + }), + ]), ]), - m('.proxy-row', [ - m('label.proxy-label', details.hiddenType === 4 ? 'I2P Address:' : 'Onion Address:'), - m('span', details.hiddenNodeAddress), - ]), - details.hiddenNodePort && m('.proxy-row', [ - m('label.proxy-label', 'Service Port:'), - m('span', String(details.hiddenNodePort)), - ]), - m('.proxy-row', [ - m('label.proxy-label', 'Local Port:'), - m('span', String(details.localPort)), + // Onion / I2P Address + Service Port row + m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center;' }, [ + m('label', { style: 'font-weight: 600; color: #475569;' }, details.hiddenType === 4 ? 'I2P Address:' : 'Onion Address:'), + m('.addr-port-group', { style: 'display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap;' }, [ + m('input[type=text]', { + style: 'flex: 1; max-width: 480px; min-width: 320px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px; background-color: #f8fafc; color: #334155; font-family: monospace;', + readOnly: true, + value: details.hiddenNodeAddress, + }), + details.hiddenNodePort && m('input[type=number]', { + style: 'width: 90px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px; background-color: #f8fafc; color: #334155;', + readOnly: true, + value: details.hiddenNodePort, + }), + ]), ]), ]), }; @@ -517,20 +645,15 @@ const Component = () => { }); }, view: () => - m('.config-network', { style: 'display:flex; flex-direction:column; gap:0.5rem;' }, [ + m('.config-network', { style: 'display:flex; flex-direction:column; gap:1rem;' }, [ m('.widget', [ m('.widget__heading', m('h3', 'Network Configuration')), m('.widget__body', [ - m('.grid-2col', [ - m(SetNwMode, { isHiddenMode }), - !isHiddenMode && m(SetNAT), - m(displayLocalIPAddress, { details }), - m(displayExternalIPAddress, { details, isHiddenMode }), - !isHiddenMode && m(SetDynamicDNS), - m(SetLimits), - !isHiddenMode && m(SetOpMode), - !isHiddenMode && m(displayIPAddresses, { details }), - ]), + m(NetworkConfigForm, { isHiddenMode }), + m('hr', { style: 'margin: 1rem 0; border: none; border-top: 1px solid #e2e8f0;' }), + m(SetLimits), + !isHiddenMode && m(SetOpMode), + !isHiddenMode && m(displayIPAddresses, { details }), ]), ]), m('.widget', [