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