Merge "Lock interfaces properties with different availability batch editing"
This commit is contained in:
commit
60366e3127
|
@ -3770,6 +3770,16 @@ input[type=range] {
|
|||
padding-left: 0;
|
||||
margin-left: @property-offset;
|
||||
}
|
||||
&.forbidden {
|
||||
opacity: 0.8;
|
||||
.glyphicon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
button {
|
||||
text-decoration: none;
|
||||
color: @gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.mtu-control {
|
||||
|
|
|
@ -453,7 +453,9 @@
|
|||
"physical_network": "Physical Network Name",
|
||||
"dpdk": "DPDK",
|
||||
"dpdk_description": "The Data Plane Development Kit (DPDK) provides high-performance packet processing libraries and user space drivers.",
|
||||
"dpdk_in_ovs_bond": "DPDK can not be disabled in OVS bond"
|
||||
"dpdk_in_ovs_bond": "DPDK can not be disabled in OVS bond",
|
||||
"different_availability": "<Different Availability>",
|
||||
"availability_tooltip": "Some network interfaces do not support this feature, therefore, these properties will not change after saving."
|
||||
},
|
||||
"configure_disks": {
|
||||
"no_disks": "No unassigned disks available.",
|
||||
|
|
|
@ -324,6 +324,15 @@ var utils = {
|
|||
},
|
||||
makePath(...args) {
|
||||
return args.join('.');
|
||||
},
|
||||
deepOmit(object, keys) {
|
||||
if (!_.isObject(object) && !_.isArray(object)) {
|
||||
return object;
|
||||
}
|
||||
return _.reduce(_.difference(_.keys(object), keys), (result, key) => {
|
||||
result[key] = this.deepOmit(object[key], keys);
|
||||
return result;
|
||||
}, _.isArray(object) ? [] : {});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ import models from 'models';
|
|||
import dispatcher from 'dispatcher';
|
||||
import Expression from 'expression';
|
||||
import OffloadingModes from 'views/cluster_page_tabs/nodes_tab_screens/offloading_modes_control';
|
||||
import {Input} from 'views/controls';
|
||||
import {Input, Tooltip} from 'views/controls';
|
||||
import {backboneMixin, unsavedChangesMixin} from 'component_mixins';
|
||||
import {DragSource, DropTarget} from 'react-dnd';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
@ -83,11 +83,48 @@ var EditNodeInterfacesScreen = React.createClass({
|
|||
};
|
||||
},
|
||||
componentWillMount() {
|
||||
this.setState({initialInterfaces: _.cloneDeep(this.interfacesToJSON(this.props.interfaces))});
|
||||
this.setState({
|
||||
initialInterfaces: _.cloneDeep(this.interfacesToJSON(this.props.interfaces)),
|
||||
limitations: this.getEditLimitations()
|
||||
});
|
||||
},
|
||||
componentDidMount() {
|
||||
this.validate();
|
||||
},
|
||||
compareInterfacesProperties(interfaces, path, iteratee = _.identity,
|
||||
source = 'interface_properties') {
|
||||
// Checks if all the sub parameters are equal for all interfaces property
|
||||
var ifcProperties = _.map(interfaces.map((ifc) => {
|
||||
var interfaceProperty = ifc.get(source);
|
||||
return _.get(interfaceProperty, path, interfaceProperty);
|
||||
}), iteratee);
|
||||
var shown = _.first(ifcProperties);
|
||||
var equal = !_.any(ifcProperties, (ifcProperty) => !_.isEqual(ifcProperty, shown));
|
||||
|
||||
return {equal, shown};
|
||||
},
|
||||
getEditLimitations() {
|
||||
// Gets limitations for interfaces parameters editing.
|
||||
// Parameter should not be editable if its differently available
|
||||
// across the nodes interfaces
|
||||
return this.props.interfaces.reduce((result, ifc, index) => {
|
||||
var interfaces = this.props.nodes.map((node) => {
|
||||
return node.interfaces.at(index);
|
||||
});
|
||||
var key = ifc.isBond() ? ifc.get('name') : ifc.id;
|
||||
result[key] = {
|
||||
offloading_modes: this.compareInterfacesProperties(
|
||||
interfaces, '',
|
||||
(value) => utils.deepOmit(value, ['state']),
|
||||
'offloading_modes'
|
||||
),
|
||||
dpdk: this.compareInterfacesProperties(interfaces, 'dpdk.available'),
|
||||
sriov: this.compareInterfacesProperties(interfaces, 'sriov.available'),
|
||||
mtu: {equal: true, shown: true}
|
||||
};
|
||||
return result;
|
||||
}, {});
|
||||
},
|
||||
isLocked() {
|
||||
return !!this.props.cluster.task({group: 'deployment', active: true}) ||
|
||||
!_.all(this.props.nodes.invoke('areInterfacesConfigurable'));
|
||||
|
@ -107,15 +144,43 @@ var EditNodeInterfacesScreen = React.createClass({
|
|||
},
|
||||
hasChangesInRemainingNodes() {
|
||||
var initialInterfacesData = _.map(this.state.initialInterfaces, this.interfacesPickFromJSON);
|
||||
var limitationsKeys = this.props.nodes.at(0).interfaces.map(
|
||||
(ifc) => ifc.get(ifc.isBond ? 'name' : 'id')
|
||||
);
|
||||
|
||||
return _.any(this.props.nodes.slice(1), (node) => {
|
||||
var interfacesData = this.interfacesToJSON(node.interfaces, true);
|
||||
return _.any(initialInterfacesData, (ifcData, index) => {
|
||||
var limitations = this.state.limitations[limitationsKeys[index]];
|
||||
var omittedProperties = _.filter(
|
||||
_.keys(limitations),
|
||||
(key) => !_.get(limitations[key], 'equal', true)
|
||||
);
|
||||
return _.any(ifcData, (data, attribute) => {
|
||||
if (attribute === 'slaves') {
|
||||
// bond 'slaves' attribute contains information about slave name only
|
||||
// but interface names can be different between nodes
|
||||
// and can not be used for the comparison
|
||||
return data.length !== (interfacesData[index].slaves || {}).length;
|
||||
// Restricted parameters should not participate in changes detection
|
||||
switch (attribute) {
|
||||
case 'offloading_modes': {
|
||||
// Do not compare offloading modes if they differ
|
||||
if (!_.get(limitations, 'offloading_modes.equal', false)) return false;
|
||||
// otherwise remove set states before it
|
||||
return !_.isEqual(..._.invoke(
|
||||
[data, interfacesData[index][attribute]],
|
||||
(value) => utils.deepOmit(value, ['state']))
|
||||
);
|
||||
}
|
||||
case 'interface_properties': {
|
||||
// Omit restricted parameters from the comparison
|
||||
return !_.isEqual(..._.invoke(
|
||||
[data, interfacesData[index][attribute]],
|
||||
_.omit, omittedProperties)
|
||||
);
|
||||
}
|
||||
case 'slaves': {
|
||||
// bond 'slaves' attribute contains information about slave name only
|
||||
// but interface names can be different between nodes
|
||||
// and can not be used for the comparison
|
||||
return data.length !== (interfacesData[index].slaves || {}).length;
|
||||
}
|
||||
}
|
||||
return !_.isEqual(data, interfacesData[index][attribute]);
|
||||
});
|
||||
|
@ -145,6 +210,37 @@ var EditNodeInterfacesScreen = React.createClass({
|
|||
revertChanges() {
|
||||
this.props.interfaces.reset(_.cloneDeep(this.state.initialInterfaces), {parse: true});
|
||||
},
|
||||
updateWithLimitations(sourceInterface, targetInterface) {
|
||||
// Interface parameters should be updated with respect to limitations:
|
||||
// restricted parameters should not be changed
|
||||
var limitations = this.state.limitations[targetInterface.id];
|
||||
var targetInterfaceProperties = targetInterface.get('interface_properties');
|
||||
var sourceInterfaceProperties = sourceInterface.get('interface_properties');
|
||||
|
||||
if (targetInterface.get('offloading_modes')
|
||||
&& _.get(limitations, 'offloading_modes.equal', false)) {
|
||||
targetInterface.set({
|
||||
offloading_modes: sourceInterface.get('offloading_modes')
|
||||
});
|
||||
// If set of offloading modes supported is the same, disable_offloading
|
||||
// parameters updated as well (it is probably obsolete)
|
||||
var disableOffloading = _.get(sourceInterfaceProperties, 'disable_offloading');
|
||||
if (!_.isUndefined(disableOffloading)) {
|
||||
_.set(targetInterfaceProperties, 'disable_offloading', disableOffloading);
|
||||
}
|
||||
}
|
||||
|
||||
_.each(sourceInterfaceProperties, (propertyValue, propertyName) => {
|
||||
// Set all unrestricted parameters values
|
||||
if (!_.isPlainObject(propertyValue)
|
||||
&& _.get(limitations, propertyName + '.equal', false)) {
|
||||
_.set(targetInterfaceProperties, propertyName, propertyValue);
|
||||
}
|
||||
});
|
||||
targetInterface.set({
|
||||
interface_properties: sourceInterfaceProperties
|
||||
});
|
||||
},
|
||||
applyChanges() {
|
||||
if (!this.isSavingPossible()) return $.Deferred().reject();
|
||||
|
||||
|
@ -187,14 +283,9 @@ var EditNodeInterfacesScreen = React.createClass({
|
|||
ifc.set({
|
||||
assigned_networks: new models.InterfaceNetworks(
|
||||
updatedIfc.get('assigned_networks').toJSON()
|
||||
),
|
||||
interface_properties: updatedIfc.get('interface_properties')
|
||||
)
|
||||
});
|
||||
if (ifc.get('offloading_modes')) {
|
||||
ifc.set({
|
||||
offloading_modes: updatedIfc.get('offloading_modes')
|
||||
});
|
||||
}
|
||||
this.updateWithLimitations(updatedIfc, ifc);
|
||||
});
|
||||
|
||||
return Backbone.sync('update', node.interfaces, {url: _.result(node, 'url') + '/interfaces'});
|
||||
|
@ -257,16 +348,21 @@ var EditNodeInterfacesScreen = React.createClass({
|
|||
this.setState({actionInProgress: true});
|
||||
var interfaces = this.props.interfaces.filter((ifc) => ifc.get('checked') && !ifc.isBond());
|
||||
var bond = this.props.interfaces.find((ifc) => ifc.get('checked') && ifc.isBond());
|
||||
var limitations = this.state.limitations;
|
||||
var bondName;
|
||||
|
||||
if (!bond) {
|
||||
// if no bond selected - create new one
|
||||
var bondMode = _.flatten(
|
||||
_.pluck(this.props.bondingConfig.properties[bondType].mode, 'values')
|
||||
)[0];
|
||||
bondName = this.props.interfaces.generateBondName(
|
||||
bondType === 'linux' ? 'bond' : 'ovs-bond'
|
||||
);
|
||||
|
||||
bond = new models.Interface({
|
||||
type: 'bond',
|
||||
name: this.props.interfaces.generateBondName(bondType === 'linux' ? 'bond' : 'ovs-bond'),
|
||||
name: bondName,
|
||||
mode: bondMode,
|
||||
assigned_networks: new models.InterfaceNetworks(),
|
||||
slaves: _.invoke(interfaces, 'pick', 'name'),
|
||||
|
@ -288,9 +384,12 @@ var EditNodeInterfacesScreen = React.createClass({
|
|||
},
|
||||
offloading_modes: this.getIntersectedOffloadingModes(interfaces)
|
||||
});
|
||||
limitations[bondName] = {};
|
||||
} else {
|
||||
// adding interfaces to existing bond
|
||||
var bondProperties = _.cloneDeep(bond.get('interface_properties'));
|
||||
bondName = bond.get('name');
|
||||
|
||||
if (bondProperties.dpdk.enabled) {
|
||||
bondProperties.dpdk.enabled = !_.any(interfaces,
|
||||
(ifc) => !ifc.get('interface_properties').dpdk.enabled
|
||||
|
@ -304,18 +403,42 @@ var EditNodeInterfacesScreen = React.createClass({
|
|||
// remove the bond to add it later and trigger re-rendering
|
||||
this.props.interfaces.remove(bond, {silent: true});
|
||||
}
|
||||
_.each(interfaces, (ifc) => {
|
||||
limitations[bondName] = _.reduce(interfaces, (result, ifc) => {
|
||||
bond.get('assigned_networks').add(ifc.get('assigned_networks').models);
|
||||
ifc.get('assigned_networks').reset();
|
||||
ifc.set({checked: false});
|
||||
});
|
||||
return this.mergeLimitations(result, limitations[ifc.id]);
|
||||
}, limitations[bondName]);
|
||||
|
||||
this.props.interfaces.add(bond);
|
||||
this.setState({actionInProgress: false});
|
||||
this.setState({
|
||||
actionInProgress: false,
|
||||
limitations: limitations
|
||||
});
|
||||
},
|
||||
mergeLimitations(limitation1, limitation2) {
|
||||
return _.merge(limitation1, limitation2, (value1, value2, interfaceProperty) => {
|
||||
switch (interfaceProperty) {
|
||||
case 'mtu':
|
||||
case 'offloading_modes':
|
||||
// Offloading modes are presumed to be calculated intersection
|
||||
return {equal: true, shown: true};
|
||||
case 'dpdk':
|
||||
if (_.isUndefined(value1) || _.isUndefined(value2)) break;
|
||||
|
||||
// Both interfaces should support DPDK in order bond to support it either
|
||||
var equal = true;
|
||||
var shown = value1.shown && value2.shown;
|
||||
return {equal: equal, shown: shown};
|
||||
case 'sriov':
|
||||
return {equal: true, shown: false};
|
||||
}
|
||||
});
|
||||
},
|
||||
unbondInterfaces() {
|
||||
this.setState({actionInProgress: true});
|
||||
_.each(this.props.interfaces.where({checked: true}), (bond) => {
|
||||
return this.removeInterfaceFromBond(bond.get('name'));
|
||||
this.removeInterfaceFromBond(bond.get('name'));
|
||||
});
|
||||
this.setState({actionInProgress: false});
|
||||
},
|
||||
|
@ -407,7 +530,7 @@ var EditNodeInterfacesScreen = React.createClass({
|
|||
return !_.chain(this.state.interfacesErrors).values().some().value() &&
|
||||
!this.state.actionInProgress && this.hasChanges();
|
||||
},
|
||||
getIfcProperty(property) {
|
||||
getInterfaceProperty(property) {
|
||||
var {interfaces, nodes} = this.props;
|
||||
var bondsCount = interfaces.filter((ifc) => ifc.isBond()).length;
|
||||
var getPropertyValues = (ifcIndex) => {
|
||||
|
@ -459,8 +582,8 @@ var EditNodeInterfacesScreen = React.createClass({
|
|||
this.validateSpeedsForBonding(checkedBonds.concat(checkedInterfaces)) ||
|
||||
interfaces.any((ifc) => ifc.isBond() && this.validateSpeedsForBonding([ifc]));
|
||||
|
||||
var interfaceSpeeds = this.getIfcProperty('current_speed');
|
||||
var interfaceNames = this.getIfcProperty('name');
|
||||
var interfaceSpeeds = this.getInterfaceProperty('current_speed');
|
||||
var interfaceNames = this.getInterfaceProperty('name');
|
||||
|
||||
return (
|
||||
<div className='row'>
|
||||
|
@ -514,12 +637,15 @@ var EditNodeInterfacesScreen = React.createClass({
|
|||
<div className='ifc-list col-xs-12'>
|
||||
{interfaces.map((ifc, index) => {
|
||||
var ifcName = ifc.get('name');
|
||||
var limitations = this.state.limitations[ifc.isBond() ? ifcName : ifc.id];
|
||||
|
||||
if (!_.contains(slaveInterfaceNames, ifcName)) {
|
||||
return (
|
||||
<NodeInterfaceDropTarget
|
||||
{...this.props}
|
||||
key={'interface-' + ifcName}
|
||||
interface={ifc}
|
||||
limitations={limitations}
|
||||
hasChanges={
|
||||
!_.isEqual(
|
||||
_.findWhere(this.state.initialInterfaces, {name: ifcName}),
|
||||
|
@ -745,42 +871,63 @@ var NodeInterface = React.createClass({
|
|||
_.set(interfaceProperties, name, value);
|
||||
this.props.interface.set('interface_properties', interfaceProperties);
|
||||
},
|
||||
renderLockTooltip(property) {
|
||||
return <Tooltip key={property + '-unavailable'} text={i18n(ns + 'availability_tooltip')}>
|
||||
<span className='glyphicon glyphicon-lock' aria-hidden='true'></span>
|
||||
</Tooltip>;
|
||||
},
|
||||
renderConfigurableAttributes() {
|
||||
var ifc = this.props.interface;
|
||||
var limitations = this.props.limitations;
|
||||
var ifcProperties = ifc.get('interface_properties');
|
||||
var errors = (this.props.errors || {}).interface_properties;
|
||||
var offloadingModes = ifc.get('offloading_modes') || [];
|
||||
var {collapsed, activeInterfaceSectionName} = this.state;
|
||||
var offloadingRestricted = !limitations.offloading_modes.equal;
|
||||
var offloadingTabClasses = {
|
||||
forbidden: offloadingRestricted,
|
||||
'property-item-container': true,
|
||||
active: !collapsed && activeInterfaceSectionName === this.renderedIfcProperties[0]
|
||||
};
|
||||
return (
|
||||
<div className='properties-list'>
|
||||
<span className={utils.classNames(offloadingTabClasses)}>
|
||||
{offloadingRestricted && this.renderLockTooltip('offloading')}
|
||||
{i18n(ns + 'offloading_modes') + ':'}
|
||||
<button
|
||||
className='btn btn-link property-item'
|
||||
onClick={() => this.switchActiveSubtab(this.renderedIfcProperties[0])}
|
||||
disabled={offloadingRestricted}
|
||||
>
|
||||
{ifcProperties.disable_offloading ?
|
||||
i18n(ns + 'disable_offloading')
|
||||
:
|
||||
offloadingModes.length ?
|
||||
this.makeOffloadingModesExcerpt()
|
||||
{offloadingRestricted ?
|
||||
i18n(ns + 'different_availability')
|
||||
:
|
||||
i18n(ns + 'default_offloading')
|
||||
offloadingModes.length ?
|
||||
this.makeOffloadingModesExcerpt()
|
||||
:
|
||||
ifcProperties.disable_offloading ?
|
||||
i18n(ns + 'disable_offloading')
|
||||
:
|
||||
i18n(ns + 'default_offloading')
|
||||
}
|
||||
</button>
|
||||
</span>
|
||||
{_.map(ifcProperties, (propertyValue, propertyName) => {
|
||||
if (_.isPlainObject(propertyValue) && !propertyValue.available) return null;
|
||||
var {equal, shown} = _.get(
|
||||
limitations, propertyName,
|
||||
{equal: true, shown: true}
|
||||
);
|
||||
var propertyShown = !equal || shown;
|
||||
|
||||
if (_.isPlainObject(propertyValue) && !propertyShown) return null;
|
||||
|
||||
if (_.contains(this.renderedIfcProperties, propertyName)) {
|
||||
var classes = {
|
||||
'text-danger': _.has(errors, propertyName),
|
||||
'property-item-container': true,
|
||||
[propertyName]: true,
|
||||
active: !collapsed && activeInterfaceSectionName === propertyName
|
||||
active: !collapsed && activeInterfaceSectionName === propertyName,
|
||||
forbidden: !equal
|
||||
};
|
||||
var commonButtonProps = {
|
||||
className: 'btn btn-link property-item',
|
||||
|
@ -792,12 +939,17 @@ var NodeInterface = React.createClass({
|
|||
case 'dpdk':
|
||||
return (
|
||||
<span key={propertyName} className={utils.classNames(classes)}>
|
||||
{!equal && this.renderLockTooltip(propertyName)}
|
||||
{i18n(ns + propertyName) + ':'}
|
||||
<button {...commonButtonProps}>
|
||||
{propertyValue.enabled ?
|
||||
i18n('common.enabled')
|
||||
:
|
||||
i18n('common.disabled')
|
||||
<button {...commonButtonProps} disabled={!equal}>
|
||||
{equal ?
|
||||
propertyValue.enabled ?
|
||||
i18n('common.enabled')
|
||||
:
|
||||
i18n('common.disabled')
|
||||
:
|
||||
i18n(ns + 'different_availability')
|
||||
|
||||
}
|
||||
</button>
|
||||
</span>
|
||||
|
@ -805,8 +957,9 @@ var NodeInterface = React.createClass({
|
|||
default:
|
||||
return (
|
||||
<span key={propertyName} className={utils.classNames(classes)}>
|
||||
{!equal && this.renderLockTooltip(propertyName)}
|
||||
{i18n(ns + propertyName) + ':'}
|
||||
<button {...commonButtonProps}>
|
||||
<button {...commonButtonProps} disabled={!equal}>
|
||||
{propertyValue || i18n(ns + propertyName + '_placeholder')}
|
||||
</button>
|
||||
</span>
|
||||
|
@ -965,6 +1118,10 @@ var NodeInterface = React.createClass({
|
|||
'glyphicon glyphicon-menu-down': true,
|
||||
rotate: !this.state.collapsed
|
||||
});
|
||||
var defaultSubtab = _.find(this.renderedIfcProperties, (ifcProperty) => {
|
||||
var limitation = _.get(this.props.limitations, ifcProperty);
|
||||
return limitation && limitation.equal && !!limitation.shown;
|
||||
});
|
||||
return (
|
||||
<div className='ifc-properties clearfix forms-box'>
|
||||
<div className='row'>
|
||||
|
@ -977,7 +1134,7 @@ var NodeInterface = React.createClass({
|
|||
onClick={() => this.switchActiveSubtab(
|
||||
isConfigurationModeOn ?
|
||||
this.state.activeInterfaceSectionName :
|
||||
this.renderedIfcProperties[0]
|
||||
defaultSubtab
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue