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.
77 lines
3.1 KiB
JavaScript
77 lines
3.1 KiB
JavaScript
import { html, PageHeader, Empty, Card, ConfirmDelete, Table, renderGuard, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, MonoText, QuickModal } from '/static/hoover/index.js?v=10';
|
|
|
|
const addRule = QuickModal({
|
|
title: 'Add Rich Rule',
|
|
fields: (d) => [
|
|
{ label: 'Zone', id: 'rule-zone', tag: 'select', options: d.zones },
|
|
{ label: 'Rule (XML)', id: 'rule-text', tag: 'textarea', placeholder: 'rule family="ipv4" source address="192.168.1.0/24" accept' },
|
|
],
|
|
submit: {
|
|
url: '/api/firewall/rich-rules',
|
|
body: () => ({ zone: $val('rule-zone'), rule: ($val('rule-text') || '').trim() }),
|
|
validate: (b) => !b.zone || !b.rule ? 'Zone and rule are required' : null,
|
|
successMsg: 'Rule added',
|
|
},
|
|
refresh: 'firewall',
|
|
});
|
|
|
|
export default definePage({
|
|
init() {
|
|
return {
|
|
firewall: getModel('firewall'),
|
|
};
|
|
},
|
|
render(state) {
|
|
const guard = renderGuard(state.firewall, 'Rules', 'Firewall rich rules', state.firewall.data?.config);
|
|
if (guard) return guard;
|
|
|
|
const cfg = state.firewall.data?.config || {};
|
|
const zones = state.firewall.data?.zones?.available || [];
|
|
const zoneData = cfg.zones || {};
|
|
const zoneRules = {};
|
|
Object.entries(zoneData).forEach(([zname, zcfg]) => {
|
|
const rr = zcfg.rich_rules || [];
|
|
if (rr.length) zoneRules[zname] = rr;
|
|
});
|
|
|
|
const cards = Object.entries(zoneRules).map(([zone, rules]) => {
|
|
const ruleRows = (Array.isArray(rules) ? rules : []).map((entry, i) => {
|
|
const ruleId = typeof entry === 'object' ? entry.id : null;
|
|
const ruleText = typeof entry === 'object' ? (entry.rule || '') : String(entry);
|
|
return html`<tr key=${i}>
|
|
<td class="text-muted">${i + 1}</td>
|
|
<td class="mono-text td-fullwidth"><${MonoText} text=${ruleText} /></td>
|
|
<td>
|
|
<${ConfirmDelete}
|
|
url=${'/api/firewall/rich-rules/' + enc(zone) + '/' + enc(ruleId || '')}
|
|
deleteKey=${zone + '-' + (ruleId || i)}
|
|
message=${'Remove rule: ' + ruleText.substring(0, 40) + '...?'}
|
|
success="Rule removed"
|
|
refresh="firewall" />
|
|
</td>
|
|
</tr>`;
|
|
});
|
|
return Card({
|
|
header: 'Zone: ' + esc(zone),
|
|
key: zone,
|
|
children: [Table({
|
|
columns: ['#', 'Rule', 'Action'],
|
|
rows: ruleRows,
|
|
emptyText: 'No rules',
|
|
wrapCard: false,
|
|
})],
|
|
});
|
|
});
|
|
|
|
return [
|
|
PageHeader({
|
|
title: 'Rules',
|
|
subtitle: 'Firewall rich rules',
|
|
actions: html`<button class="btn btn-primary"
|
|
onClick=${() => addRule({ zones })}>Add Rule</button>`,
|
|
}),
|
|
...(cards.length ? cards : [Empty({ text: 'No rich rules configured.' })]),
|
|
];
|
|
},
|
|
});
|