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);
>>>> },
>>>> });
>>>
>>>
>>>
>>>
>>>
>
next prev parent 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