Support pluggable NIC attributes

Implements blueprint nics-and-nodes-attributes-via-plugin

Change-Id: I5b5423be48cbe59ea60a6faa90a4bd4d5af7ece0
This commit is contained in:
Anton Zemlyanov 2016-06-15 16:35:40 +03:00 committed by Julia Aranovich
parent ac3d75b325
commit adece15d24
8 changed files with 815 additions and 857 deletions

View File

@ -842,7 +842,8 @@ models.Settings = BaseModel
validate(attrs, options) {
var errors = {};
var models = options ? options.models : {};
var checkRestrictions = (setting) => this.checkRestrictions(models, null, setting);
var checkRestrictions =
(setting) => this.checkRestrictions(models, ['disable', 'hide'], setting);
_.each(attrs, (group, groupName) => {
if ((group.metadata || {}).enabled === false ||
checkRestrictions(group.metadata).result) return;
@ -1059,11 +1060,13 @@ models.Interface = BaseModel
parse(response) {
response.assigned_networks = new models.InterfaceNetworks(response.assigned_networks);
response.assigned_networks.interface = this;
response.attributes = new models.InterfaceAttributes(response.attributes);
return response;
},
toJSON(options) {
return _.omit(_.extend(this.constructor.__super__.toJSON.call(this, options), {
assigned_networks: this.get('assigned_networks').toJSON()
assigned_networks: this.get('assigned_networks').toJSON(),
attributes: this.get('attributes').toJSON()
}), 'checked');
},
isBond() {
@ -1076,6 +1079,7 @@ models.Interface = BaseModel
},
validate(attrs, options) {
var errors = {};
var networkErrors = [];
var networks = new models.Networks(this.get('assigned_networks')
.invokeMap('getFullNetwork', attrs.networks));
@ -1090,8 +1094,6 @@ models.Interface = BaseModel
networkErrors.push(i18n(ns + 'too_many_untagged_networks'));
}
_.extend(errors, this.validateInterfaceProperties(options));
// check interface networks have the same vlan id
var vlans = _.reject(networks.map('vlan_start'), _.isNull);
if (_.uniq(vlans).length < vlans.length) {
@ -1111,68 +1113,25 @@ models.Interface = BaseModel
)
) networkErrors.push(i18n(ns + 'vlan_range_intersection'));
var sriov = this.get('interface_properties').sriov;
if (sriov && sriov.enabled && networks.length) {
networkErrors.push(i18n(ns + 'sriov_placement_error'));
}
var dpdk = this.get('interface_properties').dpdk;
if (dpdk && dpdk.enabled && !_.isEqual(networks.map('name'), ['private'])) {
networkErrors.push(i18n(ns + 'dpdk_placement_error'));
// network assignment validation based on interface attributes
var attributes = this.get('attributes');
if (
attributes.get('sriov.enabled.value') &&
networks.length
) networkErrors.push(i18n(ns + 'sriov_placement_error'));
if (
attributes.get('dpdk.enabled.value') &&
!_.isEqual(networks.map('name'), ['private'])
) networkErrors.push(i18n(ns + 'dpdk_placement_error'));
if (networkErrors.length) errors.networks = networkErrors;
attributes.isValid(options);
if (!_.isNull(attributes.validationError)) {
errors.attributes = attributes.validationError;
}
if (networkErrors.length) {
errors.network_errors = networkErrors;
}
return errors;
},
validateInterfaceProperties(options) {
var interfaceProperties = this.get('interface_properties');
if (!interfaceProperties) return null;
var errors = {};
var ns = 'cluster_page.nodes_tab.configure_interfaces.validation.';
var mtuValue = parseInt(interfaceProperties.mtu, 10);
if (mtuValue) {
if (_.isNaN(mtuValue) || mtuValue < 42 || mtuValue > 65536) {
errors.mtu = i18n(ns + 'invalid_mtu');
} else if (interfaceProperties.dpdk.enabled && mtuValue > 1500) {
errors.mtu = i18n(ns + 'dpdk_mtu_error');
}
}
_.extend(errors, this.validateSRIOV(options), this.validateDPDK(options));
return _.isEmpty(errors) ? null : {interface_properties: errors};
},
validateSRIOV({cluster}) {
var sriov = this.get('interface_properties').sriov;
if (!sriov || !sriov.enabled) return null;
var ns = 'cluster_page.nodes_tab.configure_interfaces.validation.';
var errors = {};
if (cluster.get('settings').get('common.libvirt_type.value') !== 'kvm') {
errors.common = i18n(ns + 'sriov_hypervisor_alert');
}
var virtualFunctionsNumber = Number(sriov.sriov_numvfs);
var totalVirtualFunctionsNumber = Number(sriov.sriov_totalvfs);
if (_.isNaN(virtualFunctionsNumber) || virtualFunctionsNumber < 0) {
errors.sriov_numvfs = i18n(ns + 'invalid_virtual_functions_number');
} else if (!_.isNaN(totalVirtualFunctionsNumber) &&
virtualFunctionsNumber > totalVirtualFunctionsNumber) {
errors.sriov_numvfs = i18n(ns + 'invalid_virtual_functions_number_max',
{max: totalVirtualFunctionsNumber}
);
}
if (sriov.physnet && !sriov.physnet.match(utils.regexes.networkName)) {
errors.physnet = i18n(ns + 'invalid_physnet');
} else if (!_.trim(sriov.physnet)) {
errors.physnet = i18n(ns + 'empty_physnet');
}
return _.isEmpty(errors) ? null : {sriov: errors};
},
validateDPDK({cluster}) {
var dppk = this.get('interface_properties').dpdk;
if (!dppk || !dppk.enabled ||
cluster.get('settings').get('common.libvirt_type.value') === 'kvm') return null;
var ns = 'cluster_page.nodes_tab.configure_interfaces.validation.';
return {dpdk: {common: i18n(ns + 'dpdk_hypervisor_alert')}};
return _.isEmpty(errors) ? null : errors;
}
});
@ -1209,6 +1168,49 @@ models.InterfaceNetworks = BaseCollection.extend({
}
});
models.InterfaceAttributes = models.Settings.extend({
constructorName: 'InterfaceAttributes',
root: null,
getValueAttribute() {
return 'value';
},
validate(attrs, options = {}) {
options.models = _.extend({nic_attributes: this, default: this}, options.configModels);
var errors = this._super('validate', [attrs, options]);
// MTU has a special validation case which depends on DPDK and cant be moved to YAML
// SRIOV virtual functions number has maximum which is set into interface plain object metadata
_.extend(errors, this.validateMTU(options), this.validateSRIOV(options));
return _.isEmpty(errors) ? null : errors;
},
validateMTU() {
var mtu = this.get('mtu.value.value');
return this.get('dpdk.enabled.value') && _.isNumber(mtu) && mtu > 1500 ? {
'mtu.value': i18n('cluster_page.nodes_tab.configure_interfaces.validation.dpdk_mtu_error')
} : null;
},
validateSRIOV({meta = {}}) {
var totalVirtualFunctionsNumber = Number((meta.sriov || {}).totalvfs);
return (meta.sriov || {}).available && this.get('sriov.enabled.value') &&
_.isNumber(totalVirtualFunctionsNumber) &&
this.get('sriov.numvfs.value') > totalVirtualFunctionsNumber ? {
'sriov.numvfs': i18n(
'cluster_page.nodes_tab.configure_interfaces.validation.big_virtual_functions_number',
{max: totalVirtualFunctionsNumber}
)
} : null;
}
});
models.BondDefaultAttributes = BaseModel
.extend(cacheMixin)
.extend({
constructorName: 'BondDefaultAttributes',
url() {
return '/api/v1/nodes/' + this.nodeId + '/bonds/attributes/defaults';
}
});
models.Network = BaseModel.extend({
constructorName: 'Network',
getVlanRange(networkingParameters) {

View File

@ -3943,27 +3943,6 @@ input[type=range] {
margin: 0;
}
.form-group {
input, select {
width: 150px;
}
&.pull-right > div {
float: left;
}
}
.checkbox-group {
label {
padding-top: 7px;
}
}
.form-group, .checkbox-group {
margin-bottom: 0;
label {
width: auto;
.font-semibold;
}
}
.ifc-info-block {
margin-bottom: @base-ifc-indent * 2;
margin-left: @base-ifc-indent * 2;
@ -4009,37 +3988,47 @@ input[type=range] {
.help-block {
display: none;
}
.form-group {
margin-bottom: 0;
label {
width: auto;
}
> div {
float: left;
select {
width: 150px;
}
}
}
.common-ifc-name {
&.no-checkbox {
padding: 7px 0 0 @base-ifc-indent * 2;
.font-semibold;
}
.checkbox-group {
margin-bottom: 0;
margin-top: 5px;
label {
.font-semibold;
}
}
}
}
.ifc-properties {
@common-offset: 10px;
border-top: 1px dotted @ifc-container-dark-color;
padding: @common-offset @common-offset * 2 @common-offset;
.checkbox-group {
.custom-tumbler {
margin-bottom: @common-offset;
}
}
.toggle-offloading, .dpdk-control, .sriov-control {
label {padding-top: 0;}
.custom-tumbler {margin-bottom: 3px;}
.help-block {display: none;}
}
.dpdk-control, .sriov-control {
label {padding-top: 5px;}
}
padding: @common-offset @common-offset * 2 @common-offset * 2;
.properties-list {
.btn-link {
text-decoration: underline;
}
.text-danger {
.btn-link {
color: @red;
&.text-danger {.text-danger;}
.glyphicon {
float: none;
position: relative;
top: 3px;
margin-left: 3px;
}
}
.property-item-container {
@ -4053,13 +4042,6 @@ input[type=range] {
&.active {
background-color: @ifc-container-light-color - 17%;
}
.property-item {
cursor: pointer;
padding-top: 4px;
padding-right: 0;
padding-left: 0;
margin-left: @property-offset;
}
&.forbidden {
opacity: 0.8;
.glyphicon {
@ -4070,44 +4052,38 @@ input[type=range] {
color: @gray;
}
}
}
}
.mtu-control {
div {
float: left;
}
}
.configuration-panel {
margin-top: 0;
> div {padding-top: @common-offset;}
.forms-box {
margin-bottom: 0;
}
.interface-sub-tab {
text-align: left;
.form-group {
margin-bottom: 7px;
}
.has-error {
.help-block {
color: @red;
display: inline;
.glyphicon-lock {
margin-right: 3px;
& + span {
margin-left: 5px;
}
}
.description {
font-size: @base-font-size - 2;
color: @base-text-color + 35%;
margin-bottom: 10px;
}
.form-group,
.checkbox-group {
padding-left: 0;
.property-item {
cursor: pointer;
padding: 4px 0 6px;
margin-left: @property-offset;
}
}
.sriov-virtual-functions {
margin-bottom: @common-offset;
&.form-group.has-error .help-block {
max-width: 655px;
}
.configuration-panel {
margin-top: 0;
padding-top: @common-offset;
.interface-sub-tab {
text-align: left;
.forms-box {
margin-bottom: 0;
.form-group, .checkbox-group {
padding-left: 0;
padding-right: 0;
&:last-child {
margin-bottom: 0;
}
&.form-group .help-block {
max-width: 585px;
}
}
}
}
}

View File

@ -59,19 +59,21 @@ registerSuite(() => {
return this.remote
.clickByCssSelector('.mtu .btn-link')
.assertElementExists(
'.mtu-control',
'.mtu-section input[name="value"]',
'MTU control is shown when navigating to MTU tab'
)
.setInputValue('.mtu-control input', '2')
.assertElementExists(
'.has-error.mtu-control',
'Error styles are applied to MTU control on invalid value'
)
.assertElementExists(
'.text-danger.mtu',
'Invalid style is applied to MTU in summary panel'
)
.setInputValue('.mtu-control input', '256')
.setInputValue('.mtu-section input[name="value"]', '2')
// FIXME(jkirnosova) restore this check after merging https://review.openstack.org/370052
//.assertElementExists(
// '.mtu-section .has-error',
// 'Error styles are applied to MTU control on invalid value'
//)
// FIXME(jkirnosova) restore this check after adding use_custom_mtu setting
//.assertElementExists(
// '.mtu .btn-link.text-danger',
// 'Invalid style is applied to MTU in summary panel'
//)
.setInputValue('.mtu-section input[name="value"]', '256')
.assertElementExists(
'.ifc-inner-container.has-changes',
'Has-Changes style is applied'
@ -79,7 +81,7 @@ registerSuite(() => {
.clickByCssSelector('.mtu .btn-link')
.sleep(500)
.assertElementNotDisplayed(
'.mtu-control',
'.mtu-section input[name="value"]',
'MTU control is hidden after clicking MTU link again'
);
},

View File

@ -15,79 +15,82 @@
**/
import OffloadingModes from
'views/cluster_page_tabs/nodes_tab_screens/offloading_modes_control';
import models from 'models';
var offloadingModesConrol, TestMode22, TestMode31, fakeOffloadingModes;
var fakeInterface = {
offloading_modes: fakeOffloadingModes,
get(key) {
assert.equal(key, 'offloading_modes',
'"offloading_modes" interface property should be used to get data');
return fakeOffloadingModes;
},
set(key, value) {
assert.equal(key, 'offloading_modes',
'"offloading_modes" interface property should be used to set data');
fakeOffloadingModes = value;
}
};
var offloadingModesConrol, TestMode22, TestMode31, offloadingModes, attributes, getModeState;
suite('Offloadning Modes control', () => {
suite('Offloading Modes control', () => {
setup(() => {
TestMode22 = {name: 'TestName22', state: false, sub: []};
TestMode31 = {name: 'TestName31', state: null, sub: []};
fakeOffloadingModes = [
attributes = new models.InterfaceAttributes({
offloading: {
modes: {
value: {
TestName1: true,
TestName11: true,
TestName31: null,
TestName12: false,
TestName13: null,
TestName2: false,
TestName21: false,
TestName22: false,
TestName23: false
}
}
}
});
TestMode22 = {name: 'TestName22', sub: []};
TestMode31 = {name: 'TestName31', sub: []};
offloadingModes = [
{
name: 'TestName1',
state: true,
sub: [
{name: 'TestName11', state: true, sub: [
{name: 'TestName11', sub: [
TestMode31
]},
{name: 'TestName12', state: false, sub: []},
{name: 'TestName13', state: null, sub: []}
{name: 'TestName12', sub: []},
{name: 'TestName13', sub: []}
]
},
{
name: 'TestName2',
state: false,
sub: [
{name: 'TestName21', state: false, sub: []},
{name: 'TestName21', sub: []},
TestMode22,
{name: 'TestName23', state: false, sub: []}
{name: 'TestName23', sub: []}
]
}
];
offloadingModesConrol = new OffloadingModes({
interface: fakeInterface
});
getModeState = (mode) => attributes.get('offloading.modes.value')[mode];
offloadingModesConrol = new OffloadingModes({attributes, offloadingModes});
});
test('Finding mode by name', () => {
var mode = offloadingModesConrol.findMode(TestMode22.name, fakeOffloadingModes);
var mode = offloadingModesConrol.findMode('TestName22', offloadingModes);
assert.deepEqual(mode, TestMode22, 'Mode can be found by name');
});
test('Set mode state logic', () => {
offloadingModesConrol.setModeState(TestMode31, true);
assert.strictEqual(TestMode31.state, true, 'Mode state is changing');
assert.strictEqual(getModeState('TestName31'), true, 'Mode state is changing');
});
test('Set submodes states logic', () => {
var mode = offloadingModesConrol.findMode('TestName1', fakeOffloadingModes);
var mode = offloadingModesConrol.findMode('TestName1', offloadingModes);
offloadingModesConrol.setModeState(mode, false);
assert.strictEqual(TestMode31.state, false,
assert.strictEqual(getModeState('TestName31'), false,
'Parent state changing leads to all child modes states changing');
});
test('Disabled reversed logic', () => {
var mode = offloadingModesConrol.findMode('TestName2', fakeOffloadingModes);
offloadingModesConrol.setModeState(TestMode22, true);
offloadingModesConrol.checkModes(null, fakeOffloadingModes);
assert.strictEqual(mode.state, null,
offloadingModesConrol.checkModes(null, offloadingModes);
assert.strictEqual(getModeState('TestName2'), false,
'Parent state changing leads to all child modes states changing');
});
test('All Modes option logic', () => {
var enableAllModes = offloadingModesConrol.onModeStateChange('All Modes', true);
enableAllModes();
var mode = offloadingModesConrol.findMode('TestName2', fakeOffloadingModes);
assert.strictEqual(mode.state, true,
assert.strictEqual(getModeState('TestName2'), true,
'All Modes option state changing leads to all parent modes states changing');
});
});

View File

@ -57,7 +57,8 @@
"count_label": "Count",
"size_label": "Size",
"enabled": "Enabled",
"disabled": "Disabled"
"disabled": "Disabled",
"default": "Default"
},
"controls": {
"invalid_value": "Invalid value",
@ -482,7 +483,6 @@
"several_bonds_warning": "This network interface is already bonded with other network interfaces.",
"interfaces_cannot_be_bonded": "Selected interfaces cannot be bonded together.",
"speed": "Speed",
"all_modes": "All Modes",
"bonding_mode": "Mode",
"bonding_modes": {
"active-backup": "Active Backup",
@ -518,33 +518,19 @@
},
"validation": {
"too_many_untagged_networks": "Untagged networks cannot be assigned to the same interface.",
"invalid_mtu": "Invalid MTU size",
"dpdk_mtu_error": "For a network interface with DPDK enabled the MTU size should not exceed 1500 bytes",
"networks_with_the_same_vlan": "Networks with the same VLAN ID cannot be assigned to the same interface.",
"vlan_range_intersection": "The network VLAN ID range should not include VLAN IDs of other networks assigned to the interface.",
"sriov_placement_error": "No network could be placed on SRIOV enabled interface",
"non_default_physnet": "Only \"physnet2\" will be configured by Fuel in Neutron. Configuration of other physical networks is up to Operator or plugin. Fuel will just configure appropriate pci_passthrough_whitelist option in nova.conf for such interface and physical networks.",
"invalid_virtual_functions_number": "Virtual Functions number should be a positive number",
"invalid_virtual_functions_number_max": "Virtual Functions number should be not more than __max__",
"invalid_physnet": "Invalid name",
"empty_physnet": "Physical Network Name should not be empty",
"sriov_hypervisor_alert": "Only KVM hypervisor works with SR-IOV.",
"dpdk_hypervisor_alert": "Only KVM hypervisor works with DPDK.",
"big_virtual_functions_number": "Virtual Functions number should be not more than __max__",
"dpdk_placement_error": "DPDK enabled interface requires Private network to be assigned. Other networks are not allowed."
},
"disable_offloading": "Offloading Disabled",
"default_offloading": "Default Offloading",
"offloading_modes": "Offloading Modes",
"offloading_mode": "Mode",
"offloading_default": "Default",
"mtu": "MTU",
"mtu_placeholder": "Default",
"sriov": "SR-IOV",
"sriov_description": "Single-root I/O Virtualization (SR-IOV) is a specification that, when implemented by a physical PCIe device, enables it to appear as multiple separate PCIe devices. This enables multiple virtualized guests to share direct access to the physical device, offering improved performance over an equivalent virtual device.",
"virtual_functions": "Number of Virtual Functions",
"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.",
"offloading": {
"offloading_modes": "Offloading Modes",
"offloading_mode": "Mode",
"all_modes": "All Modes"
},
"locked_dpdk_bond": "DPDK cannot be enabled because not all slave interfaces support it",
"different_availability": "<Different Availability>",
"availability_tooltip": "Some network interfaces do not support this feature, therefore, these properties will not change after saving.",
@ -1754,7 +1740,6 @@
},
"validation": {
"too_many_untagged_networks": "无法分配未标注网络至同一接口。",
"invalid_mtu": "非法MTU值",
"networks_with_the_same_vlan": "无法分配相同VLAN ID的网络至同一接口。",
"vlan_range_intersection": "网络VLAN ID范围不应包括分配给该接口的其它网络的VLAN ID。"
},
@ -2655,8 +2640,7 @@
"load_defaults_warning": "デフォルト設定を読み込めませんでした"
},
"validation": {
"too_many_untagged_networks": "タグなしのネットワークは同じインタフェースに割り当てることはできません",
"invalid_mtu": "無効なMTU値です"
"too_many_untagged_networks": "タグなしのネットワークは同じインタフェースに割り当てることはできません"
},
"disable_offloading": "オフロードを無効にする",
"mtu": "MTU"

View File

@ -18,31 +18,42 @@ import i18n from 'i18n';
import React from 'react';
import utils from 'utils';
var ns = 'cluster_page.nodes_tab.configure_interfaces.';
var ns = 'cluster_page.nodes_tab.configure_interfaces.offloading.';
var OffloadingModesControl = React.createClass({
propTypes: {
interface: React.PropTypes.object
attributes: React.PropTypes.object,
offloadingModes: React.PropTypes.array
},
setModeState(mode, state) {
mode.state = state;
_.each(mode.sub, (mode) => this.setModeState(mode, state));
setModeState({name, sub}, state, recursive = true) {
var {attributes, onChange} = this.props;
var offloadingModeStates = _.cloneDeep(attributes.get('offloading.modes.value'));
offloadingModeStates[name] = state;
attributes.set('offloading.modes.value', offloadingModeStates);
if (onChange) onChange();
if (recursive) _.each(sub, (mode) => this.setModeState(mode, state));
},
checkModes(mode, sub) {
var changedState = sub.reduce((state, childMode) => {
if (!_.isEmpty(childMode.sub)) {
this.checkModes(childMode, childMode.sub);
}
return (state === 0 || state === childMode.state) ? childMode.state : -1;
},
0
);
var oldState;
// process children first
var offloadingModeStates = this.props.attributes.get('offloading.modes.value');
_.each(sub, (childMode) => {
this.checkModes(childMode, childMode.sub);
});
if (mode && mode.state !== changedState) {
oldState = mode.state;
mode.state = oldState === false ? null : (changedState === false ? false : oldState);
// root node or leaf node
if (_.isNull(mode) || !sub.length) return;
// Case 1. all children disabled - parent go disabled
if (_.every(sub, ({name}) => offloadingModeStates[name] === false)) {
this.setModeState(mode, false, false);
}
// Case 2. any child is default and parent is disabled - parent go default
var parentModeState = offloadingModeStates[mode.name];
if (
parentModeState === false &&
_.some(sub, ({name}) => offloadingModeStates[name] === null)
) this.setModeState(mode, null, false);
},
findMode(name, modes) {
var result, mode;
@ -62,61 +73,54 @@ var OffloadingModesControl = React.createClass({
return result;
},
onModeStateChange(name, state) {
var modes = _.cloneDeep(this.props.interface.get('offloading_modes') || []);
var mode = this.findMode(name, modes);
var {offloadingModes} = this.props;
var mode = this.findMode(name, offloadingModes);
return () => {
if (mode) {
this.setModeState(mode, state);
this.checkModes(null, modes);
this.checkModes(null, offloadingModes);
} else {
// handle All Modes click
_.each(modes, (mode) => this.setModeState(mode, state));
_.each(offloadingModes, (mode) => this.setModeState(mode, state));
}
this.props.interface.set('offloading_modes', modes);
};
},
renderChildModes(modes, level) {
return modes.map((mode) => {
var {offloadingModes, attributes, disabled} = this.props;
var offloadingModeStates = attributes.get('offloading.modes.value');
return modes.map(({name, sub}) => {
var lines = [
<tr key={mode.name} className={'level' + level}>
<td>{mode.name}</td>
<tr key={name} className={'level' + level}>
<td>{i18n(ns + name, {defaultValue: name})}</td>
{[true, false, null].map((modeState) => {
var styles = {
'btn-link': true,
active: mode.state === modeState
};
var state = name === 'all_modes' ?
_.uniq(_.map(offloadingModes, ({name}) => offloadingModeStates[name])).length === 1 ?
offloadingModeStates[offloadingModes[0].name] : undefined
:
offloadingModeStates[name];
return (
<td key={mode.name + modeState}>
<td key={name + modeState}>
<button
className={utils.classNames(styles)}
disabled={this.props.disabled}
onClick={this.onModeStateChange(mode.name, modeState)}>
<i className='glyphicon glyphicon-ok'></i>
className={utils.classNames({
'btn-link': true,
active: state === modeState
})}
disabled={disabled}
onClick={this.onModeStateChange(name, modeState)}>
<i className='glyphicon glyphicon-ok' />
</button>
</td>
);
})}
</tr>
];
if (mode.sub) {
return _.union([lines, this.renderChildModes(mode.sub, level + 1)]);
}
if (sub) return _.union([lines, this.renderChildModes(sub, level + 1)]);
return lines;
});
},
render() {
var modes = [];
var ifcModes = this.props.interface.get('offloading_modes');
if (ifcModes) {
modes.push({
name: i18n(ns + 'all_modes'),
state: _.uniq(_.map(ifcModes, 'state')).length === 1 ? ifcModes[0].state : undefined,
sub: ifcModes
});
}
var offloadingModes = [{name: 'all_modes', sub: this.props.offloadingModes}];
return (
<div className='offloading-modes'>
<table className='table'>
@ -125,11 +129,11 @@ var OffloadingModesControl = React.createClass({
<th>{i18n(ns + 'offloading_mode')}</th>
<th>{i18n('common.enabled')}</th>
<th>{i18n('common.disabled')}</th>
<th>{i18n(ns + 'offloading_default')}</th>
<th>{i18n('common.default')}</th>
</tr>
</thead>
<tbody>
{this.renderChildModes(modes, 1)}
{this.renderChildModes(offloadingModes, 1)}
</tbody>
</table>
</div>

View File

@ -22,6 +22,11 @@ import {Input, RadioGroup} from 'views/controls';
import customControls from 'views/custom_controls';
var SettingSection = React.createClass({
getDefaultProps() {
return {
showHeader: true
};
},
processRestrictions(setting, settingName) {
var result = false;
var restrictionsCheck = this.props.checkRestrictions('disable', setting);
@ -257,7 +262,7 @@ var SettingSection = React.createClass({
/>;
},
render() {
var {cluster, settings, sectionName, locked, settingsToDisplay} = this.props;
var {cluster, settings, sectionName, locked, settingsToDisplay, showHeader} = this.props;
var section = settings.get(sectionName);
var isPlugin = settings.isPlugin && settings.isPlugin(section);
var {metadata} = section;
@ -289,21 +294,25 @@ var SettingSection = React.createClass({
return (
<div className={'setting-section setting-section-' + sectionName}>
<h3>
{metadata.toggleable ?
<Input
type='checkbox'
name='metadata'
label={groupLabel}
defaultChecked={metadata.enabled}
disabled={isGroupDisabled || processedGroupDependencies.result}
tooltipText={showSettingGroupWarning && groupWarning}
onChange={isPlugin ? _.partial(this.togglePlugin, sectionName) : this.props.onChange}
/>
:
<span className={'subtab-group-' + sectionName}>{groupLabel}</span>
}
</h3>
{showHeader &&
<h3>
{metadata.toggleable ?
<Input
type='checkbox'
name='metadata'
label={groupLabel}
defaultChecked={metadata.enabled}
disabled={isGroupDisabled || processedGroupDependencies.result}
tooltipText={showSettingGroupWarning && groupWarning}
onChange={
isPlugin ? _.partial(this.togglePlugin, sectionName) : this.props.onChange
}
/>
:
<span className={'subtab-group-' + sectionName}>{groupLabel}</span>
}
</h3>
}
<div>
{cluster.get('status') !== 'new' &&
isPlugin && metadata.enabled && !metadata.hot_pluggable &&