From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id B740F1FF13C for ; Thu, 19 Mar 2026 13:51:15 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 176371DECC; Thu, 19 Mar 2026 13:51:28 +0100 (CET) From: David Riley To: pve-devel@lists.proxmox.com Subject: [PATCH pve-manager] fix: #4490 ui: backup job window: add filter/search for virtual guest grid Date: Thu, 19 Mar 2026 13:45:27 +0100 Message-ID: <20260319124527.4343-1-d.riley@proxmox.com> X-Mailer: git-send-email 2.47.3 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1773924638674 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.145 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0.408 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_RPBL_BLOCKED 0.819 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_SAFE_BLOCKED 0.903 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: YGJ7N3NCZMPTULXCCFAZJLHC23HS6GYF X-Message-ID-Hash: YGJ7N3NCZMPTULXCCFAZJLHC23HS6GYF X-MailFrom: d.riley@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: David Riley X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Add a collapsible form for filtering by Name, Status, Pool, Type, Tags, and HA State. Add corresponding columns (pool, type, tags, hastate) to the guest grid so the visual output matches the available filters. Expand/collapse the filter UI depending on the selected Backup Mode (All, Pool, Include/Exclude). Signed-off-by: David Riley --- www/manager6/dc/Backup.js | 354 +++++++++++++++++++++++++++++++++++++- 1 file changed, 350 insertions(+), 4 deletions(-) diff --git a/www/manager6/dc/Backup.js b/www/manager6/dc/Backup.js index 956a7cdf..28d63bbb 100644 --- a/www/manager6/dc/Backup.js +++ b/www/manager6/dc/Backup.js @@ -63,6 +63,7 @@ Ext.define('PVE.dc.BackupEdit', { me.lookup('storageSelector').setNodename(value); let vmgrid = me.lookup('vmgrid'); let store = vmgrid.getStore(); + me.clearFilters(); store.clearFilter(); store.filterBy(function (rec) { @@ -94,6 +95,7 @@ Ext.define('PVE.dc.BackupEdit', { selectPoolMembers: function () { let me = this; let mode = me.lookup('modeSelector').getValue(); + me.clearFilters(); if (mode !== 'pool') { return; @@ -120,17 +122,20 @@ Ext.define('PVE.dc.BackupEdit', { let me = this; let vmgrid = me.lookup('vmgrid'); vmgrid.getStore().removeFilter('poolFilter'); + me.clearFilters(); if (oldValue === 'all' && value !== 'all') { vmgrid.getSelectionModel().deselectAll(true); } if (value === 'all') { + me.lookup('filters').collapse(); vmgrid.getSelectionModel().selectAll(true); - } - - if (value === 'pool') { + } else if (value === 'pool') { + me.lookup('filters').collapse(); me.selectPoolMembers(); + } else { + me.lookup('filters').expand(); } }, @@ -181,8 +186,175 @@ Ext.define('PVE.dc.BackupEdit', { return data; }, + filterChange: function (f, value) { + let me = this; + + if (!f.reference) { + return; + } + + if (f.reference === 'namefilter') { + me.filterState[f.reference] = value ? value.toLowerCase() : ''; + } else { + me.filterState[f.reference] = value; + } + + me.updateFilterUI(); + me.lookup('vmgrid').getStore().addFilter(me.activeFilter); + }, + + updateFilterUI: function () { + let me = this; + + let count = 0; + Object.values(me.filterState).forEach((val) => { + if (val && val.length > 0) { + count++; + } + }); + + let fieldSet = me.lookup('filters'); + let clearBtn = me.lookup('clearBtn'); + + if (count) { + fieldSet.setTitle(Ext.String.format(gettext('Filters ({0})'), count)); + clearBtn.setDisabled(false); + } else { + fieldSet.setTitle(gettext('Filters')); + clearBtn.setDisabled(true); + } + }, + + filterFn: function (rec) { + let me = this; + let state = me.filterState; + + if (state.namefilter && state.namefilter !== '') { + let name = (rec.get('name') || '').toLowerCase(); + if (!name.includes(state.namefilter)) { + return false; + } + } + + if (state.statusfilter && state.statusfilter !== '') { + if (rec.get('status') !== state.statusfilter) { + return false; + } + } + + if (state.typefilter && state.typefilter !== '') { + if (rec.get('type') !== state.typefilter) { + return false; + } + } + + if (state.poolfilter && state.poolfilter.length > 0) { + if (!state.poolfilter.includes(rec.get('pool'))) { + return false; + } + } + + if (state.hastatefilter && state.hastatefilter.length > 0) { + if (!state.hastatefilter.includes(rec.get('hastate'))) { + return false; + } + } + + let recTags = rec.get('tags'); + let splitTags = []; + + if (Ext.isArray(recTags)) { + splitTags = recTags; + } else if (recTags) { + splitTags = recTags.split(/[,; ]/); + } + + if (state.includetagfilter && state.includetagfilter.length > 0) { + if (!state.includetagfilter.some((tag) => splitTags.includes(tag))) { + return false; + } + } + + if (state.excludetagfilter && state.excludetagfilter.length > 0) { + if (state.excludetagfilter.some((tag) => splitTags.includes(tag))) { + return false; + } + } + + return true; + }, + + clearFilters: function () { + let me = this; + let vmgrid = me.lookup('vmgrid'); + let store = vmgrid.getStore(); + + let filterRefs = Object.keys(me.filterState); + filterRefs.forEach((ref) => { + let field = me.lookup(ref); + if (field) { + field.suspendEvents(false); + field.setValue(field.multiSelect ? [] : ''); + field.resumeEvents(); + } + }); + + me.filterState = {}; + me.updateFilterUI(); + + store.removeFilter('filter'); + }, + + prepareFilters: function () { + let me = this; + + me.filterState = {}; + me.activeFilter = new Ext.util.Filter({ + id: 'filter', + scope: me, + filterFn: me.filterFn, + }); + + let statusMap = {}; + let poolMap = {}; + let haMap = {}; + let tagMap = {}; + + PVE.data.ResourceStore.each((rec) => { + if (['qemu', 'lxc'].indexOf(rec.data.type) !== -1) { + statusMap[rec.data.status] = true; + } + if (rec.data.type === 'pool') { + poolMap[rec.data.pool] = true; + } + if (rec.data.hastate !== '') { + haMap[rec.data.hastate] = true; + } + if (rec.data.tags !== '') { + rec.data.tags.split(/[,; ]/).forEach((tag) => { + if (tag !== '') { + tagMap[tag] = true; + } + }); + } + }); + + let statusList = Object.keys(statusMap).map((key) => [key, key]); + statusList.unshift(['', gettext('All')]); + let poolList = Object.keys(poolMap).map((key) => [key, key]); + let haList = Object.keys(haMap).map((key) => [key, key]); + let tagList = Object.keys(tagMap).map((key) => ({ value: key })); + + me.lookup('includetagfilter').getStore().setData(tagList); + me.lookup('excludetagfilter').getStore().setData(tagList); + me.lookup('statusfilter').getStore().setData(statusList); + me.lookup('hastatefilter').getStore().setData(haList); + me.lookup('poolfilter').getStore().setData(poolList); + }, + init: function (view) { let me = this; + me.prepareFilters(); if (view.isCreate) { me.lookup('modeSelector').setValue('include'); @@ -330,6 +502,171 @@ Ext.define('PVE.dc.BackupEdit', { 'data-qtip': gettext('Description of the job'), }, }, + { + xtype: 'fieldset', + reference: 'filters', + collapsible: true, + title: gettext('Filters'), + layout: 'hbox', + margin: '0 2 10 0', + items: [ + { + xtype: 'container', + flex: 1, + padding: 5, + layout: { + type: 'vbox', + align: 'stretch', + }, + defaults: { + listeners: { + change: 'filterChange', + }, + isFormField: false, + }, + items: [ + { + fieldLabel: gettext('Name'), + reference: 'namefilter', + xtype: 'textfield', + }, + { + xtype: 'combobox', + reference: 'statusfilter', + fieldLabel: gettext('Status'), + emptyText: gettext('All'), + editable: false, + value: '', + store: [], + }, + { + xtype: 'combobox', + reference: 'poolfilter', + fieldLabel: gettext('Pool'), + emptyText: gettext('All'), + editable: false, + multiSelect: true, + store: [], + }, + { + xtype: 'combobox', + reference: 'typefilter', + fieldLabel: gettext('Type'), + emptyText: gettext('All'), + editable: false, + value: '', + store: [ + ['', gettext('All')], + ['lxc', gettext('CT')], + ['qemu', gettext('VM')], + ], + }, + ], + }, + { + xtype: 'container', + layout: { + type: 'vbox', + align: 'stretch', + }, + flex: 1, + padding: 5, + defaults: { + isFormField: false, + }, + items: [ + { + xtype: 'proxmoxComboGrid', + reference: 'includetagfilter', + fieldLabel: gettext('Include Tags'), + emptyText: gettext('All'), + editable: false, + multiSelect: true, + valueField: 'value', + displayField: 'value', + listConfig: { + userCls: 'proxmox-tags-full', + columns: [ + { + dataIndex: 'value', + flex: 1, + renderer: (value) => + PVE.Utils.renderTags( + value, + PVE.UIOptions.tagOverrides, + ), + }, + ], + }, + store: { + data: [], + }, + listeners: { + change: 'filterChange', + }, + }, + { + xtype: 'proxmoxComboGrid', + reference: 'excludetagfilter', + fieldLabel: gettext('Exclude Tags'), + emptyText: gettext('None'), + multiSelect: true, + editable: false, + valueField: 'value', + displayField: 'value', + listConfig: { + userCls: 'proxmox-tags-full', + columns: [ + { + dataIndex: 'value', + flex: 1, + renderer: (value) => + PVE.Utils.renderTags( + value, + PVE.UIOptions.tagOverrides, + ), + }, + ], + }, + store: { + data: [], + }, + listeners: { + change: 'filterChange', + }, + }, + { + xtype: 'combobox', + reference: 'hastatefilter', + fieldLabel: gettext('HA status'), + emptyText: gettext('All'), + multiSelect: true, + editable: false, + store: [], + listeners: { + change: 'filterChange', + }, + }, + { + xtype: 'container', + layout: { + type: 'vbox', + align: 'end', + }, + items: [ + { + xtype: 'button', + reference: 'clearBtn', + text: gettext('Clear Filters'), + disabled: true, + handler: 'clearFilters', + }, + ], + }, + ], + }, + ], + }, { xtype: 'vmselector', reference: 'vmgrid', @@ -337,7 +674,16 @@ Ext.define('PVE.dc.BackupEdit', { name: 'vmid', disabled: true, allowBlank: false, - columnSelection: ['vmid', 'node', 'status', 'name', 'type'], + columnSelection: [ + 'vmid', + 'node', + 'status', + 'name', + 'pool', + 'type', + 'tags', + 'hastate', + ], bind: { disabled: '{disableVMSelection}', }, -- 2.47.3