From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id B46381FF13B for ; Wed, 06 May 2026 10:49:10 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 531FD1A647; Wed, 6 May 2026 10:49:08 +0200 (CEST) Message-ID: <3e2705c9-09a4-42b7-80a3-aee28e13ed12@proxmox.com> Date: Wed, 6 May 2026 10:48:26 +0200 MIME-Version: 1.0 User-Agent: Mozilla Thunderbird Subject: Re: [PATCH manager] ui: qemu hardware view: split out disks and nics into grids To: Dominik Csapak , pve-devel@lists.proxmox.com References: <20260227091158.715596-1-d.csapak@proxmox.com> Content-Language: en-US From: David Riley In-Reply-To: Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1778057199686 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.234 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment POISEN_SPAM_PILL 0.1 Meta: its spam POISEN_SPAM_PILL_1 0.1 random spam to be learned in bayes POISEN_SPAM_PILL_3 0.1 random spam to be learned in bayes SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [parsed.media,parsed.id] Message-ID-Hash: LAJ7KIED3BL4V4B27AIRMIHBRT3WOFFS X-Message-ID-Hash: LAJ7KIED3BL4V4B27AIRMIHBRT3WOFFS X-MailFrom: d.riley@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: 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. 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 >> --- >> 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 += '
'; >> +                                txt += `${pendingTxt}`; >> +                            } else { >> +                                txt = `
${txt}
`; >> +                            } >> +                        } else { >> +                            txt = `${pendingTxt}`; >> +                        } >> +                    } >> +                } else { >> +                    let hwRec = hardwareGrid.getStore().getById(rec.get('id')); >> +                    if (hwRec.data.delete && txt) { >> +                        txt = `
${txt}
`; >> +                    } >> +                } >> + >> +                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); >>       }, >>   }); > > > > >