public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH manager v3 00/13] ui: split out disks and nics into grids
@ 2026-05-15  8:44 Dominik Csapak
  2026-05-15  8:44 ` [PATCH manager v3 01/13] ui: utils: factor out 'media=cdrom' check Dominik Csapak
                   ` (13 more replies)
  0 siblings, 14 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-05-15  8:44 UTC (permalink / raw)
  To: pve-devel

This is basically a complete rework of the series. Since the need
for having the buttons in all grids made things a bit more complicated
than i anticipated.

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.

changes from v2:
* reorganized code/commits for the most part, so it doesn't make much
  sense to compare it to the previous version

changes from v1:
* refactor some regexes/checks
* fix some bugs I encountered during testing again

Dominik Csapak (13):
  ui: utils: factor out 'media=cdrom' check
  ui: factor out the guest key nic regex check
  ui: parser: qemu drive: allow '-' in key names
  ui: add pending grid
  ui: revert button: add parentXType and reloadCallback
  ui: button: add config remove button
  ui: qemu: hardware: wrap in container
  ui: qemu: introduce hardware disk grid
  ui: qemu: introduce hardware net grid
  ui: qemu: hardware view: separate disks into own grid
  ui: qemu: hardware view: separate nics into own grid
  ui: qemu: hardware view: inline edit/remove/revert button in general
    grid
  ui: qemu: hardware view: inline 'add efi' menuitem

 www/manager6/Makefile                 |   4 +
 www/manager6/Parser.js                |   2 +-
 www/manager6/Utils.js                 |  23 +-
 www/manager6/button/ConfigRemove.js   |  74 +++
 www/manager6/button/Revert.js         |   8 +-
 www/manager6/grid/PendingGrid.js      | 134 ++++
 www/manager6/lxc/Network.js           |   4 +-
 www/manager6/qemu/BootOrderEdit.js    |  12 +-
 www/manager6/qemu/CreateWizard.js     |   4 +-
 www/manager6/qemu/HardwareDiskGrid.js | 213 ++++++
 www/manager6/qemu/HardwareNetGrid.js  |  81 +++
 www/manager6/qemu/HardwareView.js     | 925 +++++++++++++++-----------
 www/manager6/window/GuestImport.js    |   2 +-
 13 files changed, 1079 insertions(+), 407 deletions(-)
 create mode 100644 www/manager6/button/ConfigRemove.js
 create mode 100644 www/manager6/grid/PendingGrid.js
 create mode 100644 www/manager6/qemu/HardwareDiskGrid.js
 create mode 100644 www/manager6/qemu/HardwareNetGrid.js

-- 
2.47.3





^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH manager v3 01/13] ui: utils: factor out 'media=cdrom' check
  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 ` Dominik Csapak
  2026-05-15  8:44 ` [PATCH manager v3 02/13] ui: factor out the guest key nic regex check Dominik Csapak
                   ` (12 subsequent siblings)
  13 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-05-15  8:44 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 01e80682..a4d0e92b 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1966,6 +1966,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 = {};
@@ -1976,7 +1983,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 521a3d6e..c7117c1a 100644
--- a/www/manager6/qemu/BootOrderEdit.js
+++ b/www/manager6/qemu/BootOrderEdit.js
@@ -51,7 +51,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);
@@ -97,7 +97,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);
@@ -196,7 +196,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 64cb4a7b..471e3a8f 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;
             }
@@ -417,7 +417,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;
@@ -757,7 +757,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 9c9b0f1b..9a01e81d 100644
--- a/www/manager6/window/GuestImport.js
+++ b/www/manager6/window/GuestImport.js
@@ -1065,7 +1065,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 v3 02/13] ui: factor out the guest key nic regex check
  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 ` Dominik Csapak
  2026-05-15  8:44 ` [PATCH manager v3 03/13] ui: parser: qemu drive: allow '-' in key names Dominik Csapak
                   ` (11 subsequent siblings)
  13 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-05-15  8:44 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 a4d0e92b..f7eec0f7 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1973,6 +1973,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 c7117c1a..12f3b672 100644
