all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH manager] ui: HD edit: Add multiple disks & tabs
@ 2021-06-16 11:34 Dominic Jäger
  2021-06-18 12:56 ` Dominik Csapak
  0 siblings, 1 reply; 2+ messages in thread
From: Dominic Jäger @ 2021-06-16 11:34 UTC (permalink / raw)
  To: pve-devel

Enable adding multiple disks in VM create wizard.
This is a first step for future import features.

Split disk edit panel into multiple tabbed panels to make it less cluttered.
This affects the create wizard & the HD edit windows in the VM hardware view.

Signed-off-by: Dominic Jäger <d.jaeger@proxmox.com>
---
 www/manager6/Makefile                         |   6 +-
 www/manager6/form/ControllerSelector.js       |  20 +
 www/manager6/qemu/CDEdit.js                   |   3 -
 www/manager6/qemu/CreateWizard.js             |  16 +-
 www/manager6/qemu/HDEdit.js                   | 409 ------------------
 www/manager6/qemu/HardwareView.js             |   9 +-
 www/manager6/qemu/OSTypeEdit.js               |  12 +-
 .../qemu/disk/DiskBandwidthOptions.js         | 192 ++++++++
 www/manager6/qemu/disk/DiskBasicOptions.js    | 153 +++++++
 www/manager6/qemu/disk/DiskCollection.js      | 282 ++++++++++++
 www/manager6/qemu/disk/DiskData.js            | 241 +++++++++++
 www/manager6/qemu/disk/HardDisk.js            | 215 +++++++++
 www/manager6/window/Wizard.js                 |   2 +
 13 files changed, 1139 insertions(+), 421 deletions(-)
 delete mode 100644 www/manager6/qemu/HDEdit.js
 create mode 100644 www/manager6/qemu/disk/DiskBandwidthOptions.js
 create mode 100644 www/manager6/qemu/disk/DiskBasicOptions.js
 create mode 100644 www/manager6/qemu/disk/DiskCollection.js
 create mode 100644 www/manager6/qemu/disk/DiskData.js
 create mode 100644 www/manager6/qemu/disk/HardDisk.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 6776d4ce..95f03d88 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -198,7 +198,11 @@ JSSRC= 							\
 	qemu/Config.js					\
 	qemu/CreateWizard.js				\
 	qemu/DisplayEdit.js				\
-	qemu/HDEdit.js					\
+	qemu/disk/DiskCollection.js			\
+	qemu/disk/HardDisk.js				\
+	qemu/disk/DiskData.js				\
+	qemu/disk/DiskBasicOptions.js			\
+	qemu/disk/DiskBandwidthOptions.js		\
 	qemu/HDEfi.js					\
 	qemu/HDMove.js					\
 	qemu/HDResize.js				\
diff --git a/www/manager6/form/ControllerSelector.js b/www/manager6/form/ControllerSelector.js
index daca2432..85f66956 100644
--- a/www/manager6/form/ControllerSelector.js
+++ b/www/manager6/form/ControllerSelector.js
@@ -72,6 +72,26 @@ Ext.define('PVE.form.ControllerSelector', {
 	deviceid.validate();
     },
 
+    deleteFromVMConfig: function(key) {
+	const me = this;
+	delete me.vmconfig[key];
+    },
+
+    getValues: function() {
+	return this.query('field').map(x => x.getValue());
+    },
+
+    getValuesAsString: function() {
+	return this.getValues().join('');
+    },
+
+    setValue: function(value) {
+	const regex = /([a-z]+)(\d+)/;
+	const [_, controller, deviceid] = regex.exec(value);
+	this.down('field[name=controller]').setValue(controller);
+	this.down('field[name=deviceid]').setValue(deviceid);
+    },
+
     initComponent: function() {
 	var me = this;
 
diff --git a/www/manager6/qemu/CDEdit.js b/www/manager6/qemu/CDEdit.js
index 72c01037..27092d32 100644
--- a/www/manager6/qemu/CDEdit.js
+++ b/www/manager6/qemu/CDEdit.js
@@ -84,9 +84,6 @@ Ext.define('PVE.qemu.CDInputPanel', {
 	    checked: true,
 	    listeners: {
 		change: function(f, value) {
-		    if (!me.rendered) {
-			return;
-		    }
 		    me.down('field[name=cdstorage]').setDisabled(!value);
 		    var cdImageField = me.down('field[name=cdimage]');
 		    cdImageField.setDisabled(!value);
diff --git a/www/manager6/qemu/CreateWizard.js b/www/manager6/qemu/CreateWizard.js
index d4535c9d..2405b6f7 100644
--- a/www/manager6/qemu/CreateWizard.js
+++ b/www/manager6/qemu/CreateWizard.js
@@ -154,7 +154,7 @@ Ext.define('PVE.qemu.CreateWizard', {
 	    insideWizard: true,
 	},
 	{
-	    xtype: 'pveQemuHDInputPanel',
+	    xtype: 'pveQemuDiskCollection',
 	    bind: {
 		nodename: '{nodename}',
 	    },
@@ -251,6 +251,20 @@ Ext.define('PVE.qemu.CreateWizard', {
 	    },
 	},
     ],
+
+    getValues: function() {
+	let values = this.callParent();
+	for (const [key, value] of Object.entries(values)) {
+	    const re = /ide\d+|sata\d+|virtio\d+|scsi\d+|import_sources/;
+	    if (key.match(re) && Array.isArray(value)) {
+		    // Collected from different panels => array
+		    // But API & some GUI functions expect not array
+		    const sep = key === 'import_sources' ? '\0' : ',';
+		    values[key] = value.join(sep);
+	    }
+	}
+	return values;
+    },
 });
 
 
diff --git a/www/manager6/qemu/HDEdit.js b/www/manager6/qemu/HDEdit.js
deleted file mode 100644
index 95a98b0b..00000000
--- a/www/manager6/qemu/HDEdit.js
+++ /dev/null
@@ -1,409 +0,0 @@
-/* 'change' property is assigned a string and then a function */
-Ext.define('PVE.qemu.HDInputPanel', {
-    extend: 'Proxmox.panel.InputPanel',
-    alias: 'widget.pveQemuHDInputPanel',
-    onlineHelp: 'qm_hard_disk',
-
-    insideWizard: false,
-
-    unused: false, // ADD usused disk imaged
-
-    vmconfig: {}, // used to select usused disks
-
-    viewModel: {},
-
-    controller: {
-
-	xclass: 'Ext.app.ViewController',
-
-	onControllerChange: function(field) {
-	    var value = field.getValue();
-
-	    var allowIOthread = value.match(/^(virtio|scsi)/);
-	    this.lookup('iothread').setDisabled(!allowIOthread);
-	    if (!allowIOthread) {
-		this.lookup('iothread').setValue(false);
-	    }
-
-	    var virtio = value.match(/^virtio/);
-	    this.lookup('ssd').setDisabled(virtio);
-	    if (virtio) {
-		this.lookup('ssd').setValue(false);
-	    }
-
-	    this.lookup('scsiController').setVisible(value.match(/^scsi/));
-	},
-
-	control: {
-	    'field[name=controller]': {
-		change: 'onControllerChange',
-		afterrender: 'onControllerChange',
-	    },
-	    'field[name=iothread]': {
-		change: function(f, value) {
-		    if (!this.getView().insideWizard) {
-			return;
-		    }
-		    var vmScsiType = value ? 'virtio-scsi-single': 'virtio-scsi-pci';
-		    this.lookupReference('scsiController').setValue(vmScsiType);
-		},
-	    },
-	},
-
-	init: function(view) {
-	    var vm = this.getViewModel();
-	    if (view.isCreate) {
-		vm.set('isIncludedInBackup', true);
-	    }
-	},
-    },
-
-    onGetValues: function(values) {
-	var me = this;
-
-	var params = {};
-	var confid = me.confid || values.controller + values.deviceid;
-
-	if (me.unused) {
-	    me.drive.file = me.vmconfig[values.unusedId];
-	    confid = values.controller + values.deviceid;
-	} else if (me.isCreate) {
-	    if (values.hdimage) {
-		me.drive.file = values.hdimage;
-	    } else {
-		me.drive.file = values.hdstorage + ":" + values.disksize;
-	    }
-	    me.drive.format = values.diskformat;
-	}
-
-	PVE.Utils.propertyStringSet(me.drive, !values.backup, 'backup', '0');
-	PVE.Utils.propertyStringSet(me.drive, values.noreplicate, 'replicate', 'no');
-	PVE.Utils.propertyStringSet(me.drive, values.discard, 'discard', 'on');
-	PVE.Utils.propertyStringSet(me.drive, values.ssd, 'ssd', 'on');
-	PVE.Utils.propertyStringSet(me.drive, values.iothread, 'iothread', 'on');
-	PVE.Utils.propertyStringSet(me.drive, values.cache, 'cache');
-
-        var names = ['mbps_rd', 'mbps_wr', 'iops_rd', 'iops_wr'];
-        Ext.Array.each(names, function(name) {
-            var burst_name = name + '_max';
-	    PVE.Utils.propertyStringSet(me.drive, values[name], name);
-	    PVE.Utils.propertyStringSet(me.drive, values[burst_name], burst_name);
-        });
-
-
-	params[confid] = PVE.Parser.printQemuDrive(me.drive);
-
-	return params;
-    },
-
-    setVMConfig: function(vmconfig) {
-	var me = this;
-
-	me.vmconfig = vmconfig;
-
-	if (me.bussel) {
-	    me.bussel.setVMConfig(vmconfig);
-	    me.scsiController.setValue(vmconfig.scsihw);
-	}
-	if (me.unusedDisks) {
-	    var disklist = [];
-	    Ext.Object.each(vmconfig, function(key, value) {
-		if (key.match(/^unused\d+$/)) {
-		    disklist.push([key, value]);
-		}
-	    });
-	    me.unusedDisks.store.loadData(disklist);
-	    me.unusedDisks.setValue(me.confid);
-	}
-    },
-
-    setDrive: function(drive) {
-	var me = this;
-
-	me.drive = drive;
-
-	var values = {};
-	var match = drive.file.match(/^([^:]+):/);
-	if (match) {
-	    values.hdstorage = match[1];
-	}
-
-	values.hdimage = drive.file;
-	values.backup = PVE.Parser.parseBoolean(drive.backup, 1);
-	values.noreplicate = !PVE.Parser.parseBoolean(drive.replicate, 1);
-	values.diskformat = drive.format || 'raw';
-	values.cache = drive.cache || '__default__';
-	values.discard = drive.discard === 'on';
-	values.ssd = PVE.Parser.parseBoolean(drive.ssd);
-	values.iothread = PVE.Parser.parseBoolean(drive.iothread);
-
-	values.mbps_rd = drive.mbps_rd;
-	values.mbps_wr = drive.mbps_wr;
-	values.iops_rd = drive.iops_rd;
-	values.iops_wr = drive.iops_wr;
-	values.mbps_rd_max = drive.mbps_rd_max;
-	values.mbps_wr_max = drive.mbps_wr_max;
-	values.iops_rd_max = drive.iops_rd_max;
-	values.iops_wr_max = drive.iops_wr_max;
-
-	me.setValues(values);
-    },
-
-    setNodename: function(nodename) {
-	var me = this;
-	me.down('#hdstorage').setNodename(nodename);
-	me.down('#hdimage').setStorage(undefined, nodename);
-    },
-
-    initComponent: function() {
-	var me = this;
-
-	var labelWidth = 140;
-
-	me.drive = {};
-
-	me.column1 = [];
-	me.column2 = [];
-
-	me.advancedColumn1 = [];
-	me.advancedColumn2 = [];
-
-	if (!me.confid || me.unused) {
-	    me.bussel = Ext.create('PVE.form.ControllerSelector', {
-		vmconfig: me.insideWizard ? { ide2: 'cdrom' } : {},
-	    });
-	    me.column1.push(me.bussel);
-
-	    me.scsiController = Ext.create('Ext.form.field.Display', {
-		fieldLabel: gettext('SCSI Controller'),
-		reference: 'scsiController',
-		bind: me.insideWizard ? {
-		    value: '{current.scsihw}',
-		} : undefined,
-		renderer: PVE.Utils.render_scsihw,
-		submitValue: false,
-		hidden: true,
-	    });
-	    me.column1.push(me.scsiController);
-	}
-
-	if (me.unused) {
-	    me.unusedDisks = Ext.create('Proxmox.form.KVComboBox', {
-		name: 'unusedId',
-		fieldLabel: gettext('Disk image'),
-		matchFieldWidth: false,
-		listConfig: {
-		    width: 350,
-		},
-		data: [],
-		allowBlank: false,
-	    });
-	    me.column1.push(me.unusedDisks);
-	} else if (me.isCreate) {
-	    me.column1.push({
-		xtype: 'pveDiskStorageSelector',
-		storageContent: 'images',
-		name: 'disk',
-		nodename: me.nodename,
-		autoSelect: me.insideWizard,
-	    });
-	} else {
-	    me.column1.push({
-		xtype: 'textfield',
-		disabled: true,
-		submitValue: false,
-		fieldLabel: gettext('Disk image'),
-                name: 'hdimage',
-	    });
-	}
-
-	me.column2.push(
-	    {
-		xtype: 'CacheTypeSelector',
-		name: 'cache',
-		value: '__default__',
-		fieldLabel: gettext('Cache'),
-	    },
-	    {
-		xtype: 'proxmoxcheckbox',
-		fieldLabel: gettext('Discard'),
-		reference: 'discard',
-		name: 'discard',
-	    },
-	);
-
-	me.advancedColumn1.push(
-	    {
-		xtype: 'proxmoxcheckbox',
-		disabled: me.confid && me.confid.match(/^virtio/),
-		fieldLabel: gettext('SSD emulation'),
-		labelWidth: labelWidth,
-		name: 'ssd',
-		reference: 'ssd',
-	    },
-	    {
-		xtype: 'proxmoxcheckbox',
-		disabled: me.confid && !me.confid.match(/^(virtio|scsi)/),
-		fieldLabel: 'IO thread',
-		labelWidth: labelWidth,
-		reference: 'iothread',
-		name: 'iothread',
-	    },
-	    {
-		xtype: 'numberfield',
-		name: 'mbps_rd',
-		minValue: 1,
-		step: 1,
-		fieldLabel: gettext('Read limit') + ' (MB/s)',
-		labelWidth: labelWidth,
-		emptyText: gettext('unlimited'),
-	    },
-	    {
-		xtype: 'numberfield',
-		name: 'mbps_wr',
-		minValue: 1,
-		step: 1,
-		fieldLabel: gettext('Write limit') + ' (MB/s)',
-		labelWidth: labelWidth,
-		emptyText: gettext('unlimited'),
-	    },
-	    {
-		xtype: 'proxmoxintegerfield',
-		name: 'iops_rd',
-		minValue: 10,
-		step: 10,
-		fieldLabel: gettext('Read limit') + ' (ops/s)',
-		labelWidth: labelWidth,
-		emptyText: gettext('unlimited'),
-	    },
-	    {
-		xtype: 'proxmoxintegerfield',
-		name: 'iops_wr',
-		minValue: 10,
-		step: 10,
-		fieldLabel: gettext('Write limit') + ' (ops/s)',
-		labelWidth: labelWidth,
-		emptyText: gettext('unlimited'),
-	    },
-	);
-
-	me.advancedColumn2.push(
-	    {
-		xtype: 'proxmoxcheckbox',
-		fieldLabel: gettext('Backup'),
-		autoEl: {
-		    tag: 'div',
-		    'data-qtip': gettext('Include volume in backup job'),
-		},
-		labelWidth: labelWidth,
-		name: 'backup',
-		bind: {
-		    value: '{isIncludedInBackup}',
-		},
-	    },
-	    {
-		xtype: 'proxmoxcheckbox',
-		fieldLabel: gettext('Skip replication'),
-		labelWidth: labelWidth,
-		name: 'noreplicate',
-	    },
-	    {
-		xtype: 'numberfield',
-		name: 'mbps_rd_max',
-		minValue: 1,
-		step: 1,
-		fieldLabel: gettext('Read max burst') + ' (MB)',
-		labelWidth: labelWidth,
-		emptyText: gettext('default'),
-	    },
-	    {
-		xtype: 'numberfield',
-		name: 'mbps_wr_max',
-		minValue: 1,
-		step: 1,
-		fieldLabel: gettext('Write max burst') + ' (MB)',
-		labelWidth: labelWidth,
-		emptyText: gettext('default'),
-	    },
-	    {
-		xtype: 'proxmoxintegerfield',
-		name: 'iops_rd_max',
-		minValue: 10,
-		step: 10,
-		fieldLabel: gettext('Read max burst') + ' (ops)',
-		labelWidth: labelWidth,
-		emptyText: gettext('default'),
-	    },
-	    {
-		xtype: 'proxmoxintegerfield',
-		name: 'iops_wr_max',
-		minValue: 10,
-		step: 10,
-		fieldLabel: gettext('Write max burst') + ' (ops)',
-		labelWidth: labelWidth,
-		emptyText: gettext('default'),
-	    },
-	);
-
-	me.callParent();
-    },
-});
-
-Ext.define('PVE.qemu.HDEdit', {
-    extend: 'Proxmox.window.Edit',
-
-    isAdd: true,
-
-    backgroundDelay: 5,
-
-    initComponent: function() {
-	var me = this;
-
-	var nodename = me.pveSelNode.data.node;
-	if (!nodename) {
-	    throw "no node name specified";
-	}
-
-	var unused = me.confid && me.confid.match(/^unused\d+$/);
-
-	me.isCreate = me.confid ? unused : true;
-
-	var ipanel = Ext.create('PVE.qemu.HDInputPanel', {
-	    confid: me.confid,
-	    nodename: nodename,
-	    unused: unused,
-	    isCreate: me.isCreate,
-	});
-
-	if (unused) {
-	    me.subject = gettext('Unused Disk');
-	} else if (me.isCreate) {
-            me.subject = gettext('Hard Disk');
-	} else {
-           me.subject = gettext('Hard Disk') + ' (' + me.confid + ')';
-	}
-
-	me.items = [ipanel];
-
-	me.callParent();
-	/* 'data' is assigned an empty array in same file, and here we
-	 * use it like an object
-	 */
-	me.load({
-	    success: function(response, options) {
-		ipanel.setVMConfig(response.result.data);
-		if (me.confid) {
-		    var value = response.result.data[me.confid];
-		    var drive = PVE.Parser.parseQemuDrive(me.confid, value);
-		    if (!drive) {
-			Ext.Msg.alert(gettext('Error'), 'Unable to parse drive options');
-			me.close();
-			return;
-		    }
-		    ipanel.setDrive(drive);
-		    me.isValid(); // trigger validation
-		}
-	    },
-	});
-    },
-});
diff --git a/www/manager6/qemu/HardwareView.js b/www/manager6/qemu/HardwareView.js
index 200e3c28..5126fab8 100644
--- a/www/manager6/qemu/HardwareView.js
+++ b/www/manager6/qemu/HardwareView.js
@@ -220,7 +220,7 @@ Ext.define('PVE.qemu.HardwareView', {
 	    rows[confid] = {
 		group: 10,
 		iconCls: 'hdd-o',
-		editor: 'PVE.qemu.HDEdit',
+		editor: 'PVE.qemu.HardDiskWindow',
 		isOnStorageBus: true,
 		header: gettext('Hard Disk') + ' (' + confid +')',
 		cdheader: gettext('CD/DVD Drive') + ' (' + confid +')',
@@ -290,7 +290,7 @@ Ext.define('PVE.qemu.HardwareView', {
 		order: i,
 		iconCls: 'hdd-o',
 		del_extra_msg: gettext('This will permanently erase all data.'),
-		editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.HDEdit' : undefined,
+		editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.HardDiskWindow' : undefined,
 		header: gettext('Unused Disk') + ' ' + i.toString(),
 	    };
 	}
@@ -630,9 +630,10 @@ Ext.define('PVE.qemu.HardwareView', {
 				iconCls: 'fa fa-fw fa-hdd-o black',
 				disabled: !caps.vms['VM.Config.Disk'],
 				handler: function() {
-				    let win = Ext.create('PVE.qemu.HDEdit', {
+				    let win = Ext.create('PVE.qemu.HardDiskWindow', {
 					url: '/api2/extjs/' + baseurl,
-					pveSelNode: me.pveSelNode,
+					nodename: me.pveSelNode.data.node,
+					isCreate: true,
 				    });
 				    win.on('destroy', me.reload, me);
 				    win.show();
diff --git a/www/manager6/qemu/OSTypeEdit.js b/www/manager6/qemu/OSTypeEdit.js
index 438d7c6b..641d9394 100644
--- a/www/manager6/qemu/OSTypeEdit.js
+++ b/www/manager6/qemu/OSTypeEdit.js
@@ -3,6 +3,7 @@ Ext.define('PVE.qemu.OSTypeInputPanel', {
     alias: 'widget.pveQemuOSTypePanel',
     onlineHelp: 'qm_os_settings',
     insideWizard: false,
+    ignoreDisks: false,
 
     controller: {
 	xclass: 'Ext.app.ViewController',
@@ -20,13 +21,18 @@ Ext.define('PVE.qemu.OSTypeInputPanel', {
 	},
 	onOSTypeChange: function(field) {
 	    var me = this, ostype = field.getValue();
-	    if (!me.getView().insideWizard) {
+	    const view = me.getView();
+	    if (!view.insideWizard) {
 		return;
 	    }
 	    var targetValues = PVE.qemu.OSDefaults.getDefaults(ostype);
-
-	    me.setWidget('pveBusSelector', targetValues.busType);
+	    if (!view.ignoreDisks) {
+		const ids = Ext.ComponentQuery.query('pveBusSelector')
+		    .reduce((acc, cur) => acc.concat(cur.id), []);
+		ids.forEach(i => me.setWidget(`#${i}`, targetValues.busType));
+	    }
 	    me.setWidget('pveNetworkCardSelector', targetValues.networkCard);
