Merge "[React] Edit node interfaces screen"
This commit is contained in:
commit
436e82b212
@ -2743,8 +2743,8 @@ hr.slim {
|
||||
.physical-network-box {
|
||||
display: block;
|
||||
&.nodrag {
|
||||
opacity: 0.7;
|
||||
.network-box-item {
|
||||
opacity: 0.7;
|
||||
border-color: @fuel-dark-red;
|
||||
background-color: #f2dede;
|
||||
}
|
||||
@ -2757,6 +2757,9 @@ hr.slim {
|
||||
padding: 8px 15px;
|
||||
label {
|
||||
margin: 0;
|
||||
.label-wrapper {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2844,6 +2847,11 @@ hr.slim {
|
||||
> b {
|
||||
color: @font-color-base;
|
||||
}
|
||||
.parameter-name .label-wrapper {
|
||||
min-width: 60px;
|
||||
float: left;
|
||||
margin-top: 0px;
|
||||
}
|
||||
select {
|
||||
width: 160px;
|
||||
margin-top: -4px;
|
||||
@ -3833,3 +3841,7 @@ i.btn-cluster-details {
|
||||
p { margin-bottom: 10px; }
|
||||
ul { margin-bottom: 20px; }
|
||||
}
|
||||
|
||||
.ui-sortable-placeholder {
|
||||
display: none;
|
||||
}
|
||||
|
@ -643,10 +643,10 @@ define([
|
||||
return _.contains(slaveInterfaceNames, slaveInterface.get('name'));
|
||||
});
|
||||
},
|
||||
validate: function() {
|
||||
validate: function(attrs) {
|
||||
var errors = [];
|
||||
var networks = new models.Networks(this.get('assigned_networks').invoke('getFullNetwork'));
|
||||
var untaggedNetworks = networks.filter(function(network) {return _.isNull(network.getVlanRange());});
|
||||
var networks = new models.Networks(this.get('assigned_networks').invoke('getFullNetwork', attrs.networks));
|
||||
var untaggedNetworks = networks.filter(function(network) { return _.isNull(network.getVlanRange(attrs.networkingParameters)); });
|
||||
// public and floating networks are allowed to be assigned to the same interface
|
||||
var maxUntaggedNetworksCount = networks.where({name: 'public'}).length && networks.where({name: 'floating'}).length ? 2 : 1;
|
||||
if (untaggedNetworks.length > maxUntaggedNetworksCount) {
|
||||
@ -674,7 +674,10 @@ define([
|
||||
});
|
||||
|
||||
models.InterfaceNetwork = BaseModel.extend({
|
||||
constructorName: 'InterfaceNetwork'
|
||||
constructorName: 'InterfaceNetwork',
|
||||
getFullNetwork: function(networks) {
|
||||
return networks.findWhere({name: this.get('name')});
|
||||
}
|
||||
});
|
||||
|
||||
models.InterfaceNetworks = BaseCollection.extend({
|
||||
@ -687,7 +690,15 @@ define([
|
||||
});
|
||||
|
||||
models.Network = BaseModel.extend({
|
||||
constructorName: 'Network'
|
||||
constructorName: 'Network',
|
||||
getVlanRange: function(networkingParameters) {
|
||||
if (!this.get('meta').neutron_vlan_range) {
|
||||
var externalNetworkData = this.get('meta').ext_net_data;
|
||||
var vlanStart = externalNetworkData ? networkingParameters.get(externalNetworkData[0]) : this.get('vlan_start');
|
||||
return _.isNull(vlanStart) ? vlanStart : [vlanStart, externalNetworkData ? vlanStart + networkingParameters.get(externalNetworkData[1]) - 1 : vlanStart];
|
||||
}
|
||||
return networkingParameters.get('vlan_range');
|
||||
}
|
||||
});
|
||||
|
||||
models.Networks = BaseCollection.extend({
|
||||
|
@ -23,7 +23,7 @@ define(
|
||||
'jsx!views/cluster_page_tabs/nodes_tab_screens/add_nodes_screen',
|
||||
'jsx!views/cluster_page_tabs/nodes_tab_screens/edit_nodes_screen',
|
||||
'views/cluster_page_tabs/nodes_tab_screens/edit_node_disks_screen',
|
||||
'views/cluster_page_tabs/nodes_tab_screens/edit_node_interfaces_screen'
|
||||
'jsx!views/cluster_page_tabs/nodes_tab_screens/edit_node_interfaces_screen'
|
||||
],
|
||||
function($, _, React, BackboneViewWrapper, ClusterNodesScreen, AddNodesScreen, EditNodesScreen, EditNodeDisksScreen, EditNodeInterfacesScreen) {
|
||||
'use strict';
|
||||
@ -49,7 +49,7 @@ function($, _, React, BackboneViewWrapper, ClusterNodesScreen, AddNodesScreen, E
|
||||
add: AddNodesScreen,
|
||||
edit: EditNodesScreen,
|
||||
disks: BackboneViewWrapper(EditNodeDisksScreen),
|
||||
interfaces: BackboneViewWrapper(EditNodeInterfacesScreen)
|
||||
interfaces: EditNodeInterfacesScreen
|
||||
};
|
||||
},
|
||||
checkScreen: function(newScreen) {
|
||||
@ -58,20 +58,22 @@ function($, _, React, BackboneViewWrapper, ClusterNodesScreen, AddNodesScreen, E
|
||||
}
|
||||
},
|
||||
changeScreen: function(newScreen, screenOptions) {
|
||||
var NewscreenComponent = this.getAvailableScreens()[newScreen];
|
||||
if (!NewscreenComponent) return;
|
||||
var NewScreenComponent = this.getAvailableScreens()[newScreen];
|
||||
if (!NewScreenComponent) return;
|
||||
var options = {cluster: this.props.cluster, screenOptions: screenOptions};
|
||||
return (NewscreenComponent.fetchData ? NewscreenComponent.fetchData(options) : $.Deferred().resolve())
|
||||
return (NewScreenComponent.fetchData ? NewScreenComponent.fetchData(options) : $.Deferred().resolve())
|
||||
.done(_.bind(function(data) {
|
||||
this.setState({
|
||||
screen: newScreen,
|
||||
screenOptions: screenOptions,
|
||||
screenData: data
|
||||
screenData: data || {}
|
||||
});
|
||||
}, this));
|
||||
},
|
||||
componentWillMount: function() {
|
||||
this.checkScreen(this.props.tabOptions[0]);
|
||||
var newScreen = this.props.tabOptions[0] || 'list';
|
||||
this.checkScreen(newScreen);
|
||||
this.changeScreen(newScreen, this.props.tabOptions.slice(1));
|
||||
},
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
var newScreen = newProps.tabOptions[0] || 'list';
|
||||
@ -80,7 +82,7 @@ function($, _, React, BackboneViewWrapper, ClusterNodesScreen, AddNodesScreen, E
|
||||
},
|
||||
render: function() {
|
||||
var Screen = this.getAvailableScreens()[this.state.screen];
|
||||
if (!Screen) return null;
|
||||
if (!Screen || !_.isObject(this.state.screenData)) return null;
|
||||
return (
|
||||
<ReactTransitionGroup component='div' className='wrapper' transitionName='screen'>
|
||||
<ScreenTransitionWrapper key={this.state.screen}>
|
||||
|
@ -1,374 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013 Mirantis, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
**/
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'underscore',
|
||||
'i18n',
|
||||
'backbone',
|
||||
'utils',
|
||||
'models',
|
||||
'views/cluster_page_tabs/nodes_tab_screens/edit_node_screen',
|
||||
'text!templates/cluster/edit_node_interfaces.html',
|
||||
'text!templates/cluster/node_interface.html',
|
||||
'jquery-ui'
|
||||
],
|
||||
function($, _, i18n, Backbone, utils, models, EditNodeScreen, editNodeInterfacesScreenTemplate, nodeInterfaceTemplate) {
|
||||
'use strict';
|
||||
var EditNodeInterfacesScreen, NodeInterface;
|
||||
|
||||
EditNodeInterfacesScreen = EditNodeScreen.extend({
|
||||
className: 'edit-node-networks-screen',
|
||||
constructorName: 'EditNodeInterfacesScreen',
|
||||
template: _.template(editNodeInterfacesScreenTemplate),
|
||||
events: {
|
||||
'click .btn-bond:not(:disabled)': 'bondInterfaces',
|
||||
'click .btn-unbond:not(:disabled)': 'unbondInterfaces',
|
||||
'click .btn-defaults': 'loadDefaults',
|
||||
'click .btn-revert-changes:not(:disabled)': 'revertChanges',
|
||||
'click .btn-apply:not(:disabled)': 'applyChanges',
|
||||
'click .btn-return:not(:disabled)': 'returnToNodeList'
|
||||
},
|
||||
initButtons: function() {
|
||||
this.constructor.__super__.initButtons.apply(this);
|
||||
this.bondInterfacesButton = new Backbone.Model({disabled: true});
|
||||
this.unbondInterfacesButton = new Backbone.Model({disabled: true});
|
||||
},
|
||||
updateButtonsState: function(state) {
|
||||
this.constructor.__super__.updateButtonsState.apply(this, arguments);
|
||||
this.bondInterfacesButton.set('disabled', state);
|
||||
this.unbondInterfacesButton.set('disabled', state);
|
||||
},
|
||||
setupButtonsBindings: function() {
|
||||
this.constructor.__super__.setupButtonsBindings.apply(this);
|
||||
var bindings = {attributes: [{name: 'disabled', observe: 'disabled'}]};
|
||||
this.stickit(this.bondInterfacesButton, {'.btn-bond': bindings});
|
||||
this.stickit(this.unbondInterfacesButton, {'.btn-unbond': bindings});
|
||||
},
|
||||
isLocked: function() {
|
||||
var nodesAvailableForChanges = this.nodes.filter(function(node) {
|
||||
return node.get('pending_addition') || node.get('status') == 'error';
|
||||
});
|
||||
return !nodesAvailableForChanges.length || this.constructor.__super__.isLocked.apply(this);
|
||||
},
|
||||
hasChanges: function() {
|
||||
function getInterfaceConfiguration(interfaces) {
|
||||
return _.map(interfaces.toJSON(), function(ifc) {
|
||||
return _.pick(ifc, ['assigned_networks', 'type', 'mode', 'slaves']);
|
||||
});
|
||||
}
|
||||
var currentConfiguration = getInterfaceConfiguration(this.interfaces);
|
||||
return !this.nodes.reduce(function(result, node) {
|
||||
return result && _.isEqual(currentConfiguration, getInterfaceConfiguration(node.interfaces));
|
||||
}, true);
|
||||
},
|
||||
checkForChanges: function() {
|
||||
this.updateButtonsState(this.isLocked() || !this.hasChanges());
|
||||
this.loadDefaultsButton.set('disabled', this.isLocked());
|
||||
this.updateBondingControlsState();
|
||||
},
|
||||
validateInterfacesSpeedsForBonding: function(interfaces) {
|
||||
var slaveInterfaces = _.flatten(_.invoke(interfaces, 'getSlaveInterfaces'), true);
|
||||
var speeds = _.invoke(slaveInterfaces, 'get', 'current_speed');
|
||||
// warn if not all speeds are the same or there are interfaces with unknown speed
|
||||
return _.uniq(speeds).length > 1 || !_.compact(speeds).length;
|
||||
},
|
||||
bondingAvailable: function() {
|
||||
var isExperimental = _.contains(app.version.get('feature_groups'), 'experimental');
|
||||
var iserDisabled = this.cluster.get('settings').get('storage.iser.value') != true;
|
||||
var mellanoxSriovDisabled = this.cluster.get('settings').get('neutron_mellanox.plugin.value') != 'ethernet';
|
||||
return !this.isLocked() && isExperimental && this.cluster.get('net_provider') == 'neutron' && iserDisabled && mellanoxSriovDisabled;
|
||||
},
|
||||
updateBondingControlsState: function() {
|
||||
var checkedInterfaces = this.interfaces.filter(function(ifc) {return ifc.get('checked') && !ifc.isBond();});
|
||||
var checkedBonds = this.interfaces.filter(function(ifc) {return ifc.get('checked') && ifc.isBond();});
|
||||
var creatingNewBond = checkedInterfaces.length >= 2 && !checkedBonds.length;
|
||||
var addingInterfacesToExistingBond = !!checkedInterfaces.length && checkedBonds.length == 1;
|
||||
var bondingPossible = creatingNewBond || addingInterfacesToExistingBond;
|
||||
var newBondWillHaveInvalidSpeeds = bondingPossible && this.validateInterfacesSpeedsForBonding(checkedBonds.concat(checkedInterfaces));
|
||||
var existingBondHasInvalidSpeeds = !!this.interfaces.find(function(ifc) {
|
||||
return ifc.isBond() && this.validateInterfacesSpeedsForBonding(ifc.getSlaveInterfaces());
|
||||
}, this);
|
||||
this.bondInterfacesButton.set('disabled', !bondingPossible);
|
||||
this.unbondInterfacesButton.set('disabled', checkedInterfaces.length || !checkedBonds.length);
|
||||
this.$('.bond-speed-warning').toggle(newBondWillHaveInvalidSpeeds || existingBondHasInvalidSpeeds);
|
||||
},
|
||||
bondInterfaces: function() {
|
||||
var interfaces = this.interfaces.filter(function(ifc) {return ifc.get('checked') && !ifc.isBond();});
|
||||
var bond = this.interfaces.find(function(ifc) {return ifc.get('checked') && ifc.isBond();});
|
||||
if (!bond) {
|
||||
// if no bond selected - create new one
|
||||
bond = new models.Interface({
|
||||
type: 'bond',
|
||||
name: this.interfaces.generateBondName(),
|
||||
mode: models.Interface.prototype.bondingModes[0],
|
||||
assigned_networks: new models.InterfaceNetworks(),
|
||||
slaves: _.invoke(interfaces, 'pick', 'name')
|
||||
});
|
||||
} else {
|
||||
// adding interfaces to existing bond
|
||||
bond.set({slaves: bond.get('slaves').concat(_.invoke(interfaces, 'pick', 'name'))});
|
||||
// remove the bond to add it later and trigger re-rendering
|
||||
this.interfaces.remove(bond, {silent: true});
|
||||
}
|
||||
_.each(interfaces, function(ifc) {
|
||||
bond.get('assigned_networks').add(ifc.get('assigned_networks').models);
|
||||
ifc.get('assigned_networks').reset();
|
||||
ifc.set({checked: false});
|
||||
});
|
||||
this.interfaces.add(bond);
|
||||
},
|
||||
unbondInterfaces: function() {
|
||||
_.each(this.interfaces.where({checked: true}), function(bond) {
|
||||
// assign all networks from the bond to the first slave interface
|
||||
var ifc = this.interfaces.findWhere({name: bond.get('slaves')[0].name});
|
||||
ifc.get('assigned_networks').add(bond.get('assigned_networks').models);
|
||||
bond.get('assigned_networks').reset();
|
||||
bond.set({checked: false});
|
||||
this.interfaces.remove(bond);
|
||||
}, this);
|
||||
},
|
||||
loadDefaults: function() {
|
||||
this.disableControls(true);
|
||||
this.interfaces.fetch({url: _.result(this.nodes.at(0), 'url') + '/interfaces/default_assignment', reset: true})
|
||||
.fail(_.bind(function() {
|
||||
utils.showErrorDialog({
|
||||
title: i18n('cluster_page.nodes_tab.configure_interfaces.configuration_error.title'),
|
||||
message: i18n('cluster_page.nodes_tab.configure_interfaces.configuration_error.load_defaults_warning')
|
||||
});
|
||||
}, this));
|
||||
},
|
||||
revertChanges: function() {
|
||||
this.interfaces.reset(_.cloneDeep(this.nodes.at(0).interfaces.toJSON()), {parse: true});
|
||||
},
|
||||
applyChanges: function() {
|
||||
this.disableControls(true);
|
||||
var bonds = this.interfaces.filter(function(ifc) {return ifc.isBond();});
|
||||
// bonding map contains indexes of slave interfaces
|
||||
// it is needed to build the same configuration for all the nodes
|
||||
// as interface names might be different, so we use indexes
|
||||
var bondingMap = _.map(bonds, function(bond) {
|
||||
return _.map(bond.get('slaves'), function(slave) {
|
||||
return this.interfaces.indexOf(this.interfaces.findWhere(slave));
|
||||
}, this);
|
||||
}, this);
|
||||
return $.when.apply($, this.nodes.map(function(node) {
|
||||
// removing previously configured bonds
|
||||
var oldNodeBonds = node.interfaces.filter(function(ifc) {return ifc.isBond();});
|
||||
node.interfaces.remove(oldNodeBonds);
|
||||
// creating node-specific bonds without slaves
|
||||
var nodeBonds = _.map(bonds, function(bond) {
|
||||
return new models.Interface(_.omit(bond.toJSON(), 'slaves'), {parse: true});
|
||||
}, this);
|
||||
node.interfaces.add(nodeBonds);
|
||||
// determining slaves using bonding map
|
||||
_.each(nodeBonds, function(bond, bondIndex) {
|
||||
var slaveIndexes = bondingMap[bondIndex];
|
||||
var slaveInterfaces = _.map(slaveIndexes, node.interfaces.at, node.interfaces);
|
||||
bond.set({slaves: _.invoke(slaveInterfaces, 'pick', 'name')});
|
||||
});
|
||||
// assigning networks according to user choice
|
||||
node.interfaces.each(function(ifc, index) {
|
||||
ifc.set({assigned_networks: new models.InterfaceNetworks(this.interfaces.at(index).get('assigned_networks').toJSON())});
|
||||
}, this);
|
||||
return Backbone.sync('update', node.interfaces, {url: _.result(node, 'url') + '/interfaces'});
|
||||
}, this))
|
||||
.done(function() {
|
||||
app.page.removeFinishedNetworkTasks();
|
||||
})
|
||||
.always(_.bind(this.checkForChanges, this))
|
||||
.fail(function() {
|
||||
utils.showErrorDialog({
|
||||
title: i18n('cluster_page.nodes_tab.configure_interfaces.configuration_error.title'),
|
||||
message: i18n('cluster_page.nodes_tab.configure_interfaces.configuration_error.saving_warning')
|
||||
});
|
||||
});
|
||||
},
|
||||
initialize: function() {
|
||||
this.constructor.__super__.initialize.apply(this, arguments);
|
||||
if (this.nodes.length) {
|
||||
this.cluster.on('change:status', function() {
|
||||
this.revertChanges();
|
||||
this.render();
|
||||
this.checkForChanges();
|
||||
}, this);
|
||||
this.networkConfiguration = this.cluster.get('networkConfiguration');
|
||||
this.loading = $.when.apply($, this.nodes.map(function(node) {
|
||||
node.interfaces = new models.Interfaces();
|
||||
node.interfaces.url = _.result(node, 'url') + '/interfaces';
|
||||
return node.interfaces.fetch();
|
||||
}, this).concat(this.networkConfiguration.fetch({cache: true})))
|
||||
.done(_.bind(function() {
|
||||
this.interfaces = new models.Interfaces(this.nodes.at(0).interfaces.toJSON(), {parse: true});
|
||||
this.interfaces.on('reset add remove change:slaves', this.render, this);
|
||||
this.interfaces.on('sync change:mode', this.checkForChanges, this);
|
||||
this.interfaces.on('change:checked reset', this.updateBondingControlsState, this);
|
||||
// FIXME: modifying prototype to easily access NetworkConfiguration cluster
|
||||
// should be reimplemented in a less hacky way
|
||||
var networks = this.networkConfiguration.get('networks');
|
||||
models.InterfaceNetwork.prototype.getFullNetwork = function() {
|
||||
return networks.findWhere({name: this.get('name')});
|
||||
};
|
||||
var networkingParameters = this.networkConfiguration.get('networking_parameters');
|
||||
models.Network.prototype.getVlanRange = function() {
|
||||
if (!this.get('meta').neutron_vlan_range) {
|
||||
var externalNetworkData = this.get('meta').ext_net_data;
|
||||
var vlanStart = externalNetworkData ? networkingParameters.get(externalNetworkData[0]) : this.get('vlan_start');
|
||||
return _.isNull(vlanStart) ? vlanStart : [vlanStart, externalNetworkData ? vlanStart + networkingParameters.get(externalNetworkData[1]) - 1 : vlanStart];
|
||||
}
|
||||
return networkingParameters.get('vlan_range');
|
||||
};
|
||||
this.render();
|
||||
}, this))
|
||||
.fail(_.bind(this.goToNodeList, this));
|
||||
} else {
|
||||
this.goToNodeList();
|
||||
}
|
||||
this.initButtons();
|
||||
},
|
||||
beforeTearDown: function() {
|
||||
delete models.InterfaceNetwork.prototype.getFullNetwork;
|
||||
delete models.Network.prototype.getVlanRange;
|
||||
},
|
||||
renderInterfaces: function() {
|
||||
this.tearDownRegisteredSubViews();
|
||||
this.$('.node-networks').html('');
|
||||
var slaveInterfaceNames = _.pluck(_.flatten(_.filter(this.interfaces.pluck('slaves'))), 'name');
|
||||
this.interfaces.each(_.bind(function(ifc) {
|
||||
if (!_.contains(slaveInterfaceNames, ifc.get('name'))) {
|
||||
var nodeInterface = new NodeInterface({cluster: ifc, screen: this});
|
||||
this.registerSubView(nodeInterface);
|
||||
this.$('.node-networks').append(nodeInterface.render().el);
|
||||
}
|
||||
}, this));
|
||||
// if any errors found disable apply button
|
||||
_.each(this.interfaces.invoke('validate'), _.bind(function(interfaceValidationResult) {
|
||||
if (!_.isEmpty(interfaceValidationResult)) {
|
||||
this.applyChangesButton.set('disabled', true);
|
||||
}
|
||||
}, this));
|
||||
},
|
||||
render: function() {
|
||||
this.$el.html(this.template({
|
||||
nodes: this.nodes,
|
||||
locked: this.isLocked(),
|
||||
bondingAvailable: this.bondingAvailable()
|
||||
})).i18n();
|
||||
if (this.loading && this.loading.state() != 'pending') {
|
||||
this.renderInterfaces();
|
||||
this.checkForChanges();
|
||||
}
|
||||
this.setupButtonsBindings();
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
NodeInterface = Backbone.View.extend({
|
||||
template: _.template(nodeInterfaceTemplate),
|
||||
templateHelpers: _.pick(utils, 'showBandwidth'),
|
||||
events: {
|
||||
'sortremove .logical-network-box': 'dragStart',
|
||||
'sortstart .logical-network-box': 'dragStart',
|
||||
'sortreceive .logical-network-box': 'dragStop',
|
||||
'sortstop .logical-network-box': 'dragStop',
|
||||
'sortactivate .logical-network-box': 'dragActivate',
|
||||
'sortdeactivate .logical-network-box': 'dragDeactivate',
|
||||
'sortover .logical-network-box': 'updateDropTarget',
|
||||
'click .btn-remove-interface': 'removeInterface'
|
||||
},
|
||||
bindings: {
|
||||
'input[type=checkbox]': {
|
||||
observe: 'checked'
|
||||
},
|
||||
'select[name=mode]': {
|
||||
observe: 'mode',
|
||||
selectOptions: {
|
||||
collection: function() {
|
||||
return _.map(models.Interface.prototype.bondingModes, function(mode) {
|
||||
return {value: mode, label: i18n('cluster_page.nodes_tab.configure_interfaces.bonding_modes.' + mode, {defaultValue: mode})};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
removeInterface: function(e) {
|
||||
var slaveInterfaceId = parseInt($(e.currentTarget).data('interface-id'), 10);
|
||||
var slaveInterface = this.screen.interfaces.get(slaveInterfaceId);
|
||||
this.cluster.set('slaves', _.reject(this.cluster.get('slaves'), {name: slaveInterface.get('name')}));
|
||||
},
|
||||
dragStart: function(event, ui) {
|
||||
var networkNames = $(ui.item).find('.logical-network-item').map(function(index, el) {
|
||||
return $(el).data('name');
|
||||
}).get();
|
||||
this.screen.draggedNetworks = this.cluster.get('assigned_networks').filter(function(network) {
|
||||
return _.contains(networkNames, network.get('name'));
|
||||
});
|
||||
if (event.type == 'sortstart') {
|
||||
this.updateDropTarget();
|
||||
} else if (event.type == 'sortremove') {
|
||||
this.cluster.get('assigned_networks').remove(this.screen.draggedNetworks);
|
||||
}
|
||||
},
|
||||
dragStop: function(event) {
|
||||
if (event.type == 'sortreceive') {
|
||||
this.cluster.get('assigned_networks').add(this.screen.draggedNetworks);
|
||||
}
|
||||
this.render();
|
||||
this.screen.draggedNetworks = null;
|
||||
},
|
||||
updateDropTarget: function() {
|
||||
this.screen.dropTarget = this;
|
||||
},
|
||||
checkIfEmpty: function() {
|
||||
this.$('.network-help-message').toggle(!this.cluster.get('assigned_networks').length && !this.screen.isLocked());
|
||||
},
|
||||
initialize: function(options) {
|
||||
_.defaults(this, options);
|
||||
this.cluster.get('assigned_networks').on('add remove', this.checkIfEmpty, this);
|
||||
this.cluster.get('assigned_networks').on('add remove', this.screen.checkForChanges, this.screen);
|
||||
},
|
||||
handleValidationErrors: function() {
|
||||
var validationResult = this.cluster.validate();
|
||||
if (validationResult.length) {
|
||||
this.screen.applyChangesButton.set('disabled', true);
|
||||
_.each(validationResult, _.bind(function(error) {
|
||||
this.$('.physical-network-box[data-name=' + this.cluster.get('name') + ']')
|
||||
.addClass('nodrag')
|
||||
.next('.network-box-error-message').text(error);
|
||||
}, this));
|
||||
}
|
||||
},
|
||||
render: function() {
|
||||
this.$el.html(this.template(_.extend({
|
||||
ifc: this.cluster,
|
||||
locked: this.screen.isLocked(),
|
||||
bondingAvailable: this.screen.bondingAvailable()
|
||||
}, this.templateHelpers))).i18n();
|
||||
this.checkIfEmpty();
|
||||
this.$('.logical-network-box').sortable({
|
||||
connectWith: '.logical-network-box',
|
||||
items: '.logical-network-group:not(.disabled)',
|
||||
containment: this.screen.$('.node-networks'),
|
||||
disabled: this.screen.isLocked()
|
||||
}).disableSelection();
|
||||
this.handleValidationErrors();
|
||||
this.stickit(this.cluster);
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
return EditNodeInterfacesScreen;
|
||||
});
|
@ -0,0 +1,592 @@
|
||||
/*
|
||||
* Copyright 2015 Mirantis, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
**/
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'underscore',
|
||||
'backbone',
|
||||
'react',
|
||||
'i18n',
|
||||
'utils',
|
||||
'models',
|
||||
'jsx!views/dialogs',
|
||||
'jsx!views/controls',
|
||||
'jsx!component_mixins',
|
||||
'jquery-ui'
|
||||
],
|
||||
function($, _, Backbone, React, i18n, utils, models, dialogs, controls, ComponentMixins) {
|
||||
'use strict';
|
||||
|
||||
var cx = React.addons.classSet,
|
||||
ScreenMixin, EditNodeInterfacesScreen, NodeInterface;
|
||||
|
||||
ScreenMixin = {
|
||||
goToNodeList: function() {
|
||||
app.navigate('#cluster/' + this.props.cluster.get('id') + '/nodes', {trigger: true});
|
||||
},
|
||||
isLockedScreen: function() {
|
||||
return this.props.cluster && !!this.props.cluster.tasks({group: 'deployment', status: 'running'}).length;
|
||||
},
|
||||
returnToNodeList: function() {
|
||||
if (this.hasChanges()) {
|
||||
utils.showDialog(dialogs.DiscardSettingsChangesDialog, {cb: _.bind(this.goToNodeList, this)});
|
||||
} else {
|
||||
this.goToNodeList();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
EditNodeInterfacesScreen = React.createClass({
|
||||
mixins: [
|
||||
ScreenMixin,
|
||||
ComponentMixins.backboneMixin('interfaces', 'change:checked change:slaves reset sync'),
|
||||
ComponentMixins.backboneMixin('cluster', 'change:status change:networkConfiguration change:nodes sync'),
|
||||
ComponentMixins.backboneMixin('nodes', 'change sync')
|
||||
],
|
||||
statics: {
|
||||
fetchData: function(options) {
|
||||
var cluster = options.cluster,
|
||||
nodeIds = utils.deserializeTabOptions(options.screenOptions[0]).nodes.split(',').map(function(id) {return parseInt(id, 10);}),
|
||||
nodeLoadingErrorNS = 'cluster_page.nodes_tab.configure_interfaces.node_loading_error.',
|
||||
nodes,
|
||||
networkConfiguration;
|
||||
|
||||
networkConfiguration = cluster.get('networkConfiguration');
|
||||
nodes = new models.Nodes(cluster.get('nodes').getByIds(nodeIds));
|
||||
if (nodes.length != nodeIds.length) {
|
||||
utils.showErrorDialog({
|
||||
title: i18n(nodeLoadingErrorNS + 'title'),
|
||||
message: i18n(nodeLoadingErrorNS + 'load_error')
|
||||
});
|
||||
ScreenMixin.goToNodeList();
|
||||
return;
|
||||
}
|
||||
|
||||
return $.when.apply($, nodes.map(function(node) {
|
||||
node.interfaces = new models.Interfaces();
|
||||
return node.interfaces.fetch({
|
||||
url: _.result(node, 'url') + '/interfaces',
|
||||
reset: true
|
||||
}, this);
|
||||
}, this).concat([networkConfiguration.fetch({cache: true})]))
|
||||
.then(_.bind(function() {
|
||||
var interfaces = new models.Interfaces();
|
||||
interfaces.set(_.cloneDeep(nodes.at(0).interfaces.toJSON()), {parse: true});
|
||||
|
||||
return {
|
||||
interfaces: interfaces,
|
||||
nodes: nodes
|
||||
};
|
||||
}, this));
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
actionInProgress: false,
|
||||
interfaceErrors: {}
|
||||
};
|
||||
},
|
||||
componentWillMount: function() {
|
||||
this.setState({initialInterfaces: this.interfacesToJSON(this.props.interfaces)});
|
||||
},
|
||||
componentDidMount: function() {
|
||||
this.validate();
|
||||
},
|
||||
getDraggedNetworks: function() {
|
||||
return this.draggedNetworks || null;
|
||||
},
|
||||
setDraggedNetworks: function(networks) {
|
||||
this.draggedNetworks = networks;
|
||||
},
|
||||
interfacesPickFromJSON: function(json) {
|
||||
// Pick certain interface fields that have influence on hasChanges.
|
||||
return _.pick(json, ['assigned_networks', 'mode', 'type', 'slaves']);
|
||||
},
|
||||
interfacesToJSON: function(interfaces, remainingNodesMode) {
|
||||
// Sometimes 'state' is sent from the API and sometimes not
|
||||
// It's better to just unify all inputs to the one without state.
|
||||
var picker = remainingNodesMode ? this.interfacesPickFromJSON : function(json) { return _.omit(json, 'state'); };
|
||||
|
||||
return interfaces.map(function(ifc) {
|
||||
return picker(ifc.toJSON());
|
||||
});
|
||||
},
|
||||
hasChangesInRemainingNodes: function() {
|
||||
var initialInterfacesOmitted = _.map(this.state.initialInterfaces, this.interfacesPickFromJSON);
|
||||
|
||||
return _.any(this.props.nodes.slice(1), _.bind(function(node) {
|
||||
return !_.isEqual(initialInterfacesOmitted, this.interfacesToJSON(node.interfaces, true));
|
||||
}, this));
|
||||
},
|
||||
hasChanges: function() {
|
||||
return !_.isEqual(this.state.initialInterfaces, this.interfacesToJSON(this.props.interfaces)) ||
|
||||
this.hasChangesInRemainingNodes();
|
||||
},
|
||||
loadDefaults: function() {
|
||||
this.setState({actionInProgress: true});
|
||||
$.when(this.props.interfaces.fetch({
|
||||
url: _.result(this.props.nodes.at(0), 'url') + '/interfaces/default_assignment', reset: true
|
||||
}, this)).done(_.bind(function() {
|
||||
this.setState({actionInProgress: false});
|
||||
}, this)).fail(_.bind(function() {
|
||||
var errorNS = 'cluster_page.nodes_tab.configure_interfaces.configuration_error.';
|
||||
|
||||
utils.showErrorDialog({
|
||||
title: i18n(errorNS + 'title'),
|
||||
message: i18n(errorNS + 'load_defaults_warning')
|
||||
});
|
||||
this.goToNodeList();
|
||||
}, this));
|
||||
},
|
||||
revertChanges: function() {
|
||||
this.props.interfaces.reset(_.cloneDeep(this.state.initialInterfaces), {parse: true});
|
||||
},
|
||||
applyChanges: function() {
|
||||
var nodes = this.props.nodes,
|
||||
interfaces = this.props.interfaces,
|
||||
bonds = interfaces.filter(function(ifc) {return ifc.isBond();});
|
||||
// bonding map contains indexes of slave interfaces
|
||||
// it is needed to build the same configuration for all the nodes
|
||||
// as interface names might be different, so we use indexes
|
||||
var bondingMap = _.map(bonds, function(bond) {
|
||||
return _.map(bond.get('slaves'), function(slave) {
|
||||
return interfaces.indexOf(interfaces.findWhere(slave));
|
||||
});
|
||||
});
|
||||
this.setState({actionInProgress: true});
|
||||
return $.when.apply($, nodes.map(function(node) {
|
||||
var oldNodeBonds, nodeBonds;
|
||||
// removing previously configured bonds
|
||||
oldNodeBonds = node.interfaces.filter(function(ifc) {return ifc.isBond();});
|
||||
node.interfaces.remove(oldNodeBonds);
|
||||
// creating node-specific bonds without slaves
|
||||
nodeBonds = _.map(bonds, function(bond) {
|
||||
return new models.Interface(_.omit(bond.toJSON(), 'slaves'), {parse: true});
|
||||
}, this);
|
||||
node.interfaces.add(nodeBonds);
|
||||
// determining slaves using bonding map
|
||||
_.each(nodeBonds, function(bond, bondIndex) {
|
||||
var slaveIndexes = bondingMap[bondIndex],
|
||||
slaveInterfaces = _.map(slaveIndexes, node.interfaces.at, node.interfaces);
|
||||
bond.set({slaves: _.invoke(slaveInterfaces, 'pick', 'name')});
|
||||
});
|
||||
|
||||
// Assigning networks according to user choice
|
||||
node.interfaces.each(function(ifc, index) {
|
||||
ifc.set({assigned_networks: new models.InterfaceNetworks(interfaces.at(index).get('assigned_networks').toJSON())});
|
||||
}, this);
|
||||
|
||||
return Backbone.sync('update', node.interfaces, {url: _.result(node, 'url') + '/interfaces'});
|
||||
}, this))
|
||||
.done(_.bind(function() {
|
||||
this.setState({initialInterfaces: this.interfacesToJSON(this.props.interfaces)});
|
||||
app.page.removeFinishedNetworkTasks();
|
||||
}, this))
|
||||
.fail(function() {
|
||||
var errorNS = 'cluster_page.nodes_tab.configure_interfaces.configuration_error.';
|
||||
|
||||
utils.showErrorDialog({
|
||||
title: i18n(errorNS + 'title'),
|
||||
message: i18n(errorNS + 'saving_warning')
|
||||
});
|
||||
}).always(_.bind(function() {
|
||||
this.setState({actionInProgress: false});
|
||||
}, this));
|
||||
},
|
||||
isLocked: function() {
|
||||
var hasLockedNodes = this.props.nodes.any(function(node) {
|
||||
return !(node.get('pending_addition') ||
|
||||
node.get('status') == 'ready' ||
|
||||
node.get('status') == 'error');
|
||||
});
|
||||
return hasLockedNodes || this.isLockedScreen();
|
||||
},
|
||||
bondingAvailable: function() {
|
||||
var cluster = this.props.cluster,
|
||||
isExperimental = _.contains(app.version.get('feature_groups'), 'experimental'),
|
||||
iserDisabled = !cluster.get('settings').get('storage.iser.value'),
|
||||
mellanoxSriovDisabled = cluster.get('settings').get('neutron_mellanox.plugin.value') != 'ethernet';
|
||||
return !this.isLocked() && isExperimental && cluster.get('net_provider') == 'neutron' && iserDisabled && mellanoxSriovDisabled;
|
||||
},
|
||||
bondInterfaces: function() {
|
||||
this.setState({actionInProgress: true});
|
||||
var interfaces = this.props.interfaces.filter(function(ifc) {return ifc.get('checked') && !ifc.isBond();}),
|
||||
bonds = this.props.interfaces.find(function(ifc) {return ifc.get('checked') && ifc.isBond();});
|
||||
if (!bonds) {
|
||||
// if no bond selected - create new one
|
||||
bonds = new models.Interface({
|
||||
type: 'bond',
|
||||
name: this.props.interfaces.generateBondName(),
|
||||
mode: models.Interface.prototype.bondingModes[0],
|
||||
assigned_networks: new models.InterfaceNetworks(),
|
||||
slaves: _.invoke(interfaces, 'pick', 'name')
|
||||
});
|
||||
} else {
|
||||
// adding interfaces to existing bond
|
||||
bonds.set({slaves: bonds.get('slaves').concat(_.invoke(interfaces, 'pick', 'name'))});
|
||||
// remove the bond to add it later and trigger re-rendering
|
||||
this.props.interfaces.remove(bonds, {silent: true});
|
||||
}
|
||||
_.each(interfaces, function(ifc) {
|
||||
bonds.get('assigned_networks').add(ifc.get('assigned_networks').models);
|
||||
ifc.get('assigned_networks').reset();
|
||||
ifc.set({checked: false});
|
||||
});
|
||||
this.props.interfaces.add(bonds);
|
||||
this.setState({actionInProgress: false});
|
||||
},
|
||||
unbondInterfaces: function() {
|
||||
this.setState({actionInProgress: true});
|
||||
_.each(this.props.interfaces.where({checked: true}), function(bond) {
|
||||
// assign all networks from the bond to the first slave interface
|
||||
var ifc = this.props.interfaces.findWhere({name: bond.get('slaves')[0].name});
|
||||
ifc.get('assigned_networks').add(bond.get('assigned_networks').models);
|
||||
bond.get('assigned_networks').reset();
|
||||
bond.set({checked: false});
|
||||
this.props.interfaces.remove(bond);
|
||||
}, this);
|
||||
this.setState({actionInProgress: false});
|
||||
},
|
||||
validate: function() {
|
||||
var interfaceErrors = {},
|
||||
validationResult,
|
||||
networkConfiguration = this.props.cluster.get('networkConfiguration'),
|
||||
networkingParameters = networkConfiguration.get('networking_parameters'),
|
||||
networks = networkConfiguration.get('networks');
|
||||
|
||||
if (!this.props.interfaces) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.interfaces.each(_.bind(function(ifc) {
|
||||
validationResult = ifc.validate({
|
||||
networkingParameters: networkingParameters,
|
||||
networks: networks
|
||||
});
|
||||
if (validationResult.length) {
|
||||
interfaceErrors[ifc.get('name')] = validationResult.join(' ');
|
||||
}
|
||||
}), this);
|
||||
if (!_.isEqual(this.state.interfaceErrors, interfaceErrors)) {
|
||||
this.setState({interfaceErrors: interfaceErrors});
|
||||
}
|
||||
},
|
||||
refresh: function() {
|
||||
this.forceUpdate();
|
||||
},
|
||||
render: function() {
|
||||
var configureInterfacesTransNS = 'cluster_page.nodes_tab.configure_interfaces.',
|
||||
nodes = this.props.nodes,
|
||||
nodeNames = nodes.pluck('name'),
|
||||
interfaces = this.props.interfaces,
|
||||
locked = this.isLocked(),
|
||||
bondingAvailable = this.bondingAvailable(),
|
||||
checkedInterfaces = interfaces.filter(function(ifc) {return ifc.get('checked') && !ifc.isBond();}),
|
||||
checkedBonds = interfaces.filter(function(ifc) {return ifc.get('checked') && ifc.isBond();}),
|
||||
creatingNewBond = checkedInterfaces.length >= 2 && !checkedBonds.length,
|
||||
addingInterfacesToExistingBond = !!checkedInterfaces.length && checkedBonds.length == 1,
|
||||
bondingPossible = creatingNewBond || addingInterfacesToExistingBond,
|
||||
unbondingPossible = !checkedInterfaces.length && !!checkedBonds.length,
|
||||
hasChanges = this.hasChanges(),
|
||||
hasErrors = _.chain(this.state.interfaceErrors).values().some().value(),
|
||||
slaveInterfaceNames = _.pluck(_.flatten(_.filter(interfaces.pluck('slaves'))), 'name'),
|
||||
returnEnabled = !this.state.actionInProgress,
|
||||
loadDefaultsEnabled = !this.state.actionInProgress && !locked,
|
||||
revertChangesEnabled = !this.state.actionInProgress && hasChanges,
|
||||
applyEnabled = !hasErrors && !this.state.actionInProgress && hasChanges;
|
||||
|
||||
return (
|
||||
<div className='edit-node-networks-screen' style={{display: 'block'}} ref='nodeNetworksScreen'>
|
||||
<div className={cx({'edit-node-interfaces': true, 'changes-locked': locked})}>
|
||||
<h3>
|
||||
{i18n(configureInterfacesTransNS + 'title', {count: nodes.length, name: nodeNames.join(', ')})}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className='row'>
|
||||
<div className='page-control-box'>
|
||||
<div className='page-control-button-placeholder'>
|
||||
<button className='btn btn-bond' disabled={!bondingAvailable || !bondingPossible} onClick={this.bondInterfaces}>{i18n(configureInterfacesTransNS + 'bond_button')}</button>
|
||||
<button className='btn btn-unbond' disabled={!bondingAvailable || !unbondingPossible} onClick={this.unbondInterfaces}>{i18n(configureInterfacesTransNS + 'unbond_button')}</button>
|
||||
</div>
|
||||
</div>
|
||||
{bondingAvailable &&
|
||||
<div className='bond-speed-warning alert hide'>{i18n(configureInterfacesTransNS + 'bond_speed_warning')}</div>
|
||||
}
|
||||
|
||||
<div className='node-networks'>
|
||||
{
|
||||
interfaces.map(_.bind(function(ifc) {
|
||||
if (!_.contains(slaveInterfaceNames, ifc.get('name'))) {
|
||||
return <NodeInterface {...this.props}
|
||||
key={'interface-' + ifc.get('name')}
|
||||
interface={ifc}
|
||||
locked={locked}
|
||||
bondingAvailable={bondingAvailable}
|
||||
getDraggedNetworks={this.getDraggedNetworks}
|
||||
setDraggedNetworks={this.setDraggedNetworks}
|
||||
errors={this.state.interfaceErrors[ifc.get('name')]}
|
||||
validate={this.validate}
|
||||
refresh={this.refresh}
|
||||
/>;
|
||||
}
|
||||
}, this))
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className='page-control-box'>
|
||||
<div className='back-button pull-left'>
|
||||
<button className='btn btn-return' onClick={this.returnToNodeList} disabled={!returnEnabled}>{i18n('cluster_page.nodes_tab.back_to_nodes_button')}</button>
|
||||
</div>
|
||||
<div className='page-control-button-placeholder'>
|
||||
<button className='btn btn-defaults' onClick={this.loadDefaults} disabled={!loadDefaultsEnabled}>{i18n('common.load_defaults_button')}</button>
|
||||
<button className='btn btn-revert-changes' onClick={this.revertChanges} disabled={!revertChangesEnabled}>{i18n('common.cancel_changes_button')}</button>
|
||||
<button className='btn btn-success btn-apply' onClick={this.applyChanges} disabled={!applyEnabled}>{i18n('common.apply_button')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
NodeInterface = React.createClass({
|
||||
mixins: [
|
||||
ComponentMixins.backboneMixin('cluster', 'change:status'),
|
||||
ComponentMixins.backboneMixin('interface', 'change:checked change:mode'),
|
||||
ComponentMixins.backboneMixin({
|
||||
modelOrCollection: function(props) {
|
||||
return props.interface.get('assigned_networks');
|
||||
},
|
||||
renderOn: 'add change remove'
|
||||
})
|
||||
],
|
||||
propTypes: {
|
||||
bondingAvailable: React.PropTypes.bool,
|
||||
locked: React.PropTypes.bool,
|
||||
refresh: React.PropTypes.func
|
||||
},
|
||||
onModelChange: function() {
|
||||
this.props.refresh();
|
||||
},
|
||||
componentDidMount: function() {
|
||||
$(this.refs.logicalNetworkBox.getDOMNode()).sortable({
|
||||
connectWith: '.logical-network-box',
|
||||
items: '.logical-network-group:not(.disabled)',
|
||||
containment: $('.node-networks'),
|
||||
disabled: this.props.locked,
|
||||
receive: this.dragStop,
|
||||
remove: this.dragStart,
|
||||
start: this.dragStart,
|
||||
stop: this.dragStop
|
||||
}).disableSelection();
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
this.props.validate();
|
||||
},
|
||||
dragStart: function(e, ui) {
|
||||
var networkNames = $(ui.item).find('.logical-network-item').map(function(index, el) {
|
||||
// NOTE(pkaminski): .data('name') returns an incorrect result here.
|
||||
// This is probably caused by jQuery .data cache (attr reads directly from DOM).
|
||||
// http://api.jquery.com/data/#data-html5
|
||||
return $(el).attr('data-name');
|
||||
});
|
||||
if (e.type == 'sortstart') {
|
||||
// NOTE(pkaminski): Save initial networks state -- this is used for blocking
|
||||
// of dragging within one interface -- see also this.dragStop
|
||||
this.initialNetworks = this.props.interface.get('assigned_networks').pluck('name');
|
||||
}
|
||||
if (e.type == 'sortremove') {
|
||||
$(this.refs.logicalNetworkBox.getDOMNode()).sortable('cancel');
|
||||
this.props.interface.get('assigned_networks').remove(this.props.getDraggedNetworks());
|
||||
} else {
|
||||
this.props.setDraggedNetworks(this.props.interface.get('assigned_networks').filter(function(network) {
|
||||
return _.contains(networkNames, network.get('name'));
|
||||
})[0]
|
||||
);
|
||||
}
|
||||
},
|
||||
dragStop: function(e) {
|
||||
var networks;
|
||||
|
||||
if (e.type == 'sortreceive') {
|
||||
this.props.interface.get('assigned_networks').add(this.props.getDraggedNetworks());
|
||||
} else if (e.type == 'sortstop') {
|
||||
// Block dragging within an interface
|
||||
networks = this.props.interface.get('assigned_networks').pluck('name');
|
||||
if (!_.xor(networks, this.initialNetworks).length) {
|
||||
$(this.refs.logicalNetworkBox.getDOMNode()).sortable('cancel');
|
||||
}
|
||||
this.initialNetworks = [];
|
||||
}
|
||||
this.props.setDraggedNetworks(null);
|
||||
},
|
||||
bondingChanged: function(name, value) {
|
||||
this.props.interface.set({checked: value});
|
||||
},
|
||||
bondingModeChanged: function(name, value) {
|
||||
this.props.interface.set({mode: value});
|
||||
},
|
||||
bondingRemoveInterface: function(slaveName) {
|
||||
var slaves = _.reject(this.props.interface.get('slaves'), {name: slaveName});
|
||||
this.props.interface.set('slaves', slaves);
|
||||
},
|
||||
render: function() {
|
||||
var configureInterfacesTransNS = 'cluster_page.nodes_tab.configure_interfaces.',
|
||||
ifc = this.props.interface,
|
||||
cluster = this.props.cluster,
|
||||
locked = this.props.locked,
|
||||
networkConfiguration = cluster.get('networkConfiguration'),
|
||||
networks = networkConfiguration.get('networks'),
|
||||
networkingParameters = networkConfiguration.get('networking_parameters'),
|
||||
slaveInterfaces = ifc.getSlaveInterfaces(),
|
||||
assignedNetworks = ifc.get('assigned_networks'),
|
||||
bondable = this.props.bondingAvailable && assignedNetworks && !assignedNetworks.find(function(interfaceNetwork) {
|
||||
return interfaceNetwork.getFullNetwork(networks).get('meta').unmovable;
|
||||
}),
|
||||
slaveOnlineClass = function(slave) {
|
||||
var slaveDown = slave.get('state') == 'down';
|
||||
return {
|
||||
'interface-online': !slaveDown,
|
||||
'interface-offline': slaveDown
|
||||
};
|
||||
},
|
||||
assignedNetworksGrouped = [],
|
||||
networksToAdd = [],
|
||||
showHelpMessage = !locked && !assignedNetworks.length;
|
||||
assignedNetworks.each(function(interfaceNetwork) {
|
||||
if (interfaceNetwork.getFullNetwork(networks).get('name') != 'floating') {
|
||||
if (networksToAdd.length) {
|
||||
assignedNetworksGrouped.push(networksToAdd);
|
||||
}
|
||||
networksToAdd = [];
|
||||
}
|
||||
networksToAdd.push(interfaceNetwork);
|
||||
});
|
||||
if (networksToAdd.length) {
|
||||
assignedNetworksGrouped.push(networksToAdd);
|
||||
}
|
||||
return (
|
||||
<div className={cx({'physical-network-box': true, nodrag: this.props.errors})}>
|
||||
<div className='network-box-item'>
|
||||
{ifc.isBond() &&
|
||||
<div className='network-box-name'>
|
||||
{this.props.bondingAvailable ?
|
||||
<controls.Input
|
||||
type='checkbox'
|
||||
label={ifc.get('name')}
|
||||
labelClassName='pull-left'
|
||||
onChange={this.bondingChanged}
|
||||
checked={ifc.get('checked')} />
|
||||
:
|
||||
<div className='network-bond-name pull-left disabled'>{ifc.get('name')}</div>
|
||||
}
|
||||
<div className='network-bond-mode pull-right'>
|
||||
<controls.Input
|
||||
type='select'
|
||||
disabled={!this.props.bondingAvailable}
|
||||
onChange={this.bondingModeChanged}
|
||||
value={ifc.get('mode')}
|
||||
label={i18n(configureInterfacesTransNS + 'bonding_mode') + ':'}
|
||||
children={_.map(models.Interface.prototype.bondingModes, function(mode) {
|
||||
return <option key={'option-' + mode} value={mode}>{i18n(configureInterfacesTransNS + 'bonding_modes.' + mode)}</option>;
|
||||
})} />
|
||||
</div>
|
||||
<div className='clearfix'></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className='physical-network-checkbox'>
|
||||
{!ifc.isBond() && bondable && <controls.Input type='checkbox' onChange={this.bondingChanged} checked={ifc.get('checked')} />}
|
||||
</div>
|
||||
|
||||
<div className='network-connections-block'>
|
||||
{_.map(slaveInterfaces, function(slaveInterface) {
|
||||
return <div key={'network-connections-slave-' + slaveInterface.get('name')} className='network-interfaces-status'>
|
||||
<div className={cx(slaveOnlineClass(slaveInterface))}></div>
|
||||
<div className='network-interfaces-name'>{slaveInterface.get('name')}</div>
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className='network-connections-info-block'>
|
||||
{_.map(slaveInterfaces, function(slaveInterface) {
|
||||
return <div key={'network-connections-info-' + slaveInterface.get('name')} className='network-connections-info-block-item'>
|
||||
<div className='network-connections-info-position'></div>
|
||||
<div className='network-connections-info-description'>
|
||||
<div>{i18n(configureInterfacesTransNS + 'mac')}: {slaveInterface.get('mac')}</div>
|
||||
<div>{i18n(configureInterfacesTransNS + 'speed')}:
|
||||
{utils.showBandwidth(slaveInterface.get('current_speed'))}</div>
|
||||
{(this.props.bondingAvailable && slaveInterfaces.length >= 3) &&
|
||||
<button className='btn btn-link btn-remove-interface'
|
||||
type='button'
|
||||
onClick={this.bondingRemoveInterface.bind(this, slaveInterface.get('name'))}>{i18n('common.remove_button')}</button>
|
||||
}
|
||||
</div>
|
||||
</div>;
|
||||
}, this)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className='logical-network-box' ref='logicalNetworkBox'>
|
||||
{!showHelpMessage ? _.map(assignedNetworksGrouped, function(networkGroup) {
|
||||
var network = networkGroup[0].getFullNetwork(networks);
|
||||
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
var classes = {
|
||||
'logical-network-group': true,
|
||||
disabled: locked || network.get('meta').unmovable
|
||||
},
|
||||
vlanRange = network.getVlanRange(networkingParameters);
|
||||
|
||||
return <div key={'network-box-' + network.get('id')} className={cx(classes)}>
|
||||
{_.map(networkGroup, function(interfaceNetwork) {
|
||||
return (
|
||||
<div key={'interface-network-' + interfaceNetwork.get('name')}
|
||||
className='logical-network-item' data-name={interfaceNetwork.get('name')}>
|
||||
<div className='name'>{i18n('network.' + interfaceNetwork.get('name'), {defaultValue: interfaceNetwork.get('name')})}</div>
|
||||
{vlanRange &&
|
||||
<div className='id'>
|
||||
{i18n(configureInterfacesTransNS + 'vlan_id', {count: _.uniq(vlanRange).length})}:
|
||||
{_.uniq(vlanRange).join('-')}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
}, this)
|
||||
: <div className='network-help-message'>{i18n(configureInterfacesTransNS + 'drag_and_drop_description')}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{this.props.errors &&
|
||||
<div className='network-box-error-message common enable-selection'>
|
||||
{this.props.errors}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return EditNodeInterfacesScreen;
|
||||
});
|
@ -1,36 +0,0 @@
|
||||
<div class="edit-node-interfaces <%= locked ? 'changes-locked' : '' %>">
|
||||
<h3>
|
||||
<%- i18n('cluster_page.nodes_tab.configure_interfaces.title', {count: nodes.length, name: nodes.length && nodes.at(0).get('name')}) %>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<% if (bondingAvailable) { %>
|
||||
<div class="page-control-box">
|
||||
<div class="page-control-button-placeholder">
|
||||
<button class="btn btn-bond" data-i18n="cluster_page.nodes_tab.configure_interfaces.bond_button"></button>
|
||||
<button class="btn btn-unbond" data-i18n="cluster_page.nodes_tab.configure_interfaces.unbond_button"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bond-speed-warning alert hide" data-i18n="cluster_page.nodes_tab.configure_interfaces.bond_speed_warning"></div>
|
||||
|
||||
<% } %>
|
||||
<div class="node-networks">
|
||||
<div class="progress-bar">
|
||||
<div class="progress progress-striped progress-success active"><div class="bar"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- page-control box -->
|
||||
<div class="page-control-box">
|
||||
<div class="back-button pull-left">
|
||||
<button class="btn btn-return" data-i18n="cluster_page.nodes_tab.back_to_nodes_button"></button>
|
||||
</div>
|
||||
<div class="page-control-button-placeholder">
|
||||
<button data-i18n="common.load_defaults_button" class="btn btn-defaults"></button>
|
||||
<button data-i18n="common.cancel_changes_button" class="btn btn-revert-changes"></button>
|
||||
<button data-i18n="common.apply_button" class="btn btn-success btn-apply"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -10,6 +10,7 @@
|
||||
"apply_button": "Apply",
|
||||
"rename_button": "Rename",
|
||||
"delete_button": "Delete",
|
||||
"remove_button": "Remove",
|
||||
"stop_button": "Stop",
|
||||
"reset_button": "Reset",
|
||||
"save_settings_button": "Save Settings",
|
||||
@ -241,6 +242,7 @@
|
||||
"title_plural": "Configure interfaces on __count__ nodes",
|
||||
"vlan_id": "VLAN ID",
|
||||
"vlan_id_plural": "VLAN IDs",
|
||||
"mac": "MAC",
|
||||
"bond_button": "Bond Interfaces",
|
||||
"unbond_button": "Unbond Interfaces",
|
||||
"bond_speed_warning": "Bonding interfaces of different speeds is not recommended",
|
||||
@ -257,6 +259,10 @@
|
||||
"saving_warning": "Unable to apply changes",
|
||||
"load_defaults_warning": "Unable to load default configuration"
|
||||
},
|
||||
"node_loading_error": {
|
||||
"title": "Error loading nodes",
|
||||
"load_error": "Some nodes failed to load"
|
||||
},
|
||||
"validation": {
|
||||
"too_many_untagged_networks": "Untagged networks can not be assigned to the same interface"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user