--- a/www/manager6/qemu/BootOrderEdit.js
+++ b/www/manager6/qemu/BootOrderEdit.js
@@ -60,7 +60,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))
         );
@@ -105,7 +105,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);
                         }
                     });
@@ -194,7 +194,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 v3 03/13] ui: parser: qemu drive: allow '-' in key names
  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 ` Dominik Csapak
  2026-05-15  8:44 ` [PATCH manager v3 04/13] ui: add pending grid Dominik Csapak
                   ` (10 subsequent siblings)
  13 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-05-15  8:44 UTC (permalink / raw)
  To: pve-devel

when we want to use this for efidisk0/tpmstate0 too, we have to allow
'-' in key names for properties such as 'ms-cert'.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/manager6/Parser.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/www/manager6/Parser.js b/www/manager6/Parser.js
index 36df8e9d..a82b3d3d 100644
--- a/www/manager6/Parser.js
+++ b/www/manager6/Parser.js
@@ -207,7 +207,7 @@ Ext.define('PVE.Parser', {
                 if (!p || p.match(/^\s*$/)) {
                     return undefined; // continue
                 }
-                let match = p.match(/^([a-z_]+)=(\S+)$/);
+                let match = p.match(/^([a-z_-]+)=(\S+)$/);
                 if (!match) {
                     if (!p.match(/[=]/)) {
                         res.file = p;
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH manager v3 04/13] ui: add pending grid
  2026-05-15  8:44 [PATCH manager v3 00/13] ui: split out disks and nics into grids Dominik Csapak
                   ` (2 preceding siblings ...)
  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 ` Dominik Csapak
  2026-05-15  8:44 ` [PATCH manager v3 05/13] ui: revert button: add parentXType and reloadCallback Dominik Csapak
                   ` (9 subsequent siblings)
  13 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-05-15  8:44 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 280a9ca6..c2c1dffb 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -100,6 +100,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 v3 05/13] ui: revert button: add parentXType and reloadCallback
  2026-05-15  8:44 [PATCH manager v3 00/13] ui: split out disks and nics into grids Dominik Csapak
                   ` (3 preceding siblings ...)
  2026-05-15  8:44 ` [PATCH manager v3 04/13] ui: add pending grid Dominik Csapak
@ 2026-05-15  8:44 ` Dominik Csapak
  2026-05-15  8:44 ` [PATCH manager v3 06/13] ui: button: add config remove button Dominik Csapak
                   ` (8 subsequent siblings)
  13 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-05-15  8:44 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 v3 06/13] ui: button: add config remove button
  2026-05-15  8:44 [PATCH manager v3 00/13] ui: split out disks and nics into grids Dominik Csapak
                   ` (4 preceding siblings ...)
  2026-05-15  8:44 ` [PATCH manager v3 05/13] ui: revert button: add parentXType and reloadCallback Dominik Csapak
@ 2026-05-15  8:44 ` Dominik Csapak
  2026-05-15  8:44 ` [PATCH manager v3 07/13] ui: qemu: hardware: wrap in container Dominik Csapak
                   ` (7 subsequent siblings)
  13 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-05-15  8:44 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 c2c1dffb..e4964b32 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 471e3a8f..58f86779 100644
--- a/www/manager6/qemu/HardwareView.js
+++ b/www/manager6/qemu/HardwareView.js
@@ -608,67 +608,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 v3 07/13] ui: qemu: hardware: wrap in container
  2026-05-15  8:44 [PATCH manager v3 00/13] ui: split out disks and nics into grids Dominik Csapak
                   ` (5 preceding siblings ...)
  2026-05-15  8:44 ` [PATCH manager v3 06/13] ui: button: add config remove button Dominik Csapak
@ 2026-05-15  8:44 ` Dominik Csapak
  2026-05-15  8:44 ` [PATCH manager v3 08/13] ui: qemu: introduce hardware disk grid Dominik Csapak
                   ` (6 subsequent siblings)
  13 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-05-15  8:44 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.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/manager6/qemu/HardwareView.js | 328 +++++++++++++++++-------------
 1 file changed, 181 insertions(+), 147 deletions(-)

