ui: amber pending-edit markers for unapplied config changes
Add hoover/dirty.js: line-matching helpers that flag UI rows/cards edited (saved to config) but not yet applied, consuming the pending state the daemon already streams — status.pending_diff for hash subsystems, firewall pending zone+type for firewalld. Visual language is amber (.config-dirty + PendingDot), distinct from the red .pending-delete; orphanInfo surfaces removed entries (e.g. WireGuard peers) on their container table. Wired into the backends, dhcp, interfaces, nat, proxy, rules, wireguard, and zones pages; Card and Table gain cls/title props. Covered by 27 node tests (tests/test-dirty.js).
This commit is contained in:
+22
-11
@@ -1,4 +1,4 @@
|
||||
import { html, h, PageHeader, ConfirmDelete, Tabs, Table, ServiceStatus, renderGuardMulti, esc, enc, $val, apiFetch, toast, definePage, getModel, ActionButton, ActionGroup, QuickModal, ApplyConfirm } from '/static/hoover/index.js';
|
||||
import { html, h, PageHeader, ConfirmDelete, Tabs, Table, ServiceStatus, renderGuardMulti, esc, enc, $val, apiFetch, toast, definePage, getModel, ActionButton, ActionGroup, QuickModal, ApplyConfirm, PendingDot, dirtySet, dirtyInfo } from '/static/hoover/index.js';
|
||||
|
||||
function makeAddRange(activeZones, interfaces) {
|
||||
const opts = [
|
||||
@@ -118,6 +118,7 @@ export default definePage({
|
||||
const guard = renderGuardMulti('DHCP & DNS', 'Dnsmasq management', state.dnsmasq, state.firewall);
|
||||
if (guard) return guard;
|
||||
|
||||
const set = dirtySet(state.dnsmasq.data?.status);
|
||||
const cfg = state.dnsmasq.data?.config || {};
|
||||
const dhcpCfg = cfg.dhcp || {};
|
||||
const dnsCfg = cfg.dns || {};
|
||||
@@ -126,8 +127,10 @@ export default definePage({
|
||||
const dnsRecords = dnsCfg.custom_records || [];
|
||||
const status = state.dnsmasq.data?.status || {};
|
||||
|
||||
const rangesRows = ranges.map((r) => html`<tr key=${(r.interface || '_g') + '-' + r.start + '-' + r.end}>
|
||||
<td>${r.interface || '(global)'}</td>
|
||||
const rangesRows = ranges.map((r, i) => {
|
||||
const info = dirtyInfo(set, 'dhcp.ranges[' + i + ']');
|
||||
return html`<tr key=${(r.interface || '_g') + '-' + r.start + '-' + r.end} class=${info.class || undefined} title=${info.title || undefined}>
|
||||
<td>${info.dirty ? PendingDot({}) : ''}${r.interface || '(global)'}</td>
|
||||
<td>${esc(r.start)}</td>
|
||||
<td>${esc(r.end)}</td>
|
||||
<td>${esc(r.lease_time || '12h')}</td>
|
||||
@@ -139,10 +142,13 @@ export default definePage({
|
||||
body=${{ interface: r.interface || '', start: r.start, end: r.end }}
|
||||
success="Range removed" />
|
||||
</td>
|
||||
</tr>`);
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
const leaseRows = staticLeases.map((l) => html`<tr key=${l.mac}>
|
||||
<td>${esc(l.mac)}</td>
|
||||
const leaseRows = staticLeases.map((l, i) => {
|
||||
const info = dirtyInfo(set, 'dhcp.static_leases[' + i + ']');
|
||||
return html`<tr key=${l.mac} class=${info.class || undefined} title=${info.title || undefined}>
|
||||
<td>${info.dirty ? PendingDot({}) : ''}${esc(l.mac)}</td>
|
||||
<td>${esc(l.ip)}</td>
|
||||
<td>${l.hostname || '-'}</td>
|
||||
<td>
|
||||
@@ -152,10 +158,13 @@ export default definePage({
|
||||
message=${'Remove lease ' + l.mac + '?'}
|
||||
success="Lease removed" />
|
||||
</td>
|
||||
</tr>`);
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
const dnsRows = dnsRecords.map((rec) => html`<tr key=${rec.name}>
|
||||
<td><strong>${esc(rec.name || 'unnamed')}</strong></td>
|
||||
const dnsRows = dnsRecords.map((rec, i) => {
|
||||
const info = dirtyInfo(set, 'dns.custom_records[' + i + ']');
|
||||
return html`<tr key=${rec.name} class=${info.class || undefined} title=${info.title || undefined}>
|
||||
<td>${info.dirty ? PendingDot({}) : ''}<strong>${esc(rec.name || 'unnamed')}</strong></td>
|
||||
<td class="text-sm">${esc(rec.address || '-')}</td>
|
||||
<td>
|
||||
<${ConfirmDelete}
|
||||
@@ -164,7 +173,8 @@ export default definePage({
|
||||
message=${'Remove DNS record ' + (rec.name || 'unnamed') + '?'}
|
||||
success="Record removed" />
|
||||
</td>
|
||||
</tr>`);
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
const _setDomain = async (domain) => {
|
||||
const res = await apiFetch('/api/dhcp/domain', {
|
||||
@@ -180,7 +190,8 @@ export default definePage({
|
||||
};
|
||||
|
||||
const currentDomain = dnsCfg.domain || null;
|
||||
const domainSection = html`<div class="domain-config" style="margin-bottom: 1rem;">
|
||||
const domainInfo = dirtyInfo(set, 'dns.domain');
|
||||
const domainSection = html`<div class="domain-config ${domainInfo.class}" title=${domainInfo.title || undefined} style="margin-bottom: 1rem;">
|
||||
<label style="font-weight: 600;">Search Domain</label>
|
||||
<p class="text-sm" style="margin: 0.25rem 0 0.5rem;">${currentDomain ? esc(currentDomain) : '<span class="text-muted">(not set)</span>'}</p>
|
||||
<div class="form-inline" style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
|
||||
Reference in New Issue
Block a user