[pve-devel] [PATCH manager 10/10] ui: add qemu/MultiHDEdit and use it in the wizard
Dominik Csapak
d.csapak at proxmox.com
Mon Sep 20 14:23:38 CEST 2021
this adds a new panel where a user can add multiple disks.
Has a simple grid for displaying the already added disks and displays
a warning triangle if the disk is not valid.
This allows also to create a vm without any disk by removing all of them.
Signed-off-by: Dominik Csapak <d.csapak at proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/qemu/CreateWizard.js | 5 +-
www/manager6/qemu/HDEdit.js | 9 +-
www/manager6/qemu/MultiHDEdit.js | 291 ++++++++++++++++++++++++++++++
4 files changed, 301 insertions(+), 5 deletions(-)
create mode 100644 www/manager6/qemu/MultiHDEdit.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 7d491f57..d76acf14 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -213,6 +213,7 @@ JSSRC= \
qemu/MachineEdit.js \
qemu/MemoryEdit.js \
qemu/Monitor.js \
+ qemu/MultiHDEdit.js \
qemu/NetworkEdit.js \
qemu/OSDefaults.js \
qemu/OSTypeEdit.js \
diff --git a/www/manager6/qemu/CreateWizard.js b/www/manager6/qemu/CreateWizard.js
index 015a099d..75836aab 100644
--- a/www/manager6/qemu/CreateWizard.js
+++ b/www/manager6/qemu/CreateWizard.js
@@ -154,14 +154,11 @@ Ext.define('PVE.qemu.CreateWizard', {
insideWizard: true,
},
{
- xtype: 'pveQemuHDInputPanel',
- padding: 0,
+ xtype: 'pveMultiHDPanel',
bind: {
nodename: '{nodename}',
},
title: gettext('Hard Disk'),
- isCreate: true,
- insideWizard: true,
},
{
xtype: 'pveQemuProcessorPanel',
diff --git a/www/manager6/qemu/HDEdit.js b/www/manager6/qemu/HDEdit.js
index 2142c746..9c453b2a 100644
--- a/www/manager6/qemu/HDEdit.js
+++ b/www/manager6/qemu/HDEdit.js
@@ -107,6 +107,12 @@ Ext.define('PVE.qemu.HDInputPanel', {
return params;
},
+ updateVMConfig: function(vmconfig) {
+ var me = this;
+ me.vmconfig = vmconfig;
+ me.bussel?.updateVMConfig(vmconfig);
+ },
+
setVMConfig: function(vmconfig) {
var me = this;
@@ -183,7 +189,8 @@ Ext.define('PVE.qemu.HDInputPanel', {
if (!me.confid || me.unused) {
me.bussel = Ext.create('PVE.form.ControllerSelector', {
- vmconfig: me.insideWizard ? { ide2: 'cdrom' } : {},
+ vmconfig: me.vmconfig,
+ selectFree: true,
});
column1.push(me.bussel);
diff --git a/www/manager6/qemu/MultiHDEdit.js b/www/manager6/qemu/MultiHDEdit.js
new file mode 100644
index 00000000..079a6fc6
--- /dev/null
+++ b/www/manager6/qemu/MultiHDEdit.js
@@ -0,0 +1,291 @@
+Ext.define('PVE.qemu.MultiHDPanel', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveMultiHDPanel',
+
+ onlineHelp: 'qm_hard_disk',
+
+ setNodename: function(nodename) {
+ this.items.each((panel) => panel.setNodename(nodename));
+ },
+
+ border: false,
+ bodyBorder: false,
+
+ layout: 'card',
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ vmconfig: {},
+
+ onAdd: function() {
+ let me = this;
+ me.lookup('addButton').setDisabled(true);
+ me.addDisk();
+ let count = me.lookup('grid').getStore().getCount() + 1; // +1 is from ide2
+ me.lookup('addButton').setDisabled(count >= me.maxCount);
+ },
+
+ addDisk: function() {
+ let me = this;
+ let view = me.getView();
+ let grid = me.lookup('grid');
+ let store = grid.getStore();
+
+ // get free disk id
+ let vmconfig = me.getVMConfig(true);
+ let clist = PVE.Utils.sortByPreviousUsage(vmconfig);
+ let nextFreeDisk = PVE.Utils.nextFreeDisk(clist, vmconfig);
+ if (!nextFreeDisk) {
+ return;
+ }
+
+ // add store entry + panel
+ let itemId = 'disk-card-' + ++Ext.idSeed;
+ let rec = store.add({
+ name: nextFreeDisk.confid,
+ itemId,
+ })[0];
+
+ let panel = view.add({
+ vmconfig,
+ border: false,
+ showAdvanced: Ext.state.Manager.getProvider().get('proxmox-advanced-cb'),
+ xtype: 'pveQemuHDInputPanel',
+ bind: {
+ nodename: '{nodename}',
+ },
+ padding: '0 0 0 5',
+ itemId,
+ isCreate: true,
+ insideWizard: true,
+ });
+
+ panel.updateVMConfig(vmconfig);
+
+ // we need to setup a validitychange handler, so that we can show
+ // that a disk has invalid fields
+ let fields = panel.query('field');
+ fields.forEach((el) => el.on('validitychange', () => {
+ let valid = fields.every((field) => field.isValid());
+ rec.set('valid', valid);
+ me.checkValidity();
+ }));
+
+ store.sort();
+
+ // select if the panel added is the only one
+ if (store.getCount() === 1) {
+ grid.getSelectionModel().select(0, false);
+ }
+ },
+
+ getVMConfig: function(all) {
+ let me = this;
+
+ let vmconfig = {
+ ide2: 'media=cdrom',
+ scsihw: me.getViewModel().get('current.scsihw'),
+ };
+
+ me.lookup('grid').getStore().each((rec) => {
+ if (all || rec.get('valid')) {
+ vmconfig[rec.get('name')] = rec.get('itemId');
+ }
+ });
+
+ return vmconfig;
+ },
+
+ checkValidity: function() {
+ let me = this;
+ let valid = me.lookup('grid').getStore().findExact('valid', false) !== -1;
+ me.lookup('validationfield').setValue(valid);
+ },
+
+ updateVMConfig: function() {
+ let me = this;
+ let view = me.getView();
+ let grid = me.lookup('grid');
+ let store = grid.getStore();
+
+ let vmconfig = me.getVMConfig();
+
+ me.getViewModel().set('current.scsihw', vmconfig.scsihw);
+
+ let valid = true;
+
+ store.each((rec) => {
+ let itemId = rec.get('itemId');
+ let name = rec.get('name');
+ let panel = view.getComponent(itemId);
+ if (!panel) {
+ throw "unexpected missing panel";
+ }
+
+ // copy config for each panel and remote its own id
+ let panel_vmconfig = Ext.apply({}, vmconfig);
+ if (panel_vmconfig[name] === itemId) {
+ delete panel_vmconfig[name];
+ }
+
+ if (!rec.get('valid')) {
+ valid = false;
+ }
+
+ panel.updateVMConfig(panel_vmconfig);
+ });
+
+ me.lookup('validationfield').setValue(valid);
+ },
+
+ onChange: function(panel, newVal) {
+ let me = this;
+ let store = me.lookup('grid').getStore();
+
+ let el = store.findRecord('itemId', panel.itemId, 0, false, true, true);
+ if (el.get('name') === newVal) {
+ // do not update if there was no change
+ return;
+ }
+
+ el.set('name', newVal);
+ el.commit();
+
+ store.sort();
+
+ // so that it happens after the layouting
+ setTimeout(function() {
+ me.updateVMConfig();
+ }, 10);
+ },
+
+ onRemove: function(tableview, rowIndex, colIndex, item, event, record) {
+ let me = this;
+ let grid = me.lookup('grid');
+ let store = grid.getStore();
+ let removed_idx = store.indexOf(record);
+
+ let selection = grid.getSelection()[0];
+ let selected_idx = store.indexOf(selection);
+
+ if (selected_idx === removed_idx) {
+ let newidx = store.getCount() > removed_idx + 1 ? removed_idx + 1: removed_idx - 1;
+ grid.getSelectionModel().select(newidx, false);
+ }
+
+ store.remove(record);
+ me.getView().remove(record.get('itemId'));
+ me.lookup('addButton').setDisabled(false);
+ me.checkValidity();
+ },
+
+ onSelectionChange: function(grid, selection) {
+ let me = this;
+ if (!selection || selection.length < 1) {
+ return;
+ }
+
+ me.getView().setActiveItem(selection[0].data.itemId);
+ },
+
+ control: {
+ 'pveQemuHDInputPanel': {
+ diskidchange: 'onChange',
+ },
+ 'grid[reference=grid]': {
+ selectionchange: 'onSelectionChange',
+ },
+ },
+
+ init: function(view) {
+ let me = this;
+ me.onAdd();
+ me.lookup('grid').getSelectionModel().select(0, false);
+
+ // only calculate once
+ me.maxCount = Object.values(PVE.Utils.diskControllerMaxIDs)
+ .reduce((previous, current) => previous+current, 0);
+ },
+ },
+
+ dockedItems: [
+ {
+ xtype: 'container',
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+ dock: 'left',
+ border: false,
+ width: 150,
+ items: [
+ {
+ xtype: 'grid',
+ hideHeaders: true,
+ reference: 'grid',
+ flex: 1,
+ emptyText: gettext('No Disks'),
+ margin: '0 0 5 0',
+ store: {
+ sorters: [{
+ sorterFn: function(rec1, rec2) {
+ let [, name1, id1] = PVE.Utils.bus_match.exec(rec1.data.name);
+ let [, name2, id2] = PVE.Utils.bus_match.exec(rec2.data.name);
+
+ if (name1 === name2) {
+ return parseInt(id1, 10) - parseInt(id2, 10);
+ }
+
+ return name1 < name2 ? -1 : 1;
+ },
+ }],
+ fields: ['name', 'itemId', 'valid'],
+ data: [],
+ },
+ columns: [
+ {
+ dataIndex: 'name',
+ renderer: function(val, md, rec) {
+ let warn = '';
+ if (!rec.get('valid')) {
+ warn = ' <i class="fa warning fa-warning"></i>';
+ }
+ return val + warn;
+ },
+ flex: 4,
+ },
+ {
+ flex: 1,
+ xtype: 'actioncolumn',
+ align: 'center',
+ menuDisabled: true,
+ items: [
+ {
+ iconCls: 'x-fa fa-trash critical',
+ tooltip: 'Delete',
+ handler: 'onRemove',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ xtype: 'button',
+ reference: 'addButton',
+ text: gettext('Add'),
+ iconCls: 'fa fa-plus-circle',
+ handler: 'onAdd',
+ },
+ {
+ // dummy field to control wizard validation
+ xtype: 'textfield',
+ hidden: true,
+ reference: 'validationfield',
+ value: true,
+ validator: (val) => !!val,
+ },
+ ],
+ },
+ ],
+});
--
2.30.2
More information about the pve-devel
mailing list