[pve-devel] [PATCH manager] ui: HD edit: Add multiple disks & tabs
Dominic Jäger
d.jaeger at proxmox.com
Wed Jun 16 13:34:51 CEST 2021
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 at 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
More information about the pve-devel
mailing list