pve-manager/www/manager6/dc/ClusterEdit.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

366 lines
7.9 KiB
JavaScript

/*jslint confusion: true*/
Ext.define('PVE.ClusterCreateWindow', {
extend: 'Proxmox.window.Edit',
xtype: 'pveClusterCreateWindow',
title: gettext('Create Cluster'),
width: 600,
method: 'POST',
url: '/cluster/config',
isCreate: true,
subject: gettext('Cluster'),
showTaskViewer: true,
onlineHelp: 'pvecm_create_cluster',
items: {
xtype: 'inputpanel',
items: [{
xtype: 'textfield',
fieldLabel: gettext('Cluster Name'),
allowBlank: false,
maxLength: 15,
name: 'clustername'
},
{
xtype: 'fieldcontainer',
fieldLabel: gettext("Cluster Links"),
style: {
'padding-top': '5px',
},
items: [
{
xtype: 'pveCorosyncLinkEditor',
style: {
'padding-bottom': '5px',
},
name: 'links'
},
{
xtype: 'label',
text: gettext("Multiple links are used as failover, lower numbers have higher priority.")
}
]
}]
}
});
Ext.define('PVE.ClusterInfoWindow', {
extend: 'Ext.window.Window',
xtype: 'pveClusterInfoWindow',
mixins: ['Proxmox.Mixin.CBind'],
width: 800,
modal: true,
resizable: false,
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('Join 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')
}]
}]
});
Ext.define('PVE.ClusterJoinNodeWindow', {
extend: 'Proxmox.window.Edit',
xtype: 'pveClusterJoinNodeWindow',
title: gettext('Cluster Join'),
width: 800,
method: 'POST',
url: '/cluster/config/join',
defaultFocus: 'textarea[name=serializedinfo]',
isCreate: true,
bind: {
submitText: '{submittxt}',
},
showTaskViewer: true,
onlineHelp: 'pvecm_join_node_to_cluster',
viewModel: {
parent: null,
data: {
info: {
fp: '',
ip: '',
clusterName: ''
}
},
formulas: {
submittxt: function(get) {
let cn = get('info.clusterName');
if (cn) {
return Ext.String.format(gettext('Join {0}'), `'${cn}'`);
}
return gettext('Join');
},
},
},
controller: {
xclass: 'Ext.app.ViewController',
control: {
'#': {
close: function() {
delete PVE.Utils.silenceAuthFailures;
}
},
'proxmoxcheckbox[name=assistedEntry]': {
change: 'onInputTypeChange'
},
'textarea[name=serializedinfo]': {
change: 'recomputeSerializedInfo',
enable: 'resetField'
},
'textfield': {
disable: 'resetField'
}
},
resetField: function(field) {
field.reset();
},
onInputTypeChange: function(field, assistedInput) {
let linkEditor = this.lookup('linkEditor');
// this also clears all links
linkEditor.setAllowNumberEdit(!assistedInput);
if (!assistedInput) {
linkEditor.setInfoText();
linkEditor.setDefaultLinks();
}
},
recomputeSerializedInfo: function(field, value) {
let vm = this.getViewModel();
let assistedEntryBox = this.lookup('assistedEntry');
if (!assistedEntryBox.getValue()) {
// not in assisted entry mode, nothing to do
return;
}
let linkEditor = this.lookup('linkEditor');
let jsons = Ext.util.Base64.decode(value);
let joinInfo = Ext.JSON.decode(jsons, true);
let info = {
fp: '',
ip: '',
clusterName: ''
};
if (!(joinInfo && joinInfo.totem)) {
field.valid = false;
linkEditor.setLinks([]);
linkEditor.setInfoText();
} else {
let interfaces = joinInfo.totem.interface;
let links = Object.values(interfaces).map(iface => {
return {
number: iface.linknumber,
value: '',
text: '',
allowBlank: false
};
});
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,
clusterName: joinInfo.totem.cluster_name
};
field.valid = true;
}
vm.set('info', info);
}
},
submit: function() {
// joining may produce temporarily auth failures, ignore as long the task runs
PVE.Utils.silenceAuthFailures = true;
this.callParent();
},
taskDone: function(success) {
delete PVE.Utils.silenceAuthFailures;
if (success) {
// reload always (if user wasn't faster), but wait a bit for pveproxy
Ext.defer(function() {
window.location.reload(true);
}, 5000);
var txt = gettext('Cluster join task finished, node certificate may have changed, reload GUI!');
// ensure user cannot do harm
Ext.getBody().mask(txt, ['pve-static-mask']);
// TaskView may hide above mask, so tell him directly
Ext.Msg.show({
title: gettext('Join Task Finished'),
icon: Ext.Msg.INFO,
msg: txt
});
}
},
items: [{
xtype: 'proxmoxcheckbox',
reference: 'assistedEntry',
name: 'assistedEntry',
itemId: 'assistedEntry',
submitValue: false,
value: true,
autoEl: {
tag: 'div',
'data-qtip': gettext('Select if join information should be extracted from pasted cluster information, deselect for manual entering')
},
boxLabel: gettext('Assisted join: Paste encoded cluster join information and enter password.')
},
{
xtype: 'textarea',
name: 'serializedinfo',
submitValue: false,
allowBlank: false,
fieldLabel: gettext('Information'),
emptyText: gettext('Paste encoded Cluster Information here'),
validator: function(val) {
return val === '' || this.valid ||
gettext('Does not seem like a valid encoded Cluster Information!');
},
bind: {
disabled: '{!assistedEntry.checked}',
hidden: '{!assistedEntry.checked}'
},
value: ''
},
{
xtype: 'panel',
width: 776,
layout: {
type: 'hbox',
align: 'center'
},
items: [
{
xtype: 'textfield',
flex: 1,
margin: '0 5px 0 0',
fieldLabel: gettext('Peer Address'),
allowBlank: false,
bind: {
value: '{info.ip}',
readOnly: '{assistedEntry.checked}'
},
name: 'hostname'
},
{
xtype: 'textfield',
flex: 1,
margin: '0 0 10px 5px',
inputType: 'password',
emptyText: gettext("Peer's root password"),
fieldLabel: gettext('Password'),
allowBlank: false,
name: 'password'
},
]
},
{
xtype: 'textfield',
fieldLabel: gettext('Fingerprint'),
allowBlank: false,
bind: {
value: '{info.fp}',
readOnly: '{assistedEntry.checked}'
},
name: 'fingerprint'
},
{
xtype: 'fieldcontainer',
fieldLabel: gettext("Cluster Links"),
items: [
{
xtype: 'pveCorosyncLinkEditor',
itemId: 'linkEditor',
reference: 'linkEditor',
allowNumberEdit: false
},
]
}]
});