From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id D85CB6B603 for ; Mon, 20 Sep 2021 14:23:47 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 7B1E621103 for ; Mon, 20 Sep 2021 14:23:47 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id BCF8720EE8 for ; Mon, 20 Sep 2021 14:23:40 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 976D04499B for ; Mon, 20 Sep 2021 14:23:40 +0200 (CEST) From: Dominik Csapak To: pve-devel@lists.proxmox.com Date: Mon, 20 Sep 2021 14:23:38 +0200 Message-Id: <20210920122338.840436-11-d.csapak@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20210920122338.840436-1-d.csapak@proxmox.com> References: <20210920122338.840436-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.374 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pve-devel] [PATCH manager 10/10] ui: add qemu/MultiHDEdit and use it in the wizard X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Mon, 20 Sep 2021 12:23:47 -0000 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 --- 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 = ' '; + } + 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