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: Re: [PATCH manager] ui: qemu hardware view: split out disks and nics into grids
Date: Tue, 5 May 2026 10:40:31 +0200	[thread overview]
Message-ID: <c02e6f57-ed1c-4c4a-b017-e6d31b5f9c60@proxmox.com> (raw)
In-Reply-To: <20260227091158.715596-1-d.csapak@proxmox.com>

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+$/)) {
> +                    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) {
> +                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-05  8:41 UTC|newest]

Thread overview: 2+ 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 [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=c02e6f57-ed1c-4c4a-b017-e6d31b5f9c60@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