pve-manager/www/manager6/dc/CorosyncLinkEdit.js
Stefan Reiter 5957f47f72 gui/cluster: add CorosyncLinkEdit component to support up to 8 links
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. To make space in layout, move 'password' field to the side
(where the network-selector previously was) and use 'hbox' panel for
horizontal layouting to avoid spacing issues with languages where the
fieldLabel doesn't fit on one line.

Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
2020-05-04 18:44:15 +02:00

412 lines
9.0 KiB
JavaScript

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 > 0) {
data.sort((a, b) => a.iface.localeCompare(b.iface));
let addresses = [];
for (let net of data) {
if (net.address) {
addresses.push(net.address);
}
if (net.address6) {
addresses.push(net.address6);
}
}
vm.set('networks', addresses);
}
// 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();
}
}
});
}
});