feat(connections): surface pve/pbs scope toggles in node modal

Proxmox VE and PBS nodes already carry Monitor* booleans on their config
structs, but the node modal only surfaced the advanced PVE toggles (SMART,
temperature) and none of the PBS surfaces — so save paths hardcoded every
VE/PBS collector to true. The PMG side has had a real Data-collection UI
for a while; bring PVE and PBS to parity so users can opt out of surfaces
without going through the retired "Stop this surface" dialog.

- nodeModalPresentation: add monitorVMs/Containers/Storage/Backups and
  monitorDatastores/SyncJobs/VerifyJobs/PruneJobs/GarbageJobs fields to
  the form data shape; default all true.
- useNodeModalState: populate the new fields from editingNode (matching
  the existing pmg pattern) so edit-mode reflects stored scope.
- buildNodeModalMonitoringPayload: dispatch form values through instead
  of hardcoded true, so the save body honors the toggles.
- NodeModalMonitoringSection: render Data-collection sections for pve
  and pbs mirroring the pmg section, with short per-surface copy.

Tests: extend nodeModalPresentation.test to assert scope toggles round-trip
through buildNodeModalMonitoringPayload for pve and pbs.
This commit is contained in:
rcourtman 2026-04-19 22:03:43 +01:00
parent a7ba4206fe
commit e769c3be7f
4 changed files with 266 additions and 9 deletions

View file

@ -59,6 +59,81 @@ export const NodeModalMonitoringSection: Component<NodeModalMonitoringSectionPro
</div>
<Show when={modalProps.nodeType === 'pve'}>
<div class="space-y-3">
<SectionHeader
title="Data collection"
size="sm"
class="mb-1"
titleClass="text-base-content"
/>
<p class="text-xs text-muted">
Control which Proxmox VE resources Pulse ingests. Disable individual collectors to
reduce API load or trim dashboard noise.
</p>
<label class="flex items-start gap-2 text-sm text-base-content">
<input
type="checkbox"
checked={state.formData().monitorVMs}
onChange={(event) => state.updateField('monitorVMs', event.currentTarget.checked)}
class={formCheckbox + ' mt-0.5'}
/>
<div>
<div>Virtual machines</div>
<p class="text-xs text-muted mt-1">
QEMU guest state, resource usage, and per-VM metrics.
</p>
</div>
</label>
<label class="flex items-start gap-2 text-sm text-base-content">
<input
type="checkbox"
checked={state.formData().monitorContainers}
onChange={(event) =>
state.updateField('monitorContainers', event.currentTarget.checked)
}
class={formCheckbox + ' mt-0.5'}
/>
<div>
<div>Containers</div>
<p class="text-xs text-muted mt-1">
LXC container state and resource usage.
</p>
</div>
</label>
<label class="flex items-start gap-2 text-sm text-base-content">
<input
type="checkbox"
checked={state.formData().monitorStorage}
onChange={(event) => state.updateField('monitorStorage', event.currentTarget.checked)}
class={formCheckbox + ' mt-0.5'}
/>
<div>
<div>Storage</div>
<p class="text-xs text-muted mt-1">
Per-storage capacity, status, and content type breakdown.
</p>
</div>
</label>
<label class="flex items-start gap-2 text-sm text-base-content">
<input
type="checkbox"
checked={state.formData().monitorBackups}
onChange={(event) => state.updateField('monitorBackups', event.currentTarget.checked)}
class={formCheckbox + ' mt-0.5'}
/>
<div>
<div>Backups</div>
<p class="text-xs text-muted mt-1">
vzdump schedules and recent backup activity.
</p>
</div>
</label>
</div>
<div class="space-y-4">
<SectionHeader
title="Advanced monitoring"
@ -150,6 +225,106 @@ export const NodeModalMonitoringSection: Component<NodeModalMonitoringSectionPro
</div>
</Show>
<Show when={modalProps.nodeType === 'pbs'}>
<div class="space-y-3">
<SectionHeader
title="Data collection"
size="sm"
class="mb-1"
titleClass="text-base-content"
/>
<p class="text-xs text-muted">
Control which Proxmox Backup Server surfaces Pulse ingests. Disable collectors you
don't need.
</p>
<label class="flex items-start gap-2 text-sm text-base-content">
<input
type="checkbox"
checked={state.formData().monitorDatastores}
onChange={(event) =>
state.updateField('monitorDatastores', event.currentTarget.checked)
}
class={formCheckbox + ' mt-0.5'}
/>
<div>
<div>Datastores</div>
<p class="text-xs text-muted mt-1">
Datastore capacity, usage, and deduplication status.
</p>
</div>
</label>
<label class="flex items-start gap-2 text-sm text-base-content">
<input
type="checkbox"
checked={state.formData().monitorSyncJobs}
onChange={(event) =>
state.updateField('monitorSyncJobs', event.currentTarget.checked)
}
class={formCheckbox + ' mt-0.5'}
/>
<div>
<div>Sync jobs</div>
<p class="text-xs text-muted mt-1">
Remote sync schedules and their last-run state.
</p>
</div>
</label>
<label class="flex items-start gap-2 text-sm text-base-content">
<input
type="checkbox"
checked={state.formData().monitorVerifyJobs}
onChange={(event) =>
state.updateField('monitorVerifyJobs', event.currentTarget.checked)
}
class={formCheckbox + ' mt-0.5'}
/>
<div>
<div>Verify jobs</div>
<p class="text-xs text-muted mt-1">
Backup verification schedules and outcomes.
</p>
</div>
</label>
<label class="flex items-start gap-2 text-sm text-base-content">
<input
type="checkbox"
checked={state.formData().monitorPruneJobs}
onChange={(event) =>
state.updateField('monitorPruneJobs', event.currentTarget.checked)
}
class={formCheckbox + ' mt-0.5'}
/>
<div>
<div>Prune jobs</div>
<p class="text-xs text-muted mt-1">
Retention policy runs and the snapshots they remove.
</p>
</div>
</label>
<label class="flex items-start gap-2 text-sm text-base-content">
<input
type="checkbox"
checked={state.formData().monitorGarbageJobs}
onChange={(event) =>
state.updateField('monitorGarbageJobs', event.currentTarget.checked)
}
class={formCheckbox + ' mt-0.5'}
/>
<div>
<div>Garbage collection</div>
<p class="text-xs text-muted mt-1">
GC runs that reclaim orphaned chunks from the datastore.
</p>
</div>
</label>
</div>
</Show>
<Show when={modalProps.nodeType === 'pmg'}>
<div class="space-y-3">
<SectionHeader

