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