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() { this.addLink(); // discard parameters to allow being called from 'handler' }, addLink: function(link) { let me = this; let view = me.getView(); let vm = view.getViewModel(); let linkCount = vm.get('linkCount'); if (linkCount >= vm.get('maxLinkCount')) { return; } link = link || {}; if (link.number === undefined) { link.number = me.getNextFreeNumber(); } if (link.value === undefined) { link.value = me.getNextFreeNetwork(); } let linkSelector = Ext.create('PVE.form.CorosyncLinkSelector', { maxLinkNumber: vm.get('maxLinkCount') - 1, allowNumberEdit: vm.get('allowNumberEdit'), allowBlankNetwork: link.allowBlank, initNumber: link.number, initNetwork: link.value, text: link.text, emptyText: link.emptyText, // 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 networksInUse = view.query('proxmoxNetworkSelector').map(selector => selector.value); for (const network of vm.get('networks')) { if (!networksInUse.includes(network)) { return network; } } return undefined; // default to empty field, user has to set up link manually }, getNextFreeNumber: function() { let view = this.getView(); let vm = view.getViewModel(); let numbersInUse = view.query('numberfield').map(field => field.value); for (let i = 0; i < vm.get('maxLinkCount'); i++) { if (!numbersInUse.includes(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, emptyText: '', // values initNumber: 0, initNetwork: '', text: '', layout: 'hbox', bodyPadding: 5, border: 0, items: [ { xtype: 'displayfield', fieldLabel: 'Link', cbind: { hidden: '{allowNumberEdit}', value: '{initNumber}', }, width: 45, labelWidth: 30, allowBlank: false, }, { xtype: 'numberfield', fieldLabel: 'Link', cbind: { maxValue: '{maxLinkNumber}', hidden: '{!allowNumberEdit}', value: '{initNumber}', }, width: 80, labelWidth: 30, minValue: 0, submitValue: false, // see getSubmitValue of network selector allowBlank: false, }, { xtype: 'proxmoxNetworkSelector', cbind: { allowBlank: '{allowBlankNetwork}', value: '{initNetwork}', emptyText: '{emptyText}', }, autoSelect: false, valueField: 'address', displayField: 'address', width: 220, margin: '0 5px 0 5px', getSubmitValue: function() { let me = this; // link number is encoded into key, so we need to set field name before value retrieval let linkNumber = me.prev('numberfield').getValue(); // always the correct one 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(); } }, }, { xtype: 'label', margin: '-1px 0 0 5px', // for muted effect cls: 'x-form-item-label-default', cbind: { text: '{text}', }, }, ], initComponent: function() { let me = this; me.callParent(); let numSelect = me.down('numberfield'); let netSelect = me.down('proxmoxNetworkSelector'); numSelect.validator = me.createNoDuplicatesValidator( 'numberfield', gettext("Duplicate link number not allowed."), ); netSelect.validator = me.createNoDuplicatesValidator( 'proxmoxNetworkSelector', gettext("Duplicate link address not allowed."), ); }, createNoDuplicatesValidator: function(queryString, errorMsg) { // linkSelector generator let view = this; // eslint-disable-line consistent-this /** @this is the field itself, as the validator this is called from scopes it that way */ return function(val) { let me = this; let form = view.up('form'); let linkEditor = view.up('pveCorosyncLinkEditor'); if (!form.validating) { // avoid recursion/double validation by setting temporary states me.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; me.validating = false; } else if (me.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)) { return true; // let this be caught by allowBlank, if at all } let allFields = linkEditor.query(queryString); for (const field of allFields) { if (field !== me && String(field.getValue()) === String(val)) { return errorMsg; } } return 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', border: false, padding: '6 0 6 0', 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)); }, 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); vm.set('infoText', me.infoText || ''); 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(); } }, }); }, });