View file

@ -276,6 +276,27 @@ export const useNodeModalState = (props: NodeModalProps) => {
username = parts[0];
}
const pveConfig =
node.type === 'pve'
? (node as NodeConfig & {
monitorVMs?: boolean;
monitorContainers?: boolean;
monitorStorage?: boolean;
monitorBackups?: boolean;
})
: undefined;
const pbsConfig =
node.type === 'pbs'
? (node as NodeConfig & {
monitorDatastores?: boolean;
monitorSyncJobs?: boolean;
monitorVerifyJobs?: boolean;
monitorPruneJobs?: boolean;
monitorGarbageJobs?: boolean;
})
: undefined;
const pmgConfig =
node.type === 'pmg'
? (node as NodeConfig & {
@ -298,6 +319,10 @@ export const useNodeModalState = (props: NodeModalProps) => {
tokenValue: '',
fingerprint: ('fingerprint' in node ? node.fingerprint : '') || '',
verifySSL: node.verifySSL ?? true,
monitorVMs: pveConfig?.monitorVMs ?? true,
monitorContainers: pveConfig?.monitorContainers ?? true,
monitorStorage: pveConfig?.monitorStorage ?? true,
monitorBackups: pveConfig?.monitorBackups ?? true,
monitorPhysicalDisks:
node.type === 'pve'
? ((node as NodeConfig & { monitorPhysicalDisks?: boolean }).monitorPhysicalDisks ?? true)
@ -307,6 +332,11 @@ export const useNodeModalState = (props: NodeModalProps) => {
? ((node as NodeConfig & { physicalDiskPollingMinutes?: number })
.physicalDiskPollingMinutes ?? 5)
: 5,
monitorDatastores: pbsConfig?.monitorDatastores ?? true,
monitorSyncJobs: pbsConfig?.monitorSyncJobs ?? true,
monitorVerifyJobs: pbsConfig?.monitorVerifyJobs ?? true,
monitorPruneJobs: pbsConfig?.monitorPruneJobs ?? true,
monitorGarbageJobs: pbsConfig?.monitorGarbageJobs ?? true,
monitorMailStats: pmgConfig?.monitorMailStats ?? true,
monitorQueues: pmgConfig?.monitorQueues ?? true,
monitorQuarantine: pmgConfig?.monitorQuarantine ?? true,

View file

@ -97,6 +97,40 @@ describe('nodeModalPresentation', () => {
});
});
it('propagates per-surface scope toggles through the pve payload', () => {
const form = {
...getNodeModalDefaultFormData('pve'),
monitorVMs: false,
monitorContainers: true,
monitorStorage: false,
monitorBackups: true,
};
expect(buildNodeModalMonitoringPayload('pve', form)).toMatchObject({
monitorVMs: false,
monitorContainers: true,
monitorStorage: false,
monitorBackups: true,
});
});
it('propagates per-surface scope toggles through the pbs payload', () => {
const form = {
...getNodeModalDefaultFormData('pbs'),
monitorDatastores: true,
monitorSyncJobs: false,
monitorVerifyJobs: true,
monitorPruneJobs: false,
monitorGarbageJobs: false,
};
expect(buildNodeModalMonitoringPayload('pbs', form)).toMatchObject({
monitorDatastores: true,
monitorSyncJobs: false,
monitorVerifyJobs: true,
monitorPruneJobs: false,
monitorGarbageJobs: false,
});
});
it('returns canonical node test result presentation', () => {
expect(getNodeModalTestResultPresentation('success')).toMatchObject({
icon: 'success',

View file

@ -18,8 +18,17 @@ export interface NodeModalFormData {
tokenValue: string;
fingerprint: string;
verifySSL: boolean;
monitorVMs: boolean;
monitorContainers: boolean;
monitorStorage: boolean;
monitorBackups: boolean;
monitorPhysicalDisks: boolean;
physicalDiskPollingMinutes: number;
monitorDatastores: boolean;
monitorSyncJobs: boolean;
monitorVerifyJobs: boolean;
monitorPruneJobs: boolean;
monitorGarbageJobs: boolean;
monitorMailStats: boolean;
monitorQueues: boolean;
monitorQuarantine: boolean;
@ -47,8 +56,17 @@ export function getNodeModalDefaultFormData(
tokenValue: '',
fingerprint: '',
verifySSL: true,
monitorVMs: true,
monitorContainers: true,
monitorStorage: true,
monitorBackups: true,
monitorPhysicalDisks: false,
physicalDiskPollingMinutes: 5,
monitorDatastores: true,
monitorSyncJobs: true,
monitorVerifyJobs: true,
monitorPruneJobs: true,
monitorGarbageJobs: true,
monitorMailStats: true,
monitorQueues: true,
monitorQuarantine: true,
@ -150,20 +168,20 @@ export function buildNodeModalMonitoringPayload(
switch (nodeType) {
case 'pve':
return {
monitorVMs: true,
monitorContainers: true,
monitorStorage: true,
monitorBackups: true,
monitorVMs: formData.monitorVMs,
monitorContainers: formData.monitorContainers,
monitorStorage: formData.monitorStorage,
monitorBackups: formData.monitorBackups,
monitorPhysicalDisks: formData.monitorPhysicalDisks,
physicalDiskPollingMinutes: formData.physicalDiskPollingMinutes,
};
case 'pbs':
return {
monitorDatastores: true,
monitorSyncJobs: true,
monitorVerifyJobs: true,
monitorPruneJobs: true,
monitorGarbageJobs: true,
monitorDatastores: formData.monitorDatastores,
monitorSyncJobs: formData.monitorSyncJobs,
monitorVerifyJobs: formData.monitorVerifyJobs,
monitorPruneJobs: formData.monitorPruneJobs,
monitorGarbageJobs: formData.monitorGarbageJobs,
};
case 'pmg':
return {