diff --git a/www/manager6/qemu/HardwareView.js b/www/manager6/qemu/HardwareView.js
index 58f86779..b5b2600d 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 () {
@@ -612,7 +589,7 @@ Ext.define('PVE.qemu.HardwareView', {
             baseurl,
             rows,
             renderKey: function () {
-                return me.renderKey(...arguments);
+                return me.lookup('hardwareGrid').renderKey(...arguments);
             },
             reloadCallback: () => me.reload(),
         });
@@ -649,7 +626,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
@@ -699,7 +677,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);
@@ -766,118 +744,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();
@@ -885,6 +919,6 @@ 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);
     },
 });
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH manager v3 08/13] ui: qemu: introduce hardware disk grid
  2026-05-15  8:44 [PATCH manager v3 00/13] ui: split out disks and nics into grids Dominik Csapak
                   ` (6 preceding siblings ...)
  2026-05-15  8:44 ` [PATCH manager v3 07/13] ui: qemu: hardware: wrap in container Dominik Csapak
@ 2026-05-15  8:44 ` Dominik Csapak
  2026-05-15  8:44 ` [PATCH manager v3 09/13] ui: qemu: introduce hardware net grid Dominik Csapak
                   ` (5 subsequent siblings)
  13 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-05-15  8:44 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 e4964b32..edef39eb 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -265,6 +265,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 f7eec0f7..7df16555 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -2120,6 +2120,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 v3 09/13] ui: qemu: introduce hardware net grid
  2026-05-15  8:44 [PATCH manager v3 00/13] ui: split out disks and nics into grids Dominik Csapak
                   ` (7 preceding siblings ...)
  2026-05-15  8:44 ` [PATCH manager v3 08/13] ui: qemu: introduce hardware disk grid Dominik Csapak
@ 2026-05-15  8:44 ` Dominik Csapak
  2026-05-15  8:44 ` [PATCH manager v3 10/13] ui: qemu: hardware view: separate disks into own grid Dominik Csapak
                   ` (4 subsequent siblings)
  13 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-05-15  8:44 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 edef39eb..a53a9946 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -266,6 +266,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 v3 10/13] ui: qemu: hardware view: separate disks into own grid
  2026-05-15  8:44 [PATCH manager v3 00/13] ui: split out disks and nics into grids Dominik Csapak
                   ` (8 preceding siblings ...)
  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
  2026-05-15  8:44 ` [PATCH manager v3 11/13] ui: qemu: hardware view: separate nics " Dominik Csapak
                   ` (3 subsequent siblings)
  13 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-05-15  8:44 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 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





^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH manager v3 11/13] ui: qemu: hardware view: separate nics into own grid
  2026-05-15  8:44 [PATCH manager v3 00/13] ui: split out disks and nics into grids Dominik Csapak
                   ` (9 preceding siblings ...)
  2026-05-15  8:44 ` [PATCH manager v3 10/13] ui: qemu: hardware view: separate disks into own grid Dominik Csapak
@ 2026-05-15  8:44 ` 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
                   ` (2 subsequent siblings)
  13 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-05-15  8:44 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 48c54a6c..e888bb46 100644
--- a/www/manager6/qemu/HardwareView.js
+++ b/www/manager6/qemu/HardwareView.js
@@ -657,7 +657,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) {
@@ -733,13 +733,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',
@@ -1024,6 +1017,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 v3 12/13] ui: qemu: hardware view: inline edit/remove/revert button in general grid
  2026-05-15  8:44 [PATCH manager v3 00/13] ui: split out disks and nics into grids Dominik Csapak
                   ` (10 preceding siblings ...)
  2026-05-15  8:44 ` [PATCH manager v3 11/13] ui: qemu: hardware view: separate nics " Dominik Csapak
@ 2026-05-15  8:44 ` 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
  13 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-05-15  8:44 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 e888bb46..e16436ab 100644
