Update status checks and add optgroup support to form modals
- modal.js: add optgroup support for select options in formModal - dhcp.js: use zone-based interface selector for DHCP ranges - dashboard.js, wireguard.js: use status.up for state checks - proxy.js: use inner.querySelector for modal field lookup
This commit is contained in:
@@ -95,6 +95,8 @@ export function refreshModals() {
|
|||||||
*
|
*
|
||||||
* Field shape:
|
* Field shape:
|
||||||
* { label, id, [tag: 'input'|'select'|'textarea'], [type], [value], [placeholder], [options] }
|
* { label, id, [tag: 'input'|'select'|'textarea'], [type], [value], [placeholder], [options] }
|
||||||
|
* - options can be string[], [value, selected][] tuples, or objects with { group, options }
|
||||||
|
* for <optgroup> grouping. Nested options follow the same string/tuple format.
|
||||||
*
|
*
|
||||||
* Action shape:
|
* Action shape:
|
||||||
* { label, cls, action, handler }
|
* { label, cls, action, handler }
|
||||||
@@ -105,11 +107,28 @@ export function formModal(inner, title, fields, actions) {
|
|||||||
if (f.tag === 'select')
|
if (f.tag === 'select')
|
||||||
return '<div class="form-group"><label>' + esc(f.label) + '</label><select id="' + att_esc(f.id) + '"'
|
return '<div class="form-group"><label>' + esc(f.label) + '</label><select id="' + att_esc(f.id) + '"'
|
||||||
+ (f.multiple ? ' multiple' : '') + '>'
|
+ (f.multiple ? ' multiple' : '') + '>'
|
||||||
+ (f.options || []).map(o =>
|
+ (f.options || []).map(function(o) {
|
||||||
typeof o === 'string'
|
if (o === null || o === undefined) return '';
|
||||||
? '<option value="' + att_esc(o) + '">' + esc(o) + '</option>'
|
if (typeof o === 'string')
|
||||||
: '<option value="' + att_esc(o[0]) + '"' + (o[1] ? ' selected' : '') + '>' + esc(o[1]) + '</option>',
|
return '<option value="' + att_esc(o) + '">' + esc(o) + '</option>';
|
||||||
).join('') + '</select></div>';
|
if (Array.isArray(o)) {
|
||||||
|
// Support [value, label] and [value, selectedBoolean]
|
||||||
|
if (o.length < 2)
|
||||||
|
return '<option value="' + att_esc(o[0]) + '">' + att_esc(o[0]) + '</option>';
|
||||||
|
if (typeof o[1] === 'boolean')
|
||||||
|
return '<option value="' + att_esc(o[0]) + '"' + (o[1] ? ' selected' : '') + '>' + att_esc(o[0]) + '</option>';
|
||||||
|
return '<option value="' + att_esc(o[0]) + '">' + esc(o[1]) + '</option>';
|
||||||
|
}
|
||||||
|
if ('group' in o) {
|
||||||
|
return '<optgroup label="' + att_esc(o.group) + '">'
|
||||||
|
+ o.options.map(function(item) {
|
||||||
|
return typeof item === 'string'
|
||||||
|
? '<option value="' + att_esc(item) + '">' + esc(item) + '</option>'
|
||||||
|
: '<option value="' + att_esc(item[0]) + '">' + esc(item[1]) + '</option>';
|
||||||
|
}).join('') + '</optgroup>';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}).join('') + '</select></div>';
|
||||||
|
|
||||||
const tag = f.tag || 'input';
|
const tag = f.tag || 'input';
|
||||||
return '<div class="form-group"><label>' + esc(f.label) + '</label><' + tag + ' id="' + att_esc(f.id) + '"'
|
return '<div class="form-group"><label>' + esc(f.label) + '</label><' + tag + ' id="' + att_esc(f.id) + '"'
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default definePage({
|
|||||||
meta=${Object.keys(fwZones).join(', ') || 'None'} />
|
meta=${Object.keys(fwZones).join(', ') || 'None'} />
|
||||||
<${StatCard} label="Interfaces Up" value=${upC + '/' + nCount}
|
<${StatCard} label="Interfaces Up" value=${upC + '/' + nCount}
|
||||||
meta=${upI.map(i => i.name).join(', ') || 'None up'} />
|
meta=${upI.map(i => i.name).join(', ') || 'None up'} />
|
||||||
<${StatCard} label="WireGuard" value=${String(d.wg?.state || 'unknown')}
|
<${StatCard} label="WireGuard" value=${String((d.wg?.status?.up) ? 'up' : 'down')}
|
||||||
meta=${wP.length + ' peers'} />
|
meta=${wP.length + ' peers'} />
|
||||||
<${StatCard} label="Certificates" value=${certs.length}
|
<${StatCard} label="Certificates" value=${certs.length}
|
||||||
meta=${certW.length + ' expiring/expired'} />
|
meta=${certW.length + ' expiring/expired'} />
|
||||||
@@ -37,8 +37,8 @@ export default definePage({
|
|||||||
<div class="card-header">Services</div>
|
<div class="card-header">Services</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ul class="service-list">
|
<ul class="service-list">
|
||||||
<li><${ServiceStatus} state=${dmsk.state || 'down'} label="Dnsmasq" /></li>
|
<li><${ServiceStatus} state=${dmsk.service_active ? 'up' : 'down'} label="Dnsmasq" /></li>
|
||||||
<li><${ServiceStatus} state=${d.wg?.state || 'down'} label="WireGuard" /></li>
|
<li><${ServiceStatus} state=${(d.wg?.status?.up) ? 'up' : 'down'} label="WireGuard" /></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
import { html, PageHeader, Badge, ConfirmDelete, Tabs, Table, ServiceStatus, renderGuard, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionGroup, QuickModal } from '/static/hoover/index.js?v=7';
|
import { html, PageHeader, ConfirmDelete, Tabs, Table, ServiceStatus, renderGuardMulti, esc, enc, $val, apiFetch, toast, definePage, getModel, ActionButton, ActionGroup, QuickModal } from '/static/hoover/index.js?v=7';
|
||||||
|
|
||||||
const addRange = QuickModal({
|
function makeAddRange(activeZones) {
|
||||||
|
const opts = [
|
||||||
|
['', '(global)'],
|
||||||
|
...Object.entries(activeZones || {})
|
||||||
|
.filter(([, ifaces]) => Array.isArray(ifaces) && ifaces.length > 0)
|
||||||
|
.flatMap(([zone, ifaces]) => {
|
||||||
|
const label = zone + ' \u2192 ';
|
||||||
|
if (ifaces.length === 1)
|
||||||
|
return [[ifaces[0], label + ifaces[0]]];
|
||||||
|
return [{ group: zone, options: ifaces.map(i => [i, label + i]) }];
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
return QuickModal({
|
||||||
title: 'Add DHCP Range',
|
title: 'Add DHCP Range',
|
||||||
fields: [
|
fields: [
|
||||||
{ label: 'Interface (optional)', id: 'r-iface', placeholder: 'Leave empty for global' },
|
{ label: 'Interface (optional)', id: 'r-iface', tag: 'select', options: opts },
|
||||||
{ label: 'Start IP', id: 'r-start', placeholder: '192.168.1.100' },
|
{ label: 'Start IP', id: 'r-start', placeholder: '192.168.1.100' },
|
||||||
{ label: 'End IP', id: 'r-end', placeholder: '192.168.1.200' },
|
{ label: 'End IP', id: 'r-end', placeholder: '192.168.1.200' },
|
||||||
{ label: 'Lease Time', id: 'r-lease', placeholder: '12h' },
|
{ label: 'Lease Time', id: 'r-lease', placeholder: '12h' },
|
||||||
@@ -21,6 +34,7 @@ const addRange = QuickModal({
|
|||||||
},
|
},
|
||||||
refresh: 'dnsmasq',
|
refresh: 'dnsmasq',
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const addLease = QuickModal({
|
const addLease = QuickModal({
|
||||||
title: 'Add Static Lease',
|
title: 'Add Static Lease',
|
||||||
@@ -61,11 +75,12 @@ export default definePage({
|
|||||||
init() {
|
init() {
|
||||||
return {
|
return {
|
||||||
dnsmasq: getModel('dnsmasq'),
|
dnsmasq: getModel('dnsmasq'),
|
||||||
|
firewall: getModel('firewall'),
|
||||||
activeTab: 'ranges',
|
activeTab: 'ranges',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
render(state) {
|
render(state) {
|
||||||
const guard = renderGuard(state.dnsmasq, 'DHCP & DNS', 'Dnsmasq management', state.dnsmasq.data);
|
const guard = renderGuardMulti('DHCP & DNS', 'Dnsmasq management', state.dnsmasq, state.firewall);
|
||||||
if (guard) return guard;
|
if (guard) return guard;
|
||||||
|
|
||||||
const cfg = state.dnsmasq.data?.config || {};
|
const cfg = state.dnsmasq.data?.config || {};
|
||||||
@@ -116,7 +131,7 @@ export default definePage({
|
|||||||
|
|
||||||
const tabNames = ['ranges', 'leases', 'dns', 'active'];
|
const tabNames = ['ranges', 'leases', 'dns', 'active'];
|
||||||
const actions = ActionGroup(
|
const actions = ActionGroup(
|
||||||
html`<button class="btn btn-primary" onClick=${() => addRange(state)}>Add Range</button>`,
|
html`<button class="btn btn-primary" onClick=${() => makeAddRange(state.firewall.data?.zones?.active)(state)}>Add Range</button>`,
|
||||||
html`<button class="btn btn-outline" onClick=${() => addLease(state)}>Static Lease</button>`,
|
html`<button class="btn btn-outline" onClick=${() => addLease(state)}>Static Lease</button>`,
|
||||||
html`<button class="btn btn-outline" onClick=${() => addDns(state)}>DNS Record</button>`,
|
html`<button class="btn btn-outline" onClick=${() => addDns(state)}>DNS Record</button>`,
|
||||||
ActionButton({
|
ActionButton({
|
||||||
@@ -141,7 +156,7 @@ export default definePage({
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
PageHeader({ title: 'DHCP & DNS', subtitle: 'Dnsmasq management', actions }),
|
PageHeader({ title: 'DHCP & DNS', subtitle: 'Dnsmasq management', actions }),
|
||||||
ServiceStatus({ state: status.state || 'down', label: 'Dnsmasq' }),
|
ServiceStatus({ state: status.service_active ? 'up' : 'down', label: 'Dnsmasq' }),
|
||||||
Tabs({ state, tabs: tabNames }),
|
Tabs({ state, tabs: tabNames }),
|
||||||
state.activeTab === 'ranges'
|
state.activeTab === 'ranges'
|
||||||
? Table({ columns: ['Interface', 'Start', 'End', 'Lease', 'Action'], rows: rangesRows, emptyText: 'No DHCP ranges' }) : null,
|
? Table({ columns: ['Interface', 'Start', 'End', 'Lease', 'Action'], rows: rangesRows, emptyText: 'No DHCP ranges' }) : null,
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ function addDomain(state) {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const certSelect = document.getElementById('p-cert');
|
const certSelect = inner.querySelector('#p-cert');
|
||||||
const domainInput = document.getElementById('p-domain');
|
const domainInput = inner.querySelector('#p-domain');
|
||||||
if (certSelect && domainInput) {
|
if (certSelect && domainInput) {
|
||||||
certSelect.addEventListener('change', () => {
|
certSelect.addEventListener('change', () => {
|
||||||
const val = certSelect.value;
|
const val = certSelect.value;
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default definePage({
|
|||||||
if (guard) return guard;
|
if (guard) return guard;
|
||||||
|
|
||||||
const st = state.wireguard.data?.status || {};
|
const st = state.wireguard.data?.status || {};
|
||||||
const isUp = st.state === 'up';
|
const isUp = st.up || false;
|
||||||
const listenPort = (state.wireguard.data?.config?.interface || {}).listen_port || '-';
|
const listenPort = (state.wireguard.data?.config?.interface || {}).listen_port || '-';
|
||||||
|
|
||||||
const peerRows = (state.wireguard.data?.peers || []).map(p => {
|
const peerRows = (state.wireguard.data?.peers || []).map(p => {
|
||||||
@@ -107,10 +107,10 @@ export default definePage({
|
|||||||
return [
|
return [
|
||||||
PageHeader({
|
PageHeader({
|
||||||
title: 'WireGuard',
|
title: 'WireGuard',
|
||||||
subtitle: 'Tunnel: ' + (st.state || 'unknown') + ', Listen: ' + listenPort,
|
subtitle: 'Tunnel: ' + (st.up ? 'up' : 'down') + ', Listen: ' + listenPort,
|
||||||
actions,
|
actions,
|
||||||
}),
|
}),
|
||||||
ServiceStatus({ state: st.state || 'down' }),
|
ServiceStatus({ state: st.up ? 'up' : 'down' }),
|
||||||
peerRows.length
|
peerRows.length
|
||||||
? Table({
|
? Table({
|
||||||
columns: ['Peer', 'Public Key', 'Allowed IPs', 'Endpoint', 'Handshake', 'Transfer', 'Actions'],
|
columns: ['Peer', 'Public Key', 'Allowed IPs', 'Endpoint', 'Handshake', 'Transfer', 'Actions'],
|
||||||
|
|||||||
Reference in New Issue
Block a user