633505e7dc
- Add quick modal, table, service status, and confirmation dialog components - Refactor all pages (certs, dhcp, proxy, etc.) to use new component patterns - Introduce refactor load utility and render guard for consistent UX - Add hoover documentation and update AGENTS.md, architecture, overview
92 lines
3.8 KiB
JavaScript
92 lines
3.8 KiB
JavaScript
import { h, PageHeader, Empty, Card, ConfirmDelete, Table, renderGuard, esc, enc, $val, apiFetch, toast, definePage, refactorLoad, MonoText, QuickModal } from '/static/hoover/index.js?v=6';
|
|
|
|
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: (s) => ({ zone: $val('rule-zone'), rule: ($val('rule-text') || '').trim() }),
|
|
validate: (b) => !b.zone || !b.rule ? 'Zone and rule are required' : null,
|
|
successMsg: 'Rule added',
|
|
},
|
|
reload: (s) => load(s._s),
|
|
});
|
|
|
|
async function load(state, abortController, entry) {
|
|
await refactorLoad(state,
|
|
s => Object.keys(s.config || {}).length,
|
|
async (s, sig, isAborted) => {
|
|
const r = await apiFetch('/api/firewall/config', { signal: sig });
|
|
if (isAborted()) return;
|
|
if (r.ok) s.config = r.data || {};
|
|
else s.error = r.error;
|
|
const zr = await apiFetch('/api/firewall/zones', { signal: sig });
|
|
if (isAborted()) return;
|
|
if (zr.ok) s.zones = Object.keys(zr.data?.active || {});
|
|
else if (!s.error) s.error = zr.error;
|
|
},
|
|
{ entry, abortController },
|
|
);
|
|
}
|
|
|
|
export default definePage({
|
|
init() {
|
|
return { config: {}, zones: [] };
|
|
},
|
|
subscribe: ['firewall'],
|
|
load,
|
|
render(state) {
|
|
const guard = renderGuard(state, 'Rules', 'Firewall rich rules', state.config);
|
|
if (guard) return guard;
|
|
|
|
const cfg = state.config || {};
|
|
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]) => {
|
|
return Card({
|
|
header: 'Zone: ' + esc(zone),
|
|
key: zone,
|
|
children: [Table({
|
|
columns: ['#', 'Rule', 'Action'],
|
|
rows: (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 h('tr', { key: i },
|
|
h('td', { class: 'text-muted' }, i + 1),
|
|
h('td', { class: 'mono-text td-fullwidth' }, MonoText({ text: ruleText })),
|
|
h('td', null,
|
|
ConfirmDelete({
|
|
url: '/api/firewall/rich-rules/' + enc(zone) + '/' + enc(ruleId || ''),
|
|
message: 'Remove rule: ' + ruleText.substring(0, 40) + '...?',
|
|
success: 'Rule removed',
|
|
reload: () => load(state),
|
|
}),
|
|
),
|
|
);
|
|
}),
|
|
emptyText: 'No rules',
|
|
wrapCard: false,
|
|
})],
|
|
});
|
|
});
|
|
|
|
return [
|
|
PageHeader({
|
|
title: 'Rules',
|
|
subtitle: 'Firewall rich rules',
|
|
actions: h('button', { class: 'btn btn-primary',
|
|
'on:click': () => addRule({ zones: state.zones, _s: state }), }, 'Add Rule'),
|
|
}),
|
|
...(cards.length ? cards : [Empty({ text: 'No rich rules configured.' })]),
|
|
];
|
|
},
|
|
});
|