Handle new plugin versions structure in UI

Related-Bug: #1518993
Closes-Bug: #1516976

Change-Id: Ia0d61a2e92b72018c830e9305c6c9102331d1c05
This commit is contained in:
Julia Aranovich 2015-12-23 12:41:16 +03:00
parent f83c4ea23b
commit 55c83a7651
6 changed files with 170 additions and 73 deletions

View File

@ -101,19 +101,22 @@ define([
models.cacheMixin = cacheMixin;
var restrictionMixin = models.restrictionMixin = {
expandRestrictions: function(restrictions, path) {
path = path || 'restrictions';
expandRestrictions: function(restrictions, path = 'restrictions') {
this.expandedRestrictions = this.expandedRestrictions || {};
this.expandedRestrictions[path] = _.map(restrictions, utils.expandRestriction, this);
},
checkRestrictions: function(models, action, path) {
path = path || 'restrictions';
checkRestrictions: function(models, action, path = 'restrictions') {
var restrictions = (this.expandedRestrictions || {})[path];
if (action) restrictions = _.where(restrictions, {action: action});
var satisfiedRestrictions = _.filter(restrictions, function(restriction) {
return new Expression(restriction.condition, models, restriction).evaluate();
});
return {result: !!satisfiedRestrictions.length, message: _.compact(_.pluck(satisfiedRestrictions, 'message')).join(' ')};
if (action) {
restrictions = _.where(restrictions, {action: action});
}
var satisfiedRestrictions = _.filter(restrictions,
(restriction) => new Expression(restriction.condition, models, restriction).evaluate()
);
return {
result: !!satisfiedRestrictions.length,
message: _.compact(_.pluck(satisfiedRestrictions, 'message')).join(' ')
};
},
expandLimits: function(limits) {
this.expandedLimits = this.expandedLimits || {};
@ -625,24 +628,61 @@ define([
isNew: function() {
return false;
},
isPlugin: function(section) {
return (section.metadata || {}).class == 'plugin';
},
parse: function(response) {
return response[this.root];
},
mergePluginSettings: function() {
_.each(this.attributes, (section, sectionName) => {
if (this.isPlugin(section)) {
var chosenVersionData = section.metadata.versions.find(
(version) => version.metadata.plugin_id == section.metadata.chosen_id
);
// merge metadata of a chosen plugin version
_.extend(section.metadata, _.omit(chosenVersionData.metadata, 'plugin_id', 'plugin_version'));
// merge settings of a chosen plugin version
this.attributes[sectionName] = _.extend(_.pick(section, 'metadata'), _.omit(chosenVersionData, 'metadata'));
}
}, this);
},
toJSON: function() {
if (!this.root) return this._super('toJSON', arguments);
var data = {};
data[this.root] = this._super('toJSON', arguments);
return data;
var settings = this._super('toJSON', arguments);
if (!this.root) return settings;
// update plugin settings
_.each(settings, (section, sectionName) => {
if (this.isPlugin(section)) {
var chosenVersionData = section.metadata.versions.find(
(version) => version.metadata.plugin_id == section.metadata.chosen_id
);
section.metadata = _.omit(section.metadata, _.without(_.keys(chosenVersionData.metadata), 'plugin_id', 'plugin_version'));
_.each(section, (setting, settingName) => {
if (settingName != 'metadata') chosenVersionData[settingName].value = setting.value;
});
settings[sectionName] = _.pick(section, 'metadata');
}
});
return {[this.root]: settings};
},
processRestrictions: function() {
_.each(this.attributes, function(group, groupName) {
if (group.metadata) {
this.expandRestrictions(group.metadata.restrictions, groupName + '.metadata');
_.each(this.attributes, function(section, sectionName) {
if (section.metadata) {
this.expandRestrictions(section.metadata.restrictions, this.makePath(sectionName, 'metadata'));
if (this.isPlugin(section)) {
_.each(section.metadata.versions, (version) => {
this.expandRestrictions(version.metadata.restrictions, this.makePath(sectionName, 'metadata'));
_.each(version,
(setting, settingName) => this.expandRestrictions(setting.restrictions, this.makePath(sectionName, settingName))
);
}, this);
}
}
_.each(group, function(setting, settingName) {
this.expandRestrictions(setting.restrictions, this.makePath(groupName, settingName));
_.each(setting.values, function(value) {
this.expandRestrictions(value.restrictions, this.makePath(groupName, settingName, value.data));
_.each(section, (setting, settingName) => {
this.expandRestrictions(setting.restrictions, this.makePath(sectionName, settingName));
_.each(setting.values, (value) => {
this.expandRestrictions(value.restrictions, this.makePath(sectionName, settingName, value.data));
}, this);
}, this);
}, this);
@ -650,14 +690,15 @@ define([
initialize: function() {
// FIXME(vkramskikh): this will work only if there won't be
// any restrictions added later in the same model
this.once('change', this.processRestrictions, this);
this.once('change', () => {
this.processRestrictions();
this.mergePluginSettings();
}, this);
},
validate: function(attrs, options) {
var errors = {},
models = options ? options.models : {},
checkRestrictions = _.bind(function(path) {
return this.checkRestrictions(models, null, path);
}, this);
checkRestrictions = (path) => this.checkRestrictions(models, null, path);
_.each(attrs, function(group, groupName) {
if ((group.metadata || {}).enabled === false || checkRestrictions(this.makePath(groupName, 'metadata')).result) return;
_.each(group, function(setting, settingName) {
@ -694,7 +735,7 @@ define([
}
return result || _.any(group, function(setting, settingName) {
if (this.checkRestrictions(models, null, this.makePath(groupName, settingName)).result) return false;
return !_.isEqual(setting.value, initialAttributes[groupName][settingName].value);
return !_.isEqual(setting.value, (initialAttributes[groupName][settingName] || {}).value);
}, this);
}, this);
},

View File

@ -3770,6 +3770,25 @@ input[type=range] {
}
}
}
.plugin-versions {
margin-bottom: 25px;
.radio-group {
> h4, > div {
display: block;
float: left;
margin: 0;
}
h4 {
position: relative;
top: -2px;
}
.custom-tumbler + span {
position: relative;
top: 1px;
padding-right: 5px;
}
}
}
}
// Settings tab
@ -3782,6 +3801,11 @@ input[type=range] {
padding-top: 0;
}
}
.plugin-versions {
.radio-group {
margin-left: 10px;
}
}
}
// Networks tab

View File

@ -575,7 +575,8 @@
"invalid_repo": "Invalid repo format",
"invalid_priority": "Invalid priority"
}
}
},
"plugin_versions": "Versions"
},
"dashboard_tab": {
"delete_environment": "Delete Environment",

View File

@ -601,7 +601,10 @@ function($, _, i18n, Backbone, React, models, dispatcher, utils, dialogs, compon
networkConfiguration.get('networking_parameters').set(_.cloneDeep(this.state.initialConfiguration.networking_parameters));
},
loadInitialSettings: function() {
this.props.cluster.get('settings').set(_.cloneDeep(this.state.initialSettingsAttributes)).isValid({models: this.state.configModels});
var settings = this.props.cluster.get('settings');
settings.set(_.cloneDeep(this.state.initialSettingsAttributes), {silent: true, validate: false});
settings.mergePluginSettings();
settings.isValid({models: this.state.configModels});
},
updateInitialConfiguration: function() {
this.setState({initialConfiguration: _.cloneDeep(this.props.cluster.get('networkConfiguration').toJSON())});
@ -1500,7 +1503,7 @@ function($, _, i18n, Backbone, React, models, dispatcher, utils, dialogs, compon
.filter(
(sectionName) => {
var section = settings.get(sectionName);
return (section.metadata.group == 'network' || !section.metadata.group && _.any(section, {group: 'network'})) &&
return (section.metadata.group == 'network' || _.any(section, {group: 'network'})) &&
!this.checkRestrictions('hide', settings.makePath(sectionName, 'metadata')).result;
}
)
@ -1518,7 +1521,7 @@ function($, _, i18n, Backbone, React, models, dispatcher, utils, dialogs, compon
!this.checkRestrictions('hide', settings.makePath(sectionName, settingName)).result
) return settingName;
}));
if (_.isEmpty(settingsToDisplay)) return null;
if (_.isEmpty(settingsToDisplay) && !settings.isPlugin(section)) return null;
return <SettingSection
key={sectionName}
cluster={this.props.cluster}

View File

@ -112,13 +112,13 @@ function(_, i18n, utils, React, Expression, controls, customControls) {
}
}, this);
// collect dependencies
_.each(this.props.settings.attributes, function(group, sectionName) {
_.each(this.props.settings.attributes, function(section, sectionName) {
// don't take into account hidden dependent settings
if (this.props.checkRestrictions('hide', this.props.makePath(sectionName, 'metadata')).result) return;
_.each(group, function(setting, settingName) {
_.each(section, function(setting, settingName) {
// we support dependecies on checkboxes, toggleable setting groups, dropdowns and radio groups
var pathToCheck = this.props.makePath(sectionName, settingName);
if (!this.areCalculationsPossible(setting) || pathToCheck == path || this.props.checkRestrictions('hide', pathToCheck).result) return;
if (!this.areCalculationsPossible(setting) || pathToCheck == path || this.props.checkRestrictions('hide', sectionName, settingName).result) return;
if (setting[this.props.getValueAttribute(settingName)] == true) {
addDependentRestrictions(pathToCheck, setting.label);
} else {
@ -148,42 +148,70 @@ function(_, i18n, utils, React, Expression, controls, customControls) {
);
});
},
onPluginVersionChange: function(pluginName, version) {
var settings = this.props.settings;
// FIXME: the following hacks cause we can't pass {validate: true} option to set method
// this form of validation isn't supported in Backbone DeepModel
settings.validationError = null;
settings.set(this.props.makePath(pluginName, 'metadata', 'chosen_id'), Number(version));
settings.mergePluginSettings();
settings.isValid({models: this.props.configModels});
this.props.settingsForChecks.set(_.cloneDeep(settings.attributes));
},
render: function() {
var group = this.props.settings.get(this.props.sectionName),
metadata = group.metadata,
sortedSettings = _.sortBy(this.props.settingsToDisplay, function(settingName) {return group[settingName].weight;}),
processedGroupRestrictions = this.processRestrictions(this.props.sectionName, 'metadata'),
processedGroupDependencies = this.checkDependencies(this.props.sectionName, 'metadata'),
var {settings, sectionName} = this.props,
section = settings.get(sectionName),
isPlugin = settings.isPlugin(section),
metadata = section.metadata,
sortedSettings = _.sortBy(this.props.settingsToDisplay, (settingName) => section[settingName].weight),
processedGroupRestrictions = this.processRestrictions(sectionName, 'metadata'),
processedGroupDependencies = this.checkDependencies(sectionName, 'metadata'),
isGroupDisabled = this.props.locked || (this.props.lockedCluster && !metadata.always_editable) || processedGroupRestrictions.result,
showSettingGroupWarning = !this.props.lockedCluster || metadata.always_editable,
groupWarning = _.compact([processedGroupRestrictions.message, processedGroupDependencies.message]).join(' ');
return (
<div className='setting-section'>
{showSettingGroupWarning && processedGroupRestrictions.message &&
<div className='alert alert-warning'>{processedGroupRestrictions.message}</div>
}
<h3>
{metadata.toggleable ?
<controls.Input
type='checkbox'
name='metadata'
label={metadata.label || this.props.sectionName}
label={metadata.label || sectionName}
defaultChecked={metadata.enabled}
disabled={isGroupDisabled || processedGroupDependencies.result}
tooltipText={showSettingGroupWarning && groupWarning}
onChange={this.props.onChange}
/>
:
<span className={'subtab-group-' + this.props.sectionName}>{this.props.sectionName == 'common' ? i18n('cluster_page.settings_tab.groups.common') : metadata.label || this.props.sectionName}</span>
<span className={'subtab-group-' + sectionName}>{sectionName == 'common' ? i18n('cluster_page.settings_tab.groups.common') : metadata.label || sectionName}</span>
}
</h3>
<div>
{isPlugin &&
<div className='plugin-versions clearfix'>
<controls.RadioGroup
name={sectionName}
label={i18n('cluster_page.settings_tab.plugin_versions')}
values={_.map(metadata.versions, (version) => {
return {
data: version.metadata.plugin_id,
label: version.metadata.plugin_version,
defaultChecked: version.metadata.plugin_id == metadata.chosen_id,
disabled: isGroupDisabled || (metadata.toggleable && !metadata.enabled)
};
})}
onChange={this.onPluginVersionChange}
/>
</div>
}
{_.map(sortedSettings, function(settingName) {
var setting = group[settingName],
path = this.props.makePath(this.props.sectionName, settingName),
error = (this.props.settings.validationError || {})[path],
processedSettingRestrictions = this.processRestrictions(this.props.sectionName, settingName),
processedSettingDependencies = this.checkDependencies(this.props.sectionName, settingName),
var setting = section[settingName],
settingKey = settingName + (isPlugin ? '-' + metadata.chosen_id : ''),
path = this.props.makePath(sectionName, settingName),
error = (settings.validationError || {})[path],
processedSettingRestrictions = this.processRestrictions(sectionName, settingName),
processedSettingDependencies = this.checkDependencies(sectionName, settingName),
isSettingDisabled = isGroupDisabled || (metadata.toggleable && !metadata.enabled) || processedSettingRestrictions.result || processedSettingDependencies.result,
showSettingWarning = showSettingGroupWarning && !isGroupDisabled && (!metadata.toggleable || metadata.enabled),
settingWarning = _.compact([processedSettingRestrictions.message, processedSettingDependencies.message]).join(' ');
@ -194,7 +222,7 @@ function(_, i18n, utils, React, Expression, controls, customControls) {
return <CustomControl
{...setting}
{... _.pick(this.props, 'cluster', 'settings', 'configModels')}
key={settingName}
key={settingKey}
path={path}
error={error}
disabled={isSettingDisabled}
@ -217,7 +245,7 @@ function(_, i18n, utils, React, Expression, controls, customControls) {
.compact()
.value();
if (setting.type == 'radio') return <controls.RadioGroup {...this.props}
key={settingName}
key={settingKey}
name={settingName}
label={setting.label}
values={values}
@ -230,7 +258,7 @@ function(_, i18n, utils, React, Expression, controls, customControls) {
<span dangerouslySetInnerHTML={{__html: utils.urlify(_.escape(setting.description))}} />;
return <controls.Input
{... _.pick(setting, 'type', 'label')}
key={settingName}
key={settingKey}
name={settingName}
description={settingDescription}
children={setting.type == 'select' ? this.composeOptions(setting.values) : null}

View File

@ -69,14 +69,6 @@ function($, _, i18n, React, utils, models, componentMixins, SettingSection) {
actionInProgress: false
};
},
componentWillMount: function() {
var settings = this.props.cluster.get('settings');
if (this.checkRestrictions('hide', settings.makePath(this.props.activeSettingsSectionName, 'metadata')).result) {
// FIXME: First group might also be hidded by restrictions
// which would cause no group selected
this.props.setActiveSettingsGroupName();
}
},
componentDidMount: function() {
this.props.cluster.get('settings').isValid({models: this.state.configModels});
},
@ -131,8 +123,8 @@ function($, _, i18n, React, utils, models, componentMixins, SettingSection) {
if (deferred) {
this.setState({actionInProgress: true});
deferred
.done(_.bind(function() {
_.each(settings.attributes, function(section, sectionName) {
.done(() => {
_.each(settings.attributes, (section, sectionName) => {
if ((!lockedCluster || section.metadata.always_editable) && section.metadata.group != 'network') {
_.each(section, function(setting, settingName) {
// do not update hidden settings (hack for #1442143),
@ -143,20 +135,20 @@ function($, _, i18n, React, utils, models, componentMixins, SettingSection) {
});
}
});
settings.mergePluginSettings();
settings.isValid({models: this.state.configModels});
this.setState({
actionInProgress: false,
key: _.now()
});
}, this))
.fail(function(response) {
utils.showErrorDialog({
})
.fail(
(response) => utils.showErrorDialog({
title: i18n('cluster_page.settings_tab.settings_error.title'),
message: i18n('cluster_page.settings_tab.settings_error.load_defaults_warning'),
response: response
});
});
})
);
}
},
revertChanges: function() {
@ -164,7 +156,10 @@ function($, _, i18n, React, utils, models, componentMixins, SettingSection) {
this.setState({key: _.now()});
},
loadInitialSettings: function() {
this.props.cluster.get('settings').set(_.cloneDeep(this.state.initialAttributes)).isValid({models: this.state.configModels});
var settings = this.props.cluster.get('settings');
settings.set(_.cloneDeep(this.state.initialAttributes), {silent: true, validate: false});
settings.mergePluginSettings();
settings.isValid({models: this.state.configModels});
},
onChange: function(groupName, settingName, value) {
var settings = this.props.cluster.get('settings'),
@ -177,8 +172,7 @@ function($, _, i18n, React, utils, models, componentMixins, SettingSection) {
settings.isValid({models: this.state.configModels});
},
checkRestrictions: function(action, path) {
var settings = this.props.cluster.get('settings');
return settings.checkRestrictions(this.state.configModels, action, path);
return this.props.cluster.get('settings').checkRestrictions(this.state.configModels, action, path);
},
isSavingPossible: function() {
var cluster = this.props.cluster,
@ -207,16 +201,16 @@ function($, _, i18n, React, utils, models, componentMixins, SettingSection) {
// Prepare list of settings organized by groups
var groupedSettings = {};
_.each(settingsGroupList, function(group) {
groupedSettings[group] = {};
});
_.each(settingsGroupList, (group) => groupedSettings[group] = {});
_.each(settings.attributes, function(section, sectionName) {
var isHidden = this.checkRestrictions('hide', settings.makePath(sectionName, 'metadata')).result;
if (!isHidden) {
var group = section.metadata.group,
hasErrors = invalidSections[sectionName];
if (group) {
if (group != 'network') groupedSettings[settings.sanitizeGroup(group)][sectionName] = {invalid: hasErrors};
if (group != 'network') {
groupedSettings[settings.sanitizeGroup(group)][sectionName] = {invalid: hasErrors};
}
} else {
// Settings like 'Common' can be splitted to different groups
var settingGroups = _.chain(section)
@ -225,6 +219,12 @@ function($, _, i18n, React, utils, models, componentMixins, SettingSection) {
.unique()
.without('network')
.value();
// to support plugins without settings (just for user to be able to switch its version)
if (!settingGroups.length && settings.isPlugin(section)) {
groupedSettings.other[sectionName] = {settings: [], invalid: hasErrors};
}
_.each(settingGroups, function(settingGroup) {
var calculatedGroup = settings.sanitizeGroup(settingGroup),
pickedSettings = _.compact(_.map(section, function(setting, settingName) {