+	    me.setWidget('pveQemuBiosSelector', targetValues.bios);
 	    var scsihw = targetValues.scsihw || '__default__';
 	    this.getViewModel().set('current.scsihw', scsihw);
 	},
diff --git a/www/manager6/qemu/disk/DiskBandwidthOptions.js b/www/manager6/qemu/disk/DiskBandwidthOptions.js
new file mode 100644
index 00000000..58e08f59
--- /dev/null
+++ b/www/manager6/qemu/disk/DiskBandwidthOptions.js
@@ -0,0 +1,192 @@
+/* 'change' property is assigned a string and then a function */
+Ext.define('PVE.qemu.DiskBandwidthOptions', {
+    extend: 'Proxmox.panel.InputPanel',
+    alias: 'widget.pveQemuDiskBandwidthOptions',
+    onlineHelp: 'qm_hard_disk',
+
+    insideWizard: false,
+
+    unused: false, // ADD usused disk imaged
+
+    padding: '10 10 10 10',
+
+    vmconfig: {}, // used to select usused disks
+
+    viewModel: {},
+
+    /**
+     * All radiofields in pveQemuDiskCollection have the same scope
+     * Make name of radiofields unique for each disk panel
+     */
+    getRadioName() {
+	return 'radio_' + this.id;
+    },
+
+    onGetValues: function(values) {
+	const me = this;
+
+	let params = {};
+
+	const confid = me.up('pveQemuHardDisk').down('pveQemuDiskData').getConfid();
+
+	const names = ['mbps_rd', 'mbps_wr', 'iops_rd', 'iops_wr'];
+	Ext.Array.each(names, function(name) {
+	    let burst_name = name + '_max';
+	    PVE.Utils.propertyStringSet(me.drive, values[name], name);
+	    PVE.Utils.propertyStringSet(me.drive, values[burst_name], burst_name);
+	});
+
+	me.drive.file = 'dummy';
+	// no values => no comma
+	params[confid] = PVE.Parser.printQemuDrive(me.drive).replace(/^dummy,?/, "");
+
+	return params;
+    },
+
+    setVMConfig: function(vmconfig) {
+	let me = this;
+
+	me.vmconfig = vmconfig;
+
+	if (me.bussel) {
+	    me.bussel.setVMConfig(vmconfig);
+	    me.scsiController.setValue(vmconfig.scsihw);
+	}
+	if (me.unusedDisks) {
+	    let disklist = [];
+	    Ext.Object.each(vmconfig, function(key, value) {
+		if (key.match(/^unused\d+$/)) {
+		    disklist.push([key, value]);
+		}
+	    });
+	    me.unusedDisks.store.loadData(disklist);
+	    me.unusedDisks.setValue(me.confid);
+	}
+    },
+
+    setDrive: function(drive) {
+	let me = this;
+
+	me.drive = {};
+	[
+	    'interface',
+	    'index',
+	    'mbps_rd',
+	    'mbps_wr',
+	    'iops_rd',
+	    'iops_wr',
+	    'mbps_rd_max',
+	    'mbps_wr_max',
+	    'iops_rd_max',
+	    'iops_wr_max',
+	].forEach(o => { me.drive[o] = drive[o]; });
+
+	let values = {};
+	values.mbps_rd = drive.mbps_rd;
+	values.mbps_wr = drive.mbps_wr;
+	values.iops_rd = drive.iops_rd;
+	values.iops_wr = drive.iops_wr;
+	values.mbps_rd_max = drive.mbps_rd_max;
+	values.mbps_wr_max = drive.mbps_wr_max;
+	values.iops_rd_max = drive.iops_rd_max;
+	values.iops_wr_max = drive.iops_wr_max;
+
+	me.setValues(values);
+    },
+
+
+    setNodename: function(nodename) {
+	// nothing
+    },
+
+    initComponent: function() {
+	let me = this;
+
+	let labelWidth = 140;
+
+	me.drive = {};
+
+	me.column1 = [];
+	me.column2 = [];
+
+	me.column1.push(
+	    {
+		xtype: 'numberfield',
+		name: 'mbps_rd',
+		minValue: 1,
+		step: 1,
+		fieldLabel: gettext('Read limit') + ' (MB/s)',
+		labelWidth: labelWidth,
+		emptyText: gettext('unlimited'),
+	    },
+	    {
+		xtype: 'numberfield',
+		name: 'mbps_wr',
+		minValue: 1,
+		step: 1,
+		fieldLabel: gettext('Write limit') + ' (MB/s)',
+		labelWidth: labelWidth,
+		emptyText: gettext('unlimited'),
+	    },
+	    {
+		xtype: 'proxmoxintegerfield',
+		name: 'iops_rd',
+		minValue: 10,
+		step: 10,
+		fieldLabel: gettext('Read limit') + ' (ops/s)',
+		labelWidth: labelWidth,
+		emptyText: gettext('unlimited'),
+	    },
+	    {
+		xtype: 'proxmoxintegerfield',
+		name: 'iops_wr',
+		minValue: 10,
+		step: 10,
+		fieldLabel: gettext('Write limit') + ' (ops/s)',
+		labelWidth: labelWidth,
+		emptyText: gettext('unlimited'),
+	    },
+	);
+
+	me.column2.push(
+	    {
+		xtype: 'numberfield',
+		name: 'mbps_rd_max',
+		minValue: 1,
+		step: 1,
+		fieldLabel: gettext('Read max burst') + ' (MB)',
+		labelWidth: labelWidth,
+		emptyText: gettext('default'),
+	    },
+	    {
+		xtype: 'numberfield',
+		name: 'mbps_wr_max',
+		minValue: 1,
+		step: 1,
+		fieldLabel: gettext('Write max burst') + ' (MB)',
+		labelWidth: labelWidth,
+		emptyText: gettext('default'),
+	    },
+	    {
+		xtype: 'proxmoxintegerfield',
+		name: 'iops_rd_max',
+		minValue: 10,
+		step: 10,
+		fieldLabel: gettext('Read max burst') + ' (ops)',
+		labelWidth: labelWidth,
+		emptyText: gettext('default'),
+	    },
+	    {
+		xtype: 'proxmoxintegerfield',
+		name: 'iops_wr_max',
+		minValue: 10,
+		step: 10,
+		fieldLabel: gettext('Write max burst') + ' (ops)',
+		labelWidth: labelWidth,
+		emptyText: gettext('default'),
+	    },
+	);
+
+	me.callParent();
+    },
+});
diff --git a/www/manager6/qemu/disk/DiskBasicOptions.js b/www/manager6/qemu/disk/DiskBasicOptions.js
new file mode 100644
index 00000000..d582b1df
--- /dev/null
+++ b/www/manager6/qemu/disk/DiskBasicOptions.js
@@ -0,0 +1,153 @@
+/* 'change' property is assigned a string and then a function */
+Ext.define('PVE.qemu.DiskBasicOptions', {
+    extend: 'Proxmox.panel.InputPanel',
+    alias: 'widget.pveQemuDiskBasicOptions',
+    onlineHelp: 'qm_hard_disk',
+
+    insideWizard: false,
+
+    unused: false, // ADD usused disk imaged
+
+    padding: '10 10 10 10',
+
+    vmconfig: {}, // used to select usused disks
+
+    viewModel: {},
+
+    /**
+     * All radiofields in pveQemuDiskCollection have the same scope
+     * Make name of radiofields unique for each disk panel
+     */
+    getRadioName() {
+	return 'radio_' + this.id;
+    },
+
+    onGetValues: function(values) {
+	const me = this;
+
+	let params = {};
+
+	const confid = me.up('pveQemuHardDisk').down('pveQemuDiskData').getConfid();
+
+	PVE.Utils.propertyStringSet(me.drive, !values.backup, 'backup', '0');
+	PVE.Utils.propertyStringSet(me.drive, values.noreplicate, 'replicate', 'no');
+	PVE.Utils.propertyStringSet(me.drive, values.ssd, 'ssd', 'on');
+	PVE.Utils.propertyStringSet(me.drive, values.iothread, 'iothread', 'on');
+
+	me.drive.file = 'dummy';
+	// no values => no comma
+	params[confid] = PVE.Parser.printQemuDrive(me.drive).replace(/^dummy,?/, "");
+
+	return params;
+    },
+
+    setVMConfig: function(vmconfig) {
+	let me = this;
+
+	me.vmconfig = vmconfig;
+
+	if (me.bussel) {
+	    me.bussel.setVMConfig(vmconfig);
+	    me.scsiController.setValue(vmconfig.scsihw);
+	}
+	if (me.unusedDisks) {
+	    let disklist = [];
+	    Ext.Object.each(vmconfig, function(key, value) {
+		if (key.match(/^unused\d+$/)) {
+		    disklist.push([key, value]);
+		}
+	    });
+	    me.unusedDisks.store.loadData(disklist);
+	    me.unusedDisks.setValue(me.confid);
+	}
+    },
+
+    setDrive: function(drive) {
+	let me = this;
+
+	me.drive = {};
+
+	[
+	    'interface',
+	    'index',
+	    'backup',
+	    'replicate',
+	    'ssd',
+	    'iothread',
+	].forEach(o => { me.drive[o] = drive[o]; });
+
+	let values = {};
+	values.backup = PVE.Parser.parseBoolean(drive.backup, 1);
+	values.noreplicate = !PVE.Parser.parseBoolean(drive.replicate, 1);
+	values.ssd = PVE.Parser.parseBoolean(drive.ssd);
+	values.iothread = PVE.Parser.parseBoolean(drive.iothread);
+
+	me.setValues(values);
+    },
+
+
+    setNodename: function(nodename) {
+	// nothing
+    },
+
+    initComponent: function() {
+	let me = this;
+
+	let labelWidth = 140;
+
+	me.drive = {};
+
+	me.column1 = [];
+	me.column2 = [];
+
+	me.column1.push(
+	    {
+		xtype: 'proxmoxcheckbox',
+		disabled: me.confid && me.confid.match(/^virtio/),
+		fieldLabel: gettext('SSD emulation'),
+		labelWidth: labelWidth,
+		name: 'ssd',
+		reference: 'ssd',
+	    },
+	    {
+		xtype: 'proxmoxcheckbox',
+		disabled: me.confid && !me.confid.match(/^(virtio|scsi)/),
+		fieldLabel: 'IO thread',
+		labelWidth: labelWidth,
+		reference: 'iothread',
+		name: 'iothread',
+		listeners: {
+		    change: function(f, value) {
+			const disk = f.up('pveQemuHardDisk');
+			if (disk.insideWizard) {
+			    const vmScsiType = value ? 'virtio-scsi-single' : 'virtio-scsi-pci';
+			    disk.down('field[name=scsiController]').setValue(vmScsiType);
+			}
+		    },
+		},
+	    },
+	);
+
+	me.column2.push(
+	    {
+		xtype: 'proxmoxcheckbox',
+		fieldLabel: gettext('Backup'),
+		autoEl: {
+		    tag: 'div',
+		    'data-qtip': gettext('Include volume in backup job'),
+		},
+		labelWidth: labelWidth,
+		name: 'backup',
+		value: me.isCreate,
+	    },
+	    {
+		xtype: 'proxmoxcheckbox',
+		fieldLabel: gettext('Skip replication'),
+		labelWidth: labelWidth,
+		name: 'noreplicate',
+	    },
+	);
+
+	me.callParent();
+    },
+});
diff --git a/www/manager6/qemu/disk/DiskCollection.js b/www/manager6/qemu/disk/DiskCollection.js
new file mode 100644
index 00000000..79815244
--- /dev/null
+++ b/www/manager6/qemu/disk/DiskCollection.js
@@ -0,0 +1,282 @@
+Ext.define('PVE.qemu.DiskCollection', {
+    extend: 'Proxmox.panel.InputPanel',
+    alias: 'widget.pveQemuDiskCollection',
+
+    insideWizard: false,
+
+    isCreate: false,
+
+    hiddenDisks: [],
+
+    leftColumnRatio: 0.25,
+
+    column1: [
+	{
+	    // Adding to the panelContainer below automatically adds
+	    // items to the store
+	    xtype: 'gridpanel',
+	    scrollable: true,
+	    store: {
+		xtype: 'store',
+		storeId: 'diskstorage',
+		// Use the panel as id
+		// Panels have are objects and therefore unique
+		// E.g. while adding new panels 'device' is ambiguous
+		fields: ['device', 'panel'],
+		removeByPanel: function(panel) {
+		    const recordIndex = this.findBy(record => record.data.panel === panel);
+		    this.removeAt(recordIndex);
+		    return recordIndex;
+		},
+		getLast: function() {
+		    const last = this.getCount() - 1;
+		    return this.getAt(last);
+		},
+		listeners: {
+		    remove: function(store, records, index, isMove, eOpts) {
+			const view = Ext.ComponentQuery.query('pveQemuDiskCollection').shift();
+			records.forEach(r => {
+			    view.removePanel(r.get('panel'));
+			    view.deleteFromVMConfig(r.get('device'));
+			});
+		    },
+		},
+	    },
+	    enableColumnMove: false,
+	    enableColumnResize: false,
+	    enableColumnHide: false,
+	    columns: [
+		{
+		    text: gettext('Device'),
+		    dataIndex: 'device',
+		    flex: 4,
+		    menuDisabled: true,
+		},
+		{
+		    flex: 1,
+		    xtype: 'actioncolumn',
+		    align: 'center',
+		    menuDisabled: true,
+		    items: [
+			{
+			    iconCls: 'x-fa fa-trash critical',
+			    tooltip: 'Delete',
+			    handler: function(tableview, rowIndex, colIndex, item, event, record) {
+				Ext.getStore('diskstorage').remove(record);
+			    },
+			},
+		    ],
+		},
+	    ],
+	    listeners: {
+		select: function(_, record) {
+		    this.up('pveQemuDiskCollection')
+			.down('#panelContainer')
+			.setActiveItem(record.data.panel);
+		},
+	    },
+	    anchor: '100% 90%',
+	    selectLast: function() {
+		this.setSelection(this.store.getLast());
+	    },
+	    dockedItems: [
+		{
+		    xtype: 'toolbar',
+		    dock: 'bottom',
+		    ui: 'footer',
+		    style: {
+			backgroundColor: 'transparent',
+		    },
+		    layout: {
+			pack: 'center',
+		    },
+		    items: [
+			{
+			    iconCls: 'fa fa-plus-circle',
+			    itemId: 'addDisk',
+			    minWidth: '60',
+			    handler: function(button) {
+				button.up('pveQemuDiskCollection').addDisk();
+			    },
+			},
+		    ],
+		},
+	    ],
+	},
+    ],
+    column2: [
+	{
+	    itemId: 'panelContainer',
+	    xtype: 'container',
+	    layout: 'card',
+	    items: [],
+	    listeners: {
+		beforeRender: function() {
+		    // Initial disk if none have been added by manifest yet
+		    if (this.items.items.length === 0) {
+			this.addDisk();
+		    }
+		},
+		add: function(container, newPanel) {
+		    const store = Ext.getStore('diskstorage');
+		    store.add({ device: newPanel.getDevice(), panel: newPanel });
+		    container.setActiveItem(newPanel);
+		},
+	    },
+	    defaultItem: {
+		xtype: 'pveQemuHardDisk',
+		bind: {
+		    nodename: '{nodename}',
+		},
+		listeners: {
+		    // newPanel ... cloned + added defaultItem
+		    added: function(newPanel) {
+			Ext.Array.each(newPanel.down('pveControllerSelector').query('field'),
+			    function(field) {
+				//the fields don't exist earlier
+				field.on('change', function() {
+				    const store = Ext.getStore('diskstorage');
+
+				    // find by panel object because it is unique
+				    const recordIndex = store.findBy(record =>
+					record.data.panel === field.up('pveQemuHardDisk'),
+				    );
+				    const controllerSelector = field.up('pveControllerSelector');
+				    const newControllerAndId = controllerSelector.getValuesAsString();
+				    store.getAt(recordIndex).set('device', newControllerAndId);
+				});
+			    },
+			);
+			const wizard = this.up('pveQemuCreateWizard');
+			Ext.Array.each(this.query('field'), function(field) {
+			    field.on('change', wizard.validcheck);
+			    field.on('validitychange', wizard.validcheck);
+			});
+		    },
+		},
+		validator: function() {
+		    let valid = true;
+		    const fields = this.query('field, fieldcontainer');
+		    Ext.Array.each(fields, function(field) {
+			if (Ext.isFunction(field.isValid) && !field.isValid()) {
+				field.isValid();
+			    valid = false;
+			}
+		    });
+		    return valid;
+		},
+	    },
+
+	    // device ... device that the new disk should be assigned to, e.g. ide0, sata2
+	    // path ... content of the textfield with source path
+	    addDisk(device, path) {
+		const initialValues = this.up('window').getValues();
+		const item = Ext.clone(this.defaultItem);
+		item.insideWizard = this.insideWizard;
+		item.isCreate = this.isCreate;
+		const added = this.add(item);
+		// values in the storage will be updated by listeners
+		if (path) {
+		    // Need to explicitly deactivate when not rendered
+		    added.down('radiofield[inputValue=empty]').setValue(false);
+		    added.down('radiofield[inputValue=path]').setValue(true);
+		    added.down('textfield[name=sourcePath]').setValue(path);
+		}
+
+		const selector = added.down('pveControllerSelector');
+		if (device) {
+		    selector.setValue(device);
+		} else {
+		    selector.setVMConfig(initialValues);
+		}
+
+		return added;
+	    },
+	    removePanel: function(panelId) {
+		this.remove(panelId, true);
+	    },
+	},
+    ],
+
+    addDisk: function(device, path) {
+	this.down('#panelContainer').addDisk(device, path);
+	this.down('gridpanel').selectLast();
+    },
+
+    removePanel: function(panelId) {
+	this.down('#panelContainer').removePanel(panelId);
+    },
+
+    beforeRender: function() {
+	const me = this;
+	const leftColumnPanel = me.items.get(0).items.get(0); // not the gridpanel
+	leftColumnPanel.setFlex(me.leftColumnRatio);
+	// any other panel because this has no height yet
+	const panelHeight = me.up('tabpanel').items.get(0).getHeight();
+	me.down('gridpanel').setHeight(panelHeight);
+    },
+
+    setNodename: function(nodename) {
+	this.nodename = nodename;
+	this.query('pveQemuHardDisk').forEach(p => p.setNodename(nodename));
+    },
+
+    listeners: {
+	afterrender: function() {
+	    const store = Ext.getStore('diskstorage');
+	    const first = store.getAt(0);
+	    if (first) {
+		this.down('gridpanel').setSelection(first);
+	    }
+	},
+    },
+
+    // values ... is optional
+    hasDuplicateDevices: function(values) {
+	if (!values) {
+	    values = this.up('form').getValues();
+	}
+	if (!Array.isArray(values.controller)) {
+	    return false;
+	}
+	for (let i = 0; i < values.controller.length - 1; i++) {
+	    for (let j = i+1; j < values.controller.length; j++) {
+		if (
+		    values.controller[i] === values.controller[j] &&
+		    values.deviceid[i] === values.deviceid[j]
+		) {
+		    return true;
+		}
+	    }
+	}
+	return false;
+    },
+
+    onGetValues: function(values) {
+	if (this.hasDuplicateDevices(values)) {
+	    Ext.Msg.alert(gettext('Error'), 'Equal target devices are forbidden. Make all unique!');
+	}
+	// Each child panel has sufficient onGetValues() => Return nothing
+    },
+
+    validator: function() {
+	const me = this;
+	const panels = me.down('#panelContainer').items.getRange();
+	return panels.every(panel => panel.validator()) && !me.hasDuplicateDevices();
+    },
+
+    initComponent: function() {
+	this.callParent();
+	this.down('tableview').markDirty = false;
+	this.down('#panelContainer').insideWizard = this.insideWizard;
+	this.down('#panelContainer').isCreate = this.isCreate;
+    },
+
+    deleteFromVMConfig: function(key) {
+	this.query('pveQemuHardDisk').forEach(p => p.deleteFromVMConfig(key));
+    },
+
+    setVMConfig: function(vmconfig) {
+	this.query('pveQemuHardDisk').forEach(p => p.setVMConfig(vmconfig));
+    },
+});
diff --git a/www/manager6/qemu/disk/DiskData.js b/www/manager6/qemu/disk/DiskData.js
new file mode 100644
index 00000000..ff1bc163
--- /dev/null
+++ b/www/manager6/qemu/disk/DiskData.js
@@ -0,0 +1,241 @@
+/* 'change' property is assigned a string and then a function */
+Ext.define('PVE.qemu.DiskData', {
+    extend: 'Proxmox.panel.InputPanel',
+    alias: 'widget.pveQemuDiskData',
+    onlineHelp: 'qm_hard_disk',
+
+    insideWizard: false,
+
+    unused: false,
+
+    padding: '10 10 10 10',
+
+    vmconfig: {}, // used to select usused disks
+
+    viewModel: {},
+
+    /**
+     * All radiofields in pveQemuDiskCollection have the same scope
+     * Make name of radiofields unique for each disk
+     */
+    getRadioName() {
+	return 'radio_' + this.id;
+    },
+
+    getConfid() {
+	const me = this;
+	if (me.confid) {
+	    return me.confid; // When editing disks
+	}
+	// In wizard
+	const pairs = Object.entries(me.getValues());
+	const confidArray = pairs.filter(([key, _]) => key !== "import_sources");
+	// confidArray contains 1 array of length 2,
+	// e.g. confidArray = [["sata1", "local:-1,format=qcow2"]]
+	const confid = confidArray.shift().shift();
+	return confid;
+    },
+
+    onGetValues: function(values) {
+	let me = this;
+
+	let params = {};
+	let confid = me.confid || values.controller + values.deviceid;
+
+	const isImport = values.sourceVolid || values.sourcePath;
+	if (me.unused) {
+	    me.drive.file = me.vmconfig[values.unusedId];
+	    confid = values.controller + values.deviceid;
+	} else if (me.isCreate) {
+	    if (values.hdimage) {
+		me.drive.file = values.hdimage;
+	    } else if (isImport) {
+		me.drive.file = `${values.hdstorage}:-1`;
+	    } else {
+		me.drive.file = values.hdstorage + ":" + values.disksize;
+	    }
+	    me.drive.format = values.diskformat;
+	}
+
+	PVE.Utils.propertyStringSet(me.drive, values.discard, 'discard', 'on');
+	PVE.Utils.propertyStringSet(me.drive, values.cache, 'cache');
+
+	if (isImport) {
+	    // exactly 1 of sourceVolid and sourcePath must be defined
+	    params.import_sources = `${confid}=${isImport}`;
+	}
+
+	params[confid] = PVE.Parser.printQemuDrive(me.drive);
+
+	return params;
+    },
+
+    setVMConfig: function(vmconfig) {
+	let me = this;
+
+	me.vmconfig = vmconfig;
+
+	if (me.bussel) {
+	    me.bussel.setVMConfig(vmconfig);
+	    me.scsiController.setValue(vmconfig.scsihw);
+	}
+	if (me.unusedDisks) {
+	    let disklist = [];
+	    Ext.Object.each(vmconfig, function(key, value) {
+		if (key.match(/^unused\d+$/)) {
+		    disklist.push([key, value]);
+		}
+	    });
+	    me.unusedDisks.store.loadData(disklist);
+	    me.unusedDisks.setValue(me.confid);
+	}
+    },
+
+    deleteFromVMConfig: function(key) {
+	const me = this;
+	if (me.bussel) {
+	    me.bussel.deleteFromVMConfig(key);
+	}
+    },
+
+    setDrive: function(drive) {
+	let me = this;
+
+	me.drive = {};
+	[
+	    'interface',
+	    'index',
+	    'file',
+	    'format',
+	    'cache',
+	    'discard',
+	].forEach(o => { me.drive[o] = drive[o]; });
+
+	let values = {};
+	let match = drive.file.match(/^([^:]+):/);
+	if (match) {
+	    values.hdstorage = match[1];
+	}
+
+	values.hdimage = drive.file;
+	values.diskformat = drive.format || 'raw';
+	values.cache = drive.cache || '__default__';
+	values.discard = drive.discard === 'on';
+
+	me.setValues(values);
+    },
+
+    getDevice: function() {
+	return this.bussel.getValuesAsString();
+    },
+
+    setNodename: function(nodename) {
+	const me = this;
+	const hdstorage = me.down('#hdstorage');
+	if (hdstorage) { // iff me.isCreate
+	    hdstorage.setNodename(nodename);
+	}
+    },
+
+    initComponent: function() {
+	let me = this;
+
+
+	me.drive = {};
+
+	me.column1 = [];
+	me.column2 = [];
+
+	const nodename = me.getViewModel().get('nodename');
+
+	if (!me.confid || me.unused) {
+	    const controllerColumn = me.column2;
+	    me.scsiController = Ext.create('Ext.form.field.Display', {
+		    fieldLabel: gettext('SCSI Controller'),
+		    reference: 'scsiController',
+		    name: 'scsiController',
+		    bind: me.insideWizard ? {
+			value: '{current.scsihw}',
+		    } : undefined,
+		    renderer: PVE.Utils.render_scsihw,
+		    submitValue: false,
+		    hidden: true,
+	    });
+
+	     me.bussel = Ext.create('PVE.form.ControllerSelector', {
+		itemId: 'bussel',
+		vmconfig: me.insideWizard ? { ide2: 'cdrom' } : {},
+	    });
+
+	    me.bussel.down('field[name=controller]').addListener('change', function(_, newValue) {
+		const allowIOthread = newValue.match(/^(virtio|scsi)/);
+		const iothreadField = me.up('pveQemuHardDisk').down('field[name=iothread]');
+		iothreadField.setDisabled(!allowIOthread);
+		if (!allowIOthread) {
+		    iothreadField.setValue(false);
+		}
+
+		const virtio = newValue.match(/^virtio/);
+		const ssdField = me.up('pveQemuHardDisk').down('field[name=ssd]');
+		ssdField.setDisabled(virtio);
+		if (virtio) {
+		    ssdField.setValue(false);
+		}
+
+		me.scsiController.setVisible(newValue.match(/^scsi/));
+	    });
+
+	    controllerColumn.push(me.bussel);
+	    controllerColumn.push(me.scsiController);
+	}
+
+	if (me.unused) {
+	    me.unusedDisks = Ext.create('Proxmox.form.KVComboBox', {
+		name: 'unusedId',
+		fieldLabel: gettext('Disk image'),
+		matchFieldWidth: false,
+		listConfig: {
+		    width: 350,
+		},
+		data: [],
+		allowBlank: false,
+	    });
+	    me.column1.push(me.unusedDisks);
+	} else if (me.isCreate) {
+	    let selector = {
+		xtype: 'pveDiskStorageSelector',
+		storageContent: 'images',
+		name: 'disk',
+		nodename: nodename,
+		autoSelect: me.insideWizard,
+	    };
+	    selector.storageLabel = gettext('Storage');
+	    me.column1.push(selector);
+	} else {
+	    me.column1.push({
+		xtype: 'textfield',
+		disabled: true,
+		submitValue: false,
+		fieldLabel: gettext('Disk image'),
+		name: 'hdimage',
+	    });
+	}
+
+	me.column2.push(
+	    {
+		xtype: 'CacheTypeSelector',
+		name: 'cache',
+		value: '__default__',
+		fieldLabel: gettext('Cache'),
+	    },
+	    {
+		xtype: 'proxmoxcheckbox',
+		fieldLabel: gettext('Discard'),
+		reference: 'discard',
+		name: 'discard',
+	    },
+	);
+
+	me.callParent();
+    },
+});
diff --git a/www/manager6/qemu/disk/HardDisk.js b/www/manager6/qemu/disk/HardDisk.js
new file mode 100644
index 00000000..3e352af8
--- /dev/null
+++ b/www/manager6/qemu/disk/HardDisk.js
@@ -0,0 +1,215 @@
+/* 'change' property is assigned a string and then a function */
+Ext.define('PVE.qemu.HardDisk', {
+    extend: 'Ext.tab.Panel',
+    alias: 'widget.pveQemuHardDisk',
+    onlineHelp: 'qm_hard_disk',
+
+    plain: true,
+
+    bind: {
+	nodename: '{nodename}',
+    },
+
+    insideWizard: false,
+
+    isCreate: false,
+
+    setNodename: function(nodename) {
+	this.nodename = nodename;
+	this.items.each(panel => panel.setNodename(nodename));
+    },
+
+    setDrive: function(drive) {
+	this.items.each(i => i.setDrive(drive));
+    },
+
+    getDevice: function() {
+	return this.down('pveQemuDiskData').getDevice();
+    },
+
+    items: [
+	{
+	    title: gettext('Data'),
+	    xtype: 'pveQemuDiskData',
+	    bind: {
+		nodename: '{nodename}',
+	    },
+	},
+    ],
+
+    beforeRender: function() {
+	const me = this;
+	const tabPosition = me.insideWizard ? 'bottom' : 'top';
+	me.setTabPosition(tabPosition);
+	// any other panel because this has no height yet
+	if (me.insideWizard) {
+	    const panelHeight = me.up('tabpanel').items.get(0).getHeight();
+	    me.setHeight(panelHeight);
+	}
+    },
+
+    initComponent: function() {
+	const me = this;
+
+	let diskData = me.items[0];
+	diskData.confid = me.confid;
+	diskData.isCreate = me.isCreate;
+	diskData.insideWizard = me.insideWizard;
+
+	const basicOptions = {
+	    xtype: 'pveQemuDiskBasicOptions',
+	    isCreate: me.isCreate,
+	    confid: me.confid,
+	    insideWizard: me.insideWizard,
+	    bind: {
+		nodename: '{nodename}',
+	    },
+	};
+	const bandwidthOptions = {
+	    xtype: 'pveQemuDiskBandwidthOptions',
+	    isCreate: me.isCreate,
+	    insideWizard: me.insideWizard,
+	    bind: {
+		nodename: '{nodename}',
+	    },
+	};
+
+	if (me.insideWizard) {
+	    me.items = me.items.concat([
+		{
+		    title: gettext('Options'),
+		    xtype: 'panel',
+		    layout: {
+			type: 'vbox',
+		    },
+		    defaults: {
+			width: '100%',
+			margin: '0 0 10 0',
+		    },
+		    items: [
+			basicOptions,
+			{
+				xtype: 'box',
+				autoEl: { tag: 'hr' },
+			},
+			bandwidthOptions,
+		    ],
+		    setNodename: function(nodename) {
+			this.nodename = nodename;
+			if (basicOptions.setNodename) { // added after initialization
+			    basicOptions.setNodename(nodename);
+			    bandwidthOptions.setNodename(nodename);
+			}
+		    },
+		    setVMConfig: function(vmconfig) {
+			this.down('pveQemuDiskBasicOptions').setVMConfig(vmconfig);
+			this.down('pveQemuDiskBandwidthOptions').setVMConfig(vmconfig);
+		    },
+		    deleteFromVMConfig: function(key) {
+			const panel = this.up('pveQemuHardDisk').down('pveQemuDiskData');
+			if (panel) {
+			    panel.deleteFromVMConfig(key);
+			}
+		    },
+		    setDrive: function(drive) {
+			this.down('pveQemuDiskBasicOptions').setDrive(drive);
+			this.down('pveQemuDiskBandwidthOptions').setDrive(drive);
+		    },
+		},
+	    ]);
+	} else {
+	    basicOptions.title = gettext('Options');
+	    bandwidthOptions.title = gettext('Bandwidth');
+	    me.items = me.items.concat([basicOptions, bandwidthOptions]);
+	}
+
+	me.callParent();
+    },
+
+    setVMConfig: function(vmconfig) {
+	this.items.each(panel => panel.setVMConfig(vmconfig));
+    },
+    deleteFromVMConfig: function(key) {
+	this.items.each(panel => panel.deleteFromVMConfig(key));
+    },
+});
+
+Ext.define('PVE.qemu.HardDiskWindow', {
+    extend: 'Proxmox.window.Edit',
+
+    isAdd: true,
+
+    backgroundDelay: 5,
+
+    setNodename: function(nodename) {
+	this.nodename = nodename;
+	this.down('pveQemuHDTabpanel').setNodename(nodename);
+    },
+
+    initComponent: function() {
+	let me = this;
+
+	const selnode = me.pveSelNode && me.pveSelNode.data && me.pveSelNode.data.node;
+	if (selnode && !me.nodename) {
+	    me.nodename = selnode;
+	}
+	if (!me.nodename) {
+	    throw "no node name specified";
+	}
+
+	const unused = me.confid && me.confid.match(/^unused\d+$/);
+
+	me.isCreate = me.confid ? unused : true;
+
+	let ipanel = Ext.create('PVE.qemu.HardDisk', {
+	    confid: me.confid,
+	    unused: unused,
+	    isCreate: me.isCreate,
+	});
+	ipanel.setNodename(me.nodename);
+
+	if (unused) {
+	    me.subject = gettext('Unused Disk');
+	} else if (me.isCreate) {
+	    me.subject = gettext('Hard Disk');
+	} else {
+	    me.subject = gettext('Hard Disk') + ' (' + me.confid + ')';
+	}
+
+	me.items = [ipanel];
+
+	me.callParent();
+	/* 'data' is assigned an empty array in same file, and here we
+	 * use it like an object
+	 */
+	me.load({
+	    success: function(response, options) {
+		ipanel.setVMConfig(response.result.data);
+		if (me.confid) {
+		    let value = response.result.data[me.confid];
+		    let drive = PVE.Parser.parseQemuDrive(me.confid, value);
+		    if (!drive) {
+			Ext.Msg.alert(gettext('Error'), 'Unable to parse drive options');
+			me.close();
+			return;
+		    }
+		    ipanel.setDrive(drive);
+		    me.isValid(); // trigger validation
+		}
+	    },
+	});
+    },
+    getValues: function() {
+	let values = this.callParent();
+	for (const [key, value] of Object.entries(values)) {
+	    const re = /ide\d+|sata\d+|virtio\d+|scsi\d+|import_sources/;
+	    if (key.match(re) && Array.isArray(value)) {
+		// Collected from different panels => array
+		// But API & some GUI functions expect not array
+		const sep = key === 'import_sources' ? '\0' : ','; // for API
+		values[key] = value.join(sep);
+	    }
+	}
+	return values;
+    },
+});
diff --git a/www/manager6/window/Wizard.js b/www/manager6/window/Wizard.js
index 47d60b8e..de935fd0 100644
--- a/www/manager6/window/Wizard.js
+++ b/www/manager6/window/Wizard.js
@@ -245,6 +245,8 @@ Ext.define('PVE.window.Wizard', {
 	    };
 	    field.on('change', validcheck);
 	    field.on('validitychange', validcheck);
+	    // Make available for fields that get added later
+	    me.validcheck = validcheck;
 	});
     },
 });
-- 
2.30.2





^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2021-06-18 12:56 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-06-16 11:34 [pve-devel] [PATCH manager] ui: HD edit: Add multiple disks & tabs Dominic Jäger
2021-06-18 12:56 ` Dominik Csapak

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal