ca27ea5522
component.js now creates an AbortController for each page mount, passing it to load(). On unmount, the controller is aborted to cancel in-flight requests that would otherwise mutate unmounted state. Page load functions consistently pass the signal to apiFetch and guard state mutations with abort checks. This eliminates the need for per-page abortController boilerplate and prevents stale errors from appearing on rapid navigation. Users page now guards catch block and loading state cleanup against aborted requests, matching passkeys.js pattern.
98 lines
3.8 KiB
JavaScript
98 lines
3.8 KiB
JavaScript
import { html, PageHeader, Table, renderGuardMulti, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, StatusText, QuickModal, ZoneSelect } from '/static/hoover/index.js?v=10';
|
|
|
|
async function changeZone(name, zone, state) {
|
|
const r = await apiFetch('/api/firewall/zones/' + enc(zone) + '/interfaces', {
|
|
method: 'POST',
|
|
body: { interfaces: [name] },
|
|
});
|
|
if (r.ok) {
|
|
toast(name + ' \u2192 ' + zone, 'success');
|
|
modelFetch('firewall');
|
|
modelFetch('network');
|
|
} else {
|
|
toast(r.error || 'Failed', 'error');
|
|
}
|
|
}
|
|
|
|
const cfgModalFn = QuickModal({
|
|
title: (d) => 'Config: ' + d.name,
|
|
fields: (d) => {
|
|
const cfg = d.config || {};
|
|
return [
|
|
{ label: 'Addresses (comma-separated)', id: 'cfg-addrs', value: (cfg.addresses || []).join(', '), placeholder: '192.168.1.1/24' },
|
|
{ label: 'Gateway', id: 'cfg-gw', value: cfg.gateway || '' },
|
|
{ label: 'DNS (comma-separated)', id: 'cfg-dns', value: (cfg.dns || []).join(', '), placeholder: '1.1.1.1, 8.8.8.8' },
|
|
];
|
|
},
|
|
submit: {
|
|
url: (d) => '/api/network/interfaces/' + enc(d.name),
|
|
body: () => ({
|
|
addresses: ($val('cfg-addrs') || '').split(',').map(s => s.trim()).filter(Boolean),
|
|
gateway: ($val('cfg-gw') || '').trim() || undefined,
|
|
dns: ($val('cfg-dns') || '').split(',').map(s => s.trim()).filter(Boolean),
|
|
}),
|
|
successMsg: 'Config saved',
|
|
},
|
|
refresh: ['firewall', 'network'],
|
|
});
|
|
|
|
export default definePage({
|
|
init() {
|
|
return {
|
|
firewall: getModel('firewall'),
|
|
network: getModel('network'),
|
|
};
|
|
},
|
|
render(state) {
|
|
const guard = renderGuardMulti('Interfaces', 'Network interface management', state.firewall, state.network);
|
|
if (guard) return guard;
|
|
|
|
const fwZones = state.firewall.data?.zones || {};
|
|
const netData = state.network.data?.interfaces || {};
|
|
const zones = fwZones.available || [];
|
|
const activeZones = fwZones.active || {};
|
|
|
|
const ifaces = Object.entries(netData).map(([name, entry]) => {
|
|
let zone = null;
|
|
for (const [zoneName, zIfaces] of Object.entries(activeZones)) {
|
|
if ((zIfaces || []).includes(name)) {
|
|
zone = zoneName;
|
|
break;
|
|
}
|
|
}
|
|
return {
|
|
name,
|
|
mac: entry?.runtime?.mac || null,
|
|
ips: [...(entry?.config?.addresses || []), ...(entry?.runtime?.addresses || [])],
|
|
state: (entry?.runtime?.state || '').startsWith('routable') || (entry?.runtime?.state || '').startsWith('carrier') ? 'up' : 'down',
|
|
zone,
|
|
config: entry?.config || {},
|
|
};
|
|
});
|
|
|
|
const rows = ifaces.map(iface =>
|
|
html`<tr key=${iface.name}>
|
|
<td><strong>${iface.name}</strong></td>
|
|
<td class="text-muted">${String(iface.mac || 'N/A')}</td>
|
|
<td>${(iface.ips || []).join(', ') || 'N/A'}</td>
|
|
<td><${StatusText} status=${iface.state} /></td>
|
|
<td>
|
|
<${ZoneSelect} zones=${zones} value=${iface.zone}
|
|
onChange=${(z) => changeZone(iface.name, z, state)} />
|
|
<button class="btn btn-sm btn-outline" style="margin-left:8px"
|
|
onClick=${() => cfgModalFn(iface)}>Config</button>
|
|
</td>
|
|
</tr>`
|
|
);
|
|
|
|
return [
|
|
PageHeader({ title: 'Interfaces', subtitle: 'Network interface management' }),
|
|
Table({
|
|
columns: ['Name', 'MAC', 'IPs', 'State', 'Zone / Actions'],
|
|
rows,
|
|
emptyText: 'No interfaces found',
|
|
}),
|
|
];
|
|
},
|
|
});
|