--- a/www/manager6/qemu/HardwareView.js
+++ b/www/manager6/qemu/HardwareView.js
@@ -447,13 +447,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) {
@@ -509,19 +502,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',
@@ -550,9 +530,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();
@@ -589,40 +566,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) => {
@@ -793,9 +736,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 v3 13/13] ui: qemu: hardware view: inline 'add efi' menuitem
  2026-05-15  8:44 [PATCH manager v3 00/13] ui: split out disks and nics into grids Dominik Csapak
                   ` (11 preceding siblings ...)
  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 ` Dominik Csapak
  2026-05-15  9:18 ` [PATCH manager v3 00/13] ui: split out disks and nics into grids Dominik Csapak
  13 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-05-15  8:44 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 e16436ab..21914a29 100644
--- a/www/manager6/qemu/HardwareView.js
+++ b/www/manager6/qemu/HardwareView.js
@@ -502,25 +502,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 = () => {
@@ -560,7 +541,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(
@@ -842,7 +823,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 v3 00/13] ui: split out disks and nics into grids
  2026-05-15  8:44 [PATCH manager v3 00/13] ui: split out disks and nics into grids Dominik Csapak
                   ` (12 preceding siblings ...)
  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 ` Dominik Csapak
  2026-05-15  9:20   ` Dominik Csapak
  13 siblings, 1 reply; 16+ messages in thread
From: Dominik Csapak @ 2026-05-15  9:18 UTC (permalink / raw)
  To: pve-devel

sorry this should have been v4, resending now....

On 5/15/26 10:54 AM, Dominik Csapak wrote:
> This is basically a complete rework of the series. Since the need
> for having the buttons in all grids made things a bit more complicated
> than i anticipated.
> 
> 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.
> 
> changes from v2:
> * reorganized code/commits for the most part, so it doesn't make much
>    sense to compare it to the previous version
> 
> changes from v1:
> * refactor some regexes/checks
> * fix some bugs I encountered during testing again
> 
> Dominik Csapak (13):
>    ui: utils: factor out 'media=cdrom' check
>    ui: factor out the guest key nic regex check
>    ui: parser: qemu drive: allow '-' in key names
>    ui: add pending grid
>    ui: revert button: add parentXType and reloadCallback
>    ui: button: add config remove button
>    ui: qemu: hardware: wrap in container
>    ui: qemu: introduce hardware disk grid
>    ui: qemu: introduce hardware net grid
>    ui: qemu: hardware view: separate disks into own grid
>    ui: qemu: hardware view: separate nics into own grid
>    ui: qemu: hardware view: inline edit/remove/revert button in general
>      grid
>    ui: qemu: hardware view: inline 'add efi' menuitem
> 
>   www/manager6/Makefile                 |   4 +
>   www/manager6/Parser.js                |   2 +-
>   www/manager6/Utils.js                 |  23 +-
>   www/manager6/button/ConfigRemove.js   |  74 +++
>   www/manager6/button/Revert.js         |   8 +-
>   www/manager6/grid/PendingGrid.js      | 134 ++++
>   www/manager6/lxc/Network.js           |   4 +-
>   www/manager6/qemu/BootOrderEdit.js    |  12 +-
>   www/manager6/qemu/CreateWizard.js     |   4 +-
>   www/manager6/qemu/HardwareDiskGrid.js | 213 ++++++
>   www/manager6/qemu/HardwareNetGrid.js  |  81 +++
>   www/manager6/qemu/HardwareView.js     | 925 +++++++++++++++-----------
>   www/manager6/window/GuestImport.js    |   2 +-
>   13 files changed, 1079 insertions(+), 407 deletions(-)
>   create mode 100644 www/manager6/button/ConfigRemove.js
>   create mode 100644 www/manager6/grid/PendingGrid.js
>   create mode 100644 www/manager6/qemu/HardwareDiskGrid.js
>   create mode 100644 www/manager6/qemu/HardwareNetGrid.js
> 





