public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Dominik Csapak <d.csapak@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH manager v2 3/3] ui: qemu hardware view: split out disks and nics into grids
Date: Fri,  8 May 2026 15:38:06 +0200	[thread overview]
Message-ID: <20260508134110.4001168-4-d.csapak@proxmox.com> (raw)
In-Reply-To: <20260508134110.4001168-1-d.csapak@proxmox.com>

This is done to improve visibility of configuration of disks and nics.
Currently only the raw property string is shown in the UI, which leads
to bad UX when having many options configured and especially when having
pending changes in these situations because it's hard to parse visually.

The disks and nics are shown below the main options, each as their own
grids with relevant columns. By default all columns that can be edited
in the gui are shown (with the exception of the bandwidth limits, since
they're not often used and blow up the number of columns). All other
options from the config are there but hidden by default.

As a fallback, in case there are new options not yet in the ui, a 'Raw
Config' column was added, which contains the raw property string again
(but it's hidden by default).

Some notes on the implementation:

The stores for the new grids are populated on each load of the main
grid, and the we have one combined pending store that's just there to
save the parsed pending values.

To keep the diff smaller, some helpers are introduced to forward some
methods to the main hardware grid.

The selection change introduces some logic to keep the selection only
in one grid, so there can be no conflict on the selected values.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes in v2:
* refactor the disk regex
* also consider newly pending disks and nics
* correct use of the boolean values (have to parseBoolean first)

 www/manager6/button/Revert.js     |   2 +-
 www/manager6/qemu/HardwareView.js | 551 ++++++++++++++++++++++++++++--
 2 files changed, 526 insertions(+), 27 deletions(-)

diff --git a/www/manager6/button/Revert.js b/www/manager6/button/Revert.js
index 14b13f5b..7d48f83f 100644
--- a/www/manager6/button/Revert.js
+++ b/www/manager6/button/Revert.js
@@ -18,7 +18,7 @@ Ext.define('PVE.button.PendingRevert', {
         }
         let view = this.pendingGrid;
 
-        let rec = view.getSelectionModel().getSelection()[0];
+        let rec = (this.selModel ?? view.getSelectionModel()).getSelection()[0];
         if (!rec) {
             return;
         }
diff --git a/www/manager6/qemu/HardwareView.js b/www/manager6/qemu/HardwareView.js
index 471e3a8f..86da04c7 100644
--- a/www/manager6/qemu/HardwareView.js
+++ b/www/manager6/qemu/HardwareView.js
@@ -1,9 +1,15 @@
 Ext.define('PVE.qemu.HardwareView', {
-    extend: 'Proxmox.grid.PendingObjectGrid',
+    extend: 'Ext.panel.Panel',
     alias: ['widget.PVE.qemu.HardwareView'],
 
     onlineHelp: 'qm_virtual_machines_settings',
 
+    layout: {
+        type: 'vbox',
+        align: 'stretch',
+    },
+    scrollable: true,
+
     renderKey: function (key, metaData, rec, rowIndex, colIndex, store) {
         var me = this;
         var rows = me.rows;
@@ -43,6 +49,22 @@ Ext.define('PVE.qemu.HardwareView', {
         }
     },
 
+    referenceHolder: true,
+
+    // helpers to redirect the methods to the hardwareGrid
+
+    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 () {
         var me = this;
 
@@ -403,9 +425,475 @@ Ext.define('PVE.qemu.HardwareView', {
 
         let baseurl = `nodes/${nodename}/qemu/${vmid}/config`;
 
-        let sm = Ext.create('Ext.selection.RowModel', {});
+        let hardwareGrid = Ext.create('Proxmox.grid.PendingObjectGrid', {
+            reference: 'hardwareGrid',
+            title: gettext('General'),
+            iconCls: 'fa fa-desktop',
+            pveSelNode: me.pveSelNode,
+            url: `/api2/json/nodes/${nodename}/qemu/${vmid}/pending`,
+            interval: 5000,
+            rows,
+            sorterFn,
+            border: false,
+            renderKey: me.renderKey,
+        });
+
+        let keyForDiskGrid = (key) => key.match(/^(ide|sata|scsi|virtio|unused)\d+$/);
+
+        hardwareGrid.getStore().addFilter({
+            filterFn: function (rec) {
+                let val = rec.get('value') || rec.get('pending');
+                let key = rec.get('key');
+                if (keyForDiskGrid(key)) {
+                    return PVE.Utils.diskIsCdrom(val) || isCloudInitKey(val);
+                } else if (PVE.Utils.keyIsNic(key)) {
+                    return false;
+                }
+                return true;
+            },
+        });
+
+        let pendingStore = Ext.create('Ext.data.Store');
+        let pendingRenderer = function (originalRenderer) {
+            originalRenderer ??= Ext.htmlEncode;
+            return function (val, metaData, rec, rowIdx, colIdx, store, view) {
+                let txt = originalRenderer(val, metaData, rec, rowIdx, colIdx, store, view);
+                let pendingTxt;
+
+                let pending = pendingStore.getById(rec.get('id'));
+                if (pending) {
+                    let dataIndex = view.up().getColumns()[colIdx].dataIndex;
+                    let value = pending.get(dataIndex);
+                    pendingTxt = originalRenderer(
+                        value,
+                        metaData,
+                        rec,
+                        rowIdx,
+                        colIdx,
+                        store,
+                        view,
+                    );
+                }
+
+                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.delete && txt) {
+                    txt = `<div style="color: darkorange; text-decoration: line-through;">${txt}</div>`;
+                }
+
+                return txt;
+            };
+        };
+
+        // boolean values parsed from property string have to be parsed to real boolean values
+        let booleanRenderer = function (value) {
+            if (Ext.isString(value)) {
+                value = PVE.Parser.parseBoolean(value);
+            }
+            return Proxmox.Utils.format_boolean(value);
+        };
+
+        let diskGrid = Ext.create('Ext.grid.Panel', {
+            reference: 'diskGrid',
+            title: gettext('Hard Disks'),
+            stateful: true,
+            stateId: 'pve-qemu-hardware-disk',
+            border: false,
+            expandable: false,
+            iconCls: 'fa fa-hdd-o',
+            minHeight: 100,
+            emptyText: gettext('No hard disks configured'),
+            store: {},
+            columns: [
+                {
+                    header: gettext('Bus/Device'),
+                    dataIndex: 'id',
+                    width: 100,
+                    renderer: pendingRenderer(),
+                },
+                {
+                    header: gettext('Volume/File'),
+                    dataIndex: 'file',
+                    flex: 1,
+                    minWidth: 200,
+                    renderer: pendingRenderer(),
+                },
+                {
+                    header: gettext('Size'),
+                    dataIndex: 'size',
+                    width: 100,
+                    renderer: pendingRenderer((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('Cache'),
+                    dataIndex: 'cache',
+                    width: 140,
+                    renderer: pendingRenderer(
+                        (v) => v ?? `${Proxmox.Utils.defaultText} (${gettext('No cache')})`,
+                    ),
+                },
+                {
+                    header: gettext('Discard'),
+                    dataIndex: 'discard',
+                    width: 80,
+                    renderer: pendingRenderer(booleanRenderer),
+                },
+                {
+                    header: gettext('IO thread'),
+                    dataIndex: 'iothread',
+                    width: 100,
+                    renderer: pendingRenderer(booleanRenderer),
+                },
+                {
+                    header: gettext('SSD emulation'),
+                    dataIndex: 'ssd',
+                    width: 120,
+                    renderer: pendingRenderer(booleanRenderer),
+                },
+                {
+                    header: gettext('Backup'),
+                    dataIndex: 'backup',
+                    width: 80,
+                    renderer: pendingRenderer((v) => booleanRenderer(v ?? true)),
+                },
+                {
+                    header: gettext('Read-Only'),
+                    dataIndex: 'ro',
+                    width: 100,
+                    renderer: pendingRenderer(booleanRenderer),
+                },
+                {
+                    header: gettext('Replicate'),
+                    dataIndex: 'replicate',
+                    width: 100,
+                    renderer: pendingRenderer(booleanRenderer),
+                },
+                {
+                    header: gettext('Async I/O'),
+                    dataIndex: 'aio',
+                    width: 100,
+                    renderer: pendingRenderer(),
+                },
+                {
+                    header: gettext('Bandwidth Limits'),
+                    hidden: true,
+                    columns: [
+                        {
+                            header: gettext('Read limit (MiB/s)'),
+                            dataIndex: 'mbps_rd',
+                            width: 110,
+                            renderer: pendingRenderer(),
+                        },
+                        {
+                            header: gettext('Write limit (MiB/s)'),
+                            dataIndex: 'mbps_wr',
+                            width: 110,
+                            renderer: pendingRenderer(),
+                        },
+                        {
+                            header: gettext('Read limit (ops/s)'),
+                            dataIndex: 'iops_rd',
+                            width: 110,
+                            renderer: pendingRenderer(),
+                        },
+                        {
+                            header: gettext('Write limit (ops/s)'),
+                            dataIndex: 'iops_wr',
+                            width: 110,
+                            renderer: pendingRenderer(),
+                        },
+                        {
+                            header: gettext('Read Burst (MiB/s)'),
+                            dataIndex: 'mbps_rd_max',
+                            width: 110,
+                            renderer: pendingRenderer(),
+                        },
+                        {
+                            header: gettext('Write Burst (MiB/s)'),
+                            dataIndex: 'mbps_wr_max',
+                            width: 110,
+                            renderer: pendingRenderer(),
+                        },
+                        {
+                            header: gettext('Read Burst (ops/s)'),
+                            dataIndex: 'iops_rd_max',
+                            width: 110,
+                            renderer: pendingRenderer(),
+                        },
+                        {
+                            header: gettext('Write Burst (ops/s)'),
+                            dataIndex: 'iops_wr_max',
+                            width: 110,
+                            renderer: pendingRenderer(),
+                        },
+                    ],
+                },
+                {
+                    header: gettext('Detect Zeroes'),
+                    dataIndex: 'detect_zeroes',
+                    hidden: true,
+                    renderer: pendingRenderer(booleanRenderer),
+                },
+                {
+                    header: gettext('Vendor'),
+                    dataIndex: 'vendor',
+                    hidden: true,
+                    renderer: pendingRenderer(),
+                },
+                {
+                    header: gettext('Product'),
+                    dataIndex: 'product',
+                    hidden: true,
+                    renderer: pendingRenderer(),
+                },
+                {
+                    header: gettext('Serial'),
+                    dataIndex: 'serial',
+                    hidden: true,
+                    renderer: pendingRenderer(),
+                },
+                {
+                    header: gettext('Queues'),
+                    dataIndex: 'queues',
+                    hidden: true,
+                    renderer: pendingRenderer(),
+                },
+                {
+                    header: gettext('Shared'),
+                    dataIndex: 'shared',
+                    hidden: true,
+                    renderer: pendingRenderer(booleanRenderer),
+                },
+                {
+                    header: gettext('Write Error Action'),
+                    dataIndex: 'werror',
+                    hidden: true,
+                    renderer: pendingRenderer(),
+                },
+                {
+                    header: 'WWN',
+                    dataIndex: 'wwn',
+                    hidden: true,
+                    renderer: pendingRenderer(),
+                },
+                {
+                    header: gettext('Raw Config'),
+                    dataIndex: 'raw',
+                    hidden: true,
+                    flex: 1,
+                    renderer: pendingRenderer(),
+                },
+            ],
+        });
+
+        let netGrid = Ext.create('Ext.grid.Panel', {
+            reference: 'netGrid',
+            title: gettext('Network Interfaces'),
+            border: false,
+            stateful: true,
+            stateId: 'pve-qemu-hardware-net',
+            expandable: false,
+            iconCls: 'fa fa-exchange',
+            minHeight: 100,
+            emptyText: gettext('No network interfaces configured'),
+            store: {},
+            columns: [
+                {
+                    header: gettext('ID'),
+                    dataIndex: 'id',
+                    flex: 1,
+                    minWidth: 80,
+                    renderer: pendingRenderer(),
+                },
+                {
+                    header: gettext('Model'),
+                    dataIndex: 'model',
+                    width: 100,
+                    renderer: pendingRenderer(),
+                },
+                {
+                    header: gettext('MAC address'),
+                    dataIndex: 'macaddr',
+                    width: 200,
+                    renderer: pendingRenderer(),
+                },
+                {
+                    header: gettext('Bridge'),
+                    dataIndex: 'bridge',
+                    width: 100,
+                    renderer: pendingRenderer(),
+                },
+                {
+                    header: gettext('Firewall enabled'),
+                    dataIndex: 'firewall',
+                    width: 150,
+                    renderer: pendingRenderer(booleanRenderer),
+                },
+                {
+                    header: gettext('VLAN tag'),
+                    dataIndex: 'tag',
+                    width: 80,
+                    renderer: pendingRenderer(),
+                },
+                {
+                    header: gettext('MTU'),
+                    dataIndex: 'mtu',
+                    width: 100,
+                    renderer: pendingRenderer(),
+                },
+                {
+                    header: gettext('Multiqueue'),
+                    dataIndex: 'queues',
+                    width: 100,
+                    renderer: pendingRenderer(),
+                },
+                {
+                    header: gettext('Rate limit (MB/s)'),
+                    dataIndex: 'rate',
+                    width: 200,
+                    renderer: pendingRenderer(),
+                },
+                {
+                    header: gettext('Disconnected'),
+                    dataIndex: 'link_down',
+                    width: 100,
+                    renderer: pendingRenderer(booleanRenderer),
+                },
+                {
+                    header: gettext('Trunks'),
+                    dataIndex: 'trunks',
+                    width: 100,
+                },
+            ],
+        });
+
+        hardwareGrid.mon(hardwareGrid.getStore(), 'refresh', function () {
+            let diskData = [];
+            let nicData = [];
+            let pendingData = [];
+            let data = hardwareGrid.getStore().getData();
+            (data.getSource() ?? data).each(function (rec) {
+                let key = rec.get('key');
+                let val = rec.get('value');
+                let pending = rec.get('pending');
+                let deleted = rec.get('delete');
+                if (keyForDiskGrid(key)) {
+                    let isDisk = (value) => !value.match(/media=cdrom/) && !isCloudInitKey(value);
+
+                    let parsed = PVE.Parser.parseQemuDrive(key, val);
+                    if (parsed && val && isDisk(val)) {
+                        parsed.id = key;
+                        parsed.raw = val;
+                        parsed.deleted = deleted;
+                        diskData.push(parsed);
+                    }
+
+                    if (pending && isDisk(pending)) {
+                        if (!parsed) {
+                            diskData.push({
+                                id: key,
+                                raw: '',
+                            });
+                        }
+                        parsed = PVE.Parser.parseQemuDrive(key, pending);
+                        parsed.id = key;
+                        parsed.raw = pending;
+                        pendingData.push(parsed);
+                    }
+                } else if (PVE.Utils.keyIsNic(key)) {
+                    let parsed = PVE.Parser.parseQemuNetwork(key, val);
+                    if (parsed) {
+                        parsed.id = key;
+                        parsed.raw = val;
+                        parsed.deleted = deleted;
+                        nicData.push(parsed);
+                    }
+                    if (pending) {
+                        if (!parsed) {
+                            nicData.push({
+                                id: key,
+                                raw: '',
+                            });
+                        }
+                        parsed = PVE.Parser.parseQemuNetwork(key, pending);
+                        parsed.id = key;
+                        parsed.raw = pending;
+                        pendingData.push(parsed);
+                    }
+                }
+            });
+
+            pendingStore.loadData(pendingData);
+            netGrid.getStore().loadData(nicData);
+            diskGrid.getStore().loadData(diskData);
+        });
+
+        let sm = Ext.create('Ext.selection.Model', {
+            getSelection: () => {
+                let hardwareSelection = hardwareGrid.getSelection();
+                if (hardwareSelection.length > 0) {
+                    return hardwareSelection;
+                }
+
+                let diskSelection = diskGrid.getSelection();
+                if (diskSelection.length > 0) {
+                    let key = diskSelection[0].get('id');
+                    // Return original record with 'pending'/'value' fields expected by tools
+                    return [hardwareGrid.rstore.getById(key)];
+                }
+
+                let nicSelection = netGrid.getSelection();
+                if (nicSelection.length > 0) {
+                    let key = nicSelection[0].get('id');
+                    // Return original record with 'pending'/'value' fields expected by tools
+                    return [hardwareGrid.rstore.getById(key)];
+                }
+                return [];
+            },
+        });
+
+        let onSelectionChange = function (sm, selected) {
+            let me = this;
+            if (selected.length) {
+                if (me !== hardwareGrid) {
+                    hardwareGrid.getSelectionModel().deselectAll();
+                }
+                if (me !== diskGrid) {
+                    diskGrid.getSelectionModel().deselectAll();
+                }
+                if (me !== netGrid) {
+                    netGrid.getSelectionModel().deselectAll();
+                }
+            }
+            set_button_status();
+        };
+
+        hardwareGrid.on('selectionchange', onSelectionChange);
+        diskGrid.on('selectionchange', onSelectionChange);
+        netGrid.on('selectionchange', onSelectionChange);
 
         let run_editor = function () {
+            let me = hardwareGrid;
             let rec = sm.getSelection()[0];
             if (!rec || !rows[rec.data.key]?.editor) {
                 return;
@@ -449,6 +937,10 @@ Ext.define('PVE.qemu.HardwareView', {
             }
         };
 
+        hardwareGrid.on('itemdblclick', run_editor);
+        diskGrid.on('itemdblclick', run_editor);
+        netGrid.on('itemdblclick', run_editor);
+
         let edit_btn = new Proxmox.button.Button({
             text: gettext('Edit'),
             selModel: sm,
@@ -492,7 +984,6 @@ Ext.define('PVE.qemu.HardwareView', {
             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) {
@@ -515,7 +1006,6 @@ Ext.define('PVE.qemu.HardwareView', {
             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) {
@@ -538,7 +1028,6 @@ Ext.define('PVE.qemu.HardwareView', {
         let resize_menuitem = new Ext.menu.Item({
             text: gettext('Resize'),
             iconCls: 'fa fa-plus',
-            selModel: sm,
             handler: () => {
                 let rec = sm.getSelection()[0];
                 if (!rec) {
@@ -621,7 +1110,7 @@ Ext.define('PVE.qemu.HardwareView', {
                 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 rendered = hardwareGrid.renderKey(rec.data.key, {}, rec);
                 let msg = Ext.String.format(warn, `'${rendered}'`);
 
                 if (rows[rec.data.key].del_extra_msg) {
@@ -630,7 +1119,8 @@ Ext.define('PVE.qemu.HardwareView', {
                 return msg;
             },
             handler: function (btn, e, rec) {
-                let params = { delete: rec.data.key };
+                let record = sm.getSelection()[0];
+                let params = { delete: record.data.key };
                 if (btn.RESTMethod === 'POST') {
                     params.background_delay = 5;
                 }
@@ -673,6 +1163,8 @@ Ext.define('PVE.qemu.HardwareView', {
 
         let revert_btn = new PVE.button.PendingRevert({
             apiurl: '/api2/extjs/' + baseurl,
+            selModel: sm,
+            pendingGrid: hardwareGrid,
         });
 
         let efidisk_menuitem = Ext.create('Ext.menu.Item', {
@@ -680,8 +1172,7 @@ Ext.define('PVE.qemu.HardwareView', {
             iconCls: 'fa fa-fw fa-hdd-o black',
             disabled: !caps.vms['VM.Config.Disk'],
             handler: function () {
-                let { data: bios } = me.rstore.getData().map.bios || {};
-
+                let { data: bios } = hardwareGrid.rstore.getData().map.bios || {};
                 Ext.create('PVE.qemu.EFIDiskEdit', {
                     autoShow: true,
                     url: '/api2/extjs/' + baseurl,
@@ -703,12 +1194,12 @@ Ext.define('PVE.qemu.HardwareView', {
         };
 
         let set_button_status = function () {
-            let selection_model = me.getSelectionModel();
+            let selection_model = sm;
             let rec = selection_model.getSelection()[0];
 
             counts = {}; // en/disable hardwarebuttons
             let hasCloudInit = false;
-            me.rstore.getData().items.forEach(function ({ id, data }) {
+            hardwareGrid.rstore.getData().items.forEach(function ({ id, data }) {
                 if (!hasCloudInit && (isCloudInitKey(data.value) || isCloudInitKey(data.pending))) {
                     hasCloudInit = true;
                     return;
@@ -751,9 +1242,8 @@ Ext.define('PVE.qemu.HardwareView', {
             }
             const { key, value } = rec.data;
             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);
@@ -821,10 +1311,6 @@ Ext.define('PVE.qemu.HardwareView', {
         };
 
         Ext.apply(me, {
-            url: `/api2/json/nodes/${nodename}/qemu/${vmid}/pending`,
-            interval: 5000,
-            selModel: sm,
-            run_editor: run_editor,
             tbar: [
                 {
                     text: gettext('Add'),
@@ -926,19 +1412,32 @@ Ext.define('PVE.qemu.HardwareView', {
                 diskaction_btn,
                 revert_btn,
             ],
-            rows: rows,
-            sorterFn: sorterFn,
-            listeners: {
-                itemdblclick: run_editor,
-                selectionchange: set_button_status,
-            },
+            items: [
+                hardwareGrid,
+                // spacer with bottom border
+                {
+                    xtype: 'panel',
+                    border: true,
+                    height: 1,
+                    margin: '20 0 0 0',
+                },
+                diskGrid,
+                // spacer with bottom border
+                {
+                    xtype: 'panel',
+                    border: true,
+                    height: 1,
+                    margin: '20 0 0 0',
+                },
+                netGrid,
+            ],
         });
 
         me.callParent();
 
-        me.on('activate', me.rstore.startUpdate, me.rstore);
-        me.on('destroy', me.rstore.stopUpdate, me.rstore);
+        me.on('activate', () => hardwareGrid.rstore.startUpdate());
+        me.on('destroy', () => hardwareGrid.rstore.stopUpdate());
 
-        me.mon(me.getStore(), 'datachanged', set_button_status, me);
+        hardwareGrid.mon(hardwareGrid.getStore(), 'datachanged', set_button_status, me);
     },
 });
-- 
2.47.3





      parent reply	other threads:[~2026-05-08 13:41 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-05-08 13:38 [PATCH manager v2 0/3] ui: split out disks and nics into grids Dominik Csapak
2026-05-08 13:38 ` [PATCH manager v2 1/3] ui: utils: factor out 'media=cdrom' check Dominik Csapak
2026-05-08 13:38 ` [PATCH manager v2 2/3] ui: factor out the guest key nic regex check Dominik Csapak
2026-05-08 13:38 ` Dominik Csapak [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260508134110.4001168-4-d.csapak@proxmox.com \
    --to=d.csapak@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is 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