From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 6D2CE1FF140 for ; Fri, 27 Mar 2026 11:41:03 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id A41834EBD; Fri, 27 Mar 2026 11:41:24 +0100 (CET) Message-ID: <7ef46393-e146-4830-92cc-35b0c2db6140@proxmox.com> Date: Fri, 27 Mar 2026 11:40:48 +0100 MIME-Version: 1.0 User-Agent: Mozilla Thunderbird Subject: superseded: [PATCH pve-manager] fix: #4490 ui: backup job window: add filter/search for virtual guest grid To: pve-devel@lists.proxmox.com References: <20260319124527.4343-1-d.riley@proxmox.com> Content-Language: en-US From: David Riley In-Reply-To: <20260319124527.4343-1-d.riley@proxmox.com> Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 7bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1774607999111 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.717 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 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: JMVKV4Q5MSFWXOINSDHA2E72PCLOG4DV X-Message-ID-Hash: JMVKV4Q5MSFWXOINSDHA2E72PCLOG4DV 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 X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: superseded-by: https://lore.proxmox.com/pve-devel/20260327103801.104735-1-d.riley@proxmox.com/T/ On 3/19/26 1:50 PM, David Riley wrote: > 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}', > },