From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 684531FF09B for ; Mon, 31 Aug 2026 14:31:32 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 686C0216BB; Mon, 31 Aug 2026 14:30:11 +0200 (CEST) From: Dominik Csapak To: pve-devel@lists.proxmox.com Subject: [PATCH manager v4 06/12] ui: qemu: hardware: wrap in container Date: Mon, 31 Aug 2026 14:27:41 +0200 Message-ID: <20260831122953.2980232-7-d.csapak@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260831122953.2980232-1-d.csapak@proxmox.com> References: <20260831122953.2980232-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.612 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: JTJ77P46CISZUZDWROOCAVRVZC4UQJ5N X-Message-ID-Hash: JTJ77P46CISZUZDWROOCAVRVZC4UQJ5N X-MailFrom: d.csapak@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: this is preparation for showing multiple grids in the hardware view, so we move the actual PendingObjectGrid one level deeper. Introduces some helpers that redirect to the PendingObjectGrid to keep the diff a bit smaller. Explicitly create the rstore outside the PendingObjectGrid because we'll need it anyway later on. To keep the selection logic when coming from a storage content grid, the selection logic must now go through the store of the disk grid, not through the rstore. Signed-off-by: Dominik Csapak --- www/manager6/qemu/HardwareView.js | 346 ++++++++++++++++-------------- 1 file changed, 190 insertions(+), 156 deletions(-) diff --git a/www/manager6/qemu/HardwareView.js b/www/manager6/qemu/HardwareView.js index 0ffce89e..fd565e36 100644 --- a/www/manager6/qemu/HardwareView.js +++ b/www/manager6/qemu/HardwareView.js @@ -1,46 +1,23 @@ Ext.define('PVE.qemu.HardwareView', { - extend: 'Proxmox.grid.PendingObjectGrid', + extend: 'Ext.container.Container', alias: ['widget.PVE.qemu.HardwareView'], onlineHelp: 'qm_virtual_machines_settings', - renderKey: function (key, metaData, rec, rowIndex, colIndex, store) { - var me = this; - var rows = me.rows; - var rowdef = rows[key] || {}; - var iconCls = rowdef.iconCls; - var icon = ''; - var txt = rowdef.header || key; - - metaData.tdAttr = 'valign=middle'; - - if (rowdef.isOnStorageBus) { - let value = me.getObjectValue(key, '', false); - if (value === '') { - value = me.getObjectValue(key, '', true); - } - if (value.match(/vm-.*-cloudinit/)) { - iconCls = 'cloud'; - txt = rowdef.cloudheader; - } else if (PVE.Utils.diskIsCdrom(value)) { - metaData.tdCls = 'pve-itype-icon-cdrom'; - return rowdef.cdheader; - } - } + referenceHolder: true, - if (rowdef.tdCls) { - metaData.tdCls = rowdef.tdCls; - } else if (iconCls) { - icon = ""; - metaData.tdCls += ' pve-itype-fa'; - } + // helpers to redirect the methods to the hardwareGrid - // only return icons in grid but not remove dialog - if (rowIndex !== undefined) { - return icon + txt; - } else { - return txt; - } + getObjectValue: function () { + let me = this; + let hardwareGrid = me.lookup('hardwareGrid'); + return hardwareGrid.getObjectValue(...arguments); + }, + + reload: function () { + let me = this; + let hardwareGrid = me.lookup('hardwareGrid'); + return hardwareGrid.reload(); }, initComponent: function () { @@ -635,7 +612,7 @@ Ext.define('PVE.qemu.HardwareView', { baseurl, rows, renderKey: function () { - return me.renderKey(...arguments); + return me.lookup('hardwareGrid').renderKey(...arguments); }, reloadCallback: () => me.reload(), }); @@ -672,7 +649,8 @@ Ext.define('PVE.qemu.HardwareView', { }; let set_button_status = function () { - let selection_model = me.getSelectionModel(); + let hardwareGrid = me.lookup('hardwareGrid'); + let selection_model = hardwareGrid.getSelectionModel(); let rec = selection_model.getSelection()[0]; counts = {}; // en/disable hardwarebuttons @@ -722,7 +700,7 @@ Ext.define('PVE.qemu.HardwareView', { const row = rows[key]; const deleted = !!rec.data.delete; - const pending = deleted || me.hasPendingChanges(key); + const pending = deleted || hardwareGrid.hasPendingChanges(key); const isRunning = me.pveSelNode.data.running; const isCloudInit = isCloudInitKey(value); @@ -789,118 +767,174 @@ Ext.define('PVE.qemu.HardwareView', { }); }; - Ext.apply(me, { + me.rstore = Ext.create('Proxmox.data.ObjectStore', { + model: 'KeyValuePendingDelete', + readArray: true, url: `/api2/json/nodes/${nodename}/qemu/${vmid}/pending`, interval: 5000, - selModel: sm, - run_editor: run_editor, - tbar: [ + rows, + }); + + Ext.apply(me, { + items: [ { - text: gettext('Add'), - menu: new Ext.menu.Menu({ - cls: 'pve-add-hw-menu', - items: [ - { - text: gettext('Hard Disk'), - iconCls: 'fa fa-fw fa-hdd-o black', - disabled: !caps.vms['VM.Config.Disk'], - handler: editorFactory('HDEdit'), - }, - { - text: gettext('Import Hard Disk'), - iconCls: 'fa fa-fw fa-cloud-download', - disabled: !caps.vms['VM.Config.Disk'], - handler: editorFactory('HDEdit', { importDisk: true }), - }, - { - text: gettext('CD/DVD Drive'), - iconCls: 'pve-itype-icon-cdrom', - disabled: !caps.vms['VM.Config.CDROM'], - handler: editorFactory('CDEdit'), - }, - { - text: gettext('Network Device'), - itemId: 'addNet', - iconCls: 'fa fa-fw fa-exchange black', - disabled: !caps.vms['VM.Config.Network'], - handler: editorFactory('NetworkEdit'), - }, - efidisk_menuitem, - { - text: gettext('TPM State'), - itemId: 'addTpmState', - iconCls: 'fa fa-fw fa-hdd-o black', - disabled: !caps.vms['VM.Config.Disk'], - handler: editorFactory('TPMDiskEdit'), - }, - { - text: gettext('USB Device'), - itemId: 'addUsb', - iconCls: 'fa fa-fw fa-usb black', - disabled: - !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'], - handler: editorFactory('USBEdit'), - }, - { - text: gettext('PCI Device'), - itemId: 'addPci', - iconCls: 'pve-itype-icon-pci', - disabled: - !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'], - handler: editorFactory('PCIEdit'), - }, - { - text: gettext('Serial Port'), - itemId: 'addSerial', - iconCls: 'pve-itype-icon-serial', - disabled: !caps.vms['VM.Config.Options'], - handler: editorFactory('SerialEdit'), - }, - { - text: gettext('CloudInit Drive'), - itemId: 'addCloudinitDrive', - iconCls: 'fa fa-fw fa-cloud black', - disabled: - !caps.vms['VM.Config.CDROM'] || - !caps.vms['VM.Config.Cloudinit'], - handler: editorFactory('CIDriveEdit'), - }, - { - text: gettext('Audio Device'), - itemId: 'addAudio', - iconCls: 'fa fa-fw fa-volume-up black', - disabled: !caps.vms['VM.Config.HWType'], - handler: editorFactory('AudioEdit'), - }, - { - text: gettext('VirtIO RNG'), - itemId: 'addRng', - iconCls: 'pve-itype-icon-die', - disabled: - !caps.vms['VM.Config.HWType'] && !caps.mapping['Mapping.Use'], - handler: editorFactory('RNGEdit'), - }, - { - text: gettext('Virtiofs'), - itemId: 'addVirtiofs', - iconCls: 'fa fa-folder', - disabled: !caps.nodes['Sys.Console'], - handler: editorFactory('VirtiofsEdit'), - }, - ], - }), + xtype: 'proxmoxPendingObjectGrid', + reference: 'hardwareGrid', + border: false, + rstore: me.rstore, + interval: 5000, + selModel: sm, + run_editor: run_editor, + renderKey: function (key, metaData, rec, rowIndex, colIndex, store) { + var me = this; + var rows = me.rows; + var rowdef = rows[key] || {}; + var iconCls = rowdef.iconCls; + var icon = ''; + var txt = rowdef.header || key; + + metaData.tdAttr = 'valign=middle'; + + if (rowdef.isOnStorageBus) { + let value = me.getObjectValue(key, '', false); + if (value === '') { + value = me.getObjectValue(key, '', true); + } + if (value.match(/vm-.*-cloudinit/)) { + iconCls = 'cloud'; + txt = rowdef.cloudheader; + } else if (PVE.Utils.diskIsCdrom(value)) { + metaData.tdCls = 'pve-itype-icon-cdrom'; + return rowdef.cdheader; + } + } + + if (rowdef.tdCls) { + metaData.tdCls = rowdef.tdCls; + } else if (iconCls) { + icon = ""; + metaData.tdCls += ' pve-itype-fa'; + } + + // only return icons in grid but not remove dialog + if (rowIndex !== undefined) { + return icon + txt; + } else { + return txt; + } + }, + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + cls: 'pve-add-hw-menu', + items: [ + { + text: gettext('Hard Disk'), + iconCls: 'fa fa-fw fa-hdd-o black', + disabled: !caps.vms['VM.Config.Disk'], + handler: editorFactory('HDEdit'), + }, + { + text: gettext('Import Hard Disk'), + iconCls: 'fa fa-fw fa-cloud-download', + disabled: !caps.vms['VM.Config.Disk'], + handler: editorFactory('HDEdit', { importDisk: true }), + }, + { + text: gettext('CD/DVD Drive'), + iconCls: 'pve-itype-icon-cdrom', + disabled: !caps.vms['VM.Config.CDROM'], + handler: editorFactory('CDEdit'), + }, + { + text: gettext('Network Device'), + itemId: 'addNet', + iconCls: 'fa fa-fw fa-exchange black', + disabled: !caps.vms['VM.Config.Network'], + handler: editorFactory('NetworkEdit'), + }, + efidisk_menuitem, + { + text: gettext('TPM State'), + itemId: 'addTpmState', + iconCls: 'fa fa-fw fa-hdd-o black', + disabled: !caps.vms['VM.Config.Disk'], + handler: editorFactory('TPMDiskEdit'), + }, + { + text: gettext('USB Device'), + itemId: 'addUsb', + iconCls: 'fa fa-fw fa-usb black', + disabled: + !caps.nodes['Sys.Console'] && + !caps.mapping['Mapping.Use'], + handler: editorFactory('USBEdit'), + }, + { + text: gettext('PCI Device'), + itemId: 'addPci', + iconCls: 'pve-itype-icon-pci', + disabled: + !caps.nodes['Sys.Console'] && + !caps.mapping['Mapping.Use'], + handler: editorFactory('PCIEdit'), + }, + { + text: gettext('Serial Port'), + itemId: 'addSerial', + iconCls: 'pve-itype-icon-serial', + disabled: !caps.vms['VM.Config.Options'], + handler: editorFactory('SerialEdit'), + }, + { + text: gettext('CloudInit Drive'), + itemId: 'addCloudinitDrive', + iconCls: 'fa fa-fw fa-cloud black', + disabled: + !caps.vms['VM.Config.CDROM'] || + !caps.vms['VM.Config.Cloudinit'], + handler: editorFactory('CIDriveEdit'), + }, + { + text: gettext('Audio Device'), + itemId: 'addAudio', + iconCls: 'fa fa-fw fa-volume-up black', + disabled: !caps.vms['VM.Config.HWType'], + handler: editorFactory('AudioEdit'), + }, + { + text: gettext('VirtIO RNG'), + itemId: 'addRng', + iconCls: 'pve-itype-icon-die', + disabled: + !caps.vms['VM.Config.HWType'] && + !caps.mapping['Mapping.Use'], + handler: editorFactory('RNGEdit'), + }, + { + text: gettext('Virtiofs'), + itemId: 'addVirtiofs', + iconCls: 'fa fa-folder', + disabled: !caps.nodes['Sys.Console'], + handler: editorFactory('VirtiofsEdit'), + }, + ], + }), + }, + remove_btn, + edit_btn, + diskaction_btn, + revert_btn, + ], + rows: rows, + sorterFn: sorterFn, + listeners: { + itemdblclick: run_editor, + selectionchange: set_button_status, + }, }, - remove_btn, - edit_btn, - diskaction_btn, - revert_btn, ], - rows: rows, - sorterFn: sorterFn, - listeners: { - itemdblclick: run_editor, - selectionchange: set_button_status, - }, }); me.callParent(); @@ -908,22 +942,22 @@ Ext.define('PVE.qemu.HardwareView', { me.on('activate', me.rstore.startUpdate, me.rstore); me.on('destroy', me.rstore.stopUpdate, me.rstore); - me.mon(me.getStore(), 'datachanged', set_button_status, me); + me.mon(me.rstore, 'datachanged', set_button_status, me); - me.mon(me.getStore(), 'datachanged', function () { + let diskGrid = me.down('pveHardwareDiskGrid'); + let diskStore = diskGrid.getStore(); + me.mon(diskStore, 'datachanged', function () { if (!me.pendingVolid) { return; } let volid = me.pendingVolid; delete me.pendingVolid; - let index = me - .getStore() - .findBy( - (r) => - typeof r.data.value === 'string' && r.data.value.split(',', 1)[0] === volid, - ); - if (index >= 0) { - me.getSelectionModel().select(index); + let index = diskStore.findBy( + (r) => typeof r.data.file === 'string' && r.data.file === volid, + ); + + if (index) { + diskGrid.getSelectionModel().select(index); } }); }, -- 2.47.3