From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id BD9A81FF14C for ; Fri, 15 May 2026 10:55:42 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id A34F211F97; Fri, 15 May 2026 10:55:06 +0200 (CEST) From: Dominik Csapak To: pve-devel@lists.proxmox.com Subject: [PATCH manager v3 10/13] ui: qemu: hardware view: separate disks into own grid Date: Fri, 15 May 2026 10:44:28 +0200 Message-ID: <20260515085349.1123127-11-d.csapak@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260515085349.1123127-1-d.csapak@proxmox.com> References: <20260515085349.1123127-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.050 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment 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: YOJSALJOURXVCG5YN7O4KCYHZWFELXQM X-Message-ID-Hash: YOJSALJOURXVCG5YN7O4KCYHZWFELXQM 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: by using the newly added HardwareDiskGrid. For this to work, the selection model as well as the button logic has to be changed a bit. There are now two add/edit/remove/revert buttons and the one of the disk grid enable/disable themselves with the enableFn instead of being enabled in the set_button_status. The disk action menu is split out into separate buttons in the disk grid, so the enableFns of these take care of enabling/disabling now. This slightly duplicates some checks, but it's now defined with each button when its enabled and disabled and we can simplify some variables. While at it, change the order of the edit and remove button in the general grid to have it more aligned with all our other grids. Signed-off-by: Dominik Csapak --- www/manager6/qemu/HardwareView.js | 419 +++++++++++++++++++----------- 1 file changed, 266 insertions(+), 153 deletions(-) diff --git a/www/manager6/qemu/HardwareView.js b/www/manager6/qemu/HardwareView.js index b5b2600d..48c54a6c 100644 --- a/www/manager6/qemu/HardwareView.js +++ b/www/manager6/qemu/HardwareView.js @@ -6,6 +6,11 @@ Ext.define('PVE.qemu.HardwareView', { referenceHolder: true, + layout: { + type: 'vbox', + align: 'stretch', + }, + // helpers to redirect the methods to the hardwareGrid getObjectValue: function () { @@ -380,7 +385,23 @@ Ext.define('PVE.qemu.HardwareView', { let baseurl = `nodes/${nodename}/qemu/${vmid}/config`; - let sm = Ext.create('Ext.selection.RowModel', {}); + let sm = Ext.create('Ext.selection.Model', { + getSelection: () => { + let selection = []; + me.items.each((item) => { + if (item.isXType('grid')) { + let itemSelection = item.getSelection(); + if (itemSelection.length > 0) { + selection = itemSelection; + return false; + } + } + return true; + }); + + return selection; + }, + }); let run_editor = function () { let rec = sm.getSelection()[0]; @@ -465,74 +486,6 @@ Ext.define('PVE.qemu.HardwareView', { }); }; - let move_menuitem = new Ext.menu.Item({ - text: gettext('Move Storage'), - tooltip: gettext('Move disk to another storage'), - iconCls: 'fa fa-database', - selModel: sm, - handler: () => { - let rec = sm.getSelection()[0]; - if (!rec) { - return; - } - Ext.create('PVE.window.HDMove', { - autoShow: true, - disk: rec.data.key, - nodename: nodename, - vmid: vmid, - type: 'qemu', - listeners: { - destroy: () => me.reload(), - }, - }); - }, - }); - - let reassign_menuitem = new Ext.menu.Item({ - text: gettext('Reassign Owner'), - tooltip: gettext('Reassign disk to another VM'), - iconCls: 'fa fa-desktop', - selModel: sm, - handler: () => { - let rec = sm.getSelection()[0]; - if (!rec) { - return; - } - - Ext.create('PVE.window.GuestDiskReassign', { - autoShow: true, - disk: rec.data.key, - nodename: nodename, - vmid: vmid, - type: 'qemu', - listeners: { - destroy: () => me.reload(), - }, - }); - }, - }); - - let resize_menuitem = new Ext.menu.Item({ - text: gettext('Resize'), - iconCls: 'fa fa-plus', - selModel: sm, - handler: () => { - let rec = sm.getSelection()[0]; - if (!rec) { - return; - } - Ext.create('PVE.window.HDResize', { - autoShow: true, - disk: rec.data.key, - nodename: nodename, - vmid: vmid, - listeners: { - destroy: () => me.reload(), - }, - }); - }, - }); - const efiEnrollMsg = gettext( 'Enroll the UEFI 2023 certificates from Microsoft required for secure boot update.', @@ -555,35 +508,6 @@ Ext.define('PVE.qemu.HardwareView', { gettext( 'Otherwise, you will be prompted for the BitLocker recovery key on the next boot!', ); - let efiEnrollMenuItem = new Ext.menu.Item({ - text: gettext('Enroll Updated Certificates'), - iconCls: 'fa fa-refresh', - selModel: sm, - disabled: true, - hidden: true, - handler: () => { - Ext.Msg.show({ - title: gettext('Confirm'), - icon: Ext.Msg.QUESTION, - message: efiEnrollMsg, - buttons: Ext.Msg.YESNO, - callback: function (btn) { - if (btn !== 'yes') { - return; - } - runEfiEnroll(); - }, - }); - }, - }); - - let diskaction_btn = new Proxmox.button.Button({ - text: gettext('Disk Action'), - disabled: true, - menu: { - items: [move_menuitem, reassign_menuitem, resize_menuitem, efiEnrollMenuItem], - }, - }); let remove_btn = new PVE.button.ConfigRemove({ baseurl, @@ -627,15 +551,15 @@ Ext.define('PVE.qemu.HardwareView', { let set_button_status = function () { let hardwareGrid = me.lookup('hardwareGrid'); - let selection_model = hardwareGrid.getSelectionModel(); - let rec = selection_model.getSelection()[0]; + let rec = hardwareGrid.getSelection()[0]; counts = {}; // en/disable hardwarebuttons let hasCloudInit = false; - me.rstore.getData().items.forEach(function ({ id, data }) { + let items = me.rstore.getData(); + (items.getSource() ?? items).each(function ({ id, data }) { if (!hasCloudInit && (isCloudInitKey(data.value) || isCloudInitKey(data.pending))) { hasCloudInit = true; - return; + return true; } let match = id.match(/^([^\d]+)\d+$/); @@ -669,7 +593,6 @@ Ext.define('PVE.qemu.HardwareView', { if (!rec) { remove_btn.disable(); edit_btn.disable(); - diskaction_btn.disable(); revert_btn.disable(); return; } @@ -678,7 +601,6 @@ Ext.define('PVE.qemu.HardwareView', { const deleted = !!rec.data.delete; const pending = deleted || hardwareGrid.hasPendingChanges(key); - const isRunning = me.pveSelNode.data.running; const isCloudInit = isCloudInitKey(value); const isCDRom = value && PVE.Utils.diskIsCdrom(value); @@ -686,31 +608,12 @@ Ext.define('PVE.qemu.HardwareView', { const isUnusedDisk = key.match(/^unused\d+/); const isUsedDisk = !isUnusedDisk && row.isOnStorageBus && !isCDRom; const isDisk = isUnusedDisk || isUsedDisk; - const isEfi = key === 'efidisk0'; - const tpmMoveable = key === 'tpmstate0' && !isRunning; let cannotDelete = deleted || row.never_delete; cannotDelete ||= isCDRom && !cdromCap; - cannotDelete ||= isDisk && !diskCap; cannotDelete ||= isCloudInit && noVMConfigCloudinitPerm; remove_btn.setDisabled(cannotDelete); - remove_btn.setText( - isUsedDisk && !isCloudInit ? remove_btn.altText : remove_btn.defaultText, - ); - remove_btn.RESTMethod = isUnusedDisk || (isDisk && isRunning) ? 'POST' : 'PUT'; - - let suggestEfiEnroll = false; - if (isEfi) { - let drive = PVE.Parser.parsePropertyString(value, 'file'); - suggestEfiEnroll = - !pending && - PVE.Parser.parseBoolean(drive['pre-enrolled-keys'], false) && - drive['ms-cert'] !== '2023k'; - } - efiEnrollMenuItem.setDisabled(!suggestEfiEnroll); - efiEnrollMenuItem.setHidden(!isEfi); - edit_btn.setDisabled( deleted || !row.editor || @@ -719,12 +622,6 @@ Ext.define('PVE.qemu.HardwareView', { (isDisk && !diskCap), ); - diskaction_btn.setDisabled( - pending || !diskCap || isCloudInit || !(isDisk || isEfi || tpmMoveable), - ); - reassign_menuitem.setDisabled(pending || isEfi || tpmMoveable); - resize_menuitem.setDisabled(pending || !isUsedDisk); - revert_btn.setDisabled(!pending); }; @@ -744,23 +641,47 @@ Ext.define('PVE.qemu.HardwareView', { }); }; + let diskGridFilter = function (rec) { + let val = rec.get('value') ?? rec.get('pending'); + let key = rec.get('key'); + return ( + key.match(/^(ide|sata|scsi|virtio|unused|efidisk|tpmstate)\d+$/) && + !PVE.Utils.diskIsCdrom(val) && + !isCloudInitKey(val) + ); + }; + me.rstore = Ext.create('Proxmox.data.ObjectStore', { model: 'KeyValuePendingDelete', readArray: true, url: `/api2/json/nodes/${nodename}/qemu/${vmid}/pending`, interval: 5000, rows, + filters: [(rec) => !diskGridFilter(rec)], }); + let onSelectionChange = function (sm, selected) { + let selectionGrid = this; + if (selected.length) { + me.items.each((item) => { + if (item.isXType('grid') && item !== selectionGrid) { + item.getSelectionModel().deselectAll(); + } + }); + } + set_button_status(); + }; + Ext.apply(me, { items: [ { + title: gettext('General'), + iconCls: 'fa fa-desktop', 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; @@ -806,18 +727,6 @@ Ext.define('PVE.qemu.HardwareView', { 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', @@ -831,14 +740,6 @@ Ext.define('PVE.qemu.HardwareView', { 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', @@ -899,17 +800,229 @@ Ext.define('PVE.qemu.HardwareView', { ], }), }, - remove_btn, edit_btn, - diskaction_btn, + remove_btn, revert_btn, ], rows: rows, sorterFn: sorterFn, listeners: { itemdblclick: run_editor, - selectionchange: set_button_status, + selectionchange: onSelectionChange, + }, + }, + // spacer with bottom border + { + xtype: 'panel', + border: true, + height: 1, + margin: '20 0 0 0', + }, + { + xtype: 'pveHardwareDiskGrid', + rstore: me.rstore, + store: { + sorter: sorterFn, + }, + renderKey: function () { + let hardwareGrid = me.lookup('hardwareGrid'); + return hardwareGrid.renderKey(...arguments); }, + storeFilter: diskGridFilter, + listeners: { + itemdblclick: run_editor, + selectionchange: onSelectionChange, + }, + tbar: [ + { + text: gettext('Add'), + 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 }), + }, + 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'), + }, + ], + }, + }, + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + disabled: true, + enableFn: (rec) => + diskCap && !rec.data.deleted && !!rows[rec.data.key].editor, + handler: run_editor, + }, + { + xtype: 'pveConfigRemoveButton', + baseurl, + renderKey: function () { + return me.lookup('hardwareGrid').renderKey(...arguments); + }, + rows, + reloadCallback: () => me.reload(), + enableFn: function (rec) { + let button = this; + const isRunning = me.pveSelNode.data.running; + const isUnusedDisk = rec.data.key.match(/^unused\d+/); + const isDisk = rows[rec.data.key].isOnStorageBus; + button.setText(isUnusedDisk ? button.defaultText : button.altText); + button.RESTMethod = + isUnusedDisk || (isDisk && isRunning) ? 'POST' : 'PUT'; + return ( + !rec.data.deleted && !rows[rec.data.key].never_delete && diskCap + ); + }, + }, + { + xtype: 'pvePendingRevertButton', + apiurl: '/api2/extjs/' + baseurl, + parentXType: 'pvePendingGrid', + enableFn: (rec) => !!rec.data.deleted || !!rec.data.pending, + reloadCallback: () => me.reload(), + }, + '-', + { + xtype: 'proxmoxButton', + text: gettext('Move Storage'), + tooltip: gettext('Move disk to another storage'), + iconCls: 'fa fa-database', + enableFn: (rec) => + diskCap && + !rec.data.deleted && + !rec.data.pending && + (rec.data.key !== 'tpmstate0' || !me.pveSelNode.data.running), + handler: () => { + let rec = sm.getSelection()[0]; + if (!rec) { + return; + } + Ext.create('PVE.window.HDMove', { + autoShow: true, + disk: rec.data.key, + nodename: nodename, + vmid: vmid, + type: 'qemu', + listeners: { + destroy: () => me.reload(), + }, + }); + }, + }, + { + xtype: 'proxmoxButton', + text: gettext('Reassign Owner'), + tooltip: gettext('Reassign disk to another VM'), + iconCls: 'fa fa-desktop', + enableFn: function (rec) { + if (!diskCap || !!rec.data.deleted || !!rec.data.pending) { + return false; + } + if (rec.data.key === 'tpmstate0' && me.pveSelNode.data.running) { + return false; + } + if (rec.data.key === 'efidisk0') { + return false; + } + return true; + }, + handler: () => { + let rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + Ext.create('PVE.window.GuestDiskReassign', { + autoShow: true, + disk: rec.data.key, + nodename: nodename, + vmid: vmid, + type: 'qemu', + listeners: { + destroy: () => me.reload(), + }, + }); + }, + }, + { + xtype: 'proxmoxButton', + text: gettext('Resize'), + iconCls: 'fa fa-plus', + enableFn: function (rec) { + if (!diskCap || !!rec.data.deleted || !!rec.data.pending) { + return false; + } + return ( + !rec.data.key.match(/^unused\d+/) && + !!rows[rec.data.key].isOnStorageBus + ); + }, + handler: () => { + let rec = sm.getSelection()[0]; + if (!rec) { + return; + } + Ext.create('PVE.window.HDResize', { + autoShow: true, + disk: rec.data.key, + nodename: nodename, + vmid: vmid, + listeners: { + destroy: () => me.reload(), + }, + }); + }, + }, + { + text: gettext('Enroll Updated Certificates'), + iconCls: 'fa fa-refresh', + disabled: true, + enableFn: function (rec) { + if (!diskCap || !!rec.data.deleted || !!rec.data.pending) { + return false; + } + if (rec.data.key !== 'efidisk0') { + return false; + } + let drive = PVE.Parser.parsePropertyString(rec.data.value, 'file'); + return ( + PVE.Parser.parseBoolean(drive['pre-enrolled-keys'], false) && + drive['ms-cert'] !== '2023k' + ); + }, + handler: () => { + Ext.Msg.show({ + title: gettext('Confirm'), + icon: Ext.Msg.QUESTION, + message: efiEnrollMsg, + buttons: Ext.Msg.YESNO, + callback: function (btn) { + if (btn !== 'yes') { + return; + } + runEfiEnroll(); + }, + }); + }, + }, + ], }, ], }); -- 2.47.3