From: Dominik Csapak <d.csapak@proxmox.com>
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 [thread overview]
Message-ID: <20260515085349.1123127-11-d.csapak@proxmox.com> (raw)
In-Reply-To: <20260515085349.1123127-1-d.csapak@proxmox.com>
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 <d.csapak@proxmox.com>
---
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
next prev parent reply other threads:[~2026-05-15 8:55 UTC|newest]
Thread overview: 16+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-05-15 8:44 [PATCH manager v3 00/13] ui: split out disks and nics into grids Dominik Csapak
2026-05-15 8:44 ` [PATCH manager v3 01/13] ui: utils: factor out 'media=cdrom' check Dominik Csapak
2026-05-15 8:44 ` [PATCH manager v3 02/13] ui: factor out the guest key nic regex check Dominik Csapak
2026-05-15 8:44 ` [PATCH manager v3 03/13] ui: parser: qemu drive: allow '-' in key names Dominik Csapak
2026-05-15 8:44 ` [PATCH manager v3 04/13] ui: add pending grid Dominik Csapak
2026-05-15 8:44 ` [PATCH manager v3 05/13] ui: revert button: add parentXType and reloadCallback Dominik Csapak
2026-05-15 8:44 ` [PATCH manager v3 06/13] ui: button: add config remove button Dominik Csapak
2026-05-15 8:44 ` [PATCH manager v3 07/13] ui: qemu: hardware: wrap in container Dominik Csapak
2026-05-15 8:44 ` [PATCH manager v3 08/13] ui: qemu: introduce hardware disk grid Dominik Csapak
2026-05-15 8:44 ` [PATCH manager v3 09/13] ui: qemu: introduce hardware net grid Dominik Csapak
2026-05-15 8:44 ` Dominik Csapak [this message]
2026-05-15 8:44 ` [PATCH manager v3 11/13] ui: qemu: hardware view: separate nics into own grid Dominik Csapak
2026-05-15 8:44 ` [PATCH manager v3 12/13] ui: qemu: hardware view: inline edit/remove/revert button in general grid Dominik Csapak
2026-05-15 8:44 ` [PATCH manager v3 13/13] ui: qemu: hardware view: inline 'add efi' menuitem Dominik Csapak
2026-05-15 9:18 ` [PATCH manager v3 00/13] ui: split out disks and nics into grids Dominik Csapak
2026-05-15 9:20 ` Dominik Csapak
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=20260515085349.1123127-11-d.csapak@proxmox.com \
--to=d.csapak@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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.