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 984631FF140 for ; Fri, 27 Mar 2026 11:38:29 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 167594B3A; Fri, 27 Mar 2026 11:38:52 +0100 (CET) From: David Riley To: pve-devel@lists.proxmox.com Subject: [PATCH pve-manager v2 1/1] fix: #4490 ui: backup job window: add search for virtual guest grid Date: Fri, 27 Mar 2026 11:38:01 +0100 Message-ID: <20260327103801.104735-2-d.riley@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260327103801.104735-1-d.riley@proxmox.com> References: <20260327103801.104735-1-d.riley@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1774607847317 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.768 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: IQJC7BTRDRRNQJDMJK4WL5CT7CE73IID X-Message-ID-Hash: IQJC7BTRDRRNQJDMJK4WL5CT7CE73IID 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: - Search field for filtering the virtual guest grid by Name or VMID. - Review toggle, when set the grid only shows selected virtual guests. - Selection count, counter below the grid showing the number of selected virtual guests. Signed-off-by: David Riley --- www/manager6/dc/Backup.js | 167 +++++++++++++++++++++++++++++++++++++- 1 file changed, 166 insertions(+), 1 deletion(-) diff --git a/www/manager6/dc/Backup.js b/www/manager6/dc/Backup.js index 956a7cdf..1990eb55 100644 --- a/www/manager6/dc/Backup.js +++ b/www/manager6/dc/Backup.js @@ -64,11 +64,22 @@ Ext.define('PVE.dc.BackupEdit', { let vmgrid = me.lookup('vmgrid'); let store = vmgrid.getStore(); + me.resetSearch(); + store.clearFilter(); store.filterBy(function (rec) { return !value || rec.get('node') === value; }); + if (value) { + let selModel = vmgrid.getSelectionModel(); + let selections = selModel.getSelection(); + let hiddenSelections = selections.filter((rec) => rec.get('node') !== value); + if (hiddenSelections.length > 0) { + selModel.deselect(hiddenSelections, true); + } + } + let mode = me.lookup('modeSelector').getValue(); if (mode === 'all') { vmgrid.selModel.selectAll(true); @@ -76,6 +87,8 @@ Ext.define('PVE.dc.BackupEdit', { if (mode === 'pool') { me.selectPoolMembers(); } + + me.updateSelectionCount(); }, storageChange: function (f, v) { @@ -114,6 +127,8 @@ Ext.define('PVE.dc.BackupEdit', { }, ]); vmgrid.selModel.selectAll(true); + + me.updateSelectionCount(); }, modeChange: function (f, value, oldValue) { @@ -121,7 +136,12 @@ Ext.define('PVE.dc.BackupEdit', { let vmgrid = me.lookup('vmgrid'); vmgrid.getStore().removeFilter('poolFilter'); - if (oldValue === 'all' && value !== 'all') { + me.resetSearch(); + + if ( + (oldValue === 'all' && value !== 'all') || + (oldValue === 'pool' && (value === 'include' || value === 'exclude')) + ) { vmgrid.getSelectionModel().deselectAll(true); } @@ -132,6 +152,8 @@ Ext.define('PVE.dc.BackupEdit', { if (value === 'pool') { me.selectPoolMembers(); } + + me.updateSelectionCount(); }, compressionChange: function (f, value, oldValue) { @@ -181,9 +203,99 @@ Ext.define('PVE.dc.BackupEdit', { return data; }, + searchFn: function (record) { + let me = this; + let searchQuery = me.searchValue; + + if (!searchQuery) { + return true; + } + + let name = (record.get('name') ?? '').toLowerCase(); + let vmid = (record.get('vmid') ?? '').toString(); + + return name.includes(searchQuery) || vmid.includes(searchQuery); + }, + + searchChange: function (_, value) { + let me = this; + let search = (value ?? '').toLowerCase(); + let vmgrid = me.lookup('vmgrid'); + let store = vmgrid.getStore(); + + me.searchValue = search; + + if (!search) { + store.removeFilter(me.searchFilter); + } else { + store.addFilter(me.searchFilter); + } + }, + + resetSearch: function () { + let me = this; + + me.searchValue = ''; + me.lookup('searchTextField').setValue(''); + me.lookup('vmgrid').getStore().removeFilter(me.searchFilter); + }, + + selectionChange: function (_, selected) { + let me = this; + let store = me.lookup('vmgrid').getStore(); + + if (store.getFilters().contains(me.reviewFilter)) { + store.removeFilter(me.reviewFilter); + store.addFilter(me.reviewFilter); + } + me.updateSelectionCount(selected); + }, + + updateSelectionCount: function (selected) { + let me = this; + let selection = selected || me.lookup('vmgrid').getSelectionModel().getSelection(); + let count = selection.length; + + let label = me.lookup('selectionCount'); + let text = Ext.String.format(gettext('Selected ({0})'), count); + label.setText(text); + }, + + reviewFn: function (record) { + let me = this; + let vmgrid = me.lookup('vmgrid'); + let selModel = vmgrid.getSelectionModel(); + return selModel.isSelected(record); + }, + + reviewModeChange: function (_, value) { + let me = this; + let store = me.lookup('vmgrid').getStore(); + + me.resetSearch(); + if (value) { + store.addFilter(me.reviewFilter); + } else { + store.removeFilter(me.reviewFilter); + } + }, + init: function (view) { let me = this; + me.searchValue = ''; + me.searchFilter = new Ext.util.Filter({ + id: 'search', + scope: me, + filterFn: me.searchFn, + }); + + me.reviewFilter = new Ext.util.Filter({ + id: 'review', + scope: me, + filterFn: me.reviewFn, + }); + if (view.isCreate) { me.lookup('modeSelector').setValue('include'); } else { @@ -250,6 +362,9 @@ Ext.define('PVE.dc.BackupEdit', { fieldLabel: gettext('Schedule'), allowBlank: false, name: 'schedule', + listeners: { + change: 'resetSearch', + }, }, { xtype: 'proxmoxKVComboBox', @@ -334,6 +449,7 @@ Ext.define('PVE.dc.BackupEdit', { xtype: 'vmselector', reference: 'vmgrid', height: 300, + padding: '0 0 2 0', name: 'vmid', disabled: true, allowBlank: false, @@ -341,6 +457,55 @@ Ext.define('PVE.dc.BackupEdit', { bind: { disabled: '{disableVMSelection}', }, + listeners: { + selectionChange: 'selectionChange', + }, + getValue: function () { + let me = this; + let selModel = me.getSelectionModel(); + let selection = selModel.getSelection(); + return selection.map((rec) => rec.get('vmid')).join(','); + }, + tbar: { + xtype: 'toolbar', + items: [ + { + xtype: 'proxmoxtextfield', + reference: 'searchTextField', + fieldLabel: gettext('Search'), + emptyText: 'Name, VMID', + flex: 1, + margin: '2 4 4 0', + labelWidth: 92, + enableKeyEvents: true, + submitValue: false, + listeners: { + buffer: 250, + change: 'searchChange', + }, + }, + ], + }, + bbar: { + xtype: 'toolbar', + padding: '4 0', + items: [ + { + xtype: 'tbtext', + reference: 'selectionCount', + text: Ext.String.format(gettext('Selected ({0})'), 0), + }, + '->', + { + xtype: 'proxmoxcheckbox', + boxLabel: gettext('Review'), + submitValue: false, + listeners: { + change: 'reviewModeChange', + }, + }, + ], + }, }, ], onGetValues: function (values) { -- 2.47.3