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:
2026-09-01 20:17:15 +00:00
parent 75b86fd60d
commit 89b64960f3
14 changed files with 649 additions and 61 deletions
+19 -4
View File
@@ -37,6 +37,13 @@ export function StatusDot(props = {}) {
return h('span', { class: `status-dot status-${v}` });
}
/**
* Small amber dot marking a pending (edited, not yet applied) element.
*/
export function PendingDot() {
return h('span', { class: 'pending-dot' });
}
/**
* Empty-state placeholder card.
*
@@ -55,16 +62,20 @@ export function Empty(props = {}) {
* @param {object} props
* @param {string} [props.header]
* @param {VNode[]} [props.children]
* @param {string} [props.cls] - Extra class appended to the outer `div.card`
* @param {string} [props.title] - Tooltip on the outer `div.card`
*/
export function Card(props = {}) {
const key = props.key !== undefined ? { key: props.key } : {};
const cls = props.cls ? `card ${props.cls}` : 'card';
const title = props.title ? { title: props.title } : {};
if (props.header) {
return h('div', { class: 'card', ...key },
return h('div', { class: cls, ...title, ...key },
h('div', { class: 'card-header' }, props.header),
h('div', { class: 'card-body' }, props.children || []),
);
}
return h('div', { class: 'card', ...key }, props.children || []);
return h('div', { class: cls, ...title, ...key }, props.children || []);
}
/**
@@ -209,6 +220,8 @@ export function ActionButton(props = {}) {
* @param {string} [props.emptyText] - Empty-state message
* @param {boolean} [props.wrapCard] - Wrap in div.card (default: true)
* @param {string} [props.key] - VNode key
* @param {string} [props.cls] - Extra class appended to the wrapper (or div.card)
* @param {string} [props.title] - Tooltip on the wrapper element
*/
export function Table(props = {}) {
const cols = props.columns || [];
@@ -225,10 +238,12 @@ export function Table(props = {}) {
),
);
const key = props.key !== undefined ? { key: props.key } : {};
const title = props.title ? { title: props.title } : {};
const cls = props.cls ? `card ${props.cls}` : 'card';
if (props.wrapCard !== false) {
return h('div', { class: 'card', ...key }, table);
return h('div', { class: cls, ...title, ...key }, table);
}
return h('div', key, table);
return h('div', { ...title, ...key }, table);
}
/**