[pve-devel] [PATCH manager] ui: declarative LXC Create
Dominik Csapak
d.csapak at proxmox.com
Thu Feb 22 13:48:44 CET 2018
general nitpick:
i would like to have more commits from this (e.g. the move of
loadSSHKeyFrom to Utils, the dataCache check move, etc.)
further comments inline
On 02/21/2018 04:45 PM, Thomas Lamprecht wrote:
> add setNodename method to FileSelector and a setUnprivileged to
> MPEdit, this allows to make those properties bindable
>
> Reset MPEdits quota checkbox when it gets disabled
>
> Move the loadSSHKeyFromFile helper to the PVE.Utils singleton
>
> And then, with all those changes transform the LXC Create wizard to
> a declarative style, in one go. Maybe it's better to just look at the
> end result than the diff...
>
> Signed-off-by: Thomas Lamprecht <t.lamprecht at proxmox.com>
> ---
>
> I did not see easy intermediate steps that make sense from a git
> history POV, so just sending ti as one change...
>
> www/manager6/Utils.js | 19 ++
> www/manager6/form/FileSelector.js | 4 +
> www/manager6/lxc/CreateWizard.js | 597 ++++++++++++++++++--------------------
> www/manager6/lxc/MPEdit.js | 13 +-
> www/manager6/lxc/Network.js | 11 +-
> 5 files changed, 325 insertions(+), 319 deletions(-)
>
> diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
> index 8f80c76a..87699e56 100644
> --- a/www/manager6/Utils.js
> +++ b/www/manager6/Utils.js
> @@ -875,6 +875,25 @@ Ext.define('PVE.Utils', { utilities: {
>
> delete values[fieldname];
> }
> + },
> +
> + loadSSHKeyFromFile: function(file, callback) {
> + // ssh-keygen produces 740 bytes for an average 4096 bit rsa key, with
> + // a user at host comment, 1420 for 8192 bits; current max is 16kbit
> + // assume: 740*8 for max. 32kbit (5920 byte file)
> + // round upwards to nearest nice number => 8192 bytes, leaves lots of comment space
> + if (file.size > 8192) {
> + Ext.Msg.alert(gettext('Error'), gettext("Invalid file size: ") + file.size);
> + return;
> + }
> + /*global
> + FileReader
> + */
> + var reader = new FileReader();
> + reader.onload = function(evt) {
> + callback(evt.target.result);
> + };
> + reader.readAsText(file);
> }
> },
>
> diff --git a/www/manager6/form/FileSelector.js b/www/manager6/form/FileSelector.js
> index 9afbf821..3dc50720 100644
> --- a/www/manager6/form/FileSelector.js
> +++ b/www/manager6/form/FileSelector.js
> @@ -47,6 +47,10 @@ Ext.define('PVE.form.FileSelector', {
> me.store.load();
> },
>
> + setNodename: function(nodename) {
> + this.setStorage(undefined, nodename);
> + },
> +
> store: {
> model: 'pve-storage-content'
> },
> diff --git a/www/manager6/lxc/CreateWizard.js b/www/manager6/lxc/CreateWizard.js
> index 5b069317..9abe7887 100644
> --- a/www/manager6/lxc/CreateWizard.js
> +++ b/www/manager6/lxc/CreateWizard.js
> @@ -1,342 +1,317 @@
> -/*global
> - FileReader
> -*/
> -
> +/*jslint confusion: true*/
> Ext.define('PVE.lxc.CreateWizard', {
> extend: 'PVE.window.Wizard',
> + mixins: ['Proxmox.Mixin.CBind'],
>
> - loadSSHKeyFromFile: function(file) {
> - var me = this;
> - // ssh-keygen produces 740 bytes for an average 4096 bit rsa key, with
> - // a user at host comment, 1420 for 8192 bits; current max is 16kbit
> - // assume: 740*8 for max. 32kbit (5920 byte file)
> - // round upwards to nearest nice number => 8192 bytes, leaves lots of comment space
> - if (file.size > 8192) {
> - Ext.Msg.alert(gettext('Error'), gettext("Invalid file size: ") + file.size);
> - return;
> + viewModel: {
> + data: {
> + nodename: '',
> + storage: '',
> + unprivileged: false
> }
> - var reader = new FileReader();
> - reader.onload = function(evt) {
> - me.sshkeyfield.setValue(evt.target.result);
> - };
> - reader.readAsText(file);
> },
>
> - initComponent: function() {
> - var me = this;
> + cbindData: {
> + nodename: undefined
> + },
> +
> + subject: gettext('LXC Container'),
>
> - var summarystore = Ext.create('Ext.data.Store', {
> - model: 'KeyValue',
> - sorters: [
> + items: [
> + {
> + xtype: 'inputpanel',
> + title: gettext('General'),
> + onlineHelp: 'pct_general',
> + column1: [
> + {
> + xtype: 'pveNodeSelector',
> + name: 'nodename',
> + cbind: {
> + selectCurNode: '{!nodename}',
> + preferredValue: '{nodename}'
> + },
> + bind: {
> + value: '{nodename}'
> + },
> + fieldLabel: gettext('Node'),
> + allowBlank: false,
> + onlineValidator: true
> + },
> + {
> + xtype: 'pveGuestIDSelector',
> + name: 'vmid', // backend only knows vmid
> + guestType: 'lxc',
> + value: '',
> + loadNextFreeID: true,
> + validateExists: false
> + },
> + {
> + xtype: 'proxmoxtextfield',
> + name: 'hostname',
> + vtype: 'DnsName',
> + value: '',
> + fieldLabel: gettext('Hostname'),
> + skipEmptyText: true,
> + allowBlank: true
> + },
> {
> - property : 'key',
> - direction: 'ASC'
> + xtype: 'proxmoxcheckbox',
> + name: 'unprivileged',
> + value: false,
> + bind: {
> + value: '{unprivileged}'
> + },
> + fieldLabel: gettext('Unprivileged container')
> + }
> + ],
> + column2: [
> + {
> + xtype: 'pvePoolSelector',
> + fieldLabel: gettext('Resource Pool'),
> + name: 'pool',
> + submitValue: false,
> + value: '',
> + allowBlank: true
> + },
> + {
> + xtype: 'textfield',
> + inputType: 'password',
> + name: 'password',
> + value: '',
> + fieldLabel: gettext('Password'),
> + allowBlank: false,
> + minLength: 5,
> + change: function(f, value) {
> + if (!f.rendered) {
> + return;
> + }
> + f.uo().down('field[name=confirmpw]').validate();
i believe this is a typo? i guess this should be: f.up().down(...
would it not be better if we would use a viewcontroller and have a
reference to the things we want to check/change?
> + }
> + },
> + {
> + xtype: 'textfield',
> + inputType: 'password',
> + name: 'confirmpw',
> + value: '',
> + fieldLabel: gettext('Confirm password'),
> + allowBlank: true,
> + submitValue: false,
> + validator: function(value) {
> + var pw = this.up().down('field[name=password]').getValue();
> + if (pw !== value) {
> + return "Passwords do not match!";
> + }
> + return true;
> + }
not necessary for now, but we have a custom vtype 'password' in the
widget toolkit we could use instead
the syntax is like:
{
xtype: 'textfield',
vtype: 'password',
initialPassField: '<name of passwordfield>',
}
which does the match check for us
> + },
> + {
> + xtype: 'proxmoxtextfield',
> + name: 'ssh-public-keys',
> + value: '',
> + fieldLabel: gettext('SSH public key'),
> + allowBlank: true,
> + validator: function(value) {
> + var pwfield = this.up().down('field[name=password]');
> + if (value.length) {
> + var key = PVE.Parser.parseSSHKey(value);
> + if (!key) {
> + return "Failed to recognize ssh key";
> + }
> + pwfield.allowBlank = true;
> + } else {
> + pwfield.allowBlank = false;
> + }
> + pwfield.validate();
> + return true;
> + },
> + afterRender: function() {
> + if (!window.FileReader) {
> + // No FileReader support in this browser
> + return;
> + }
> + var cancel = function(ev) {
> + ev = ev.event;
> + if (ev.preventDefault) {
> + ev.preventDefault();
> + }
> + };
> + var field = this;
> + field.inputEl.on('dragover', cancel);
> + field.inputEl.on('dragenter', cancel);
> + field.inputEl.on('drop', function(ev) {
> + ev = ev.event;
> + if (ev.preventDefault) {
> + ev.preventDefault();
> + }
> + var files = ev.dataTransfer.files;
> + PVE.Utils.loadSSHKeyFromFile(files[0], function(v) {
> + field.setValue(v);
> + });
> + });
> + }
> + },
> + {
> + xtype: 'filebutton',
> + name: 'file',
> + hidden: !window.FileReader,
> + text: gettext('Load SSH Key File'),
> + listeners: {
> + change: function(btn, e, value) {
> + e = e.event;
> + var field = this.up().down('proxmoxtextfield[name=ssh-public-keys]');
> + PVE.Utils.loadSSHKeyFromFile(e.target.files[0], function(v) {
> + field.setValue(v);
> + });
> + btn.reset();
> + }
> + }
> }
> ]
> - });
> -
> - var tmplsel = Ext.create('PVE.form.FileSelector', {
> - name: 'ostemplate',
> - storageContent: 'vztmpl',
> - fieldLabel: gettext('Template'),
> - allowBlank: false
> - });
> -
> - var tmplstoragesel = Ext.create('PVE.form.StorageSelector', {
> - name: 'tmplstorage',
> - fieldLabel: gettext('Storage'),
> - storageContent: 'vztmpl',
> - autoSelect: true,
> - allowBlank: false,
> - listeners: {
> - change: function(f, value) {
> - tmplsel.setStorage(value);
> + },
> + {
> + xtype: 'inputpanel',
> + title: gettext('Template'),
> + onlineHelp: 'pct_container_images',
> + column1: [
> + {
> + xtype: 'pveStorageSelector',
> + name: 'tmplstorage',
> + fieldLabel: gettext('Storage'),
> + storageContent: 'vztmpl',
> + autoSelect: true,
> + allowBlank: false,
> + bind: {
> + value: '{storage}',
> + nodename: '{nodename}'
> + }
> + },
> + {
> + xtype: 'pveFileSelector',
> + name: 'ostemplate',
> + storageContent: 'vztmpl',
> + fieldLabel: gettext('Template'),
> + bind: {
> + storage: '{storage}',
> + nodename: '{nodename}'
> + },
> + allowBlank: false
> }
> - }
> - });
> -
> - var rootfspanel = Ext.create('PVE.lxc.MountPointInputPanel', {
> + ]
> + },
> + {
> + xtype: 'pveLxcMountPointInputPanel',
> title: gettext('Root Disk'),
> insideWizard: true,
> isCreate: true,
> unused: false,
> - unprivileged: false,
> + bind: {
> + nodename: '{nodename}',
> + unprivileged: '{unprivileged}'
> + },
> confid: 'rootfs'
> - });
> -
> - var networkpanel = Ext.create('PVE.lxc.NetworkInputPanel', {
> + },
> + {
> + xtype: 'pveLxcCPUInputPanel',
> + title: gettext('CPU'),
> + insideWizard: true
> + },
> + {
> + xtype: 'pveLxcMemoryInputPanel',
> + title: gettext('Memory'),
> + insideWizard: true
> + },
> + {
> + xtype: 'pveLxcNetworkInputPanel',
> title: gettext('Network'),
> insideWizard: true,
> - dataCache: {},
> + bind: {
> + nodename: '{nodename}'
> + },
> isCreate: true
> - });
> -
> - var passwordfield = Ext.createWidget('textfield', {
> - inputType: 'password',
> - name: 'password',
> - value: '',
> - fieldLabel: gettext('Password'),
> - allowBlank: false,
> - minLength: 5,
> - change: function(f, value) {
> - if (!me.rendered) {
> - return;
> - }
> - me.down('field[name=confirmpw]').validate();
> - }
> - });
> -
> - /*jslint confusion: true */
> - /* the validator function can return either a string or a boolean */
> - me.sshkeyfield = Ext.createWidget('proxmoxtextfield', {
> - name: 'ssh-public-keys',
> - value: '',
> - fieldLabel: gettext('SSH public key'),
> - allowBlank: true,
> - validator: function(value) {
> - if (value.length) {
> - var key = PVE.Parser.parseSSHKey(value);
> - if (!key) {
> - return "Failed to recognize ssh key";
> - }
> - me.down('field[name=password]').allowBlank = true;
> - } else {
> - me.down('field[name=password]').allowBlank = false;
> - }
> - me.down('field[name=password]').validate();
> - return true;
> - },
> - afterRender: function() {
> - if (!window.FileReader) {
> - // No FileReader support in this browser
> - return;
> - }
> - var cancel = function(ev) {
> - ev = ev.event;
> - if (ev.preventDefault) {
> - ev.preventDefault();
> - }
> - };
> - me.sshkeyfield.inputEl.on('dragover', cancel);
> - me.sshkeyfield.inputEl.on('dragenter', cancel);
> - me.sshkeyfield.inputEl.on('drop', function(ev) {
> - ev = ev.event;
> - if (ev.preventDefault) {
> - ev.preventDefault();
> - }
> - var files = ev.dataTransfer.files;
> - me.loadSSHKeyFromFile(files[0]);
> - });
> - }
> - });
> -
> - var column2 = [
> - {
> - xtype: 'pvePoolSelector',
> - fieldLabel: gettext('Resource Pool'),
> - name: 'pool',
> - value: '',
> - allowBlank: true
> - },
> - passwordfield,
> - {
> - xtype: 'textfield',
> - inputType: 'password',
> - name: 'confirmpw',
> - value: '',
> - fieldLabel: gettext('Confirm password'),
> - allowBlank: true,
> - validator: function(value) {
> - var pw = me.down('field[name=password]').getValue();
> - if (pw !== value) {
> - return "Passwords do not match!";
> - }
> - return true;
> - }
> - },
> - me.sshkeyfield
> - ];
> - /*jslint confusion: false */
> -
> - if (window.FileReader) {
> - column2.push({
> - xtype: 'filebutton',
> - name: 'file',
> - text: gettext('Load SSH Key File'),
> - listeners: {
> - change: function(btn, e, value) {
> - e = e.event;
> - me.loadSSHKeyFromFile(e.target.files[0]);
> - btn.reset();
> - }
> - }
> - });
> - }
> -
> - Ext.applyIf(me, {
> - subject: gettext('LXC Container'),
> + },
> + {
> + xtype: 'pveLxcDNSInputPanel',
> + title: gettext('DNS'),
> + insideWizard: true
> + },
> + {
> + title: gettext('Confirm'),
> + layout: 'fit',
> items: [
> {
> - xtype: 'inputpanel',
> - title: gettext('General'),
> - onlineHelp: 'pct_general',
> - column1: [
> - {
> - xtype: 'pveNodeSelector',
> - name: 'nodename',
> - selectCurNode: !me.nodename,
> - preferredValue: me.nodename,
> - fieldLabel: gettext('Node'),
> - allowBlank: false,
> - onlineValidator: true,
> - listeners: {
> - change: function(f, value) {
> - tmplstoragesel.setNodename(value);
> - tmplsel.setStorage(undefined, value);
> - networkpanel.setNodename(value);
> - rootfspanel.setNodename(value);
> - }
> - }
> - },
> - {
> - xtype: 'pveGuestIDSelector',
> - name: 'vmid', // backend only knows vmid
> - guestType: 'lxc',
> - value: '',
> - loadNextFreeID: true,
> - validateExists: false
> - },
> - {
> - xtype: 'proxmoxtextfield',
> - name: 'hostname',
> - vtype: 'DnsName',
> - value: '',
> - fieldLabel: gettext('Hostname'),
> - skipEmptyText: true,
> - allowBlank: true
> - },
> - {
> - xtype: 'proxmoxcheckbox',
> - name: 'unprivileged',
> - value: '',
> - listeners: {
> - change: function(f, value) {
> - if (value) {
> - rootfspanel.down('field[name=quota]').setValue(false);
> - }
> - rootfspanel.unprivileged = value;
> - var hdsel = rootfspanel.down('#hdstorage');
> - hdsel.fireEvent('change', hdsel, hdsel.getValue());
> - }
> - },
> - fieldLabel: gettext('Unprivileged container')
> - }
> - ],
> - column2: column2,
> - onGetValues: function(values) {
> - delete values.confirmpw;
> - if (!values.pool) {
> - delete values.pool;
> - }
> - return values;
> - }
> - },
> - {
> - xtype: 'inputpanel',
> - title: gettext('Template'),
> - onlineHelp: 'pct_container_images',
> - column1: [ tmplstoragesel, tmplsel]
> - },
> - rootfspanel,
> - {
> - xtype: 'pveLxcCPUInputPanel',
> - title: gettext('CPU'),
> - insideWizard: true
> - },
> - {
> - xtype: 'pveLxcMemoryInputPanel',
> - title: gettext('Memory'),
> - insideWizard: true
> - },
> - networkpanel,
> - {
> - xtype: 'pveLxcDNSInputPanel',
> - title: gettext('DNS'),
> - insideWizard: true
> - },
> - {
> - title: gettext('Confirm'),
> - layout: 'fit',
> - items: [
> - {
> - xtype: 'grid',
> - store: summarystore,
> - columns: [
> - {header: 'Key', width: 150, dataIndex: 'key'},
> - {header: 'Value', flex: 1, dataIndex: 'value'}
> - ]
> - }
> - ],
> - listeners: {
> - show: function(panel) {
> - var form = me.down('form').getForm();
> - var kv = me.getValues();
> - var data = [];
> - Ext.Object.each(kv, function(key, value) {
> - if (key === 'delete' || key === 'tmplstorage') { // ignore
> - return;
> - }
> - if (key === 'password') { // don't show pw
> - return;
> - }
> - var html = Ext.htmlEncode(Ext.JSON.encode(value));
> - data.push({ key: key, value: value });
> - });
> - summarystore.suspendEvents();
> - summarystore.removeAll();
> - summarystore.add(data);
> - summarystore.sort();
> - summarystore.resumeEvents();
> - summarystore.fireEvent('refresh');
> - }
> + xtype: 'grid',
> + store: {
> + model: 'KeyValue',
> + sorters: [{
> + property : 'key',
> + direction: 'ASC'
> + }]
> },
> - onSubmit: function() {
> - var kv = me.getValues();
> - delete kv['delete'];
> + columns: [
> + {header: 'Key', width: 150, dataIndex: 'key'},
> + {header: 'Value', flex: 1, dataIndex: 'value'}
> + ]
> + }
> + ],
> + listeners: {
> + show: function(panel) {
> + var wizard = this.up('window');
> + var kv = wizard.getValues();
> + var data = [];
> + Ext.Object.each(kv, function(key, value) {
> + if (key === 'delete' || key === 'tmplstorage') { // ignore
> + return;
> + }
> + if (key === 'password') { // don't show pw
> + return;
> + }
> + var html = Ext.htmlEncode(Ext.JSON.encode(value));
> + data.push({ key: key, value: value });
> + });
>
> - var nodename = kv.nodename;
> - delete kv.nodename;
> - delete kv.tmplstorage;
> + var summarystore = panel.down('grid').getStore();
> + summarystore.suspendEvents();
> + summarystore.removeAll();
> + summarystore.add(data);
> + summarystore.sort();
> + summarystore.resumeEvents();
> + summarystore.fireEvent('refresh');
> + }
> + },
> + onSubmit: function() {
> + var wizard = this.up('window');
> + var kv = wizard.getValues();
> + delete kv['delete'];
>
> - if (!kv.password.length && kv['ssh-public-keys']) {
> - delete kv.password;
> - }
> + var nodename = kv.nodename;
> + delete kv.nodename;
> + delete kv.tmplstorage;
> +
> + if (!kv.password.length && kv['ssh-public-keys']) {
> + delete kv.password;
> + }
>
> - Proxmox.Utils.API2Request({
> - url: '/nodes/' + nodename + '/lxc',
> - waitMsgTarget: me,
> - method: 'POST',
> - params: kv,
> - success: function(response, opts){
> - var upid = response.result.data;
> + Proxmox.Utils.API2Request({
> + url: '/nodes/' + nodename + '/lxc',
> + waitMsgTarget: wizard,
> + method: 'POST',
> + params: kv,
> + success: function(response, opts){
> + var upid = response.result.data;
>
> - var win = Ext.create('Proxmox.window.TaskViewer', {
> - upid: upid
> - });
> - win.show();
> - me.close();
> - },
> - failure: function(response, opts) {
> - Ext.Msg.alert(gettext('Error'), response.htmlStatus);
> - }
> + var win = Ext.create('Proxmox.window.TaskViewer', {
> + upid: upid
> });
> + win.show();
> + wizard.close();
> + },
> + failure: function(response, opts) {
> + Ext.Msg.alert(gettext('Error'), response.htmlStatus);
> }
> - }
> - ]
> - });
> -
> - me.callParent();
> - }
> + });
> + }
> + }
> + ]
> });
>
>
> diff --git a/www/manager6/lxc/MPEdit.js b/www/manager6/lxc/MPEdit.js
> index 827869e8..3f84ed5f 100644
> --- a/www/manager6/lxc/MPEdit.js
> +++ b/www/manager6/lxc/MPEdit.js
> @@ -12,6 +12,10 @@ Ext.define('PVE.lxc.MountPointInputPanel', {
>
> vmconfig: {}, // used to select unused disks
>
> + setUnprivileged: function(unprivileged) {
> + this.unprivileged = unprivileged;
> + },
> +
> onGetValues: function(values) {
> var me = this;
>
> @@ -77,7 +81,6 @@ Ext.define('PVE.lxc.MountPointInputPanel', {
>
> if (mp.type === 'bind') {
> me.quota.setDisabled(true);
> - me.quota.setValue(false);
> me.acl.setDisabled(true);
> me.acl.setValue('Default');
> me.down('#hdstorage').setDisabled(true);
> @@ -222,7 +225,12 @@ Ext.define('PVE.lxc.MountPointInputPanel', {
> name: 'quota',
> defaultValue: 0,
> disabled: me.unprivileged,
> - fieldLabel: gettext('Enable quota')
> + fieldLabel: gettext('Enable quota'),
> + listeners: {
> + disable: function() {
> + this.reset();
> + }
> + }
> });
>
> me.column2 = [
> @@ -276,7 +284,6 @@ Ext.define('PVE.lxc.MountPointInputPanel', {
> }
> if (rec.data.type === 'zfs' || rec.data.type === 'zfspool') {
> me.quota.setDisabled(true);
> - me.quota.setValue(false);
> } else {
> me.quota.setDisabled(me.unprivileged);
> }
> diff --git a/www/manager6/lxc/Network.js b/www/manager6/lxc/Network.js
> index 1b574239..22d30055 100644
> --- a/www/manager6/lxc/Network.js
> +++ b/www/manager6/lxc/Network.js
> @@ -49,17 +49,18 @@ Ext.define('PVE.lxc.NetworkInputPanel', {
> initComponent : function() {
> var me = this;
>
> - if (!me.dataCache) {
> - throw "no dataCache specified";
> - }
> -
> var cdata = {};
>
> if (me.insideWizard) {
> me.ifname = 'net0';
> cdata.name = 'eth0';
> + me.dataCache = {};
> }
> -
> +
> + if (!me.dataCache) {
> + throw "no dataCache specified";
> + }
> +
> if (!me.isCreate) {
> if (!me.ifname) {
> throw "no interface name specified";
>
More information about the pve-devel
mailing list