From mboxrd@z Thu Jan  1 00:00:00 1970
Return-Path: <d.csapak@proxmox.com>
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 <pve-devel@lists.proxmox.com>; 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 <pve-devel@lists.proxmox.com>; 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 <pve-devel@lists.proxmox.com>; 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 <pve-devel@lists.proxmox.com>; Tue,  5 Oct 2021 13:29:05 +0200 (CEST)
From: Dominik Csapak <d.csapak@proxmox.com>
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 <pve-devel.lists.proxmox.com>
List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pve-devel>, 
 <mailto:pve-devel-request@lists.proxmox.com?subject=unsubscribe>
List-Archive: <http://lists.proxmox.com/pipermail/pve-devel/>
List-Post: <mailto:pve-devel@lists.proxmox.com>
List-Help: <mailto:pve-devel-request@lists.proxmox.com?subject=help>
List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel>, 
 <mailto:pve-devel-request@lists.proxmox.com?subject=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 <d.csapak@proxmox.com>
---
 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 = ' <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',
+				    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