overall looks good, and most works, comments inline
the only 'real' issue i found is with ipv6 networks
(they do not get auto added to the links)
On 3/17/20 12:11 PM, Stefan Reiter wrote:
CorosyncLinkEdit is a Panel that contains between one and 8
CorosyncLinkSelectors. These can be added or removed with according
buttons.
Values submitted to the API are calculated by each
ProxmoxNetworkSelector itself. This works because ExtJS searches
recursively through all child components for ones with a value to be
submitted, i.e. the CorosyncLinkEdit and CorosyncLinkSelector components
are not part of data submission at all.
Change ClusterEdit.js to use the new component for cluster join and
create.
Signed-off-by: Stefan Reiter <s.rei...@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/dc/ClusterEdit.js | 148 +++++-----
www/manager6/dc/CorosyncLinkEdit.js | 406 ++++++++++++++++++++++++++++
3 files changed, 478 insertions(+), 77 deletions(-)
create mode 100644 www/manager6/dc/CorosyncLinkEdit.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 41615430..0f2224af 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -224,6 +224,7 @@ JSSRC=
\
dc/Cluster.js \
dc/ClusterEdit.js \
dc/PermissionView.js \
+ dc/CorosyncLinkEdit.js \
Workspace.js
lint: ${JSSRC}
diff --git a/www/manager6/dc/ClusterEdit.js b/www/manager6/dc/ClusterEdit.js
index e0fa5d9d..0820e4a0 100644
--- a/www/manager6/dc/ClusterEdit.js
+++ b/www/manager6/dc/ClusterEdit.js
@@ -25,24 +25,20 @@ Ext.define('PVE.ClusterCreateWindow', {
name: 'clustername'
},
{
- xtype: 'proxmoxNetworkSelector',
- fieldLabel: Ext.String.format(gettext('Link {0}'), 0),
- emptyText: gettext("Optional, defaults to IP resolved by node's
hostname"),
- name: 'link0',
- autoSelect: false,
- valueField: 'address',
- displayField: 'address',
- skipEmptyText: true
- }],
- advancedItems: [{
- xtype: 'proxmoxNetworkSelector',
- fieldLabel: Ext.String.format(gettext('Link {0}'), 1),
- emptyText: gettext("Optional second link for redundancy"),
- name: 'link1',
- autoSelect: false,
- valueField: 'address',
- displayField: 'address',
- skipEmptyText: true
+ xtype: 'fieldcontainer',
+ fieldLabel: gettext("Cluster Links"),
+ style: 'padding-top: 5px',
i would rather write this either as:
style: {
'padding-top':'5px',
},
(makes it easier to add styles later)
or:
padding: '5 0 0 0',
(we use it more often)
+ items: [
+ {
+ xtype: 'pveCorosyncLinkEditor',
+ style: 'padding-bottom: 5px',
same here
+ name: 'links'
+ },
+ {
+ xtype: 'label',
+ text: gettext("Multiple links are used as failover, lower
numbers have higher priority.")
+ }
+ ]
}]
}
});
@@ -149,20 +145,10 @@ Ext.define('PVE.ClusterJoinNodeWindow', {
info: {
fp: '',
ip: '',
- clusterName: '',
- ring0Needed: false,
- ring1Possible: false,
- ring1Needed: false
+ clusterName: ''
}
},
formulas: {
- ring0EmptyText: function(get) {
- if (get('info.ring0Needed')) {
- return gettext("Cannot use default address safely");
- } else {
- return gettext("Default: IP resolved by node's hostname");
- }
- },
submittxt: function(get) {
let cn = get('info.clusterName');
if (cn) {
@@ -188,9 +174,6 @@ Ext.define('PVE.ClusterJoinNodeWindow', {
change: 'recomputeSerializedInfo',
enable: 'resetField'
},
- 'proxmoxtextfield[name=ring1_addr]': {
- enable: 'ring1Needed'
- },
'textfield': {
disable: 'resetField'
}
@@ -198,47 +181,70 @@ Ext.define('PVE.ClusterJoinNodeWindow', {
resetField: function(field) {
field.reset();
},
- ring1Needed: function(f) {
- var vm = this.getViewModel();
- f.allowBlank = !vm.get('info.ring1Needed');
- },
onInputTypeChange: function(field, assistedInput) {
- var vm = this.getViewModel();
+ var view = this.getView();
+ var linkEditor = view.down('#linkEditor');
since we have a controller, we can make use of
references and 'lookup' here
e.g. set a reference on an item:
{
...
reference: 'foobar',
}
and use it like:
let foobar = view.lookup('foobar');
a controller is also a 'referenceholder' and
(as the name promises) holds references when given
this should be faster than doing view.down,
works also on the controller directly (e.g. this.lookup)
and looks a little bit nicer (no '#' needed)
+
+ // this also clears all links
+ linkEditor.setAllowNumberEdit(!assistedInput);
+
if (!assistedInput) {
- vm.set('info.ring1Possible', true);
+ linkEditor.setInfoText();
+ linkEditor.setDefaultLinks();
}
},
this whole function could probably also be solved with
some viewModel magic and binding
i.e. bind the values of the linkEditor to some
values in the viewmodel, and bind the value of the
checkbox to the value of the viewmodel
(i know it was already this way,
but i guess this would remove some unecessary code)
recomputeSerializedInfo: function(field, value) {
var vm = this.getViewModel();
+ var view = this.getView();
+
+ var assistedEntryBox = view.down('#assistedEntry');
same 'lookup' comment as above
+ if (!assistedEntryBox.getValue()) {
+ // not in assisted entry mode, nothing to do
+ return;
+ }
+
+ var linkEditor = view.down('#linkEditor');
+
var jsons = Ext.util.Base64.decode(value);
var joinInfo = Ext.JSON.decode(jsons, true);
var info = {
fp: '',
- ring1Needed: false,
- ring1Possible: false,
ip: '',
clusterName: ''
};
- var totem = {};
if (!(joinInfo && joinInfo.totem)) {
field.valid = false;
+ linkEditor.setLinks([]);
+ linkEditor.setInfoText();
} else {
- var ring0Needed = false;
- if (joinInfo.ring_addr !== undefined) {
- ring0Needed = joinInfo.ring_addr[0] !== joinInfo.ipAddress;
+ var links = [];
+ var interfaces = joinInfo.totem['interface'];
nit: i'd rather use 'let' whenever possible for new code
+ Ext.Array.each(Object.keys(interfaces), iface => {
+ links.push({
+ number: interfaces[iface]['linknumber'],
+ value: '',
+ text: '',
+ allowBlank: false
+ });
+ });
if you do a 'for of loop' here, you can have the value directly
for (const iface of Object.values(interfaces)) {
..
}
or you can do a 'map'
links = Object.values.map(iface => { return { number: iface.linknumber
... }});
+
+ linkEditor.setInfoText();
+ if (links.length == 1 && joinInfo.ring_addr !== undefined &&
+ joinInfo.ring_addr[0] === joinInfo.ipAddress) {
+
+ links[0].allowBlank = true;
+ linkEditor.setInfoText(gettext("Leave empty to use IP resolved
by node's hostname"));
}
+ linkEditor.setLinks(links);
+
info = {
ip: joinInfo.ipAddress,
fp: joinInfo.fingerprint,
- ring0Needed: ring0Needed,
- ring1Possible: !!joinInfo.totem['interface']['1'],
- ring1Needed: !!joinInfo.totem['interface']['1'],
clusterName: joinInfo.totem['cluster_name']
};
- totem = joinInfo.totem;
field.valid = true;
}
@@ -275,6 +281,7 @@ Ext.define('PVE.ClusterJoinNodeWindow', {
xtype: 'proxmoxcheckbox',
reference: 'assistedEntry',
name: 'assistedEntry',
+ itemId: 'assistedEntry',
submitValue: false,
value: true,
autoEl: {
@@ -312,7 +319,9 @@ Ext.define('PVE.ClusterJoinNodeWindow', {
readOnly: '{assistedEntry.checked}'
},
name: 'hostname'
- },
+ }
nit: why remove the trailing comma?
+ ],
+ column2: [
{
xtype: 'textfield',
inputType: 'password',
@@ -322,38 +331,11 @@ Ext.define('PVE.ClusterJoinNodeWindow', {
name: 'password'
}
],
- column2: [
- {
- xtype: 'proxmoxNetworkSelector',
- fieldLabel: Ext.String.format(gettext('Link {0}'), 0),
- bind: {
- emptyText: '{ring0EmptyText}',
- allowBlank: '{!info.ring0Needed}'
- },
- skipEmptyText: true,
- autoSelect: false,
- valueField: 'address',
- displayField: 'address',
- name: 'link0'
- },
- {
- xtype: 'proxmoxNetworkSelector',
- fieldLabel: Ext.String.format(gettext('Link {0}'), 1),
- skipEmptyText: true,
- autoSelect: false,
- valueField: 'address',
- displayField: 'address',
- bind: {
- disabled: '{!info.ring1Possible}',
- allowBlank: '{!info.ring1Needed}',
- },
- name: 'link1'
- }
- ],
columnB: [
{
xtype: 'textfield',
fieldLabel: gettext('Fingerprint'),
+ style: 'margin-top: -10px',
same as for 'padding-top'
also, why '-10px' ?
also, the whole layout seems kinda wonky (at least with
the language set to german), the password field
is higher up that the address field...
allowBlank: false,
bind: {
value: '{info.fp}',
@@ -362,5 +344,17 @@ Ext.define('PVE.ClusterJoinNodeWindow', {
name: 'fingerprint'
}
]
+ },
+ {
+ xtype: 'fieldcontainer',
+ fieldLabel: gettext("Cluster Links"),
+ padding: '5px 0 0 0',
+ items: [
+ {
+ xtype: 'pveCorosyncLinkEditor',
+ itemId: 'linkEditor',
+ allowNumberEdit: false
+ }
+ ]
}]
});
diff --git a/www/manager6/dc/CorosyncLinkEdit.js
b/www/manager6/dc/CorosyncLinkEdit.js
new file mode 100644
index 00000000..6fb35a0e
--- /dev/null
+++ b/www/manager6/dc/CorosyncLinkEdit.js
@@ -0,0 +1,406 @@
+Ext.define('PVE.form.CorosyncLinkEditorController', {
+ extend: 'Ext.app.ViewController',
+ alias: 'controller.pveCorosyncLinkEditorController',
+
+ addLinkIfEmpty: function() {
+ let view = this.getView();
+ if (view.items || view.items.length == 0) {
+ this.addLink();
+ }
+ },
+
+ addEmptyLink: function() {
+ // discard parameters to allow being called from 'handler'
+ this.addLink();
+ },
+
+ addLink: function(number, value, allowBlank) {
+ let me = this;
+ let view = me.getView();
+ let vm = view.getViewModel();
+
+ let linkCount = vm.get('linkCount');
+ if (linkCount >= vm.get('maxLinkCount')) {
+ return;
+ }
+
+ if (number === undefined) {
+ number = me.getNextFreeNumber();
+ }
+ if (value === undefined) {
+ value = me.getNextFreeNetwork();
+ }
+
+ let linkSelector = Ext.create('PVE.form.CorosyncLinkSelector', {
+ maxLinkNumber: vm.get('maxLinkCount') - 1,
+ allowNumberEdit: vm.get('allowNumberEdit'),
+ allowBlankNetwork: allowBlank,
+ initNumber: number,
+ initNetwork: value,
+
+ // needs to be set here, because we need to update the viewmodel
+ removeBtnHandler: function() {
+ let curLinkCount = vm.get('linkCount');
+
+ if (curLinkCount <= 1) {
+ return;
+ }
+
+ vm.set('linkCount', curLinkCount - 1);
+
+ // 'this' is the linkSelector here
+ view.remove(this);
+
+ me.updateDeleteButtonState();
+ }
+ });
+
+ view.add(linkSelector);
+
+ linkCount++;
+ vm.set('linkCount', linkCount);
+
+ me.updateDeleteButtonState();
+ },
+
+ // ExtJS trips on binding this for some reason, so do it manually
+ updateDeleteButtonState: function() {
+ let view = this.getView();
+ let vm = view.getViewModel();
+
+ let disabled = vm.get('linkCount') <= 1;
+
+ let deleteButtons = view.query('button[cls=removeLinkBtn]');
+ Ext.Array.each(deleteButtons, btn => {
+ btn.setDisabled(disabled);
+ })
+ },
+
+ getNextFreeNetwork: function() {
+ let view = this.getView();
+ let vm = view.getViewModel();
+ let netsInUse = Ext.Array.map(
+ view.query('proxmoxNetworkSelector'), selector => selector.value);
+
+ // default to empty field, user has to set up link manually
+ let retval = undefined;
+
+ let nets = vm.get('networks');
+ Ext.Array.each(nets, net => {
+ if (!Ext.Array.contains(netsInUse, net)) {
+ retval = net;
+ return false; // break
+ }
+ });
+
+ return retval;
+ },
+
+ getNextFreeNumber: function() {
+ let view = this.getView();
+ let vm = view.getViewModel();
+ let numbersInUse = Ext.Array.map(
+ view.query('numberfield'), field => field.value);
+
+ for (let i = 0; i < vm.get('maxLinkCount'); i++) {
+ if (!Ext.Array.contains(numbersInUse, i)) {
+ return i;
+ }
+ }
+
+ // all numbers in use, this should never happen since add button is
+ // disabled automatically
+ return 0;
+ }
+});
+
+Ext.define('PVE.form.CorosyncLinkSelector', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'pveCorosyncLinkSelector',
+
+ mixins: ['Proxmox.Mixin.CBind' ],
+ cbindData: [],
+
+ // config
+ maxLinkNumber: 7,
+ allowNumberEdit: true,
+ allowBlankNetwork: false,
+ removeBtnHandler: undefined,
+
+ // values
+ initNumber: 0,
+ initNetwork: '',
+
+ layout: 'hbox',
+ bodyPadding: 5,
+ border: 0,
+
+ items: [
+ {
+ xtype: 'numberfield',
+ cbind: {
+ maxValue: '{maxLinkNumber}',
+ readOnly: '{!allowNumberEdit}',
+ value: '{initNumber}'
+ },
+
+ minValue: 0,
+ allowBlank: false,
+ width: 80,
+ labelWidth: 30,
+ fieldLabel: gettext('Link'),
+
+ // see getSubmitValue of network selector
+ submitValue: false
+ },
+ {
+ xtype: 'proxmoxNetworkSelector',
+ cbind: {
+ allowBlank: '{allowBlankNetwork}',
+ value: '{initNetwork}'
+ },
+
+ autoSelect: false,
+ valueField: 'address',
+ displayField: 'address',
+ margin: '0 5px 0 5px',
+ getSubmitValue: function() {
+ // link number is encoded into key, so we need to set
+ // field name before value retrieval
+ let me = this;
+ let numSelect = me.prev('numberfield');
+ let linkNumber = numSelect.getValue();
+ me.name = 'link' + linkNumber;
+ return me.getValue();
+ }
+ },
+ {
+ xtype: 'button',
+ iconCls: 'fa fa-trash-o',
+ cls: 'removeLinkBtn',
+
+ cbind: {
+ hidden: '{!allowNumberEdit}'
+ },
+
+ handler: function() {
+ let me = this;
+ let parent = me.up('pveCorosyncLinkSelector');
+ if (parent.removeBtnHandler !== undefined) {
+ parent.removeBtnHandler();
+ }
+ }
+ }
+ ],
+
+ initComponent: function() {
+ let me = this;
+
+ me.callParent();
+
+ let numSelect = me.down('numberfield');
+ let netSelect = me.down('proxmoxNetworkSelector');
+
+ numSelect.validator = this.createNoDuplicatesValidator(
+ 'numberfield',
+ gettext("Duplicate link number not allowed.")
+ );
+
+ netSelect.validator = this.createNoDuplicatesValidator(
+ 'proxmoxNetworkSelector',
+ gettext("Duplicate link address not allowed.")
+ );
+ },
+
+ createNoDuplicatesValidator: function(queryString, errorMsg) {
+ // linkSelector
+ let me = this;
+
+ return function(val) {
+ let curField = this;
+ let form = me.up('form');
+ let linkEditor = me.up('pveCorosyncLinkEditor');
+
+ if (!form.validating) {
+ // avoid recursion/double validation by setting temporary states
+ curField.validating = true;
+ form.validating = true;
+
+ // validate all other fields as well, to always mark both
+ // parties involved in a 'duplicate' error
+ form.isValid();
+
+ form.validating = false;
+ curField.validating = false;
+ } else if (curField.validating) {
+ // we'll be validated by the original call in the other
+ // if-branch, avoid double work
+ return true;
+ }
+
+ if (val === undefined || (val instanceof String && val.length ===
0)) {
+ // let this be caught by allowBlank, if at all
+ return true;
+ }
+
+ let allFields = linkEditor.query(queryString);
+ let err = undefined;
+ Ext.Array.each(allFields, field => {
+ if (field != curField && field.getValue() == val) {
+ err = errorMsg;
+ return false; // break
+ }
+ });
+
+ return err || true;
+ };
+ }
+});
+
+Ext.define('PVE.form.CorosyncLinkEditor', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'pveCorosyncLinkEditor',
+
+ controller: 'pveCorosyncLinkEditorController',
+
+ // only initial config, use setter otherwise
+ allowNumberEdit: true,
+
+ viewModel: {
+ data: {
+ linkCount: 0,
+ maxLinkCount: 8,
+ networks: null,
+ allowNumberEdit: true,
+ infoText: ''
+ },
+ formulas: {
+ addDisabled: function(get) {
+ return !get('allowNumberEdit') ||
+ get('linkCount') >= get('maxLinkCount');
+ },
+ dockHidden: function(get) {
+ return !(get('allowNumberEdit') || get('infoText'));
+ }
+ }
+ },
+
+ dockedItems: [{
+ xtype: 'toolbar',
+ dock: 'bottom',
+ defaultButtonUI : 'default',
+ bind: {
+ hidden: '{dockHidden}'
+ },
+ items: [
+ {
+ xtype: 'button',
+ text: gettext('Add'),
+ bind: {
+ disabled: '{addDisabled}',
+ hidden: '{!allowNumberEdit}'
+ },
+ handler: 'addEmptyLink'
+ },
+ {
+ xtype: 'label',
+ bind: {
+ text: '{infoText}'
+ }
+ }
+ ]
+ }],
+
+ setInfoText: function(text) {
+ let me = this;
+ let vm = me.getViewModel();
+
+ vm.set('infoText', text || '');
+ },
+
+ setLinks: function(links) {
+ let me = this;
+ let controller = me.getController();
+ let vm = me.getViewModel();
+
+ me.removeAll();
+ vm.set('linkCount', 0);
+
+ Ext.Array.each(links, link => {
+ controller.addLink(link['number'], link['value'],
link['allowBlank']);
+ });
+ },
+
+ setDefaultLinks: function() {
+ let me = this;
+ let controller = me.getController();
+ let vm = me.getViewModel();
+
+ me.removeAll();
+ vm.set('linkCount', 0);
+ controller.addLink();
+ },
+
+ // clears all links
+ setAllowNumberEdit: function(allow) {
+ let me = this;
+ let vm = me.getViewModel();
+ vm.set('allowNumberEdit', allow);
+ me.removeAll();
+ vm.set('linkCount', 0);
+ },
+
+ items: [{
+ // No links is never a valid scenario, but can occur during a slow load
+ xtype: 'hiddenfield',
+ submitValue: false,
+ isValid: function() {
+ let me = this;
+ let vm = me.up('pveCorosyncLinkEditor').getViewModel();
+ return vm.get('linkCount') > 0;
+ }
+ }],
+
+ initComponent: function() {
+ let me = this;
+ let vm = me.getViewModel();
+ let controller = me.getController();
+
+ vm.set('allowNumberEdit', me.allowNumberEdit);
+
+ me.callParent();
+
+ // Request local node networks to pre-populate first link.
+ Proxmox.Utils.API2Request({
+ url: '/nodes/localhost/network',
+ method: 'GET',
+ waitMsgTarget: me,
+ success: response => {
+ let data = response.result.data;
+ if (data.length < 1) {
+ return;
+ }
+
+ data = data.filter(net => !!net.address);
this filters all ipv6 only links
+ data.sort((a, b) => a.iface.localeCompare(b.iface));
+ data = data.map(net => net.address);
also this discards all ipv6 addresses...
+
+ vm.set('networks', data);
+
+ // Always have at least one link, but account for delay in API,
+ // someone might have called 'setLinks' in the meantime -
+ // except if 'allowNumberEdit' is false, in which case we're
+ // probably waiting for the user to input the join info
+ if (vm.get('allowNumberEdit')) {
+ controller.addLinkIfEmpty();
+ }
+ },
+ failure: () => {
+ if (vm.get('allowNumberEdit')) {
+ controller.addLinkIfEmpty();
+ }
+ }
+ });
+ }
+});
+
_______________________________________________
pve-devel mailing list
pve-devel@pve.proxmox.com
https://pve.proxmox.com/cgi-bin/mailman/listinfo/pve-devel