public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: David Riley <d.riley@proxmox.com>
To: Dominik Csapak <d.csapak@proxmox.com>, pve-devel@lists.proxmox.com
Subject: Re: [PATCH manager] ui: qemu hardware view: split out disks and nics into grids
Date: Mon, 11 May 2026 15:02:50 +0200	[thread overview]
Message-ID: <c0fa8494-a5b2-44cd-b02d-3f9925fcae65@proxmox.com> (raw)
In-Reply-To: <84642f51-675c-453a-a063-2983ccf65b3e@proxmox.com>


On 5/8/26 3:40 PM, Dominik Csapak wrote:
>
>
> On 5/6/26 10:46 AM, David Riley wrote:
>> Thanks for sending this patch.
>> I checked it out and it works as expected and visually looks good.
>>
>> The only small thing I noticed in the UI is the horizontal scrollbar that
>> is overlapping with the contents of the last row. Making it cumbersome
>> to read while scrolling horizontal. This is only the case if there is more
>> than 1 element present in the table. Not sure why the padding differs
>> if there is more than 1 element, because it looks fine if there is only
>> one element.
>
> could only observe that in firefox, in chromium it works as intended.
> Not sure if we can fix this easily since it seems like an issue
> with how extjs calculates the column widths and how firefox
> renders the scrollbar. if the column widths are small enough
> the scrollbar vanished and its not an issue.
>
> i think we'd have the same issue in every grid that has a horizontal
> scrollbar?
>
You are right this seems to be a general issue, but thanks for taking the
time to look into it.
>>
>> Some smaller nitpicks inline, but overall it looks good to me.
>>
>> On 5/5/26 10:39 AM, Dominik Csapak wrote:
>>> ping,
>>>
>>> would be nice to have something like this included IMHO
>>> was just waiting on feedback before sending for containers too
>>>
>>> On 2/27/26 10:11 AM, Dominik Csapak wrote:
>>>> 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 porperty 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.
>>>>
>>>> In the PendingRevert button, the set 'selModel' is used first to try
>>>> and get the current record, otherwise the whole handler would have to be
>>>> overwritten.
>>>>
>>>> Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
>>>> ---
>>>> If this is the way we want to continue with this, I'd send additional
>>>> patches for unifying the resources panel for contains in the same
>>>> manner. Namely splitting out the disks in a panel there and merging the
>>>> network panel into the resources.
>>>>
>>>> Also what we could do with this now is to e.g. introduce action columns
>>>> for the disks/nics to move the 'disk action' menu to, making it less
>>>> confusing and cleaning up the toolbar.
>>>> (though for that we would probably have to move the efidisk to the disks
>>>> too, but lets do it one step at a time)
>>>>
>>>>   www/manager6/button/Revert.js     |   2 +-
>>>>   www/manager6/qemu/HardwareView.js | 521 ++++++++++++++++++++++++++++--
>>>>   2 files changed, 495 insertions(+), 28 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 cf5e2a0f..577b4a8e 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;
>>>>   @@ -393,9 +415,444 @@ 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,
>>>> +        });
>>>> +
>>>> +        hardwareGrid.getStore().addFilter({
>>>> +            filterFn: function (rec) {
>>>> +                let val = rec.get('value') || rec.get('pending');
>>>> +                let key = rec.get('key');
>>>> +                if (key.match(/^(ide|sata|scsi|virtio|unused)\d+$/)) {
>>
>> nit: might make sense to have a single source of truth for these regex patterns.
>> As these are used more than once.
>> Same for:
>> * /^(ide|sata|scsi|virtio|unused)\d+$/
>> * /^net\d+$/
>> * /media=cdrom/
>>
>>>> +                    let isCdrom = val && val.match(/media=cdrom/);
>>>> +                    let isCloudInit = isCloudInitKey(val);
>>>> +                    if (!isCdrom && !isCloudInit) {
>>>> +                        return false;
>>>> +                    }
>>>> +                } else if (key.match(/^net\d+$/)) {
>>>> +                    return false;
>>>> +                }
>>>> +                return true;
>>>> +            },
>>>> +        });
>>>> +
>>>> +        let pendingStore = Ext.create('Ext.data.Store');
>>>> +        let pendingRenderer = function (originalRenderer) {
>>>> +            originalRenderer ??= Ext.htmlEncode;
>>>> +            return function (val, mD, rec, rI, cI, store, view) {
>> nit: It might be more readable to use: metaData, rowIndex, colIndex
>> took me a second to figure it out.
>>>> +                let txt = originalRenderer(val, mD, rec, rI, cI, store, view);
>>>> +                let pending = pendingStore.getById(rec.get('id'));
>>>> +                if (pending) {
>>>> +                    let dataIndex = view.up().getColumns() [cI].dataIndex;
>>>> +                    let value = pending.get(dataIndex);
>>>> +                    let pendingTxt = originalRenderer(value, mD, rec, rI, cI, store, view);
>>>> +                    if (pendingTxt !== txt) {
>>>> +                        if (txt) {
>>>> +                            if (pendingTxt) {
>>>> +                                txt += '<br />';
>>>> +                                txt += `<span style="color: darkorange">${pendingTxt}</span>`;
>>>> +                            } else {
>>>> +                                txt = `<div style="color: darkorange; text-decoration: line-through;">${txt}</div>`;
>>>> +                            }
>>>> +                        } else {
>>>> +                            txt = `<span style="color: orange">${pendingTxt}</span>`;
>>>> +                        }
>>>> +                    }
>>>> +                } else {
>>>> +                    let hwRec = hardwareGrid.getStore().getById(rec.get('id'));
>>>> +                    if (hwRec.data.delete && txt) {
>>>> +                        txt = `<div style="color: darkorange; text- decoration: line-through;">${txt}</div>`;
>>>> +                    }
>>>> +                }
>>>> +
>>>> +                return txt;
>>>> +            };
>>>> +        };
>>>> +
>>>> +        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,
>>>> +                    renderer: pendingRenderer(),
>>>> +                },
>>>> +                {
>>>> +                    header: gettext('Size'),
>>>> +                    dataIndex: 'size',
>>>> +                    width: 80,
>>>> +                    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(Proxmox.Utils.format_boolean),
>>>> +                },
>>>> +                {
>>>> +                    header: gettext('IO thread'),
>>>> +                    dataIndex: 'iothread',
>>>> +                    width: 100,
>>>> +                    renderer: pendingRenderer(Proxmox.Utils.format_boolean),
>>>> +                },
>>>> +                {
>>>> +                    header: gettext('SSD emulation'),
>>>> +                    dataIndex: 'ssd',
>>>> +                    width: 120,
>>>> +                    renderer: pendingRenderer(Proxmox.Utils.format_boolean),
>>>> +                },
>>>> +                {
>>>> +                    header: gettext('Backup'),
>>>> +                    dataIndex: 'backup',
>>>> +                    width: 80,
>>>> +                    renderer: pendingRenderer((v) => Proxmox.Utils.format_boolean(v ?? true)),
>>>> +                },
>>>> +                {
>>>> +                    header: gettext('Read-Only'),
>>>> +                    dataIndex: 'ro',
>>>> +                    width: 100,
>>>> +                    renderer: pendingRenderer(Proxmox.Utils.format_boolean),
>>>> +                },
>>>> +                {
>>>> +                    header: gettext('Replicate'),
>>>> +                    dataIndex: 'replicate',
>>>> +                    width: 100,
>>>> +                    renderer: pendingRenderer(Proxmox.Utils.format_boolean),
>>>> +                },
>>>> +                {
>>>> +                    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(Proxmox.Utils.format_boolean),
>>>> +                },
>>>> +                {
>>>> +                    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(Proxmox.Utils.format_boolean),
>>>> +                },
>>>> +                {
>>>> +                    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',
>>>> +                    width: 100,
>>>> +                    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(Proxmox.Utils.format_boolean),
>>>> +                },
>>>> +                {
>>>> +                    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(Proxmox.Utils.format_boolean),
>>>> +                },
>>>> +                {
>>>> +                    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');
>>>> +                if (key.match(/^(ide|sata|scsi|virtio|unused)\d+$/)) {
>>>> +                    let parsed = PVE.Parser.parseQemuDrive(key, val);
>>>> +                    if (parsed) {
>>>> +                        let isCdrom = parsed.media === 'cdrom';
>>>> +                        let isCloudInit = isCloudInitKey(val);
>>>> +                        if (!isCdrom && !isCloudInit && val) {
>>>> +                            parsed.id = key;
>>>> +                            parsed.raw = val;
>>>> +                            diskData.push(parsed);
>>>> +                            if (pending) {
>>>> +                                parsed = PVE.Parser.parseQemuDrive(key, pending);
>>>> +                                parsed.id = key;
>>>> +                                parsed.raw = pending;
>>>> +                                pendingData.push(parsed);
>>>> +                            }
>>>> +                        }
>>>> +                    }
>>>> +                } else if (key.match(/^net\d+$/)) {
>>>> +                    let parsed = PVE.Parser.parseQemuNetwork(key, val);
>>>> +                    if (parsed) {
>>>> +                        parsed.id = key;
>>>> +                        nicData.push(parsed);
>>>> +                        if (pending) {
>>>> +                            parsed = PVE.Parser.parseQemuNetwork(key, pending);
>>>> +                            parsed.id = key;
>>>> +                            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
>>>> +                    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
>>>> +                    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;
>>>> @@ -435,6 +892,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,
>>>> @@ -446,7 +907,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) {
>>>> @@ -469,7 +929,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) {
>>>> @@ -492,7 +951,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) {
>>>> @@ -531,7 +989,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) {
>>>> @@ -540,7 +998,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;
>>>>                   }
>>>> @@ -583,6 +1042,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', {
>>>> @@ -590,8 +1051,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,
>>>> @@ -613,12 +1073,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;
>>>> @@ -661,14 +1121,12 @@ 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);
>>>>               const isCDRom = value && !!value.toString().match(/ media=cdrom/);
>>>> -
>>>>               const isUnusedDisk = key.match(/^unused\d+/);
>>>>               const isUsedDisk = !isUnusedDisk && row.isOnStorageBus && !isCDRom;
>>>>               const isDisk = isUnusedDisk || isUsedDisk;
>>>> @@ -720,10 +1178,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'),
>>>> @@ -825,19 +1279,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);
>>>>       },
>>>>   });
>>>
>>>
>>>
>>>
>>>
>




  reply	other threads:[~2026-05-11 13:03 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-02-27  9:05 [PATCH manager] ui: qemu hardware view: split out disks and nics into grids Dominik Csapak
2026-05-05  8:40 ` Dominik Csapak
2026-05-06  8:48   ` David Riley
2026-05-08 13:42     ` Dominik Csapak
2026-05-11 13:02       ` David Riley [this message]
2026-05-08 13:43 ` superseded: " Dominik Csapak

Reply instructions:

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

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

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

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

  git send-email \
    --in-reply-to=c0fa8494-a5b2-44cd-b02d-3f9925fcae65@proxmox.com \
    --to=d.riley@proxmox.com \
    --cc=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