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 65C2171C0E for ; Tue, 5 Oct 2021 13:29:07 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 61F67274B2 for ; Tue, 5 Oct 2021 13:29:07 +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 86B912748E for ; Tue, 5 Oct 2021 13:29:05 +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 609CB45659 for ; Tue, 5 Oct 2021 13:29:05 +0200 (CEST) From: Dominik Csapak To: pve-devel@lists.proxmox.com Date: Tue, 5 Oct 2021 13:29:00 +0200 Message-Id: <20211005112903.3649291-5-d.csapak@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20211005112903.3649291-1-d.csapak@proxmox.com> References: <20211005112903.3649291-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.311 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 v3 4/7] ui: add MultiDiskPanel 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: Tue, 05 Oct 2021 11:29:07 -0000 this adds a new panel where a user can add multiple disks, intended for use in the wizard. Has a simple grid for displaying the already added disks and displays a warning triangle if the disk is not valid. this is a base panel for adding multiple disks/mps for vms/ct respectively. this combines the shared behavior and layout and defines the functions that subclasses must define Signed-off-by: Dominik Csapak --- www/manager6/Makefile | 1 + www/manager6/panel/MultiDiskEdit.js | 272 ++++++++++++++++++++++++++++ 2 files changed, 273 insertions(+) create mode 100644 www/manager6/panel/MultiDiskEdit.js diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 7d491f57..dc045e73 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -88,6 +88,7 @@ JSSRC= \ panel/GuestStatusView.js \ panel/GuestSummary.js \ panel/TemplateStatusView.js \ + panel/MultiDiskEdit.js \ tree/ResourceTree.js \ tree/SnapshotTree.js \ window/Backup.js \ diff --git a/www/manager6/panel/MultiDiskEdit.js b/www/manager6/panel/MultiDiskEdit.js new file mode 100644 index 00000000..ea1f974d --- /dev/null +++ b/www/manager6/panel/MultiDiskEdit.js @@ -0,0 +1,272 @@ +Ext.define('PVE.panel.MultiDiskPanel', { + extend: 'Ext.panel.Panel', + + 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); + }, + + getNextFreeDisk: function(vmconfig) { + throw "implement in subclass"; + }, + + addPanel: function(itemId, vmconfig, nextFreeDisk) { + throw "implement in subclass"; + }, + + // define in subclass + diskSorter: undefined, + + addDisk: function() { + let me = this; + let grid = me.lookup('grid'); + let store = grid.getStore(); + + // get free disk id + let vmconfig = me.getVMConfig(true); + let nextFreeDisk = me.getNextFreeDisk(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 = me.addPanel(itemId, vmconfig, nextFreeDisk); + 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(me.diskSorter); + + // select if the panel added is the only one + if (store.getCount() === 1) { + grid.getSelectionModel().select(0, false); + } + }, + + getBaseVMConfig: function() { + throw "implement in subclass"; + }, + + getVMConfig: function(all) { + let me = this; + + let vmconfig = me.getBaseVMConfig(); + + 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(); + + 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); + + return vmconfig; + }, + + 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(me.diskSorter); + + // 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.updateVMConfig(); + me.checkValidity(); + }, + + onSelectionChange: function(grid, selection) { + let me = this; + if (!selection || selection.length < 1) { + return; + } + + me.getView().setActiveItem(selection[0].data.itemId); + }, + + control: { + 'inputpanel': { + diskidchange: 'onChange', + }, + 'grid[reference=grid]': { + selectionchange: 'onSelectionChange', + }, + }, + + init: function(view) { + let me = this; + me.onAdd(); + me.lookup('grid').getSelectionModel().select(0, false); + }, + }, + + 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: { + fields: ['name', 'itemId', 'valid'], + data: [], + }, + columns: [ + { + dataIndex: 'name', + renderer: function(val, md, rec) { + let warn = ''; + if (!rec.get('valid')) { + warn = ' '; + } + return val + warn; + }, + flex: 1, + }, + { + xtype: 'actioncolumn', + width: 30, + align: 'center', + menuDisabled: true, + items: [ + { + iconCls: 'x-fa fa-trash critical', + tooltip: 'Delete', + handler: 'onRemove', + isActionDisabled: 'deleteDisabled', + }, + ], + }, + ], + }, + { + 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