From: David Riley <d.riley@proxmox.com>
To: pve-devel@lists.proxmox.com
Cc: David Riley <d.riley@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 [thread overview]
Message-ID: <20260319124527.4343-1-d.riley@proxmox.com> (raw)
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 <d.riley@proxmox.com>
---
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
reply other threads:[~2026-03-19 12:51 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260319124527.4343-1-d.riley@proxmox.com \
--to=d.riley@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.