This is a WIP mock up of a possible cluster management component for
our WebUI. It's *not* final, and not intended for review - I mean if
you see something obvious wrong it won't hurt to tell.

I've plans to improve visuals still a bit, making a clear tab
separation between fully manual, and assisted join.

The assisted join should get some additional displayed info.
I.e., cluster name we're joining, its IP type, how many rings it has
configured, ...

I sent this mainly to make testing the backend part, using the API
only, easier.

Oh and I found out that i missed adding

protected => 1,

For the cluster create API call (POST /cluster/config) in v6 of the
backend part, easy to fix for testing this, though, so waiting with a
v7 :)

cheers,


Signed-off-by: Thomas Lamprecht <t.lampre...@proxmox.com>
---
 www/manager6/Makefile      |   1 +
 www/manager6/dc/Cluster.js | 456 +++++++++++++++++++++++++++++++++++++++++++++
 www/manager6/dc/Config.js  |  13 +-
 3 files changed, 466 insertions(+), 4 deletions(-)
 create mode 100644 www/manager6/dc/Cluster.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index aec62613..e2145085 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -187,6 +187,7 @@ JSSRC=                                                      
\
        dc/SecurityGroups.js                            \
        dc/Config.js                                    \
        dc/NodeView.js                                  \
+       dc/Cluster.js                                   \
        Workspace.js
 
 lint: ${JSSRC}
