* [PATCH manager v4 01/12] ui: utils: factor out 'media=cdrom' check
2026-08-31 12:27 [PATCH manager v4 00/12] ui: split out disks and nics into grids Dominik Csapak
@ 2026-08-31 12:27 ` Dominik Csapak
2026-08-31 12:27 ` [PATCH manager v4 02/12] ui: factor out the guest key nic regex check Dominik Csapak
` (11 subsequent siblings)
12 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-08-31 12:27 UTC (permalink / raw)
To: pve-devel
this is a check we have all over the place and is prone to typos when
one tries to check it in a new place.
introduce a 'diskIsCdrom' check in Utils and use that in every place.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
www/manager6/Utils.js | 9 ++++++++-
www/manager6/qemu/BootOrderEdit.js | 6 +++---
www/manager6/qemu/CreateWizard.js | 4 ++--
www/manager6/qemu/HardwareView.js | 6 +++---
www/manager6/window/GuestImport.js | 2 +-
5 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 8b99371d..4eaec7b4 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -2014,6 +2014,13 @@ Ext.define('PVE.Utils', {
return true;
},
+ diskIsCdrom: function (value) {
+ if (!value) {
+ return false;
+ }
+ return !!value.toString().match(/media=cdrom/);
+ },
+
sortByPreviousUsage: function (vmconfig, nodename) {
let controllerList = ['ide', 'virtio', 'scsi', 'sata'];
let usedControllers = {};
@@ -2024,7 +2031,7 @@ Ext.define('PVE.Utils', {
for (const property of Object.keys(vmconfig)) {
if (
property.match(PVE.Utils.bus_match) &&
- !vmconfig[property].match(/media=cdrom/)
+ !PVE.Utils.diskIsCdrom(vmconfig[property])
) {
const foundController = property.match(PVE.Utils.bus_match)[1];
usedControllers[foundController]++;
diff --git a/www/manager6/qemu/BootOrderEdit.js b/www/manager6/qemu/BootOrderEdit.js
index 6239aeca..22112f8d 100644
--- a/www/manager6/qemu/BootOrderEdit.js
+++ b/www/manager6/qemu/BootOrderEdit.js
@@ -60,7 +60,7 @@ Ext.define('PVE.qemu.BootOrderPanel', {
},
},
- isCloudinit: (v) => v.match(/media=cdrom/) && v.match(/[:/]vm-\d+-cloudinit/),
+ isCloudinit: (v) => PVE.Utils.diskIsCdrom(v) && v.match(/[:/]vm-\d+-cloudinit/),
isDisk: function (value) {
return PVE.Utils.bus_match.test(value);
@@ -106,7 +106,7 @@ Ext.define('PVE.qemu.BootOrderPanel', {
Ext.Object.each(me.vmconfig, function (key, value) {
if (
me.isDisk(key) &&
- value.match(/media=cdrom/) &&
+ PVE.Utils.diskIsCdrom(value) &&
!me.isCloudinit(value)
) {
list.push(key);
@@ -205,7 +205,7 @@ Ext.define('PVE.qemu.BootOrderPanel', {
iconCls;
if (value.match(/^net\d+$/)) {
iconCls = 'exchange';
- } else if (desc.match(/media=cdrom/)) {
+ } else if (PVE.Utils.diskIsCdrom(desc)) {
metaData.tdCls = 'pve-itype-icon-cdrom';
} else {
iconCls = 'hdd-o';
diff --git a/www/manager6/qemu/CreateWizard.js b/www/manager6/qemu/CreateWizard.js
index 5eb784c4..2ee209da 100644
--- a/www/manager6/qemu/CreateWizard.js
+++ b/www/manager6/qemu/CreateWizard.js
@@ -33,14 +33,14 @@ Ext.define('PVE.qemu.CreateWizard', {
// cannot know which one is a bootable iso and hardcodes the known values (ide0/2, net0)
calculateBootOrder: function (values) {
// user selected windows + second cdrom
- if (values.ide0 && values.ide0.match(/media=cdrom/)) {
+ if (values.ide0 && PVE.Utils.diskIsCdrom(values.ide0)) {
let disk;
PVE.Utils.forEachBus(['ide', 'scsi', 'virtio', 'sata'], (type, id) => {
let confId = type + id;
if (!values[confId]) {
return undefined;
}
- if (values[confId].match(/media=cdrom/)) {
+ if (PVE.Utils.diskIsCdrom(values[confId])) {
return undefined;
}
disk = confId;
diff --git a/www/manager6/qemu/HardwareView.js b/www/manager6/qemu/HardwareView.js
index f7f4b46f..d1e5d6c1 100644
--- a/www/manager6/qemu/HardwareView.js
+++ b/www/manager6/qemu/HardwareView.js
@@ -22,7 +22,7 @@ Ext.define('PVE.qemu.HardwareView', {
if (value.match(/vm-.*-cloudinit/)) {
iconCls = 'cloud';
txt = rowdef.cloudheader;
- } else if (value.match(/media=cdrom/)) {
+ } else if (PVE.Utils.diskIsCdrom(value)) {
metaData.tdCls = 'pve-itype-icon-cdrom';
return rowdef.cdheader;
}
@@ -440,7 +440,7 @@ Ext.define('PVE.qemu.HardwareView', {
let value = me.getObjectValue(rec.data.key, '', true);
if (isCloudInitKey(value)) {
return;
- } else if (value.match(/media=cdrom/)) {
+ } else if (PVE.Utils.diskIsCdrom(value)) {
editor = 'PVE.qemu.CDEdit';
} else if (!diskCap) {
return;
@@ -780,7 +780,7 @@ Ext.define('PVE.qemu.HardwareView', {
const isRunning = me.pveSelNode.data.running;
const isCloudInit = isCloudInitKey(value);
- const isCDRom = value && !!value.toString().match(/media=cdrom/);
+ const isCDRom = value && PVE.Utils.diskIsCdrom(value);
const isUnusedDisk = key.match(/^unused\d+/);
const isUsedDisk = !isUnusedDisk && row.isOnStorageBus && !isCDRom;
diff --git a/www/manager6/window/GuestImport.js b/www/manager6/window/GuestImport.js
index 1013fe14..fd05d558 100644
--- a/www/manager6/window/GuestImport.js
+++ b/www/manager6/window/GuestImport.js
@@ -1075,7 +1075,7 @@ Ext.define('PVE.window.GuestImport', {
let cdroms = [];
for (const [id, value] of Object.entries(me.vmConfig)) {
- if (!Ext.isString(value) || !value.match(/media=cdrom/)) {
+ if (!Ext.isString(value) || !PVE.Utils.diskIsCdrom(value)) {
continue;
}
cdroms.push({
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH manager v4 02/12] ui: factor out the guest key nic regex check
2026-08-31 12:27 [PATCH manager v4 00/12] ui: split out disks and nics into grids Dominik Csapak
2026-08-31 12:27 ` [PATCH manager v4 01/12] ui: utils: factor out 'media=cdrom' check Dominik Csapak
@ 2026-08-31 12:27 ` Dominik Csapak
2026-08-31 12:27 ` [PATCH manager v4 03/12] ui: add pending grid Dominik Csapak
` (10 subsequent siblings)
12 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-08-31 12:27 UTC (permalink / raw)
To: pve-devel
It's better to have these regexes in one place, otherwise they're easily
typoed.
I noticed that some of the checks were not anchored at the end, but all
valid values these can have, should match the new check as well.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
www/manager6/Utils.js | 7 +++++++
www/manager6/lxc/Network.js | 4 ++--
www/manager6/qemu/BootOrderEdit.js | 6 +++---
3 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 4eaec7b4..e9524b11 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -2021,6 +2021,13 @@ Ext.define('PVE.Utils', {
return !!value.toString().match(/media=cdrom/);
},
+ keyIsNic: function (value) {
+ if (!value) {
+ return false;
+ }
+ return !!value.toString().match(/^net\d+$/);
+ },
+
sortByPreviousUsage: function (vmconfig, nodename) {
let controllerList = ['ide', 'virtio', 'scsi', 'sata'];
let usedControllers = {};
diff --git a/www/manager6/lxc/Network.js b/www/manager6/lxc/Network.js
index e56d47c0..b2737471 100644
--- a/www/manager6/lxc/Network.js
+++ b/www/manager6/lxc/Network.js
@@ -89,7 +89,7 @@ Ext.define('PVE.lxc.NetworkInputPanel', {
value: cdata.name,
validator: function (value) {
for (const [key, netRaw] of Object.entries(me.dataCache)) {
- if (!key.match(/^net\d+/) || key === me.ifname) {
+ if (!PVE.Utils.keyIsNic(key) || key === me.ifname) {
continue;
}
let net = PVE.Parser.parseLxcNetwork(netRaw);
@@ -385,7 +385,7 @@ Ext.define(
let records = [];
me.dataCache = confResponse.result.data || {};
for (const [key, value] of Object.entries(confResponse.result.data)) {
- if (!key.match(/^net\d+/)) {
+ if (!PVE.Utils.keyIsNic(key)) {
continue;
}
let config = PVE.Parser.parseLxcNetwork(value);
diff --git a/www/manager6/qemu/BootOrderEdit.js b/www/manager6/qemu/BootOrderEdit.js
index 22112f8d..1548bf7d 100644
--- a/www/manager6/qemu/BootOrderEdit.js
+++ b/www/manager6/qemu/BootOrderEdit.js
@@ -69,7 +69,7 @@ Ext.define('PVE.qemu.BootOrderPanel', {
isBootdev: function (dev, value) {
return (
(this.isDisk(dev) && !this.isCloudinit(value)) ||
- /^net\d+/.test(dev) ||
+ PVE.Utils.keyIsNic(dev) ||
/^hostpci\d+/.test(dev) ||
(/^usb\d+/.test(dev) && !/spice/.test(value))
);
@@ -114,7 +114,7 @@ Ext.define('PVE.qemu.BootOrderPanel', {
});
} else if (orderList[i] === 'n') {
Ext.Object.each(me.vmconfig, function (key, value) {
- if (/^net\d+/.test(key)) {
+ if (PVE.Utils.keyIsNic(key)) {
list.push(key);
}
});
@@ -203,7 +203,7 @@ Ext.define('PVE.qemu.BootOrderPanel', {
let icon = '',
iconCls;
- if (value.match(/^net\d+$/)) {
+ if (PVE.Utils.keyIsNic(value)) {
iconCls = 'exchange';
} else if (PVE.Utils.diskIsCdrom(desc)) {
metaData.tdCls = 'pve-itype-icon-cdrom';
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH manager v4 03/12] ui: add pending grid
2026-08-31 12:27 [PATCH manager v4 00/12] ui: split out disks and nics into grids Dominik Csapak
2026-08-31 12:27 ` [PATCH manager v4 01/12] ui: utils: factor out 'media=cdrom' check Dominik Csapak
2026-08-31 12:27 ` [PATCH manager v4 02/12] ui: factor out the guest key nic regex check Dominik Csapak
@ 2026-08-31 12:27 ` Dominik Csapak
2026-08-31 12:27 ` [PATCH manager v4 04/12] ui: revert button: add parentXType and reloadCallback Dominik Csapak
` (9 subsequent siblings)
12 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-08-31 12:27 UTC (permalink / raw)
To: pve-devel
A new grid type that gets its data from a PendingObjectGrid and displays
a parsed property string in various columns. Each column shows its own
pending changes so that this can be seen more easily.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/grid/PendingGrid.js | 134 +++++++++++++++++++++++++++++++
2 files changed, 135 insertions(+)
create mode 100644 www/manager6/grid/PendingGrid.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index d2ea786b..ff33e011 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -101,6 +101,7 @@ JSSRC= \
grid/FirewallAliases.js \
grid/FirewallOptions.js \
grid/FirewallRules.js \
+ grid/PendingGrid.js \
grid/PoolMembers.js \
grid/Replication.js \
grid/ResourceGrid.js \
diff --git a/www/manager6/grid/PendingGrid.js b/www/manager6/grid/PendingGrid.js
new file mode 100644
index 00000000..e2d7b6c3
--- /dev/null
+++ b/www/manager6/grid/PendingGrid.js
@@ -0,0 +1,134 @@
+/* It's main use is to be a 'sub grid' for a 'PendingObjectGrid, for qemu/HardwareView
+ * and lxc/Resources.
+ */
+Ext.define('PVE.grid.PendingGrid', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.pvePendingGrid',
+
+ /// the original store of the PendingObjectGrid, used for retrieving data
+ rstore: undefined,
+
+ /// the filter function to filter only relevant data from rstore
+ storeFilter: function (_rec) {
+ return true;
+ },
+
+ /// the parser for the data, should return an object
+ recordParser: function (key, value) {
+ let parsed = PVE.Parser.parsePropertyString(value);
+ return {
+ key,
+ ...parsed,
+ };
+ },
+
+ pendingRenderer: function (originalRenderer) {
+ originalRenderer ??= Ext.htmlEncode;
+ return function (val, mD, rec, rowIdx, colIdx, store, view) {
+ // add a ''pending parameter'
+ let txt = originalRenderer(val, mD, rec, rowIdx, colIdx, store, view, false);
+ let pendingTxt;
+
+ let pending = rec.get('pending');
+ if (pending) {
+ let dataIndex = view.up().getColumns()[colIdx].dataIndex;
+ let value = pending[dataIndex];
+ pendingTxt = originalRenderer(value, mD, rec, rowIdx, colIdx, store, view, true);
+ }
+
+ if (pendingTxt && txt && pendingTxt !== txt) {
+ txt += '<br />';
+ txt += `<span style="color: darkorange">${pendingTxt}</span>`;
+ }
+ if (pending && pendingTxt && (!txt || !rec.data.raw)) {
+ txt = `<span style="color: orange">${pendingTxt}</span>`;
+ }
+ if (pending && !pendingTxt && txt) {
+ txt = `<div style="color: darkorange; text-decoration: line-through;">${txt}</div>`;
+ }
+ if (rec.data.deleted && txt) {
+ txt = `<div style="color: darkorange; text-decoration: line-through;">${txt}</div>`;
+ }
+
+ return txt;
+ };
+ },
+
+ updateData: function () {
+ let me = this;
+ let records = [];
+ let data = me.rstore.getData();
+ (data.getSource() ?? data).each(function (rec) {
+ if (!me.storeFilter(rec)) {
+ return;
+ }
+
+ let key = rec.get('key');
+ let value = rec.get('value');
+ let deleted = rec.get('delete');
+
+ let parsed = me.recordParser(key, value);
+ if (parsed) {
+ parsed.raw = value;
+ }
+
+ let pending = rec.get('pending');
+ let parsedPending;
+ if (pending) {
+ parsedPending = me.recordParser(key, pending);
+ parsedPending.id = key;
+ parsedPending.raw = pending;
+ }
+
+ records.push({
+ ...parsed,
+ id: key,
+ key,
+ pending: parsedPending,
+ deleted,
+ });
+ });
+
+ me.getStore().setData(records);
+ },
+
+ store: {},
+
+ columns: [],
+
+ initComponent: function () {
+ let me = this;
+
+ if (!me.rstore) {
+ throw new 'no rstore defined'();
+ }
+
+ let columns = [];
+
+ // overwrite renderer to be pending
+ //
+ // CAUTION: don't modify the original column objects, rather copy them
+ for (const column of me.columns ?? []) {
+ let originalRenderer = column.renderer;
+ // might be a string (named scope)
+ if (Ext.isString(originalRenderer)) {
+ let renderString = originalRenderer.toString();
+ originalRenderer = function () {
+ return Ext.callback(renderString, me.scope, arguments, 0, me);
+ };
+ }
+
+ let renderer = me.pendingRenderer(originalRenderer);
+ columns.push({
+ ...column,
+ renderer,
+ });
+ }
+
+ me.columns = columns;
+
+ me.callParent();
+
+ me.mon(me.rstore, 'refresh', me.updateData, me);
+ },
+});
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH manager v4 04/12] ui: revert button: add parentXType and reloadCallback
2026-08-31 12:27 [PATCH manager v4 00/12] ui: split out disks and nics into grids Dominik Csapak
` (2 preceding siblings ...)
2026-08-31 12:27 ` [PATCH manager v4 03/12] ui: add pending grid Dominik Csapak
@ 2026-08-31 12:27 ` Dominik Csapak
2026-08-31 12:27 ` [PATCH manager v4 05/12] ui: button: add config remove button Dominik Csapak
` (8 subsequent siblings)
12 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-08-31 12:27 UTC (permalink / raw)
To: pve-devel
makes it useful in more situations when the direct parent is not a
PendingObjectGrid but e.g. a pvePendingGrid.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
www/manager6/button/Revert.js | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/www/manager6/button/Revert.js b/www/manager6/button/Revert.js
index 14b13f5b..d8fcca2c 100644
--- a/www/manager6/button/Revert.js
+++ b/www/manager6/button/Revert.js
@@ -7,11 +7,13 @@ Ext.define('PVE.button.PendingRevert', {
config: {
pendingGrid: null,
apiurl: undefined,
+ parentXType: 'proxmoxPendingObjectGrid',
+ reloadCallback: undefined,
},
handler: function () {
if (!this.pendingGrid) {
- this.pendingGrid = this.up('proxmoxPendingObjectGrid');
+ this.pendingGrid = this.up(this.parentXType);
if (!this.pendingGrid) {
throw 'revert button requires a pendingGrid';
}
@@ -23,7 +25,7 @@ Ext.define('PVE.button.PendingRevert', {
return;
}
- let rowdef = view.rows[rec.data.key] || {};
+ let rowdef = view.rows?.[rec.data.key] || {};
let keys = rowdef.multiKey || [rec.data.key];
Proxmox.Utils.API2Request({
@@ -34,7 +36,7 @@ Ext.define('PVE.button.PendingRevert', {
params: {
revert: keys.join(','),
},
- callback: () => view.reload(),
+ callback: () => (this.reloadCallback ? this.reloadCallback() : view.reload()),
failure: (response) => Ext.Msg.alert('Error', response.htmlStatus),
});
},
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH manager v4 05/12] ui: button: add config remove button
2026-08-31 12:27 [PATCH manager v4 00/12] ui: split out disks and nics into grids Dominik Csapak
` (3 preceding siblings ...)
2026-08-31 12:27 ` [PATCH manager v4 04/12] ui: revert button: add parentXType and reloadCallback Dominik Csapak
@ 2026-08-31 12:27 ` Dominik Csapak
2026-08-31 12:27 ` [PATCH manager v4 06/12] ui: qemu: hardware: wrap in container Dominik Csapak
` (7 subsequent siblings)
12 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-08-31 12:27 UTC (permalink / raw)
To: pve-devel
this is a special remove button that has two main features:
* removes properties from configs via PUT/POST instead of DELETE
* can show an alternative text if set
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/button/ConfigRemove.js | 74 +++++++++++++++++++++++++++++
www/manager6/qemu/HardwareView.js | 66 +++----------------------
3 files changed, 81 insertions(+), 60 deletions(-)
create mode 100644 www/manager6/button/ConfigRemove.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index ff33e011..79ec91bd 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -13,6 +13,7 @@ JSSRC= \
button/ConsoleButton.js \
button/Revert.js \
button/Split.js \
+ button/ConfigRemove.js \
controller/StorageEdit.js \
data/PermPathStore.js \
data/ResourceStore.js \
diff --git a/www/manager6/button/ConfigRemove.js b/www/manager6/button/ConfigRemove.js
new file mode 100644
index 00000000..ab195dfd
--- /dev/null
+++ b/www/manager6/button/ConfigRemove.js
@@ -0,0 +1,74 @@
+Ext.define('PVE.button.ConfigRemove', {
+ extend: 'Proxmox.button.Button',
+ alias: 'widget.pveConfigRemoveButton',
+
+ text: gettext('Remove'),
+ defaultText: gettext('Remove'),
+ altText: gettext('Detach'),
+ disabled: true,
+ dangerous: true,
+ RESTMethod: 'PUT',
+
+ // overwrite for key rendering
+ renderKey: Ext.htmlEncode,
+ // overwrite for extra message
+ rows: {},
+
+ reloadCallback: Ext.identityFn,
+
+ confirmMsg: function (rec) {
+ let me = this;
+ let warn = gettext('Are you sure you want to remove entry {0}');
+ if (this.text === this.altText) {
+ warn = gettext('Are you sure you want to detach entry {0}');
+ }
+ let rendered = me.renderKey(rec.data.key, {}, rec);
+ let msg = Ext.String.format(warn, `'${rendered}'`);
+
+ if (me.rows[rec.data.key].del_extra_msg) {
+ msg += '<br>' + me.rows[rec.data.key].del_extra_msg;
+ }
+ return msg;
+ },
+ handler: function (btn, e, rec) {
+ let me = this;
+ let params = { delete: rec.data.key };
+ if (btn.RESTMethod === 'POST') {
+ params.background_delay = 5;
+ }
+ Proxmox.Utils.API2Request({
+ url: '/api2/extjs/' + me.baseurl,
+ waitMsgTarget: me,
+ method: btn.RESTMethod,
+ params: params,
+ callback: () => me.reloadCallback(),
+ failure: (response) => Ext.Msg.alert('Error', response.htmlStatus),
+ success: function (response, options) {
+ if (btn.RESTMethod === 'POST' && response.result.data !== null) {
+ Ext.create('Proxmox.window.TaskProgress', {
+ autoShow: true,
+ upid: response.result.data,
+ listeners: {
+ destroy: () => me.reloadCallback(),
+ },
+ });
+ }
+ },
+ });
+ },
+ listeners: {
+ render: function (btn) {
+ // hack: calculate the max button width on first display to prevent the whole
+ // toolbar to move when we switch between the "Remove" and "Detach" labels
+ var def = btn.getSize().width;
+
+ btn.setText(btn.altText);
+ var alt = btn.getSize().width;
+
+ btn.setText(btn.defaultText);
+
+ var optimal = alt > def ? alt : def;
+ btn.setSize({ width: optimal });
+ },
+ },
+});
diff --git a/www/manager6/qemu/HardwareView.js b/www/manager6/qemu/HardwareView.js
index d1e5d6c1..0ffce89e 100644
--- a/www/manager6/qemu/HardwareView.js
+++ b/www/manager6/qemu/HardwareView.js
@@ -631,67 +631,13 @@ Ext.define('PVE.qemu.HardwareView', {
},
});
- let remove_btn = new Proxmox.button.Button({
- text: gettext('Remove'),
- defaultText: gettext('Remove'),
- altText: gettext('Detach'),
- selModel: sm,
- disabled: true,
- dangerous: true,
- RESTMethod: 'PUT',
- confirmMsg: function (rec) {
- let warn = gettext('Are you sure you want to remove entry {0}');
- if (this.text === this.altText) {
- warn = gettext('Are you sure you want to detach entry {0}');
- }
- let rendered = me.renderKey(rec.data.key, {}, rec);
- let msg = Ext.String.format(warn, `'${rendered}'`);
-
- if (rows[rec.data.key].del_extra_msg) {
- msg += '<br>' + rows[rec.data.key].del_extra_msg;
- }
- return msg;
- },
- handler: function (btn, e, rec) {
- let params = { delete: rec.data.key };
- if (btn.RESTMethod === 'POST') {
- params.background_delay = 5;
- }
- Proxmox.Utils.API2Request({
- url: '/api2/extjs/' + baseurl,
- waitMsgTarget: me,
- method: btn.RESTMethod,
- params: params,
- callback: () => me.reload(),
- failure: (response) => Ext.Msg.alert('Error', response.htmlStatus),
- success: function (response, options) {
- if (btn.RESTMethod === 'POST' && response.result.data !== null) {
- Ext.create('Proxmox.window.TaskProgress', {
- autoShow: true,
- upid: response.result.data,
- listeners: {
- destroy: () => me.reload(),
- },
- });
- }
- },
- });
- },
- listeners: {
- render: function (btn) {
- // hack: calculate the max button width on first display to prevent the whole
- // toolbar to move when we switch between the "Remove" and "Detach" labels
- var def = btn.getSize().width;
-
- btn.setText(btn.altText);
- var alt = btn.getSize().width;
-
- btn.setText(btn.defaultText);
-
- var optimal = alt > def ? alt : def;
- btn.setSize({ width: optimal });
- },
+ let remove_btn = new PVE.button.ConfigRemove({
+ baseurl,
+ rows,
+ renderKey: function () {
+ return me.renderKey(...arguments);
},
+ reloadCallback: () => me.reload(),
});
let revert_btn = new PVE.button.PendingRevert({
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH manager v4 06/12] ui: qemu: hardware: wrap in container
2026-08-31 12:27 [PATCH manager v4 00/12] ui: split out disks and nics into grids Dominik Csapak
` (4 preceding siblings ...)
2026-08-31 12:27 ` [PATCH manager v4 05/12] ui: button: add config remove button Dominik Csapak
@ 2026-08-31 12:27 ` Dominik Csapak
2026-08-31 12:27 ` [PATCH manager v4 07/12] ui: qemu: introduce hardware disk grid Dominik Csapak
` (6 subsequent siblings)
12 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-08-31 12:27 UTC (permalink / raw)
To: pve-devel
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 <d.csapak@proxmox.com>
---
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 = "<i class='pve-grid-fa fa fa-fw fa-" + iconCls + "'></i>";
- 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 = "<i class='pve-grid-fa fa fa-fw fa-" + iconCls + "'></i>";
+ 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
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH manager v4 07/12] ui: qemu: introduce hardware disk grid
2026-08-31 12:27 [PATCH manager v4 00/12] ui: split out disks and nics into grids Dominik Csapak
` (5 preceding siblings ...)
2026-08-31 12:27 ` [PATCH manager v4 06/12] ui: qemu: hardware: wrap in container Dominik Csapak
@ 2026-08-31 12:27 ` Dominik Csapak
2026-08-31 12:27 ` [PATCH manager v4 08/12] ui: qemu: introduce hardware net grid Dominik Csapak
` (5 subsequent siblings)
12 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-08-31 12:27 UTC (permalink / raw)
To: pve-devel
intended to be used inside the hardware view. not used yet.
The toolbar buttons will be added directly in the hardware view since
it requires accessing some data from there.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/Utils.js | 7 +
www/manager6/qemu/HardwareDiskGrid.js | 213 ++++++++++++++++++++++++++
3 files changed, 221 insertions(+)
create mode 100644 www/manager6/qemu/HardwareDiskGrid.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 79ec91bd..01715aa0 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -270,6 +270,7 @@ JSSRC= \
qemu/HDTPM.js \
qemu/HDMove.js \
qemu/HDResize.js \
+ qemu/HardwareDiskGrid.js \
qemu/HardwareView.js \
qemu/IPConfigEdit.js \
qemu/KeyboardEdit.js \
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index e9524b11..f48748db 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -2168,6 +2168,13 @@ Ext.define('PVE.Utils', {
);
return Ext.htmlEncode(description);
},
+
+ renderBooleanProperty: function (value) {
+ if (Ext.isString(value)) {
+ value = PVE.Parser.parseBoolean(value);
+ }
+ return Proxmox.Utils.format_boolean(value);
+ },
},
singleton: true,
diff --git a/www/manager6/qemu/HardwareDiskGrid.js b/www/manager6/qemu/HardwareDiskGrid.js
new file mode 100644
index 00000000..f64a38e8
--- /dev/null
+++ b/www/manager6/qemu/HardwareDiskGrid.js
@@ -0,0 +1,213 @@
+Ext.define('PVE.qemu.HardwareDiskGrid', {
+ extend: 'PVE.grid.PendingGrid',
+ alias: 'widget.pveHardwareDiskGrid',
+
+ title: gettext('Hard Disks'),
+
+ stateful: true,
+ stateId: 'pve-qemu-hardware-disk',
+
+ border: false,
+ expandable: false,
+
+ minHeight: 100,
+ emptyText: gettext('No hard disks configured'),
+
+ recordParser: PVE.Parser.parseQemuDrive,
+ // override to render the keys properly
+ renderKey: Ext.htmlEncode,
+
+ columns: [
+ {
+ header: gettext('Bus/Device'),
+ dataIndex: 'id',
+ width: 150,
+ renderer: 'renderKey',
+ },
+ {
+ header: gettext('Volume/File'),
+ dataIndex: 'file',
+ flex: 1,
+ minWidth: 200,
+ },
+ {
+ header: gettext('Size'),
+ dataIndex: 'size',
+ width: 100,
+ renderer: (value) => {
+ if (value === undefined || value === null) {
+ return '';
+ }
+
+ let size = value;
+ if (!size.toString().slice(-1).match(/[0-9]/)) {
+ // IEC unit is implicit in config, but parser expects it as 'i' suffix
+ size = `${value}i`;
+ }
+
+ return Proxmox.Utils.autoscale_size_unit(size);
+ },
+ },
+ {
+ header: gettext('Secure Boot (EFI Disk only)'),
+ dataIndex: 'pre-enrolled-keys',
+ renderer: (v, _md, rec, rI, cI, store, view, pending) => {
+ let iface = pending ? rec.data.pending?.interface : rec.data.interface;
+ if (iface !== 'efidisk') {
+ return '';
+ }
+ if (v) {
+ let msCerts = pending ? rec.data.pending?.['ms-cert'] : rec.data['ms-cert'];
+ if (msCerts) {
+ return `${gettext('Yes')} (MS Certificate: ${msCerts})`;
+ } else {
+ return gettext('Yes');
+ }
+ } else {
+ return gettext('No');
+ }
+ },
+ },
+ {
+ header: gettext('Cache'),
+ dataIndex: 'cache',
+ width: 140,
+ renderer: (v) => v ?? `${Proxmox.Utils.defaultText} (${gettext('No cache')})`,
+ },
+ {
+ header: gettext('Discard'),
+ dataIndex: 'discard',
+ width: 80,
+ renderer: PVE.Utils.renderBooleanProperty,
+ },
+ {
+ header: gettext('IO thread'),
+ dataIndex: 'iothread',
+ width: 100,
+ renderer: PVE.Utils.renderBooleanProperty,
+ },
+ {
+ header: gettext('SSD emulation'),
+ dataIndex: 'ssd',
+ width: 120,
+ renderer: PVE.Utils.renderBooleanProperty,
+ },
+ {
+ header: gettext('Backup'),
+ dataIndex: 'backup',
+ width: 80,
+ renderer: (v) => PVE.Utils.renderBooleanProperty(v ?? true),
+ },
+ {
+ header: gettext('Read-Only'),
+ dataIndex: 'ro',
+ width: 100,
+ renderer: PVE.Utils.renderBooleanProperty,
+ },
+ {
+ header: gettext('Replicate'),
+ dataIndex: 'replicate',
+ width: 100,
+ renderer: PVE.Utils.renderBooleanProperty,
+ },
+ {
+ header: gettext('Async I/O'),
+ dataIndex: 'aio',
+ width: 100,
+ },
+ {
+ header: gettext('Bandwidth Limits'),
+ hidden: true,
+ columns: [
+ {
+ header: gettext('Read limit (MiB/s)'),
+ dataIndex: 'mbps_rd',
+ width: 110,
+ },
+ {
+ header: gettext('Write limit (MiB/s)'),
+ dataIndex: 'mbps_wr',
+ width: 110,
+ },
+ {
+ header: gettext('Read limit (ops/s)'),
+ dataIndex: 'iops_rd',
+ width: 110,
+ },
+ {
+ header: gettext('Write limit (ops/s)'),
+ dataIndex: 'iops_wr',
+ width: 110,
+ },
+ {
+ header: gettext('Read Burst (MiB/s)'),
+ dataIndex: 'mbps_rd_max',
+ width: 110,
+ },
+ {
+ header: gettext('Write Burst (MiB/s)'),
+ dataIndex: 'mbps_wr_max',
+ width: 110,
+ },
+ {
+ header: gettext('Read Burst (ops/s)'),
+ dataIndex: 'iops_rd_max',
+ width: 110,
+ },
+ {
+ header: gettext('Write Burst (ops/s)'),
+ dataIndex: 'iops_wr_max',
+ width: 110,
+ },
+ ],
+ },
+ {
+ header: gettext('Detect Zeroes'),
+ dataIndex: 'detect_zeroes',
+ hidden: true,
+ renderer: PVE.Utils.renderBooleanProperty,
+ },
+ {
+ header: gettext('Vendor'),
+ dataIndex: 'vendor',
+ hidden: true,
+ },
+ {
+ header: gettext('Product'),
+ dataIndex: 'product',
+ hidden: true,
+ },
+ {
+ header: gettext('Serial'),
+ dataIndex: 'serial',
+ hidden: true,
+ },
+ {
+ header: gettext('Queues'),
+ dataIndex: 'queues',
+ hidden: true,
+ },
+ {
+ header: gettext('Shared'),
+ dataIndex: 'shared',
+ hidden: true,
+ renderer: PVE.Utils.renderBooleanProperty,
+ },
+ {
+ header: gettext('Write Error Action'),
+ dataIndex: 'werror',
+ hidden: true,
+ },
+ {
+ header: 'WWN',
+ dataIndex: 'wwn',
+ hidden: true,
+ },
+ {
+ header: gettext('Raw Config'),
+ dataIndex: 'raw',
+ hidden: true,
+ flex: 1,
+ },
+ ],
+});
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH manager v4 08/12] ui: qemu: introduce hardware net grid
2026-08-31 12:27 [PATCH manager v4 00/12] ui: split out disks and nics into grids Dominik Csapak
` (6 preceding siblings ...)
2026-08-31 12:27 ` [PATCH manager v4 07/12] ui: qemu: introduce hardware disk grid Dominik Csapak
@ 2026-08-31 12:27 ` Dominik Csapak
2026-08-31 12:27 ` [PATCH manager v4 09/12] ui: qemu: hardware view: separate disks into own grid Dominik Csapak
` (4 subsequent siblings)
12 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-08-31 12:27 UTC (permalink / raw)
To: pve-devel
intended to be used inside the hardware view.
Same as the hardware disk grid, the toolbar buttons will be added in the
hardware view itself.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/qemu/HardwareNetGrid.js | 81 ++++++++++++++++++++++++++++
2 files changed, 82 insertions(+)
create mode 100644 www/manager6/qemu/HardwareNetGrid.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 01715aa0..ec2d32b9 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -271,6 +271,7 @@ JSSRC= \
qemu/HDMove.js \
qemu/HDResize.js \
qemu/HardwareDiskGrid.js \
+ qemu/HardwareNetGrid.js \
qemu/HardwareView.js \
qemu/IPConfigEdit.js \
qemu/KeyboardEdit.js \
diff --git a/www/manager6/qemu/HardwareNetGrid.js b/www/manager6/qemu/HardwareNetGrid.js
new file mode 100644
index 00000000..22814b28
--- /dev/null
+++ b/www/manager6/qemu/HardwareNetGrid.js
@@ -0,0 +1,81 @@
+Ext.define('PVE.qemu.HardwareNetGrid', {
+ extend: 'PVE.grid.PendingGrid',
+ alias: 'widget.pveHardwareNetGrid',
+
+ title: gettext('Network Interfaces'),
+
+ stateful: true,
+ stateId: 'pve-qemu-hardware-net',
+
+ border: false,
+ expandable: false,
+
+ emptyText: gettext('No network interfaces configured'),
+
+ storeFilter: (rec) => PVE.Utils.keyIsNic(rec.data.key),
+ recordParser: PVE.Parser.parseQemuNetwork,
+
+ renderKey: Ext.htmlEncode,
+
+ columns: [
+ {
+ header: gettext('ID'),
+ dataIndex: 'id',
+ width: 200,
+ renderer: 'renderKey',
+ },
+ {
+ header: gettext('Model'),
+ dataIndex: 'model',
+ width: 100,
+ },
+ {
+ header: gettext('MAC address'),
+ dataIndex: 'macaddr',
+ minWidth: 200,
+ flex: 1,
+ },
+ {
+ header: gettext('Bridge'),
+ dataIndex: 'bridge',
+ width: 100,
+ },
+ {
+ header: gettext('Firewall enabled'),
+ dataIndex: 'firewall',
+ width: 150,
+ renderer: PVE.Utils.renderBooleanProperty,
+ },
+ {
+ header: gettext('VLAN tag'),
+ dataIndex: 'tag',
+ width: 80,
+ },
+ {
+ header: gettext('MTU'),
+ dataIndex: 'mtu',
+ width: 100,
+ },
+ {
+ header: gettext('Multiqueue'),
+ dataIndex: 'queues',
+ width: 100,
+ },
+ {
+ header: gettext('Rate limit (MB/s)'),
+ dataIndex: 'rate',
+ width: 200,
+ },
+ {
+ header: gettext('Disconnected'),
+ dataIndex: 'link_down',
+ width: 100,
+ renderer: PVE.Utils.renderBooleanProperty,
+ },
+ {
+ header: gettext('Trunks'),
+ dataIndex: 'trunks',
+ width: 100,
+ },
+ ],
+});
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH manager v4 09/12] ui: qemu: hardware view: separate disks into own grid
2026-08-31 12:27 [PATCH manager v4 00/12] ui: split out disks and nics into grids Dominik Csapak
` (7 preceding siblings ...)
2026-08-31 12:27 ` [PATCH manager v4 08/12] ui: qemu: introduce hardware net grid Dominik Csapak
@ 2026-08-31 12:27 ` Dominik Csapak
2026-08-31 12:27 ` [PATCH manager v4 10/12] ui: qemu: hardware view: separate nics " Dominik Csapak
` (3 subsequent siblings)
12 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-08-31 12:27 UTC (permalink / raw)
To: pve-devel
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 fd565e36..c4318851 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 () {
@@ -403,7 +408,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];
@@ -488,74 +509,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.',
@@ -578,35 +531,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,
@@ -650,15 +574,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+$/);
@@ -692,7 +616,6 @@ Ext.define('PVE.qemu.HardwareView', {
if (!rec) {
remove_btn.disable();
edit_btn.disable();
- diskaction_btn.disable();
revert_btn.disable();
return;
}
@@ -701,7 +624,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);
@@ -709,31 +631,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 ||
@@ -742,12 +645,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);
};
@@ -767,23 +664,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;
@@ -829,18 +750,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',
@@ -854,14 +763,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',
@@ -922,17 +823,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
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH manager v4 10/12] ui: qemu: hardware view: separate nics into own grid
2026-08-31 12:27 [PATCH manager v4 00/12] ui: split out disks and nics into grids Dominik Csapak
` (8 preceding siblings ...)
2026-08-31 12:27 ` [PATCH manager v4 09/12] ui: qemu: hardware view: separate disks into own grid Dominik Csapak
@ 2026-08-31 12:27 ` Dominik Csapak
2026-08-31 12:27 ` [PATCH manager v4 11/12] ui: qemu: hardware view: inline edit/remove/revert button in general grid Dominik Csapak
` (2 subsequent siblings)
12 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-08-31 12:27 UTC (permalink / raw)
To: pve-devel
by using the newly added HardwareNetGrid and ConfigRemoveButton.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
www/manager6/qemu/HardwareView.js | 67 +++++++++++++++++++++++++++----
1 file changed, 59 insertions(+), 8 deletions(-)
diff --git a/www/manager6/qemu/HardwareView.js b/www/manager6/qemu/HardwareView.js
index c4318851..abd6bb35 100644
--- a/www/manager6/qemu/HardwareView.js
+++ b/www/manager6/qemu/HardwareView.js
@@ -680,7 +680,7 @@ Ext.define('PVE.qemu.HardwareView', {
url: `/api2/json/nodes/${nodename}/qemu/${vmid}/pending`,
interval: 5000,
rows,
- filters: [(rec) => !diskGridFilter(rec)],
+ filters: [(rec) => !diskGridFilter(rec), (rec) => !PVE.Utils.keyIsNic(rec.data.key)],
});
let onSelectionChange = function (sm, selected) {
@@ -756,13 +756,6 @@ Ext.define('PVE.qemu.HardwareView', {
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'),
- },
{
text: gettext('USB Device'),
itemId: 'addUsb',
@@ -1047,6 +1040,64 @@ Ext.define('PVE.qemu.HardwareView', {
},
],
},
+ // spacer with bottom border
+ {
+ xtype: 'panel',
+ border: true,
+ height: 1,
+ margin: '20 0 0 0',
+ },
+ {
+ xtype: 'pveHardwareNetGrid',
+ rstore: me.rstore,
+ store: {
+ sorter: sorterFn,
+ },
+ renderKey: function () {
+ let hardwareGrid = me.lookup('hardwareGrid');
+ return hardwareGrid.renderKey(...arguments);
+ },
+ listeners: {
+ itemdblclick: run_editor,
+ selectionchange: onSelectionChange,
+ },
+ tbar: [
+ {
+ text: gettext('Add'),
+ itemId: 'addNet',
+ disabled: !caps.vms['VM.Config.Network'],
+ handler: editorFactory('NetworkEdit'),
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Edit'),
+ disabled: true,
+ enableFn: (rec) =>
+ diskCap && !rec.data.deleted && !!rows[rec.data.key].editor,
+ handler: run_editor,
+ },
+ {
+ xtype: 'pveConfigRemoveButton',
+ baseurl,
+ rows,
+ reloadCallback: () => me.reload(),
+ renderKey: function () {
+ return me.lookup('hardwareGrid').renderKey(...arguments);
+ },
+ enableFn: (rec) =>
+ caps.vms['VM.Config.Network'] &&
+ !rec.data.deleted &&
+ !rows[rec.data.key].never_delete,
+ },
+ {
+ xtype: 'pvePendingRevertButton',
+ apiurl: '/api2/extjs/' + baseurl,
+ parentXType: 'pvePendingGrid',
+ enableFn: (rec) => !!rec.data.deleted || !!rec.data.pending,
+ reloadCallback: () => me.reload(),
+ },
+ ],
+ },
],
});
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH manager v4 11/12] ui: qemu: hardware view: inline edit/remove/revert button in general grid
2026-08-31 12:27 [PATCH manager v4 00/12] ui: split out disks and nics into grids Dominik Csapak
` (9 preceding siblings ...)
2026-08-31 12:27 ` [PATCH manager v4 10/12] ui: qemu: hardware view: separate nics " Dominik Csapak
@ 2026-08-31 12:27 ` Dominik Csapak
2026-08-31 12:27 ` [PATCH manager v4 12/12] ui: qemu: hardware view: inline 'add efi' menuitem Dominik Csapak
2026-08-31 13:20 ` [PATCH manager v4 00/12] ui: split out disks and nics into grids Jonas Theisen
12 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-08-31 12:27 UTC (permalink / raw)
To: pve-devel
This means that the enableFn function handles the enabling/disabling now
for these buttons instead of set_button_status. The logic is partially
duplicated, but it's now with the respective button instead in a central
location.
Consequently, the set_button_status function now only sets the status
for the add buttons/menuitems.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
www/manager6/qemu/HardwareView.js | 118 +++++++++++++++---------------
1 file changed, 58 insertions(+), 60 deletions(-)
diff --git a/www/manager6/qemu/HardwareView.js b/www/manager6/qemu/HardwareView.js
index abd6bb35..95771ca7 100644
--- a/www/manager6/qemu/HardwareView.js
+++ b/www/manager6/qemu/HardwareView.js
@@ -470,13 +470,6 @@ Ext.define('PVE.qemu.HardwareView', {
}
};
- let edit_btn = new Proxmox.button.Button({
- text: gettext('Edit'),
- selModel: sm,
- disabled: true,
- handler: run_editor,
- });
-
let runEfiEnroll = function () {
let rec = sm.getSelection()[0];
if (!rec) {
@@ -532,19 +525,6 @@ Ext.define('PVE.qemu.HardwareView', {
'Otherwise, you will be prompted for the BitLocker recovery key on the next boot!',
);
- let remove_btn = new PVE.button.ConfigRemove({
- baseurl,
- rows,
- renderKey: function () {
- return me.lookup('hardwareGrid').renderKey(...arguments);
- },
- reloadCallback: () => me.reload(),
- });
-
- let revert_btn = new PVE.button.PendingRevert({
- apiurl: '/api2/extjs/' + baseurl,
- });
-
let efidisk_menuitem = Ext.create('Ext.menu.Item', {
text: gettext('EFI Disk'),
iconCls: 'fa fa-fw fa-hdd-o black',
@@ -573,9 +553,6 @@ Ext.define('PVE.qemu.HardwareView', {
};
let set_button_status = function () {
- let hardwareGrid = me.lookup('hardwareGrid');
- let rec = hardwareGrid.getSelection()[0];
-
counts = {}; // en/disable hardwarebuttons
let hasCloudInit = false;
let items = me.rstore.getData();
@@ -612,40 +589,6 @@ Ext.define('PVE.qemu.HardwareView', {
me.down('#addCloudinitDrive').setDisabled(
noVMConfigCDROMPerm || noVMConfigCloudinitPerm || hasCloudInit,
);
-
- if (!rec) {
- remove_btn.disable();
- edit_btn.disable();
- revert_btn.disable();
- return;
- }
- const { key, value } = rec.data;
- const row = rows[key];
-
- const deleted = !!rec.data.delete;
- const pending = deleted || hardwareGrid.hasPendingChanges(key);
-
- const isCloudInit = isCloudInitKey(value);
- const isCDRom = value && PVE.Utils.diskIsCdrom(value);
-
- const isUnusedDisk = key.match(/^unused\d+/);
- const isUsedDisk = !isUnusedDisk && row.isOnStorageBus && !isCDRom;
- const isDisk = isUnusedDisk || isUsedDisk;
-
- let cannotDelete = deleted || row.never_delete;
- cannotDelete ||= isCDRom && !cdromCap;
- cannotDelete ||= isCloudInit && noVMConfigCloudinitPerm;
- remove_btn.setDisabled(cannotDelete);
-
- edit_btn.setDisabled(
- deleted ||
- !row.editor ||
- isCloudInit ||
- (isCDRom && !cdromCap) ||
- (isDisk && !diskCap),
- );
-
- revert_btn.setDisabled(!pending);
};
let editorFactory = (classPath, extraOptions) => {
@@ -816,9 +759,64 @@ Ext.define('PVE.qemu.HardwareView', {
],
}),
},
- edit_btn,
- remove_btn,
- revert_btn,
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Edit'),
+ enableFn: function (rec) {
+ const row = rows[rec.data.key];
+ if (!!rec.data.delete || !row.editor) {
+ return false;
+ }
+ if (isCloudInitKey(rec.data.value)) {
+ return false;
+ }
+ if (PVE.Utils.diskIsCdrom(rec.data.value) && !cdromCap) {
+ return false;
+ }
+ if (
+ row.isOnStorageBus &&
+ !PVE.Utils.diskIsCdrom(rec.data.value) &&
+ !diskCap
+ ) {
+ return false;
+ }
+ return true;
+ },
+ selModel: sm,
+ disabled: true,
+ handler: run_editor,
+ },
+ {
+ xtype: 'pveConfigRemoveButton',
+ baseurl,
+ rows,
+ enableFn: function (rec) {
+ if (!!rec.data.delete || rows[rec.data.key].never_delete) {
+ return false;
+ }
+ if (PVE.Utils.diskIsCdrom(rec.data.value) && !cdromCap) {
+ return false;
+ }
+ if (
+ isCloudInitKey(rec.data.value) &&
+ !caps.vms['VM.Config.Cloudinit']
+ ) {
+ return false;
+ }
+ return true;
+ },
+ renderKey: function () {
+ return me.lookup('hardwareGrid').renderKey(...arguments);
+ },
+ reloadCallback: () => me.reload(),
+ },
+ {
+ xtype: 'pvePendingRevertButton',
+ enableFn: (rec) =>
+ !!rec.data.delete ||
+ me.lookup('hardwareGrid').hasPendingChanges(rec.data.key),
+ apiurl: '/api2/extjs/' + baseurl,
+ },
],
rows: rows,
sorterFn: sorterFn,
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread* [PATCH manager v4 12/12] ui: qemu: hardware view: inline 'add efi' menuitem
2026-08-31 12:27 [PATCH manager v4 00/12] ui: split out disks and nics into grids Dominik Csapak
` (10 preceding siblings ...)
2026-08-31 12:27 ` [PATCH manager v4 11/12] ui: qemu: hardware view: inline edit/remove/revert button in general grid Dominik Csapak
@ 2026-08-31 12:27 ` Dominik Csapak
2026-08-31 13:20 ` [PATCH manager v4 00/12] ui: split out disks and nics into grids Jonas Theisen
12 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-08-31 12:27 UTC (permalink / raw)
To: pve-devel
so it's in line with the remaining add buttons.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
www/manager6/qemu/HardwareView.js | 43 ++++++++++++++++---------------
1 file changed, 22 insertions(+), 21 deletions(-)
diff --git a/www/manager6/qemu/HardwareView.js b/www/manager6/qemu/HardwareView.js
index 95771ca7..05eb7a3b 100644
--- a/www/manager6/qemu/HardwareView.js
+++ b/www/manager6/qemu/HardwareView.js
@@ -525,25 +525,6 @@ Ext.define('PVE.qemu.HardwareView', {
'Otherwise, you will be prompted for the BitLocker recovery key on the next boot!',
);
- let efidisk_menuitem = Ext.create('Ext.menu.Item', {
- text: gettext('EFI Disk'),
- iconCls: 'fa fa-fw fa-hdd-o black',
- disabled: !caps.vms['VM.Config.Disk'],
- handler: function () {
- let { data: bios } = me.rstore.getData().map.bios || {};
-
- Ext.create('PVE.qemu.EFIDiskEdit', {
- autoShow: true,
- url: '/api2/extjs/' + baseurl,
- pveSelNode: me.pveSelNode,
- usesEFI: bios?.value === 'ovmf' || bios?.pending === 'ovmf',
- listeners: {
- destroy: () => me.reload(),
- },
- });
- },
- });
-
let counts = {};
let isAtLimit = (type) => counts[type] >= PVE.Utils.hardware_counts[type];
let isAtUsbLimit = () => {
@@ -583,7 +564,7 @@ Ext.define('PVE.qemu.HardwareView', {
me.down('#addSerial').setDisabled(noVMConfigHWTypePerm || isAtLimit('serial'));
me.down('#addNet').setDisabled(noVMConfigNetPerm || isAtLimit('net'));
me.down('#addRng').setDisabled(noVMConfigHWTypePerm || isAtLimit('rng'));
- efidisk_menuitem.setDisabled(noVMConfigDiskPerm || isAtLimit('efidisk'));
+ me.down('#addEfi').setDisabled(noVMConfigDiskPerm || isAtLimit('efidisk'));
me.down('#addTpmState').setDisabled(noVMConfigDiskPerm || isAtLimit('tpmstate'));
me.down('#addVirtiofs').setDisabled(noVMConfigOptionsPerm || isAtLimit('virtiofs'));
me.down('#addCloudinitDrive').setDisabled(
@@ -865,7 +846,27 @@ Ext.define('PVE.qemu.HardwareView', {
disabled: !caps.vms['VM.Config.Disk'],
handler: editorFactory('HDEdit', { importDisk: true }),
},
- efidisk_menuitem,
+ {
+ itemId: 'addEfi',
+ text: gettext('EFI Disk'),
+ iconCls: 'fa fa-fw fa-hdd-o black',
+ disabled: !caps.vms['VM.Config.Disk'],
+ handler: function () {
+ let { data: bios } = me.rstore.getData().map.bios || {};
+
+ Ext.create('PVE.qemu.EFIDiskEdit', {
+ autoShow: true,
+ url: '/api2/extjs/' + baseurl,
+ pveSelNode: me.pveSelNode,
+ usesEFI:
+ bios?.value === 'ovmf' ||
+ bios?.pending === 'ovmf',
+ listeners: {
+ destroy: () => me.reload(),
+ },
+ });
+ },
+ },
{
text: gettext('TPM State'),
itemId: 'addTpmState',
--
2.47.3
^ permalink raw reply related [flat|nested] 16+ messages in thread* Re: [PATCH manager v4 00/12] ui: split out disks and nics into grids
2026-08-31 12:27 [PATCH manager v4 00/12] ui: split out disks and nics into grids Dominik Csapak
` (11 preceding siblings ...)
2026-08-31 12:27 ` [PATCH manager v4 12/12] ui: qemu: hardware view: inline 'add efi' menuitem Dominik Csapak
@ 2026-08-31 13:20 ` Jonas Theisen
2026-08-31 14:11 ` Aaron Lauterer
2026-08-31 14:38 ` Jonas Theisen
12 siblings, 2 replies; 16+ messages in thread
From: Jonas Theisen @ 2026-08-31 13:20 UTC (permalink / raw)
To: pve-devel
On 8/31/26 14:29, Dominik Csapak wrote:
> Same as v3 of the series, basically just a rebase so it applies again.
>
> I factored out a few things, so it's easier to reuse (especially since i
> want to do the same thing for containers when we figured out if the
> style/layout/etc. fits)
>
> Sending again qemu only for now as i don't want to put in a lot of work
> before we know which direction this will take.
>
> I tried to keep most commits small so it's easier to see what the actual
> change are.
>
> NOTE: I took special care for the button enablement logic, but having a close
> look at that during review would make a lot of sense.
>
> -snip-
Tested the patch quickly and looks very good and improves user readability
a lot imho.
A few things i noticed:
* Unused disk stays above EFI and TPM disks while it would drop to the
bottom
of the list pre-patch. Might be intended but noticed that.
* Accessibility issue: If the new containers do not fit in the window
size, they are
not scrollable vertically so you have to resize your window or shrink the
Tasks view.
* Also as already discussed in v1 with David Riley the horizontal scrollbar
is interfering with readability of the contents of the last or only item.
But this is probably beyond the scope of this patch.
https://lore.proxmox.com/pve-devel/c0fa8494-a5b2-44cd-b02d-3f9925fcae65@proxmox.com/
* Revert button only updates on un- and reclick. Meaning that after a change
which could be reverted, the button stays disabled until the entry is
deselected
and reselected. Same the other way around, where after a revert, the
Revert button
would stay enabled after the change is already reverted.
* EFI Disk: "Volume/File" and "Size" are empty while for TPM state they
are populated
* EFI Disk: Will the EFI cert enrollment status i.e. ms-cert=### be
parsed and displayed?
--
Tested-by: Jonas Theisen <j.theisen@proxmox.com>
^ permalink raw reply [flat|nested] 16+ messages in thread* Re: [PATCH manager v4 00/12] ui: split out disks and nics into grids
2026-08-31 13:20 ` [PATCH manager v4 00/12] ui: split out disks and nics into grids Jonas Theisen
@ 2026-08-31 14:11 ` Aaron Lauterer
2026-08-31 14:38 ` Jonas Theisen
1 sibling, 0 replies; 16+ messages in thread
From: Aaron Lauterer @ 2026-08-31 14:11 UTC (permalink / raw)
To: Jonas Theisen, pve-devel
Additionally, one thing I noticed is that while the grids get a
horizontal scrollbar, the toolbar above does not (tested on Firefox).
Regarding the scrollbar issues: since I have my Firefox instance set to
always display them, I don't run into the issue of the scrollbar
overlaying the content.
To avoid that for all, what about the scrollbar-gutter property? sounds
like this would help to avoid the issue without custom paddings and so
forth.
https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/scrollbar-gutter
On 2026-08-31 15:20, Jonas Theisen wrote:
> On 8/31/26 14:29, Dominik Csapak wrote:
>> Same as v3 of the series, basically just a rebase so it applies again.
>>
>> I factored out a few things, so it's easier to reuse (especially since i
>> want to do the same thing for containers when we figured out if the
>> style/layout/etc. fits)
>>
>> Sending again qemu only for now as i don't want to put in a lot of work
>> before we know which direction this will take.
>>
>> I tried to keep most commits small so it's easier to see what the actual
>> change are.
>>
>> NOTE: I took special care for the button enablement logic, but having
>> a close
>> look at that during review would make a lot of sense.
>>
>> -snip-
>
> Tested the patch quickly and looks very good and improves user readability
> a lot imho.
> A few things i noticed:
> * Unused disk stays above EFI and TPM disks while it would drop to the
> bottom
> of the list pre-patch. Might be intended but noticed that.
> * Accessibility issue: If the new containers do not fit in the window
> size, they are
> not scrollable vertically so you have to resize your window or shrink the
> Tasks view.
> * Also as already discussed in v1 with David Riley the horizontal scrollbar
> is interfering with readability of the contents of the last or only item.
> But this is probably beyond the scope of this patch.
> https://lore.proxmox.com/pve-devel/c0fa8494-a5b2-44cd-
> b02d-3f9925fcae65@proxmox.com/
> * Revert button only updates on un- and reclick. Meaning that after a
> change
> which could be reverted, the button stays disabled until the entry is
> deselected
> and reselected. Same the other way around, where after a revert, the
> Revert button
> would stay enabled after the change is already reverted.
> * EFI Disk: "Volume/File" and "Size" are empty while for TPM state they
> are populated
> * EFI Disk: Will the EFI cert enrollment status i.e. ms-cert=### be
> parsed and displayed?
>
> --
> Tested-by: Jonas Theisen <j.theisen@proxmox.com>
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH manager v4 00/12] ui: split out disks and nics into grids
2026-08-31 13:20 ` [PATCH manager v4 00/12] ui: split out disks and nics into grids Jonas Theisen
2026-08-31 14:11 ` Aaron Lauterer
@ 2026-08-31 14:38 ` Jonas Theisen
1 sibling, 0 replies; 16+ messages in thread
From: Jonas Theisen @ 2026-08-31 14:38 UTC (permalink / raw)
To: pve-devel
On 8/31/26 15:20, Jonas Theisen wrote:
> On 8/31/26 14:29, Dominik Csapak wrote:
>> Same as v3 of the series, basically just a rebase so it applies again.
>>
>> I factored out a few things, so it's easier to reuse (especially since i
>> want to do the same thing for containers when we figured out if the
>> style/layout/etc. fits)
>>
>> Sending again qemu only for now as i don't want to put in a lot of work
>> before we know which direction this will take.
>>
>> I tried to keep most commits small so it's easier to see what the actual
>> change are.
>>
>> NOTE: I took special care for the button enablement logic, but having
>> a close
>> look at that during review would make a lot of sense.
>>
>> -snip-
>
> Tested the patch quickly and looks very good and improves user
> readability
> a lot imho.
> A few things i noticed:
> * Unused disk stays above EFI and TPM disks while it would drop to the
> bottom
> of the list pre-patch. Might be intended but noticed that.
> * Accessibility issue: If the new containers do not fit in the window
> size, they are
> not scrollable vertically so you have to resize your window or shrink the
> Tasks view.
> * Also as already discussed in v1 with David Riley the horizontal
> scrollbar
> is interfering with readability of the contents of the last or only item.
> But this is probably beyond the scope of this patch.
> https://lore.proxmox.com/pve-devel/c0fa8494-a5b2-44cd-b02d-3f9925fcae65@proxmox.com/
>
> * Revert button only updates on un- and reclick. Meaning that after a
> change
> which could be reverted, the button stays disabled until the entry is
> deselected
> and reselected. Same the other way around, where after a revert, the
> Revert button
> would stay enabled after the change is already reverted.
> * EFI Disk: "Volume/File" and "Size" are empty while for TPM state
> they are populated
> * EFI Disk: Will the EFI cert enrollment status i.e. ms-cert=### be
> parsed and displayed?
>
> --
> Tested-by: Jonas Theisen <j.theisen@proxmox.com>
Addendum:
* column "Async I/O" is empty for the Default setting while for "Cache"
it displays
"Default (No cache)". Would expect "Default (io_uring)" as shown in the
Edit panel
^ permalink raw reply [flat|nested] 16+ messages in thread