^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [PATCH manager v3 00/13] ui: split out disks and nics into grids
  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
  0 siblings, 0 replies; 16+ messages in thread
From: Dominik Csapak @ 2026-05-15  9:20 UTC (permalink / raw)
  To: pve-devel



On 5/15/26 11:18 AM, Dominik Csapak wrote:
> sorry this should have been v4, resending now....
> 

sorry, disregard that, i looked at the wrong series...
v3 is correct here...

> On 5/15/26 10:54 AM, Dominik Csapak wrote:
>> This is basically a complete rework of the series. Since the need
>> for having the buttons in all grids made things a bit more complicated
>> than i anticipated.
>>
>> 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.
>>
>> changes from v2:
>> * reorganized code/commits for the most part, so it doesn't make much
>>    sense to compare it to the previous version
>>
>> changes from v1:
>> * refactor some regexes/checks
>> * fix some bugs I encountered during testing again
>>
>> Dominik Csapak (13):
>>    ui: utils: factor out 'media=cdrom' check
>>    ui: factor out the guest key nic regex check
>>    ui: parser: qemu drive: allow '-' in key names
>>    ui: add pending grid
>>    ui: revert button: add parentXType and reloadCallback
>>    ui: button: add config remove button
>>    ui: qemu: hardware: wrap in container
>>    ui: qemu: introduce hardware disk grid
>>    ui: qemu: introduce hardware net grid
>>    ui: qemu: hardware view: separate disks into own grid
>>    ui: qemu: hardware view: separate nics into own grid
>>    ui: qemu: hardware view: inline edit/remove/revert button in general
>>      grid
>>    ui: qemu: hardware view: inline 'add efi' menuitem
>>
>>   www/manager6/Makefile                 |   4 +
>>   www/manager6/Parser.js                |   2 +-
>>   www/manager6/Utils.js                 |  23 +-
>>   www/manager6/button/ConfigRemove.js   |  74 +++
>>   www/manager6/button/Revert.js         |   8 +-
>>   www/manager6/grid/PendingGrid.js      | 134 ++++
>>   www/manager6/lxc/Network.js           |   4 +-
>>   www/manager6/qemu/BootOrderEdit.js    |  12 +-
>>   www/manager6/qemu/CreateWizard.js     |   4 +-
>>   www/manager6/qemu/HardwareDiskGrid.js | 213 ++++++
>>   www/manager6/qemu/HardwareNetGrid.js  |  81 +++
>>   www/manager6/qemu/HardwareView.js     | 925 +++++++++++++++-----------
>>   www/manager6/window/GuestImport.js    |   2 +-
>>   13 files changed, 1079 insertions(+), 407 deletions(-)
>>   create mode 100644 www/manager6/button/ConfigRemove.js
>>   create mode 100644 www/manager6/grid/PendingGrid.js
>>   create mode 100644 www/manager6/qemu/HardwareDiskGrid.js
>>   create mode 100644 www/manager6/qemu/HardwareNetGrid.js
>>
> 
> 
> 
> 
> 





^ permalink raw reply	[flat|nested] 16+ messages in thread

end of thread, other threads:[~2026-05-15  9:21 UTC | newest]

Thread overview: 16+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH manager v3 10/13] ui: qemu: hardware view: separate disks into own grid Dominik Csapak
2026-05-15  8:44 ` [PATCH manager v3 11/13] ui: qemu: hardware view: separate nics " 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

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal