* [PATCH pve-manager] fix: #4490 ui: backup job window: add filter/search for virtual guest grid
@ 2026-03-19 12:45 David Riley
0 siblings, 0 replies; only message in thread
From: David Riley @ 2026-03-19 12:45 UTC (permalink / raw)
To: pve-devel; +Cc: David Riley
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
^ permalink raw reply [flat|nested] only message in thread
only message in thread, other threads:[~2026-03-19 12:51 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-03-19 12:45 [PATCH pve-manager] fix: #4490 ui: backup job window: add filter/search for virtual guest grid David Riley
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox