public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal