public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: David Riley <d.riley@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: superseded: [PATCH pve-manager] fix: #4490 ui: backup job window: add filter/search for virtual guest grid
Date: Fri, 27 Mar 2026 11:40:48 +0100	[thread overview]
Message-ID: <7ef46393-e146-4830-92cc-35b0c2db6140@proxmox.com> (raw)
In-Reply-To: <20260319124527.4343-1-d.riley@proxmox.com>

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 <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}',
>                               },




      parent reply	other threads:[~2026-03-27 10:41 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-03-19 12:45 David Riley
2026-03-25  8:58 ` Thomas Lamprecht
2026-03-27 10:40 ` David Riley [this message]

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=7ef46393-e146-4830-92cc-35b0c2db6140@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 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