diff --git a/www/manager6/dc/Cluster.js b/www/manager6/dc/Cluster.js
new file mode 100644
index 00000000..40c1e07b
--- /dev/null
+++ b/www/manager6/dc/Cluster.js
@@ -0,0 +1,456 @@
+/*jslint confusion: true*/
+Ext.define('pve-cluster-nodes', {
+    extend: 'Ext.data.Model',
+    fields: [
+       'node', { type: 'integer', name: 'nodeid' }, 'ring0_addr', 'ring1_addr',
+       { type: 'integer', name: 'quorum_votes' }
+    ],
+    proxy: {
+        type: 'proxmox',
+       url: "/api2/json/cluster/config/nodes"
+    },
+    idProperty: 'nodeid'
+});
+
+Ext.define('pve-cluster-info', {
+    extend: 'Ext.data.Model',
+    proxy: {
+        type: 'proxmox',
+       url: "/api2/json/cluster/config/join"
+    }
+});
+
+Ext.define('PVE.ClusterCreateWindow', {
+    extend: 'Proxmox.window.Edit',
+    xtype: 'pveClusterCreateWindow',
+
+    title: gettext('Create Cluster'),
+    width: 800,
+
+    method: 'POST',
+    url: '/cluster/config',
+
+    isCreate: true,
+    subject: gettext('Cluster'),
+    showProgress: true,
+
+    items: [
+       {
+           xtype: 'textfield',
+           fieldLabel: gettext('Cluster Name'),
+           name: 'clustername'
+       },
+       {
+           xtype: 'proxmoxtextfield',
+           fieldLabel: gettext('Ring 0 Address'),
+           emptyText: gettext("IP resolved by node's hostname"),
+           name: 'ring0_addr',
+           skipEmptyText: true
+       }
+    ]
+});
+
+Ext.define('PVE.ClusterJoinNodeWindow', {
+    extend: 'Proxmox.window.Edit',
+    xtype: 'pveClusterJoinNodeWindow',
+
+    title: gettext('Cluster Join'),
+    width: 800,
+
+    method: 'POST',
+    url: '/cluster/config/join',
+
+    isCreate: true,
+    submitText: gettext('Join'),
+
+    viewModel: {
+       parent: null,
+       data: {
+           peerIP: '',
+           peerFP: '',
+           ringsNeeded: 1,
+           ipVersion: 'any',
+           useSerializedInfo: true
+       }
+    },
+
+    controller: {
+       xclass: 'Ext.app.ViewController',
+       control: {
+           'radiofield[name=inputtype]': {
+               change: 'onInputTypeChange'
+           },
+           'textarea[name=serializedinfo]': {
+               change: 'recomputeSerializedInfo',
+               disable: 'clearOnDisable'
+           },
+           'proxmoxtextfield': {
+               disable: 'clearOnDisable'
+           },
+           'textfield': {
+               disable: 'clearOnDisable'
+           }
+       },
+       clearOnDisable: function(field) {
+           field.reset();
+       },
+       onInputTypeChange: function(field, value) {
+           var vm = this.getViewModel();
+           var auto = (field.inputValue === 'auto');
+           vm.set('useSerializedInfo', !auto);
+       },
+       recomputeSerializedInfo: function(field, value) {
+           var jsons = Ext.util.Base64.decode(value);
+           var joinInfo = Ext.JSON.decode(jsons, true);
+
+           if (joinInfo === null) {
+               return;
+           }
+
+           var vm = this.getViewModel();
+           vm.set('peerIP', joinInfo.ipAddress);
+           vm.set('peerFP', joinInfo.fingerprint);
+       }
+    },
+
+    items: [{
+       xtype: 'inputpanel',
+       column1: [
+           {
+               xtype: 'radiofield',
+               name: 'inputtype',
+               submitValue: false,
+               inputValue: 'auto',
+               boxLabel: gettext('Use encoded join information'),
+               checked: true
+           },
+           {
+               xtype: 'textfield',
+               fieldLabel: gettext('Peer Address'),
+               allowBlank: false,
+               bind: {
+                   value: '{peerIP}',
+                   disabled: '{useSerializedInfo}'
+               },
+               name: 'hostname'
+           },
+           {
+               xtype: 'textfield',
+               inputType: 'password',
+               fieldLabel: gettext('Password'),
+               allowBlank: false,
+               name: 'password'
+           }
+       ],
+       column2: [
+           {
+               xtype: 'radiofield',
+               name: 'inputtype',
+               inputValue: 'manual',
+               submitValue: false,
+               boxLabel: gettext('Manual edit Join Information')
+           },
+           {
+               xtype: 'proxmoxtextfield',
+               fieldLabel: gettext('Corosync Ring 0'),
+               emptyText: gettext("IP resolved by node's hostname"),
+               skipEmptyText: true,
+               name: 'ring0_addr'
+           },
+           {
+               xtype: 'proxmoxtextfield',
+               fieldLabel: gettext('Corosync Ring 1'),
+               emptyText: gettext("Optional second ring"),
+               skipEmptyText: true,
+               bind: {
+                   disabled: '{useSerializedInfo}'
+               },
+               name: 'ring1_addr'
+           }
+       ],
+       columnB: [
+           {
+               xtype: 'textfield',
+               fieldLabel: gettext('Fingerprint'),
+               allowBlank: false,
+               bind: {
+                   value: '{peerFP}',
+                   disabled: '{useSerializedInfo}'
+               },
+               name: 'fingerprint'
+           },
+           {
+               xtype: 'textarea',
+               name: 'serializedinfo',
+               submitValue: false,
+               fieldLabel: gettext('Information'),
+               emptyText: 'Enter encoded Cluster Information',
+               bind: {
+                   disabled: '{!useSerializedInfo}'
+               },
+               value: ''
+           }
+       ]
+    }]
+});
+
+Ext.define('PVE.ClusterInfoWindow', {
+    extend: 'Ext.window.Window',
+    xtype: 'pveClusterInfoWindow',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    width: 800,
+    modal: true,
+    title: gettext('Cluster Join Information'),
+
+    joinInfo: {
+       ipAddress: undefined,
+       fingerprint: undefined,
+       totem: {}
+    },
+
+    items: [
+       {
+           xtype: 'component',
+           border: false,
+           padding: '10 10 10 10',
+           html: gettext("Copy the Join Information here and use it on the 
node you want to add.")
+       },
+       {
+           xtype: 'container',
+           layout: 'form',
+           border: false,
+           padding: '0 10 10 10',
+           items: [
+               {
+                   xtype: 'textfield',
+                   fieldLabel: gettext('IP Address'),
+                   cbind: { value: '{joinInfo.ipAddress}' },
+                   editable: false
+               },
+               {
+                   xtype: 'textfield',
+                   fieldLabel: gettext('Fingerprint'),
+                   cbind: { value: '{joinInfo.fingerprint}' },
+                   editable: false
+               },
+               {
+                   xtype: 'textarea',
+                   inputId: 'pveSerializedClusterInfo',
+                   fieldLabel: gettext('Information'),
+                   grow: true,
+                   cbind: { joinInfo: '{joinInfo}' },
+                   editable: false,
+                   listeners: {
+                       afterrender: function(field) {
+                           if (!field.joinInfo) {
+                               return;
+                           }
+                           var jsons = Ext.JSON.encode(field.joinInfo);
+                           var base64s = Ext.util.Base64.encode(jsons);
+                           field.setValue(base64s);
+                       }
+                   }
+               }
+           ]
+       }
+    ],
+    dockedItems: [{
+       dock: 'bottom',
+       xtype: 'toolbar',
+       items: [{
+           xtype: 'button',
+           handler: function(b) {
+               var el = document.getElementById('pveSerializedClusterInfo');
+               el.select();
+               document.execCommand("copy");
+           },
+           text: gettext('Copy Information')
+       }]
+    }]
+});
+
+/* bind is a function and object */
+Ext.define('PVE.ClusterAdministration', {
+    extend: 'Ext.panel.Panel',
+    xtype: 'pveClusterAdministration',
+
+    title: gettext('Cluster Administration'),
+
+    border: false,
+    defaults: { border: false },
+
+    viewModel: {
+       parent: null,
+       data: {
+           totem: {},
+           nodelist: [],
+           preferred_node: {
+               name: '',
+               fp: '',
+               addr: ''
+           },
+           nodecount: 0
+       }
+    },
+
+    items: [
+       {
+           xtype: 'grid',
+           title: gettext('Cluster Information'),
+           controller: {
+               xclass: 'Ext.app.ViewController',
+
+               init: function(view) {
+                   view.store.on('load', this.onLoad, this);
+                   //PVE.Utils.monStoreErrors(view, view.getStore());
+               },
+
+               onLoad: function(store, records, success) {
+                   if (!success || !records || !records[0].data) {
+                       return;
+                   }
+                   var vm = this.getViewModel();
+                   var data = records[0].data;
+                   vm.set('totem', data.totem);
+                   vm.set('nodelist', data.nodelist);
+
+                   var nodeinfo = Ext.Array.findBy(data.nodelist, function 
(el) {
+                       return el.name === data.preferred_node;
+                   });
+
+                   vm.set('preferred_node', {
+                       name: data.preferred_node,
+                       addr: nodeinfo.pve_addr,
+                       fp: nodeinfo.pve_fp
+                   });
+               },
+
+               onCreate: function() {
+                   var win = Ext.create('PVE.ClusterCreateWindow', {});
+                   win.show();
+               },
+
+               onJoin: function() {
+                   var win = Ext.create('PVE.ClusterJoinNodeWindow', {});
+                   win.show();
+                   win.on('destroy', function() {
+                       // fixme: logout
+                   });
+               },
+
+               onClusterInfo: function() {
+                   var vm = this.getViewModel();
+                   var win = Ext.create('PVE.ClusterInfoWindow', {
+                       joinInfo: {
+                           ipAddress: vm.get('preferred_node.addr'),
+                           fingerprint: vm.get('preferred_node.fp'),
+                           totem: vm.get('totem')
+                       }
+                   });
+                   win.show();
+               }
+           },
+           store: {
+               autoLoad: true,
+               model: 'pve-cluster-info'
+           },
+           tbar: [
+               {
+                   text: gettext('Create'),
+                   reference: 'createButton',
+                   handler: 'onCreate',
+                   bind: {
+                       disabled: '{totem.cluster_name}'
+                   }
+               },
+               {
+                   text: gettext('Cluster Information'),
+                   reference: 'addButton',
+                   handler: 'onClusterInfo',
+                   bind: {
+                       disabled: '{!nodecount}'
+                   }
+               },
+               {
+                   text: gettext('Join'),
+                   reference: 'joinButton',
+                   handler: 'onJoin',
+                   bind: {
+                       disabled: '{totem.cluster_name}'
+                   }
+               }
+           ],
+           columns: [
+               {
+                   header: gettext('TODO'),
+                   flex: 1,
+                   dataIndex: 'cluster_name'
+               }
+           ]
+       },
+       {
+           xtype: 'grid',
+           title: gettext('Nodes'),
+           controller: {
+               xclass: 'Ext.app.ViewController',
+
+               init: function(view) {
+                   view.store.on('load', this.onLoad, this);
+                   //Proxmox.Utils.monStoreErrors(view, view.getStore());
+               },
+
+               onLoad: function(store, records, success) {
+                   var vm = this.getViewModel();
+                   if (!success || !records) {
+                       return;
+                   }
+                   vm.set('nodecount', records.length);
+               }
+           },
+           store: {
+               autoLoad: true,
+               model: 'pve-cluster-nodes'
+           },
+           tbar: [
+               {
+                   text: gettext('Isolate Node'),
+                   reference: 'isolateButton',
+                   disabled: true
+                   //bind: {
+                       //disabled: '{!nodecount}'
+                   //}
+                   //handler: alert('TODO')
+               }
+           ],
+           columns: [
+               {
+                   header: gettext('Nodename'),
+                   width: 150,
+                   dataIndex: 'name'
+               },
+               {
+                   header: gettext('ID'),
+                   width: 80,
+                   dataIndex: 'nodeid'
+               },
+               {
+                   header: gettext('Votes'),
+                   width: 80,
+                   dataIndex: 'quorum_votes'
+               },
+               {
+                   // FIXME
+                   header: gettext('Ring 0'),
+                   width: 150,
+                   dataIndex: 'ring0_addr'
+               },
+               {
+                   header: gettext('Ring 1'),
+                   width: 150,
+                   dataIndex: 'ring1_addr'
+               }
+           ]
+       }
+    ]
+});
diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js
index 17d7a96a..e574f5db 100644
--- a/www/manager6/dc/Config.js
+++ b/www/manager6/dc/Config.js
@@ -22,13 +22,18 @@ Ext.define('PVE.dc.Config', {
 
        if (caps.dc['Sys.Audit']) {
            me.items.push({
-           title: gettext('Summary'),
+               title: gettext('Summary'),
                xtype: 'pveDcSummary',
                iconCls: 'fa fa-book',
                itemId: 'summary'
-           });
-
-           me.items.push({
+           },
+           {
+               title: gettext('Cluster'),
+               xtype: 'pveClusterAdministration',
+               iconCls: 'fa fa-server',
+               itemId: 'cluster'
+           },
+           {
                xtype: 'pveDcOptionView',
                title: gettext('Options'),
                iconCls: 'fa fa-gear',
-- 
2.14.2


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

Reply via email to