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.csa...@proxmox.com>
---
 www/manager6/Makefile             |   1 +
 www/manager6/qemu/CreateWizard.js |   5 +-
 www/manager6/qemu/HDEdit.js       |   9 +-
 www/manager6/qemu/MultiHDEdit.js  | 294 ++++++++++++++++++++++++++++++
 4 files changed, 304 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..2323be0d
--- /dev/null
+++ b/www/manager6/qemu/MultiHDEdit.js
@@ -0,0 +1,294 @@
+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 vm = me.getViewModel();
+
+           let vmconfig = {
+               ide2: 'media=cdrom',
+               scsihw: vm.get('current.scsihw'),
+               ostype: vm.get('current.ostype'),
+           };
+
+           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: 130,
+           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: 1,
+                       },
+                       {
+                           xtype: 'actioncolumn',
+                           width: 30,
+                           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',
+                   submitValue: false,
+                   value: true,
+                   validator: (val) => !!val,
+               },
+           ],
+       },
+    ],
+});
-- 
2.30.2



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

Reply via email to