Forcing eslint max-len rule

Implements: blueprint converge-to-eslint-config-openstack

Change-Id: I35e6abb96c4912608222cd85617764172858d191
This commit is contained in:
Alexandra Morozova 2016-01-20 10:21:03 +01:00 committed by Vitaly Kramskikh
parent d62ec107cd
commit fbdc1ccf1f
67 changed files with 2632 additions and 996 deletions

View File

@ -13,7 +13,6 @@
# to be fixed and enabled # to be fixed and enabled
eqeqeq: 0 eqeqeq: 0
max-len: [0, 120]
# extra rules # extra rules
no-unexpected-multiline: 2 no-unexpected-multiline: 2

View File

@ -26,12 +26,15 @@ function validate(translations, locales) {
}); });
function compareLocales(locale1, locale2) { function compareLocales(locale1, locale2) {
return _.without.apply(null, [processedTranslations[locale1]].concat(processedTranslations[locale2])); return _.without.apply(null, [processedTranslations[locale1]]
.concat(processedTranslations[locale2]));
} }
_.each(_.without(locales, baseLocale), function(locale) { _.each(_.without(locales, baseLocale), function(locale) {
gutil.log(gutil.colors.red('The list of keys present in', baseLocale, 'but absent in', locale, ':\n') + compareLocales(baseLocale, locale).join('\n')); gutil.log(gutil.colors.red('The list of keys present in',
gutil.log(gutil.colors.red('The list of keys missing in', baseLocale, ':\n') + compareLocales(locale, baseLocale).join('\n')); baseLocale, 'but absent in', locale, ':\n') + compareLocales(baseLocale, locale).join('\n'));
gutil.log(gutil.colors.red('The list of keys missing in', baseLocale, ':\n') +
compareLocales(locale, baseLocale).join('\n'));
}); });
} }

View File

@ -80,7 +80,8 @@ gulp.task('selenium', ['selenium:fetch'], function(cb) {
if (err) throw err; if (err) throw err;
child.on('exit', function() { child.on('exit', function() {
if (seleniumProcess) { if (seleniumProcess) {
gutil.log(gutil.colors.yellow('Selenium process died unexpectedly. Probably port', port, 'is already in use.')); gutil.log(gutil.colors.yellow('Selenium process died unexpectedly. Probably port',
port, 'is already in use.'));
} }
}); });
['exit', 'uncaughtException', 'SIGTERM', 'SIGINT'].forEach(function(event) { ['exit', 'uncaughtException', 'SIGTERM', 'SIGINT'].forEach(function(event) {
@ -127,7 +128,9 @@ function runIntern(params) {
}; };
} }
gulp.task('intern:functional', runIntern({functionalSuites: argv.suites || 'static/tests/functional/**/test_*.js'})); gulp.task('intern:functional', runIntern({
functionalSuites: argv.suites || 'static/tests/functional/**/test_*.js'
}));
gulp.task('unit-tests', function(cb) { gulp.task('unit-tests', function(cb) {
runSequence('selenium', 'karma', function(err) { runSequence('selenium', 'karma', function(err) {
@ -161,7 +164,8 @@ gulp.task('license', function(cb) {
_.each(data, function(moduleInfo) { _.each(data, function(moduleInfo) {
var name = moduleInfo.name; var name = moduleInfo.name;
var version = moduleInfo.version; var version = moduleInfo.version;
var license = _.pluck(moduleInfo.licenseSources.package.sources, 'license').join(', ') || 'unknown'; var license = _.pluck(moduleInfo.licenseSources.package.sources, 'license').join(', ') ||
'unknown';
var licenseOk = license.match(licenseRegexp); var licenseOk = license.match(licenseRegexp);
if (!licenseOk) errors.push({libraryName: name, license: license}); if (!licenseOk) errors.push({libraryName: name, license: license});
gutil.log( gutil.log(
@ -260,10 +264,11 @@ gulp.task('dev-server', function() {
] ]
}; };
_.extend(options, config.output); _.extend(options, config.output);
new WebpackDevServer(webpack(config), options).listen(devServerPort, devServerHost, function(err) { new WebpackDevServer(webpack(config), options).listen(devServerPort, devServerHost,
if (err) throw err; function(err) {
gutil.log('Development server started at ' + devServerUrl); if (err) throw err;
}); gutil.log('Development server started at ' + devServerUrl);
});
}); });
gulp.task('build', function(cb) { gulp.task('build', function(cb) {
@ -301,7 +306,8 @@ gulp.task('build', function(cb) {
rimraf.sync(config.output.path); rimraf.sync(config.output.path);
var compiler = webpack(config); var compiler = webpack(config);
var run = config.watch ? compiler.watch.bind(compiler, config.watchOptions) : compiler.run.bind(compiler); var run = config.watch ? compiler.watch.bind(compiler, config.watchOptions) :
compiler.run.bind(compiler);
run(function(err, stats) { run(function(err, stats) {
if (err) return cb(err); if (err) return cb(err);

View File

@ -70,11 +70,14 @@ class Router extends Backbone.Router {
var specialRoutes = [ var specialRoutes = [
{name: 'login', condition: () => { {name: 'login', condition: () => {
var result = app.version.get('auth_required') && !app.user.get('authenticated'); var result = app.version.get('auth_required') && !app.user.get('authenticated');
if (result && currentUrl != 'login' && currentUrl != 'logout') this.returnUrl = currentUrl; if (result && currentUrl != 'login' && currentUrl != 'logout') {
this.returnUrl = currentUrl;
}
return result; return result;
}}, }},
{name: 'welcome', condition: (previousUrl) => { {name: 'welcome', condition: (previousUrl) => {
return previousUrl != 'logout' && !app.fuelSettings.get('statistics.user_choice_saved.value'); return previousUrl != 'logout' &&
!app.fuelSettings.get('statistics.user_choice_saved.value');
}} }}
]; ];
_.each(specialRoutes, (route) => { _.each(specialRoutes, (route) => {
@ -153,7 +156,8 @@ class App {
constructor() { constructor() {
this.initialized = false; this.initialized = false;
// this is needed for IE, which caches requests resulting in wrong results (e.g /ostf/testruns/last/1) // this is needed for IE,
// which caches requests resulting in wrong results (e.g /ostf/testruns/last/1)
$.ajaxSetup({cache: false}); $.ajaxSetup({cache: false});
this.router = new Router(); this.router = new Router();
@ -217,10 +221,11 @@ class App {
} }
loadPage(Page, options = []) { loadPage(Page, options = []) {
return (Page.fetchData ? Page.fetchData(...options) : $.Deferred().resolve()).done((pageOptions) => { return (Page.fetchData ? Page.fetchData(...options) : $.Deferred().resolve())
if (!this.rootComponent) this.renderLayout(); .done((pageOptions) => {
this.setPage(Page, pageOptions); if (!this.rootComponent) this.renderLayout();
}); this.setPage(Page, pageOptions);
});
} }
setPage(Page, options) { setPage(Page, options) {

View File

@ -38,7 +38,8 @@ export function dispatcherMixin(events, callback) {
export var unsavedChangesMixin = { export var unsavedChangesMixin = {
onBeforeunloadEvent() { onBeforeunloadEvent() {
if (this.hasChanges()) return _.result(this, 'getStayMessage') || i18n('dialog.dismiss_settings.default_message'); if (this.hasChanges()) return _.result(this, 'getStayMessage') ||
i18n('dialog.dismiss_settings.default_message');
}, },
componentWillMount() { componentWillMount() {
this.eventName = _.uniqueId('unsavedchanges'); this.eventName = _.uniqueId('unsavedchanges');
@ -71,13 +72,15 @@ export function pollingMixin(updateInterval, delayedStart) {
updateInterval = updateInterval * 1000; updateInterval = updateInterval * 1000;
return { return {
scheduleDataFetch() { scheduleDataFetch() {
var shouldDataBeFetched = !_.isFunction(this.shouldDataBeFetched) || this.shouldDataBeFetched(); var shouldDataBeFetched = !_.isFunction(this.shouldDataBeFetched) ||
this.shouldDataBeFetched();
if (this.isMounted() && !this.activeTimeout && shouldDataBeFetched) { if (this.isMounted() && !this.activeTimeout && shouldDataBeFetched) {
this.activeTimeout = _.delay(this.startPolling, updateInterval); this.activeTimeout = _.delay(this.startPolling, updateInterval);
} }
}, },
startPolling(force) { startPolling(force) {
var shouldDataBeFetched = force || !_.isFunction(this.shouldDataBeFetched) || this.shouldDataBeFetched(); var shouldDataBeFetched = force || !_.isFunction(this.shouldDataBeFetched) ||
this.shouldDataBeFetched();
if (shouldDataBeFetched) { if (shouldDataBeFetched) {
this.stopPolling(); this.stopPolling();
return this.fetchData().always(this.scheduleDataFetch); return this.fetchData().always(this.scheduleDataFetch);

View File

@ -43,7 +43,8 @@ class Expression {
getCompiledExpression() { getCompiledExpression() {
var cacheEntry = expressionCache[this.expressionText]; var cacheEntry = expressionCache[this.expressionText];
if (!cacheEntry) { if (!cacheEntry) {
cacheEntry = expressionCache[this.expressionText] = ExpressionParser.parse(this.expressionText); cacheEntry = expressionCache[this.expressionText] =
ExpressionParser.parse(this.expressionText);
} }
return cacheEntry; return cacheEntry;
} }

View File

@ -90,7 +90,10 @@ export class ModelPathWrapper {
var result = this.modelPath.get(); var result = this.modelPath.get();
if (_.isUndefined(result)) { if (_.isUndefined(result)) {
if (expression.strict) { if (expression.strict) {
throw new TypeError('Value of ' + this.modelPathText + ' is undefined. Set options.strict to false to allow undefined values.'); throw new TypeError(
'Value of ' + this.modelPathText +
' is undefined. Set options.strict to false to allow undefined values.'
);
} }
result = null; result = null;
} }

View File

@ -29,7 +29,11 @@ class KeystoneClient {
authenticate(username, password, options = {}) { authenticate(username, password, options = {}) {
if (this.tokenUpdateRequest) return this.tokenUpdateRequest; if (this.tokenUpdateRequest) return this.tokenUpdateRequest;
if (!options.force && this.tokenUpdateTime && (this.cacheTokenFor > (new Date() - this.tokenUpdateTime))) { if (
!options.force &&
this.tokenUpdateTime &&
(this.cacheTokenFor > (new Date() - this.tokenUpdateTime))
) {
return $.Deferred().resolve(); return $.Deferred().resolve();
} }
var data = {auth: {}}; var data = {auth: {}};

View File

@ -72,11 +72,13 @@ _.each(collectionMethods, (method) => {
}); });
var BaseModel = models.BaseModel = Backbone.Model.extend(superMixin); var BaseModel = models.BaseModel = Backbone.Model.extend(superMixin);
var BaseCollection = models.BaseCollection = Backbone.Collection.extend(collectionMixin).extend(superMixin); var BaseCollection = models.BaseCollection =
Backbone.Collection.extend(collectionMixin).extend(superMixin);
var cacheMixin = { var cacheMixin = {
fetch(options) { fetch(options) {
if (this.cacheFor && options && options.cache && this.lastSyncTime && (this.cacheFor > (new Date() - this.lastSyncTime))) { if (this.cacheFor && options && options.cache && this.lastSyncTime &&
(this.cacheFor > (new Date() - this.lastSyncTime))) {
return $.Deferred().resolve(); return $.Deferred().resolve();
} }
return this._super('fetch', arguments); return this._super('fetch', arguments);
@ -98,7 +100,8 @@ models.cacheMixin = cacheMixin;
var restrictionMixin = models.restrictionMixin = { var restrictionMixin = models.restrictionMixin = {
checkRestrictions(models, action, setting) { checkRestrictions(models, action, setting) {
var restrictions = _.map(setting ? setting.restrictions : this.get('restrictions'), utils.expandRestriction); var restrictions = _.map(setting ? setting.restrictions : this.get('restrictions'),
utils.expandRestriction);
if (action) { if (action) {
restrictions = _.where(restrictions, {action: action}); restrictions = _.where(restrictions, {action: action});
} }
@ -125,7 +128,8 @@ var restrictionMixin = models.restrictionMixin = {
* and validate current model checking the possibility of adding/removing node * and validate current model checking the possibility of adding/removing node
* So if max = 1 and we have 1 node then: * So if max = 1 and we have 1 node then:
* - the model is valid as is (return true) -- case for checkLimitIsReached = true * - the model is valid as is (return true) -- case for checkLimitIsReached = true
* - there can be no more nodes added (return false) -- case for checkLimitIsReached = false * - there can be no more nodes added (return false) -- case for
* checkLimitIsReached = false
* limitType -- array of limit types to check. Possible choices are 'min', 'max', 'recommended' * limitType -- array of limit types to check. Possible choices are 'min', 'max', 'recommended'
**/ **/
@ -177,14 +181,16 @@ var restrictionMixin = models.restrictionMixin = {
comparator = (a, b) => a < b; comparator = (a, b) => a < b;
} }
limitValue = parseInt(evaluateExpressionHelper(obj[limitType], models).value, 10); limitValue = parseInt(evaluateExpressionHelper(obj[limitType], models).value, 10);
// Update limitValue with overrides, this way at the end we have a flattened limitValues with overrides having priority // Update limitValue with overrides, this way at the end we have a flattened
// limitValues with overrides having priority
limitValues[limitType] = limitValue; limitValues[limitType] = limitValue;
checkedLimitTypes[limitType] = true; checkedLimitTypes[limitType] = true;
if (comparator(count, limitValue)) { if (comparator(count, limitValue)) {
return { return {
type: limitType, type: limitType,
value: limitValue, value: limitValue,
message: obj.message || i18n('common.role_limits.' + limitType, {limitValue: limitValue, count: count, roleName: label}) message: obj.message || i18n('common.role_limits.' + limitType,
{limitValue: limitValue, count: count, roleName: label})
}; };
} }
}; };
@ -291,7 +297,9 @@ models.Roles = BaseCollection.extend(restrictionMixin).extend({
_.each(role.conflicts, (conflictRoleName) => { _.each(role.conflicts, (conflictRoleName) => {
var conflictingRole = this.findWhere({name: conflictRoleName}); var conflictingRole = this.findWhere({name: conflictRoleName});
if (conflictingRole) conflictingRole.conflicts = _.uniq(_.union(conflictingRole.conflicts || [], [roleName])); if (conflictingRole) {
conflictingRole.conflicts = _.uniq(_.union(conflictingRole.conflicts || [], [roleName]));
}
}); });
}); });
} }
@ -343,7 +351,8 @@ models.Cluster = BaseModel.extend({
return this.get('tasks') && this.get('tasks').filterTasks(filters); return this.get('tasks') && this.get('tasks').filterTasks(filters);
}, },
needsRedeployment() { needsRedeployment() {
return this.get('nodes').any({pending_addition: false, status: 'error'}) && this.get('status') != 'update_error'; return this.get('nodes').any({pending_addition: false, status: 'error'}) &&
this.get('status') != 'update_error';
}, },
fetchRelated(related, options) { fetchRelated(related, options) {
return this.get(related).fetch(_.extend({data: {cluster_id: this.id}}, options)); return this.get(related).fetch(_.extend({data: {cluster_id: this.id}}, options));
@ -354,7 +363,8 @@ models.Cluster = BaseModel.extend({
isDeploymentPossible() { isDeploymentPossible() {
var nodes = this.get('nodes'); var nodes = this.get('nodes');
return this.get('release').get('state') != 'unavailable' && !!nodes.length && return this.get('release').get('state') != 'unavailable' && !!nodes.length &&
(nodes.hasChanges() || this.needsRedeployment()) && !this.task({group: 'deployment', active: true}); (nodes.hasChanges() || this.needsRedeployment()) &&
!this.task({group: 'deployment', active: true});
} }
}); });
@ -388,7 +398,9 @@ models.Node = BaseModel.extend({
} else if (resourceName == 'ht_cores') { } else if (resourceName == 'ht_cores') {
resource = this.get('meta').cpu.total; resource = this.get('meta').cpu.total;
} else if (resourceName == 'hdd') { } else if (resourceName == 'hdd') {
resource = _.reduce(this.get('meta').disks, (hdd, disk) => _.isNumber(disk.size) ? hdd + disk.size : hdd, 0); resource = _.reduce(this.get('meta').disks, (hdd, disk) => {
return _.isNumber(disk.size) ? hdd + disk.size : hdd;
}, 0);
} else if (resourceName == 'ram') { } else if (resourceName == 'ram') {
resource = this.get('meta').memory.total; resource = this.get('meta').memory.total;
} else if (resourceName == 'disks') { } else if (resourceName == 'disks') {
@ -412,7 +424,8 @@ models.Node = BaseModel.extend({
return this.get('status') != 'removing'; return this.get('status') != 'removing';
}, },
hasRole(role, onlyDeployedRoles) { hasRole(role, onlyDeployedRoles) {
var roles = onlyDeployedRoles ? this.get('roles') : _.union(this.get('roles'), this.get('pending_roles')); var roles = onlyDeployedRoles ? this.get('roles') :
_.union(this.get('roles'), this.get('pending_roles'));
return _.contains(roles, role); return _.contains(roles, role);
}, },
hasChanges() { hasChanges() {
@ -505,7 +518,8 @@ models.Nodes = BaseCollection.extend({
var roles = _.union(this.at(0).get('roles'), this.at(0).get('pending_roles')); var roles = _.union(this.at(0).get('roles'), this.at(0).get('pending_roles'));
var disks = this.at(0).resource('disks'); var disks = this.at(0).resource('disks');
return !this.any((node) => { return !this.any((node) => {
var roleConflict = _.difference(roles, _.union(node.get('roles'), node.get('pending_roles'))).length; var roleConflict = _.difference(roles, _.union(node.get('roles'),
node.get('pending_roles'))).length;
return roleConflict || !_.isEqual(disks, node.resource('disks')); return roleConflict || !_.isEqual(disks, node.resource('disks'));
}); });
}, },
@ -556,10 +570,12 @@ models.Task = BaseModel.extend({
match(filters) { match(filters) {
filters = filters || {}; filters = filters || {};
if (!_.isEmpty(filters)) { if (!_.isEmpty(filters)) {
if ((filters.group || filters.name) && !_.contains(this.extendGroups(filters), this.get('name'))) { if ((filters.group || filters.name) &&
!_.contains(this.extendGroups(filters), this.get('name'))) {
return false; return false;
} }
if ((filters.status || _.isBoolean(filters.active)) && !_.contains(this.extendStatuses(filters), this.get('status'))) { if ((filters.status || _.isBoolean(filters.active)) &&
!_.contains(this.extendStatuses(filters), this.get('status'))) {
return false; return false;
} }
} }
@ -607,121 +623,134 @@ models.Notifications = BaseCollection.extend({
} }
}); });
models.Settings = Backbone.DeepModel.extend(superMixin).extend(cacheMixin).extend(restrictionMixin).extend({ models.Settings = Backbone.DeepModel
constructorName: 'Settings', .extend(superMixin)
urlRoot: '/api/clusters/', .extend(cacheMixin)
root: 'editable', .extend(restrictionMixin)
cacheFor: 60 * 1000, .extend({
groupList: ['general', 'security', 'compute', 'network', 'storage', 'logging', 'openstack_services', 'other'], constructorName: 'Settings',
isNew() { urlRoot: '/api/clusters/',
return false; root: 'editable',
}, cacheFor: 60 * 1000,
isPlugin(section) { groupList: ['general', 'security', 'compute', 'network', 'storage',
return (section.metadata || {}).class == 'plugin'; 'logging', 'openstack_services', 'other'],
}, isNew() {
parse(response) { return false;
return response[this.root]; },
}, isPlugin(section) {
mergePluginSettings() { return (section.metadata || {}).class == 'plugin';
_.each(this.attributes, (section, sectionName) => { },
if (this.isPlugin(section)) { parse(response) {
var chosenVersionData = section.metadata.versions.find( return response[this.root];
(version) => version.metadata.plugin_id == section.metadata.chosen_id },
); mergePluginSettings() {
// merge metadata of a chosen plugin version _.each(this.attributes, (section, sectionName) => {
_.extend(section.metadata, _.omit(chosenVersionData.metadata, 'plugin_id', 'plugin_version')); if (this.isPlugin(section)) {
// merge settings of a chosen plugin version var chosenVersionData = section.metadata.versions.find(
this.attributes[sectionName] = _.extend(_.pick(section, 'metadata'), _.omit(chosenVersionData, 'metadata')); (version) => version.metadata.plugin_id == section.metadata.chosen_id
} );
}, this); // merge metadata of a chosen plugin version
}, _.extend(section.metadata,
toJSON() { _.omit(chosenVersionData.metadata, 'plugin_id', 'plugin_version'));
var settings = this._super('toJSON', arguments); // merge settings of a chosen plugin version
if (!this.root) return settings; this.attributes[sectionName] = _.extend(_.pick(section, 'metadata'),
_.omit(chosenVersionData, 'metadata'));
}
}, this);
},
toJSON() {
var settings = this._super('toJSON', arguments);
if (!this.root) return settings;
// update plugin settings // update plugin settings
_.each(settings, (section, sectionName) => { _.each(settings, (section, sectionName) => {
if (this.isPlugin(section)) { if (this.isPlugin(section)) {
var chosenVersionData = section.metadata.versions.find( var chosenVersionData = section.metadata.versions.find(
(version) => version.metadata.plugin_id == section.metadata.chosen_id (version) => version.metadata.plugin_id == section.metadata.chosen_id
); );
section.metadata = _.omit(section.metadata, _.without(_.keys(chosenVersionData.metadata), 'plugin_id', 'plugin_version')); section.metadata = _.omit(section.metadata,
_.each(section, (setting, settingName) => { _.without(_.keys(chosenVersionData.metadata), 'plugin_id', 'plugin_version'));
if (settingName != 'metadata') chosenVersionData[settingName].value = setting.value; _.each(section, (setting, settingName) => {
}); if (settingName != 'metadata') chosenVersionData[settingName].value = setting.value;
settings[sectionName] = _.pick(section, 'metadata'); });
} settings[sectionName] = _.pick(section, 'metadata');
});
return {[this.root]: settings};
},
initialize() {
this.once('change', this.mergePluginSettings, this);
},
validate(attrs, options) {
var errors = {};
var models = options ? options.models : {};
var checkRestrictions = (setting) => this.checkRestrictions(models, null, setting);
_.each(attrs, (group, groupName) => {
if ((group.metadata || {}).enabled === false || checkRestrictions(group.metadata).result) return;
_.each(group, (setting, settingName) => {
if (checkRestrictions(setting).result) return;
var path = this.makePath(groupName, settingName);
// support of custom controls
var CustomControl = customControls[setting.type];
if (CustomControl) {
var error = CustomControl.validate(setting, models);
if (error) errors[path] = error;
return;
} }
});
return {[this.root]: settings};
},
initialize() {
this.once('change', this.mergePluginSettings, this);
},
validate(attrs, options) {
var errors = {};
var models = options ? options.models : {};
var checkRestrictions = (setting) => this.checkRestrictions(models, null, setting);
_.each(attrs, (group, groupName) => {
if ((group.metadata || {}).enabled === false ||
checkRestrictions(group.metadata).result) return;
_.each(group, (setting, settingName) => {
if (checkRestrictions(setting).result) return;
var path = this.makePath(groupName, settingName);
// support of custom controls
var CustomControl = customControls[setting.type];
if (CustomControl) {
var error = CustomControl.validate(setting, models);
if (error) errors[path] = error;
return;
}
if (!(setting.regex || {}).source) return; if (!(setting.regex || {}).source) return;
if (!setting.value.match(new RegExp(setting.regex.source))) errors[path] = setting.regex.error; if (!setting.value.match(new RegExp(setting.regex.source))) {
}); errors[path] = setting.regex.error;
}); }
return _.isEmpty(errors) ? null : errors;
},
makePath(...args) {
return args.join('.');
},
getValueAttribute(settingName) {
return settingName == 'metadata' ? 'enabled' : 'value';
},
hasChanges(initialAttributes, models) {
return _.any(this.attributes, (section, sectionName) => {
var metadata = section.metadata;
var result = false;
if (metadata) {
if (this.checkRestrictions(models, null, metadata).result) return result;
if (!_.isUndefined(metadata.enabled)) {
result = metadata.enabled != initialAttributes[sectionName].metadata.enabled;
}
if (!result && this.isPlugin(section)) {
result = metadata.chosen_id != initialAttributes[sectionName].metadata.chosen_id;
}
}
return result || (metadata || {}).enabled !== false && _.any(section, (setting, settingName) => {
if (this.checkRestrictions(models, null, setting).result) return false;
return !_.isEqual(setting.value, (initialAttributes[sectionName][settingName] || {}).value);
});
});
},
sanitizeGroup(group) {
return _.contains(this.groupList, group) ? group : 'other';
},
getGroupList() {
var groups = [];
_.each(this.attributes, (section) => {
if (section.metadata.group) {
groups.push(this.sanitizeGroup(section.metadata.group));
} else {
_.each(section, (setting, settingName) => {
if (settingName != 'metadata') groups.push(this.sanitizeGroup(setting.group));
}); });
} });
}); return _.isEmpty(errors) ? null : errors;
return _.intersection(this.groupList, groups); },
} makePath(...args) {
}); return args.join('.');
},
getValueAttribute(settingName) {
return settingName == 'metadata' ? 'enabled' : 'value';
},
hasChanges(initialAttributes, models) {
return _.any(this.attributes, (section, sectionName) => {
var metadata = section.metadata;
var result = false;
if (metadata) {
if (this.checkRestrictions(models, null, metadata).result) return result;
if (!_.isUndefined(metadata.enabled)) {
result = metadata.enabled != initialAttributes[sectionName].metadata.enabled;
}
if (!result && this.isPlugin(section)) {
result = metadata.chosen_id != initialAttributes[sectionName].metadata.chosen_id;
}
}
return result || (metadata || {}).enabled !== false &&
_.any(section, (setting, settingName) => {
if (this.checkRestrictions(models, null, setting).result) return false;
return !_.isEqual(setting.value,
(initialAttributes[sectionName][settingName] || {}).value);
});
});
},
sanitizeGroup(group) {
return _.contains(this.groupList, group) ? group : 'other';
},
getGroupList() {
var groups = [];
_.each(this.attributes, (section) => {
if (section.metadata.group) {
groups.push(this.sanitizeGroup(section.metadata.group));
} else {
_.each(section, (setting, settingName) => {
if (settingName != 'metadata') groups.push(this.sanitizeGroup(setting.group));
});
}
});
return _.intersection(this.groupList, groups);
}
});
models.FuelSettings = models.Settings.extend({ models.FuelSettings = models.Settings.extend({
constructorName: 'FuelSettings', constructorName: 'FuelSettings',
@ -741,7 +770,8 @@ models.Disk = BaseModel.extend({
return response; return response;
}, },
toJSON(options) { toJSON(options) {
return _.extend(this.constructor.__super__.toJSON.call(this, options), {volumes: this.get('volumes').toJSON()}); return _.extend(this.constructor.__super__.toJSON.call(this, options),
{volumes: this.get('volumes').toJSON()});
}, },
getUnallocatedSpace(options) { getUnallocatedSpace(options) {
options = options || {}; options = options || {};
@ -755,7 +785,8 @@ models.Disk = BaseModel.extend({
var error; var error;
var unallocatedSpace = this.getUnallocatedSpace({volumes: attrs.volumes}); var unallocatedSpace = this.getUnallocatedSpace({volumes: attrs.volumes});
if (unallocatedSpace < 0) { if (unallocatedSpace < 0) {
error = i18n('cluster_page.nodes_tab.configure_disks.validation_error', {size: utils.formatNumber(unallocatedSpace * -1)}); error = i18n('cluster_page.nodes_tab.configure_disks.validation_error',
{size: utils.formatNumber(unallocatedSpace * -1)});
} }
return error; return error;
} }
@ -776,7 +807,8 @@ models.Volume = BaseModel.extend({
var groupAllocatedSpace = 0; var groupAllocatedSpace = 0;
if (currentDisk && currentDisk.collection) if (currentDisk && currentDisk.collection)
groupAllocatedSpace = currentDisk.collection.reduce((sum, disk) => { groupAllocatedSpace = currentDisk.collection.reduce((sum, disk) => {
return disk.id == currentDisk.id ? sum : sum + disk.get('volumes').findWhere({name: this.get('name')}).get('size'); return disk.id == currentDisk.id ? sum : sum +
disk.get('volumes').findWhere({name: this.get('name')}).get('size');
}, 0); }, 0);
return minimum - groupAllocatedSpace; return minimum - groupAllocatedSpace;
}, },
@ -790,7 +822,8 @@ models.Volume = BaseModel.extend({
validate(attrs, options) { validate(attrs, options) {
var min = this.getMinimalSize(options.minimum); var min = this.getMinimalSize(options.minimum);
if (attrs.size < min) { if (attrs.size < min) {
return i18n('cluster_page.nodes_tab.configure_disks.volume_error', {size: utils.formatNumber(min)}); return i18n('cluster_page.nodes_tab.configure_disks.volume_error',
{size: utils.formatNumber(min)});
} }
return null; return null;
} }
@ -826,11 +859,15 @@ models.Interface = BaseModel.extend({
}, },
validate(attrs) { validate(attrs) {
var errors = []; var errors = [];
var networks = new models.Networks(this.get('assigned_networks').invoke('getFullNetwork', attrs.networks)); var networks = new models.Networks(this.get('assigned_networks')
var untaggedNetworks = networks.filter((network) => _.isNull(network.getVlanRange(attrs.networkingParameters))); .invoke('getFullNetwork', attrs.networks));
var untaggedNetworks = networks.filter((network) => {
return _.isNull(network.getVlanRange(attrs.networkingParameters));
});
var ns = 'cluster_page.nodes_tab.configure_interfaces.validation.'; var ns = 'cluster_page.nodes_tab.configure_interfaces.validation.';
// public and floating networks are allowed to be assigned to the same interface // public and floating networks are allowed to be assigned to the same interface
var maxUntaggedNetworksCount = networks.any({name: 'public'}) && networks.any({name: 'floating'}) ? 2 : 1; var maxUntaggedNetworksCount = networks.any({name: 'public'}) &&
networks.any({name: 'floating'}) ? 2 : 1;
if (untaggedNetworks.length > maxUntaggedNetworksCount) { if (untaggedNetworks.length > maxUntaggedNetworksCount) {
errors.push(i18n(ns + 'too_many_untagged_networks')); errors.push(i18n(ns + 'too_many_untagged_networks'));
} }
@ -853,7 +890,8 @@ models.Interface = BaseModel.extend({
if ( if (
_.any(vlanRanges, _.any(vlanRanges,
(currentRange) => _.any(vlanRanges, (currentRange) => _.any(vlanRanges,
(range) => !_.isEqual(currentRange, range) && range[1] >= currentRange[0] && range[0] <= currentRange[1] (range) => !_.isEqual(currentRange, range) &&
range[1] >= currentRange[0] && range[0] <= currentRange[1]
) )
) )
) errors.push(i18n(ns + 'vlan_range_intersection')); ) errors.push(i18n(ns + 'vlan_range_intersection'));
@ -877,7 +915,8 @@ models.Interfaces = BaseCollection.extend({
} }
}); });
var networkPreferredOrder = ['public', 'floating', 'storage', 'management', 'private', 'fixed', 'baremetal']; var networkPreferredOrder = ['public', 'floating', 'storage', 'management',
'private', 'fixed', 'baremetal'];
models.InterfaceNetwork = BaseModel.extend({ models.InterfaceNetwork = BaseModel.extend({
constructorName: 'InterfaceNetwork', constructorName: 'InterfaceNetwork',
@ -899,8 +938,11 @@ models.Network = BaseModel.extend({
getVlanRange(networkingParameters) { getVlanRange(networkingParameters) {
if (!this.get('meta').neutron_vlan_range) { if (!this.get('meta').neutron_vlan_range) {
var externalNetworkData = this.get('meta').ext_net_data; var externalNetworkData = this.get('meta').ext_net_data;
var vlanStart = externalNetworkData ? networkingParameters.get(externalNetworkData[0]) : this.get('vlan_start'); var vlanStart = externalNetworkData ?
return _.isNull(vlanStart) ? vlanStart : [vlanStart, externalNetworkData ? vlanStart + networkingParameters.get(externalNetworkData[1]) - 1 : vlanStart]; 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'); return networkingParameters.get('vlan_range');
} }
@ -923,7 +965,9 @@ models.NetworkConfiguration = BaseModel.extend(cacheMixin).extend({
cacheFor: 60 * 1000, cacheFor: 60 * 1000,
parse(response) { parse(response) {
response.networks = new models.Networks(response.networks); response.networks = new models.Networks(response.networks);
response.networking_parameters = new models.NetworkingParameters(response.networking_parameters); response.networking_parameters = new models.NetworkingParameters(
response.networking_parameters
);
return response; return response;
}, },
toJSON() { toJSON() {
@ -982,7 +1026,8 @@ models.NetworkConfiguration = BaseModel.extend(cacheMixin).extend({
var forbiddenVlans = novaNetManager ? networksToCheck.map((net) => { var forbiddenVlans = novaNetManager ? networksToCheck.map((net) => {
return net.id != network.id ? net.get('vlan_start') : null; return net.id != network.id ? net.get('vlan_start') : null;
}) : []; }) : [];
_.extend(networkErrors, utils.validateVlan(network.get('vlan_start'), forbiddenVlans, 'vlan_start')); _.extend(networkErrors,
utils.validateVlan(network.get('vlan_start'), forbiddenVlans, 'vlan_start'));
if (!_.isEmpty(networkErrors)) { if (!_.isEmpty(networkErrors)) {
nodeNetworkGroupErrors[network.id] = networkErrors; nodeNetworkGroupErrors[network.id] = networkErrors;
@ -995,7 +1040,9 @@ models.NetworkConfiguration = BaseModel.extend(cacheMixin).extend({
} else if (!cidrError && !utils.validateIpCorrespondsToCIDR(cidr, baremetalGateway)) { } else if (!cidrError && !utils.validateIpCorrespondsToCIDR(cidr, baremetalGateway)) {
networkingParametersErrors.baremetal_gateway = i18n(ns + 'gateway_does_not_match_cidr'); networkingParametersErrors.baremetal_gateway = i18n(ns + 'gateway_does_not_match_cidr');
} }
var baremetalRangeErrors = utils.validateIPRanges([networkParameters.get('baremetal_range')], cidrError ? null : cidr); var baremetalRangeErrors =
utils.validateIPRanges([networkParameters.get('baremetal_range')], cidrError ?
null : cidr);
if (baremetalRangeErrors.length) { if (baremetalRangeErrors.length) {
var [{start, end}] = baremetalRangeErrors; var [{start, end}] = baremetalRangeErrors;
networkingParametersErrors.baremetal_range = [start, end]; networkingParametersErrors.baremetal_range = [start, end];
@ -1014,13 +1061,15 @@ models.NetworkConfiguration = BaseModel.extend(cacheMixin).extend({
// validate networking parameters // validate networking parameters
if (novaNetManager) { if (novaNetManager) {
networkingParametersErrors = _.extend(networkingParametersErrors, utils.validateCidr(networkParameters.get('fixed_networks_cidr'), 'fixed_networks_cidr')); networkingParametersErrors = _.extend(networkingParametersErrors,
utils.validateCidr(networkParameters.get('fixed_networks_cidr'), 'fixed_networks_cidr'));
var fixedAmount = networkParameters.get('fixed_networks_amount'); var fixedAmount = networkParameters.get('fixed_networks_amount');
var fixedVlan = networkParameters.get('fixed_networks_vlan_start'); var fixedVlan = networkParameters.get('fixed_networks_vlan_start');
if (!utils.isNaturalNumber(parseInt(fixedAmount, 10))) { if (!utils.isNaturalNumber(parseInt(fixedAmount, 10))) {
networkingParametersErrors.fixed_networks_amount = i18n(ns + 'invalid_amount'); networkingParametersErrors.fixed_networks_amount = i18n(ns + 'invalid_amount');
} }
var vlanErrors = utils.validateVlan(fixedVlan, networks.pluck('vlan_start'), 'fixed_networks_vlan_start', novaNetManager == 'VlanManager'); var vlanErrors = utils.validateVlan(fixedVlan, networks.pluck('vlan_start'),
'fixed_networks_vlan_start', novaNetManager == 'VlanManager');
_.extend(networkingParametersErrors, vlanErrors); _.extend(networkingParametersErrors, vlanErrors);
if (_.isEmpty(vlanErrors)) { if (_.isEmpty(vlanErrors)) {
if (!networkingParametersErrors.fixed_networks_amount && fixedAmount > 4095 - fixedVlan) { if (!networkingParametersErrors.fixed_networks_amount && fixedAmount > 4095 - fixedVlan) {
@ -1071,7 +1120,8 @@ models.NetworkConfiguration = BaseModel.extend(cacheMixin).extend({
networkingParametersErrors.base_mac = i18n(ns + 'invalid_mac'); networkingParametersErrors.base_mac = i18n(ns + 'invalid_mac');
} }
var cidr = networkParameters.get('internal_cidr'); var cidr = networkParameters.get('internal_cidr');
networkingParametersErrors = _.extend(networkingParametersErrors, utils.validateCidr(cidr, 'internal_cidr')); networkingParametersErrors = _.extend(networkingParametersErrors,
utils.validateCidr(cidr, 'internal_cidr'));
var gateway = networkParameters.get('internal_gateway'); var gateway = networkParameters.get('internal_gateway');
if (!utils.validateIP(gateway)) { if (!utils.validateIP(gateway)) {
networkingParametersErrors.internal_gateway = i18n(ns + 'invalid_gateway'); networkingParametersErrors.internal_gateway = i18n(ns + 'invalid_gateway');
@ -1100,22 +1150,29 @@ models.NetworkConfiguration = BaseModel.extend(cacheMixin).extend({
var networkToCheckFloatingRangeData = networkToCheckFloatingRange ? { var networkToCheckFloatingRangeData = networkToCheckFloatingRange ? {
cidr: networkToCheckFloatingRange.get('cidr'), cidr: networkToCheckFloatingRange.get('cidr'),
network: _.capitalize(networkToCheckFloatingRange.get('name')), network: _.capitalize(networkToCheckFloatingRange.get('name')),
nodeNetworkGroup: nodeNetworkGroups.get(networkToCheckFloatingRange.get('group_id')).get('name') nodeNetworkGroup: nodeNetworkGroups
.get(networkToCheckFloatingRange.get('group_id'))
.get('name')
} : {}; } : {};
var networkToCheckFloatingRangeIPRanges = networkToCheckFloatingRange ? _.filter(networkToCheckFloatingRange.get('ip_ranges'), (range, index) => { var networkToCheckFloatingRangeIPRanges = networkToCheckFloatingRange ?
var ipRangeError = false; _.filter(networkToCheckFloatingRange.get('ip_ranges'), (range, index) => {
try { var ipRangeError = false;
ipRangeError = !_.all(range) || !!_.find(errors.networks[networkToCheckFloatingRange.get('group_id')][networkToCheckFloatingRange.id].ip_ranges, {index: index}); try {
} catch (error) {} ipRangeError = !_.all(range) ||
return !ipRangeError; !!_.find(errors
}) : []; .networks[networkToCheckFloatingRange
.get('group_id')][networkToCheckFloatingRange.id].ip_ranges, {index: index});
} catch (error) {}
return !ipRangeError;
}) : [];
floatingRangesErrors = utils.validateIPRanges( floatingRangesErrors = utils.validateIPRanges(
floatingRanges, floatingRanges,
networkToCheckFloatingRangeData.cidr, networkToCheckFloatingRangeData.cidr,
networkToCheckFloatingRangeIPRanges, networkToCheckFloatingRangeIPRanges,
{ {
IP_RANGES_INTERSECTION: i18n(ns + 'floating_and_public_ip_ranges_intersection', networkToCheckFloatingRangeData), IP_RANGES_INTERSECTION: i18n(ns + 'floating_and_public_ip_ranges_intersection',
networkToCheckFloatingRangeData),
IP_RANGE_IS_NOT_IN_PUBLIC_CIDR: i18n(ns + 'floating_range_is_not_in_public_cidr') IP_RANGE_IS_NOT_IN_PUBLIC_CIDR: i18n(ns + 'floating_range_is_not_in_public_cidr')
} }
); );
@ -1268,7 +1325,8 @@ models.WizardModel = Backbone.DeepModel.extend({
var errors = []; var errors = [];
_.each(options.config, (attributeConfig, attribute) => { _.each(options.config, (attributeConfig, attribute) => {
if (!(attributeConfig.regex && attributeConfig.regex.source)) return; if (!(attributeConfig.regex && attributeConfig.regex.source)) return;
var hasNoSatisfiedRestrictions = _.every(_.reject(attributeConfig.restrictions, {action: 'none'}), (restriction) => { var hasNoSatisfiedRestrictions = _.every(_.reject(attributeConfig.restrictions,
{action: 'none'}), (restriction) => {
// this probably will be changed when other controls need validation // this probably will be changed when other controls need validation
return !utils.evaluateExpression(restriction.condition, {default: this}).value; return !utils.evaluateExpression(restriction.condition, {default: this}).value;
}); });
@ -1291,14 +1349,17 @@ models.WizardModel = Backbone.DeepModel.extend({
models.MirantisCredentials = Backbone.DeepModel.extend(superMixin).extend({ models.MirantisCredentials = Backbone.DeepModel.extend(superMixin).extend({
constructorName: 'MirantisCredentials', constructorName: 'MirantisCredentials',
baseUrl: 'https://software.mirantis.com/wp-content/themes/mirantis_responsive_v_1_0/scripts/fuel_forms_api/', baseUrl: 'https://software.mirantis.com/wp-content/themes/' +
'mirantis_responsive_v_1_0/scripts/fuel_forms_api/',
validate(attrs) { validate(attrs) {
var errors = {}; var errors = {};
_.each(attrs, (group, groupName) => { _.each(attrs, (group, groupName) => {
_.each(group, (setting, settingName) => { _.each(group, (setting, settingName) => {
var path = this.makePath(groupName, settingName); var path = this.makePath(groupName, settingName);
if (!setting.regex || !setting.regex.source) return; if (!setting.regex || !setting.regex.source) return;
if (!setting.value.match(new RegExp(setting.regex.source))) errors[path] = setting.regex.error; if (!setting.value.match(new RegExp(setting.regex.source))) {
errors[path] = setting.regex.error;
}
}); });
}); });
return _.isEmpty(errors) ? null : errors; return _.isEmpty(errors) ? null : errors;
@ -1414,7 +1475,8 @@ models.ComponentModel = BaseModel.extend({
var expandProperty = (propertyName, components) => { var expandProperty = (propertyName, components) => {
var expandedComponents = []; var expandedComponents = [];
_.each(this.get(propertyName), (patternDescription) => { _.each(this.get(propertyName), (patternDescription) => {
var patternName = _.isString(patternDescription) ? patternDescription : patternDescription.name; var patternName = _.isString(patternDescription) ? patternDescription :
patternDescription.name;
var pattern = new ComponentPattern(patternName); var pattern = new ComponentPattern(patternName);
components.each((component) => { components.each((component) => {
if (pattern.match(component.id)) { if (pattern.match(component.id)) {

View File

@ -36,67 +36,74 @@ function testRegex(regexText, value) {
return regexCache[regexText].test(value); return regexCache[regexText].test(value);
} }
var BaseModel = Backbone.Model.extend(models.superMixin).extend(models.cacheMixin).extend(models.restrictionMixin).extend({ var BaseModel = Backbone.Model
constructorName: 'BaseModel', .extend(models.superMixin)
cacheFor: 60 * 1000, .extend(models.cacheMixin)
toJSON() { .extend(models.restrictionMixin)
return _.omit(this.attributes, 'metadata'); .extend({
}, constructorName: 'BaseModel',
validate() { cacheFor: 60 * 1000,
var result = {}; toJSON() {
_.each(this.attributes.metadata, (field) => { return _.omit(this.attributes, 'metadata');
if (!VmWareModels.isRegularField(field) || field.type == 'checkbox') { },
return; validate() {
} var result = {};
var isDisabled = this.checkRestrictions(restrictionModels, undefined, field); _.each(this.attributes.metadata, (field) => {
if (isDisabled.result) { if (!VmWareModels.isRegularField(field) || field.type == 'checkbox') {
return; return;
}
var value = this.get(field.name);
if (field.regex) {
if (!testRegex(field.regex.source, value)) {
result[field.name] = field.regex.error;
} }
} var isDisabled = this.checkRestrictions(restrictionModels, undefined, field);
}); if (isDisabled.result) {
return _.isEmpty(result) ? null : result; return;
}, }
testRestrictions() { var value = this.get(field.name);
var results = { if (field.regex) {
hide: {}, if (!testRegex(field.regex.source, value)) {
disable: {} result[field.name] = field.regex.error;
}; }
var metadata = this.get('metadata'); }
_.each(metadata, (field) => { });
var disableResult = this.checkRestrictions(restrictionModels, undefined, field); return _.isEmpty(result) ? null : result;
results.disable[field.name] = disableResult; },
testRestrictions() {
var results = {
hide: {},
disable: {}
};
var metadata = this.get('metadata');
_.each(metadata, (field) => {
var disableResult = this.checkRestrictions(restrictionModels, undefined, field);
results.disable[field.name] = disableResult;
var hideResult = this.checkRestrictions(restrictionModels, 'hide', field); var hideResult = this.checkRestrictions(restrictionModels, 'hide', field);
results.hide[field.name] = hideResult; results.hide[field.name] = hideResult;
}); });
return results; return results;
} }
}); });
var BaseCollection = Backbone.Collection.extend(models.superMixin).extend(models.cacheMixin).extend({ var BaseCollection = Backbone.Collection
constructorName: 'BaseCollection', .extend(models.superMixin)
model: BaseModel, .extend(models.cacheMixin)
cacheFor: 60 * 1000, .extend({
isValid() { constructorName: 'BaseCollection',
this.validationError = this.validate(); model: BaseModel,
return this.validationError; cacheFor: 60 * 1000,
}, isValid() {
validate() { this.validationError = this.validate();
var errors = _.compact(this.models.map((model) => { return this.validationError;
model.isValid(); },
return model.validationError; validate() {
})); var errors = _.compact(this.models.map((model) => {
return _.isEmpty(errors) ? null : errors; model.isValid();
}, return model.validationError;
testRestrictions() { }));
_.invoke(this.models, 'testRestrictions', restrictionModels); return _.isEmpty(errors) ? null : errors;
} },
}); testRestrictions() {
_.invoke(this.models, 'testRestrictions', restrictionModels);
}
});
VmWareModels.NovaCompute = BaseModel.extend({ VmWareModels.NovaCompute = BaseModel.extend({
constructorName: 'NovaCompute', constructorName: 'NovaCompute',
@ -209,7 +216,8 @@ VmWareModels.Glance = BaseModel.extend({constructorName: 'Glance'});
VmWareModels.VCenter = BaseModel.extend({ VmWareModels.VCenter = BaseModel.extend({
constructorName: 'VCenter', constructorName: 'VCenter',
url() { url() {
return '/api/v1/clusters/' + this.id + '/vmware_attributes' + (this.loadDefaults ? '/defaults' : ''); return '/api/v1/clusters/' + this.id + '/vmware_attributes' +
(this.loadDefaults ? '/defaults' : '');
}, },
parse(response) { parse(response) {
if (!response.editable || !response.editable.metadata || !response.editable.value) { if (!response.editable || !response.editable.metadata || !response.editable.value) {
@ -289,7 +297,8 @@ VmWareModels.VCenter = BaseModel.extend({
}); });
}); });
var unassignedNodes = restrictionModels.cluster.get('nodes').filter((node) => { var unassignedNodes = restrictionModels.cluster.get('nodes').filter((node) => {
return _.contains(node.get('pending_roles'), 'compute-vmware') && !assignedNodes[node.get('hostname')]; return _.contains(node.get('pending_roles'), 'compute-vmware') &&
!assignedNodes[node.get('hostname')];
}); });
if (unassignedNodes.length > 0) { if (unassignedNodes.length > 0) {
errors.unassigned_nodes = unassignedNodes; errors.unassigned_nodes = unassignedNodes;

View File

@ -236,7 +236,13 @@ var AvailabilityZones = React.createClass({
} }
</h3> </h3>
{this.props.collection.map((model) => { {this.props.collection.map((model) => {
return <AvailabilityZone key={model.cid} model={model} disabled={this.props.disabled} cluster={this.props.cluster} isLocked={this.props.isLocked}/>; return <AvailabilityZone
key={model.cid}
model={model}
disabled={this.props.disabled}
cluster={this.props.cluster}
isLocked={this.props.isLocked}
/>;
})} })}
</div> </div>
); );
@ -310,7 +316,8 @@ var VmWareTab = React.createClass({
this.model.setModels({ this.model.setModels({
cluster: this.props.cluster, cluster: this.props.cluster,
settings: this.props.cluster.get('settings'), settings: this.props.cluster.get('settings'),
networking_parameters: this.props.cluster.get('networkConfiguration').get('networking_parameters') networking_parameters: this.props.cluster.get('networkConfiguration')
.get('networking_parameters')
}); });
this.onModelSync(); // eslint-disable-line no-sync this.onModelSync(); // eslint-disable-line no-sync
@ -419,13 +426,25 @@ var VmWareTab = React.createClass({
<div className='col-xs-12 page-buttons content-elements'> <div className='col-xs-12 page-buttons content-elements'>
<div className='well clearfix'> <div className='well clearfix'>
<div className='btn-group pull-right'> <div className='btn-group pull-right'>
<button className='btn btn-default btn-load-defaults' onClick={this.onLoadDefaults} disabled={!editable || defaultsDisabled}> <button
className='btn btn-default btn-load-defaults'
onClick={this.onLoadDefaults}
disabled={!editable || defaultsDisabled}
>
{i18n('vmware.reset_to_defaults')} {i18n('vmware.reset_to_defaults')}
</button> </button>
<button className='btn btn-default btn-revert-changes' onClick={this.revertChanges} disabled={!hasChanges}> <button
className='btn btn-default btn-revert-changes'
onClick={this.revertChanges}
disabled={!hasChanges}
>
{i18n('vmware.cancel')} {i18n('vmware.cancel')}
</button> </button>
<button className='btn btn-success btn-apply-changes' onClick={this.applyChanges} disabled={saveDisabled}> <button
className='btn btn-success btn-apply-changes'
onClick={this.applyChanges}
disabled={saveDisabled}
>
{i18n('vmware.apply')} {i18n('vmware.apply')}
</button> </button>
</div> </div>

View File

@ -125,7 +125,8 @@ define([
var dragAndDrop = (function() { var dragAndDrop = (function() {
var dispatchEvent, createEvent; var dispatchEvent, createEvent;
// Setup methods to call the proper event creation and dispatch functions for the current platform. // Setup methods to call the proper event creation and
// dispatch functions for the current platform.
if (document.createEvent) { if (document.createEvent) {
dispatchEvent = function(element, eventName, event) { dispatchEvent = function(element, eventName, event) {
element.dispatchEvent(event); element.dispatchEvent(event);
@ -215,8 +216,8 @@ define([
try { try {
event = createEvent('MouseEvent'); event = createEvent('MouseEvent');
event.initMouseEvent(eventName, true, true, window, 0, screenX, screenY, clientX, clientY, event.initMouseEvent(eventName, true, true, window, 0, screenX, screenY, clientX,
false, false, false, false, 0, null); clientY, false, false, false, false, 0, null);
} catch (error) { } catch (error) {
event = createCustomEvent(eventName, screenX, screenY, clientX, clientY); event = createCustomEvent(eventName, screenX, screenY, clientX, clientY);
} }
@ -230,7 +231,8 @@ define([
function simulateEvent(element, eventName, dragStartEvent, options) { function simulateEvent(element, eventName, dragStartEvent, options) {
var dataTransfer = dragStartEvent ? dragStartEvent.dataTransfer : null; var dataTransfer = dragStartEvent ? dragStartEvent.dataTransfer : null;
var createEvent = eventName.indexOf('mouse') !== -1 ? createMouseEvent : createDragEvent; var createEvent = eventName.indexOf('mouse') !== -1 ? createMouseEvent :
createDragEvent;
var event = createEvent(eventName, options, dataTransfer); var event = createEvent(eventName, options, dataTransfer);
return dispatchEvent(element, eventName, event); return dispatchEvent(element, eventName, event);
} }

View File

@ -86,7 +86,9 @@ function(_, assert, Helpers, pollUntil, LoginPage, WelcomePage, ClusterPage, Clu
return self.clusterPage.removeCluster(clusterName); return self.clusterPage.removeCluster(clusterName);
}) })
.catch(function(e) { .catch(function(e) {
if (!suppressErrors) throw new Error('Unable to delete cluster ' + clusterName + ': ' + e); if (!suppressErrors) {
throw new Error('Unable to delete cluster ' + clusterName + ': ' + e);
}
}); });
}, },
doesClusterExist: function(clusterName) { doesClusterExist: function(clusterName) {
@ -115,7 +117,8 @@ function(_, assert, Helpers, pollUntil, LoginPage, WelcomePage, ClusterPage, Clu
.clickByCssSelector('button.btn-add-nodes') .clickByCssSelector('button.btn-add-nodes')
.waitForCssSelector('.node', 3000) .waitForCssSelector('.node', 3000)
.then(pollUntil(function() { .then(pollUntil(function() {
return window.$('.node-list-management-buttons').is(':visible') && window.$('.role-panel').is(':visible') || null; return window.$('.node-list-management-buttons').is(':visible') &&
window.$('.role-panel').is(':visible') || null;
}, 3000)) }, 3000))
.then(function() { .then(function() {
if (nodeNameFilter) return self.clusterPage.searchForNode(nodeNameFilter); if (nodeNameFilter) return self.clusterPage.searchForNode(nodeNameFilter);

View File

@ -51,7 +51,8 @@ define([
duration: '20s', duration: '20s',
message: null, message: null,
id: 'fuel_health.tests.check_disk', id: 'fuel_health.tests.check_disk',
description: 'Target component: Nova Scenario: 1. Check outage on controller and compute nodes' description: 'Target component: Nova ' +
'Scenario: 1. Check outage on controller and compute nodes'
}, },
{ {
status: null, status: null,
@ -73,7 +74,8 @@ define([
duration: '30s.', duration: '30s.',
message: null, message: null,
id: 'fuel_health.tests.general', id: 'fuel_health.tests.general',
description: 'Target component: Logging Scenario: 1. Check logrotate cron job on all controller and compute nodes' description: 'Target component: Logging ' +
'Scenario: 1. Check logrotate cron job on all controller and compute nodes'
}, },
{ {
status: null, status: null,
@ -84,7 +86,8 @@ define([
duration: '1sec', duration: '1sec',
message: null, message: null,
id: 'fuel_health.tests.credentials', id: 'fuel_health.tests.credentials',
description: 'Target component: Configuration Scenario: 1. Check user can not ssh on master node with default credentials. ' description: 'Target component: Configuration ' +
'Scenario: 1. Check user can not ssh on master node with default credentials.'
}, },
{ {
status: null, status: null,
@ -95,7 +98,8 @@ define([
duration: '', duration: '',
message: null, message: null,
id: 'fuel_health.tests.credentials_change', id: 'fuel_health.tests.credentials_change',
description: 'Target component: Configuration Scenario: 1. Check if default credentials for OpenStack cluster have changed. ' description: 'Target component: Configuration ' +
'Scenario: 1. Check if default credentials for OpenStack cluster have changed.'
}, },
{ {
status: null, status: null,
@ -117,7 +121,8 @@ define([
duration: '', duration: '',
message: null, message: null,
id: 'fuel_health.tests.credentials_erros', id: 'fuel_health.tests.credentials_erros',
description: 'Target component: Configuration Scenario: 1. Check if default credentials for OpenStack cluster have changed. ' description: 'Target component: Configuration ' +
'Scenario: 1. Check if default credentials for OpenStack cluster have changed. '
}] }]
) )
]); ]);
@ -154,7 +159,8 @@ define([
duration: '20s', duration: '20s',
message: null, message: null,
id: 'fuel_health.tests.check_disk', id: 'fuel_health.tests.check_disk',
description: 'Target component: Nova Scenario: 1. Check outage on controller and compute nodes' description: 'Target component: Nova ' +
'Scenario: 1. Check outage on controller and compute nodes'
}, },
{ {
status: 'stopped', status: 'stopped',
@ -176,7 +182,8 @@ define([
duration: '30s.', duration: '30s.',
message: 'Fast fail with message', message: 'Fast fail with message',
id: 'fuel_health.tests.general', id: 'fuel_health.tests.general',
description: 'Target component: Logging Scenario: 1. Check logrotate cron job on all controller and compute nodes' description: 'Target component: Logging ' +
'Scenario: 1. Check logrotate cron job on all controller and compute nodes'
}, },
{ {
status: 'wait_running', status: 'wait_running',
@ -187,7 +194,8 @@ define([
duration: '1sec', duration: '1sec',
message: 'failure text message', message: 'failure text message',
id: 'fuel_health.tests.credentials', id: 'fuel_health.tests.credentials',
description: 'Target component: Configuration Scenario: 1. Check user can not ssh on master node with default credentials. ' description: 'Target component: Configuration ' +
'Scenario: 1. Check user can not ssh on master node with default credentials.'
}, },
{ {
status: 'running', status: 'running',
@ -198,7 +206,8 @@ define([
duration: '', duration: '',
message: 'Error message', message: 'Error message',
id: 'fuel_health.tests.credentials_change', id: 'fuel_health.tests.credentials_change',
description: ' Target component: Configuration Scenario: 1. Check if default credentials for OpenStack cluster have changed. ' description: ' Target component: Configuration ' +
'Scenario: 1. Check if default credentials for OpenStack cluster have changed.'
}, },
{ {
status: 'wait_running', status: 'wait_running',
@ -220,7 +229,8 @@ define([
duration: '', duration: '',
message: null, message: null,
id: 'fuel_health.tests.credentials_erros', id: 'fuel_health.tests.credentials_erros',
description: 'Target component: Configuration Scenario: 1. Check if error credentials for OpenStack cluster not suit.' description: 'Target component: Configuration ' +
'Scenario: 1. Check if error credentials for OpenStack cluster not suit.'
}] }]
) )
]); ]);
@ -244,7 +254,8 @@ define([
duration: '20s', duration: '20s',
message: null, message: null,
id: 'fuel_health.tests.check_disk', id: 'fuel_health.tests.check_disk',
description: 'Target component: Nova Scenario: 1. Check outage on controller and compute nodes' description: 'Target component: Nova ' +
'Scenario: 1. Check outage on controller and compute nodes'
}, },
{ {
status: 'stopped', status: 'stopped',
@ -266,7 +277,8 @@ define([
duration: '30s.', duration: '30s.',
message: 'Fast fail with message', message: 'Fast fail with message',
id: 'fuel_health.tests.general', id: 'fuel_health.tests.general',
description: 'Target component: Logging Scenario: 1. Check logrotate cron job on all controller and compute nodes' description: 'Target component: Logging ' +
'Scenario: 1. Check logrotate cron job on all controller and compute nodes'
}, },
{ {
status: 'wait_running', status: 'wait_running',
@ -277,7 +289,8 @@ define([
duration: '1sec', duration: '1sec',
message: 'failure text message', message: 'failure text message',
id: 'fuel_health.tests.credentials', id: 'fuel_health.tests.credentials',
description: 'Target component: Configuration Scenario: 1. Check user can not ssh on master node with default credentials. ' description: 'Target component: Configuration ' +
'Scenario: 1. Check user can not ssh on master node with default credentials.'
}, },
{ {
status: 'running', status: 'running',
@ -288,7 +301,9 @@ define([
duration: '', duration: '',
message: 'Error message', message: 'Error message',
id: 'fuel_health.tests.credentials_change', id: 'fuel_health.tests.credentials_change',
description: ' Target component: Configuration Scenario: 1. Check if default credentials for OpenStack cluster have changed. ' description: ' Target component: Configuration ' +
'Scenario: ' +
'1. Check if default credentials for OpenStack cluster have changed.'
}, },
{ {
status: 'wait_running', status: 'wait_running',
@ -310,7 +325,8 @@ define([
duration: '', duration: '',
message: null, message: null,
id: 'fuel_health.tests.credentials_erros', id: 'fuel_health.tests.credentials_erros',
description: 'Target component: Configuration Scenario: 1. Check if error credentials for OpenStack cluster not suit.' description: 'Target component: Configuration ' +
'Scenario: 1. Check if error credentials for OpenStack cluster not suit.'
} }
] ]
}] }]
@ -345,7 +361,8 @@ define([
duration: '20s', duration: '20s',
message: null, message: null,
id: 'fuel_health.tests.check_disk', id: 'fuel_health.tests.check_disk',
description: 'Target component: Nova Scenario: 1. Check outage on controller and compute nodes' description: 'Target component: Nova ' +
'Scenario: 1. Check outage on controller and compute nodes'
}, },
{ {
status: 'stopped', status: 'stopped',
@ -367,7 +384,9 @@ define([
duration: '30s.', duration: '30s.',
message: 'Fast fail with message', message: 'Fast fail with message',
id: 'fuel_health.tests.general', id: 'fuel_health.tests.general',
description: 'Target component: Logging Scenario: 1. Check logrotate cron job on all controller and compute nodes' description: 'Target component: Logging ' +
'Scenario: ' +
'1. Check logrotate cron job on all controller and compute nodes'
}, },
{ {
status: 'skipped', status: 'skipped',
@ -378,7 +397,8 @@ define([
duration: '1sec', duration: '1sec',
message: 'failure text message', message: 'failure text message',
id: 'fuel_health.tests.credentials', id: 'fuel_health.tests.credentials',
description: 'Target component: Configuration Scenario: 1. Check user can not ssh on master node with default credentials. ' description: 'Target component: Configuration ' +
'Scenario: 1. Check user can not ssh on master node with default credentials.'
}, },
{ {
status: 'success', status: 'success',
@ -389,7 +409,9 @@ define([
duration: '', duration: '',
message: 'Error message', message: 'Error message',
id: 'fuel_health.tests.credentials_change', id: 'fuel_health.tests.credentials_change',
description: ' Target component: Configuration Scenario: 1. Check if default credentials for OpenStack cluster have changed. ' description: ' Target component: Configuration ' +
'Scenario: ' +
'1. Check if default credentials for OpenStack cluster have changed. '
}, },
{ {
status: 'failure', status: 'failure',
@ -411,7 +433,8 @@ define([
duration: '', duration: '',
message: null, message: null,
id: 'fuel_health.tests.credentials_erros', id: 'fuel_health.tests.credentials_erros',
description: 'Target component: Configuration Scenario: 1. Check if error credentials for OpenStack cluster not suit.' description: 'Target component: Configuration ' +
'Scenario: 1. Check if error credentials for OpenStack cluster not suit.'
} }
]) ])
]); ]);
@ -435,7 +458,8 @@ define([
duration: '20s', duration: '20s',
message: null, message: null,
id: 'fuel_health.tests.check_disk', id: 'fuel_health.tests.check_disk',
description: 'Target component: Nova Scenario: 1. Check outage on controller and compute nodes' description: 'Target component: Nova ' +
'Scenario: 1. Check outage on controller and compute nodes'
}, },
{ {
status: 'stopped', status: 'stopped',
@ -457,7 +481,9 @@ define([
duration: '30s.', duration: '30s.',
message: 'Fast fail with message', message: 'Fast fail with message',
id: 'fuel_health.tests.general', id: 'fuel_health.tests.general',
description: 'Target component: Logging Scenario: 1. Check logrotate cron job on all controller and compute nodes' description: 'Target component: Logging ' +
'Scenario: ' +
'1. Check logrotate cron job on all controller and compute nodes'
}, },
{ {
status: 'skipped', status: 'skipped',
@ -468,7 +494,9 @@ define([
duration: '1sec', duration: '1sec',
message: 'failure text message', message: 'failure text message',
id: 'fuel_health.tests.credentials', id: 'fuel_health.tests.credentials',
description: 'Target component: Configuration Scenario: 1. Check user can not ssh on master node with default credentials. ' description: 'Target component: Configuration ' +
'Scenario: ' +
'1. Check user can not ssh on master node with default credentials. '
}, },
{ {
status: 'success', status: 'success',
@ -479,7 +507,9 @@ define([
duration: '', duration: '',
message: 'Error message', message: 'Error message',
id: 'fuel_health.tests.credentials_change', id: 'fuel_health.tests.credentials_change',
description: ' Target component: Configuration Scenario: 1. Check if default credentials for OpenStack cluster have changed. ' description: ' Target component: Configuration ' +
'Scenario: ' +
'1. Check if default credentials for OpenStack cluster have changed. '
}, },
{ {
status: 'failure', status: 'failure',
@ -501,7 +531,8 @@ define([
duration: '', duration: '',
message: null, message: null,
id: 'fuel_health.tests.credentials_erros', id: 'fuel_health.tests.credentials_erros',
description: 'Target component: Configuration Scenario: 1. Check if error credentials for OpenStack cluster not suit.' description: 'Target component: Configuration ' +
'Scenario: 1. Check if error credentials for OpenStack cluster not suit.'
} }
] ]
}] }]

View File

@ -133,7 +133,8 @@ define([
return bondElement return bondElement
.findAllByCssSelector('.ifc-name') .findAllByCssSelector('.ifc-name')
.then(function(ifcNamesElements) { .then(function(ifcNamesElements) {
assert.equal(ifcNamesElements.length, ifcsNames.length, 'Unexpected number of interfaces in bond'); assert.equal(ifcNamesElements.length, ifcsNames.length,
'Unexpected number of interfaces in bond');
return ifcNamesElements.forEach( return ifcNamesElements.forEach(
function(ifcNameElement) { function(ifcNameElement) {

View File

@ -35,7 +35,8 @@ define([
}, },
checkTitle: function(expectedTitle) { checkTitle: function(expectedTitle) {
return this.remote return this.remote
.assertElementContainsText(this.modalSelector + ' h4.modal-title', expectedTitle, 'Unexpected modal window title'); .assertElementContainsText(this.modalSelector + ' h4.modal-title', expectedTitle,
'Unexpected modal window title');
}, },
close: function() { close: function() {
var self = this; var self = this;

View File

@ -86,8 +86,10 @@ define([
.then(function() { .then(function() {
return dashboardPage.setClusterName(initialName); return dashboardPage.setClusterName(initialName);
}) })
.assertElementAppears('.rename-block.has-error', 1000, 'Error style for duplicate name is applied') .assertElementAppears('.rename-block.has-error', 1000,
.assertElementTextEquals('.rename-block .text-danger', 'Environment with this name already exists', 'Error style for duplicate name is applied')
.assertElementTextEquals('.rename-block .text-danger',
'Environment with this name already exists',
'Duplicate name error text appears' 'Duplicate name error text appears'
) )
.findByCssSelector(renameInputSelector) .findByCssSelector(renameInputSelector)
@ -129,7 +131,8 @@ define([
return clusterPage.goToTab('Dashboard'); return clusterPage.goToTab('Dashboard');
}) })
.assertElementContainsText('.warnings-block', .assertElementContainsText('.warnings-block',
'Please verify your network settings before deployment', 'Network verification warning is shown') 'Please verify your network settings before deployment',
'Network verification warning is shown')
.then(function() { .then(function() {
return dashboardPage.discardChanges(); return dashboardPage.discardChanges();
}); });
@ -143,7 +146,8 @@ define([
.then(function() { .then(function() {
return clusterPage.goToTab('Dashboard'); return clusterPage.goToTab('Dashboard');
}) })
.assertElementDisabled(dashboardPage.deployButtonSelector, 'No deployment should be possible without controller nodes added') .assertElementDisabled(dashboardPage.deployButtonSelector,
'No deployment should be possible without controller nodes added')
.assertElementExists('div.instruction.invalid', 'Invalid configuration message is shown') .assertElementExists('div.instruction.invalid', 'Invalid configuration message is shown')
.assertElementContainsText('.environment-alerts ul.text-danger li', .assertElementContainsText('.environment-alerts ul.text-danger li',
'At least 1 Controller nodes are required (0 selected currently).', 'At least 1 Controller nodes are required (0 selected currently).',
@ -178,7 +182,8 @@ define([
var operatingSystemNodes = 1; var operatingSystemNodes = 1;
var virtualNodes = 1; var virtualNodes = 1;
var valueSelector = '.statistics-block .cluster-info-value'; var valueSelector = '.statistics-block .cluster-info-value';
var total = controllerNodes + storageCinderNodes + computeNodes + operatingSystemNodes + virtualNodes; var total = controllerNodes + storageCinderNodes + computeNodes + operatingSystemNodes +
virtualNodes;
return this.remote return this.remote
.then(function() { .then(function() {
return common.addNodesToCluster(controllerNodes, ['Controller']); return common.addNodesToCluster(controllerNodes, ['Controller']);
@ -201,11 +206,13 @@ define([
.assertElementTextEquals(valueSelector + '.total', total, .assertElementTextEquals(valueSelector + '.total', total,
'The number of Total nodes in statistics is updated according to added nodes') 'The number of Total nodes in statistics is updated according to added nodes')
.assertElementTextEquals(valueSelector + '.controller', controllerNodes, .assertElementTextEquals(valueSelector + '.controller', controllerNodes,
'The number of controllerNodes nodes in statistics is updated according to added nodes') 'The number of controllerNodes nodes in statistics is updated according to ' +
'added nodes')
.assertElementTextEquals(valueSelector + '.compute', computeNodes, .assertElementTextEquals(valueSelector + '.compute', computeNodes,
'The number of Compute nodes in statistics is updated according to added nodes') 'The number of Compute nodes in statistics is updated according to added nodes')
.assertElementTextEquals(valueSelector + '.base-os', operatingSystemNodes, .assertElementTextEquals(valueSelector + '.base-os', operatingSystemNodes,
'The number of Operating Systems nodes in statistics is updated according to added nodes') 'The number of Operating Systems nodes in statistics is updated according to ' +
'added nodes')
.assertElementTextEquals(valueSelector + '.virt', virtualNodes, .assertElementTextEquals(valueSelector + '.virt', virtualNodes,
'The number of Virtual nodes in statistics is updated according to added nodes') 'The number of Virtual nodes in statistics is updated according to added nodes')
.assertElementTextEquals(valueSelector + '.offline', 1, .assertElementTextEquals(valueSelector + '.offline', 1,
@ -213,7 +220,8 @@ define([
.assertElementTextEquals(valueSelector + '.error', 1, .assertElementTextEquals(valueSelector + '.error', 1,
'The number of Error nodes in statistics is updated according to added nodes') 'The number of Error nodes in statistics is updated according to added nodes')
.assertElementTextEquals(valueSelector + '.pending_addition', total, .assertElementTextEquals(valueSelector + '.pending_addition', total,
'The number of Pending Addition nodes in statistics is updated according to added nodes') 'The number of Pending Addition nodes in statistics is updated according to ' +
'added nodes')
.then(function() { .then(function() {
return dashboardPage.discardChanges(); return dashboardPage.discardChanges();
}); });
@ -232,8 +240,10 @@ define([
.then(function() { .then(function() {
return dashboardPage.startDeployment(); return dashboardPage.startDeployment();
}) })
.assertElementDisappears('.dashboard-block .progress', 60000, 'Progress bar disappears after deployment') .assertElementDisappears('.dashboard-block .progress', 60000,
.assertElementAppears('.dashboard-tab .alert strong', 1000, 'Error message is shown when adding error node') 'Progress bar disappears after deployment')
.assertElementAppears('.dashboard-tab .alert strong', 1000,
'Error message is shown when adding error node')
.assertElementTextEquals('.dashboard-tab .alert strong', 'Error', .assertElementTextEquals('.dashboard-tab .alert strong', 'Error',
'Deployment failed in case of adding offline nodes') 'Deployment failed in case of adding offline nodes')
.then(function() { .then(function() {

View File

@ -58,7 +58,8 @@ define([
}, },
'No deployment button when there are no nodes added': function() { 'No deployment button when there are no nodes added': function() {
return this.remote return this.remote
.assertElementNotExists(dashboardPage.deployButtonSelector, 'No deployment should be possible without nodes added'); .assertElementNotExists(dashboardPage.deployButtonSelector,
'No deployment should be possible without nodes added');
}, },
'Discard changes': function() { 'Discard changes': function() {
return this.remote return this.remote
@ -73,14 +74,16 @@ define([
.then(function() { .then(function() {
return modal.waitToOpen(); return modal.waitToOpen();
}) })
.assertElementContainsText('h4.modal-title', 'Discard Changes', 'Discard Changes confirmation modal expected') .assertElementContainsText('h4.modal-title', 'Discard Changes',
'Discard Changes confirmation modal expected')
.then(function() { .then(function() {
return modal.clickFooterButton('Discard'); return modal.clickFooterButton('Discard');
}) })
.then(function() { .then(function() {
return modal.waitToClose(); return modal.waitToClose();
}) })
.assertElementAppears('.dashboard-block a.btn-add-nodes', 2000, 'All changes discarded, add nodes button gets visible in deploy readiness block'); .assertElementAppears('.dashboard-block a.btn-add-nodes', 2000,
'All changes discarded, add nodes button gets visible in deploy readiness block');
}, },
'Start/stop deployment': function() { 'Start/stop deployment': function() {
this.timeout = 100000; this.timeout = 100000;
@ -96,14 +99,18 @@ define([
return dashboardPage.startDeployment(); return dashboardPage.startDeployment();
}) })
.assertElementAppears('div.deploy-process div.progress', 2000, 'Deployment started') .assertElementAppears('div.deploy-process div.progress', 2000, 'Deployment started')
.assertElementAppears('button.stop-deployment-btn:not(:disabled)', 5000, 'Stop button appears') .assertElementAppears('button.stop-deployment-btn:not(:disabled)', 5000,
'Stop button appears')
.then(function() { .then(function() {
return dashboardPage.stopDeployment(); return dashboardPage.stopDeployment();
}) })
.assertElementDisappears('div.deploy-process div.progress', 20000, 'Deployment stopped') .assertElementDisappears('div.deploy-process div.progress', 20000, 'Deployment stopped')
.assertElementAppears(dashboardPage.deployButtonSelector, 1000, 'Deployment button available') .assertElementAppears(dashboardPage.deployButtonSelector, 1000,
.assertElementContainsText('div.alert-warning strong', 'Success', 'Deployment successfully stopped alert is expected') 'Deployment button available')
.assertElementNotExists('.go-to-healthcheck', 'Healthcheck link is not visible after stopped deploy') .assertElementContainsText('div.alert-warning strong', 'Success',
'Deployment successfully stopped alert is expected')
.assertElementNotExists('.go-to-healthcheck',
'Healthcheck link is not visible after stopped deploy')
// Reset environment button is available // Reset environment button is available
.then(function() { .then(function() {
return clusterPage.resetEnvironment(clusterName); return clusterPage.resetEnvironment(clusterName);
@ -134,7 +141,8 @@ define([
.then(function() { .then(function() {
return dashboardPage.startDeployment(); return dashboardPage.startDeployment();
}) })
.assertElementDisappears('.dashboard-block .progress', 60000, 'Progress bar disappears after deployment') .assertElementDisappears('.dashboard-block .progress', 60000,
'Progress bar disappears after deployment')
.assertElementAppears('.links-block', 5000, 'Deployment completed') .assertElementAppears('.links-block', 5000, 'Deployment completed')
.assertElementExists('.go-to-healthcheck', 'Healthcheck link is visible after deploy') .assertElementExists('.go-to-healthcheck', 'Healthcheck link is visible after deploy')
.findByLinkText('Horizon') .findByLinkText('Horizon')
@ -149,7 +157,8 @@ define([
.then(function(isLocked) { .then(function(isLocked) {
assert.isTrue(isLocked, 'Networks tab should turn locked after deployment'); assert.isTrue(isLocked, 'Networks tab should turn locked after deployment');
}) })
.assertElementEnabled('.add-nodegroup-btn', 'Add Node network group button is enabled after cluster deploy') .assertElementEnabled('.add-nodegroup-btn',
'Add Node network group button is enabled after cluster deploy')
.then(function() { .then(function() {
return clusterPage.isTabLocked('Settings'); return clusterPage.isTabLocked('Settings');
}) })

View File

@ -64,11 +64,16 @@ define([
return clusterPage.goToTab('Health Check'); return clusterPage.goToTab('Health Check');
}) })
.assertElementsAppear('.healthcheck-table', 5000, 'Healthcheck tables are rendered') .assertElementsAppear('.healthcheck-table', 5000, 'Healthcheck tables are rendered')
.assertElementDisabled('.custom-tumbler input[type=checkbox]', 'Test checkbox is disabled') .assertElementDisabled('.custom-tumbler input[type=checkbox]',
.assertElementContainsText('.alert-warning', 'Before you can test an OpenStack environment, you must deploy the OpenStack environment', 'Test checkbox is disabled')
.assertElementContainsText('.alert-warning',
'Before you can test an OpenStack environment, ' +
'you must deploy the OpenStack environment',
'Warning to deploy cluster is shown') 'Warning to deploy cluster is shown')
.assertElementNotExists('.run-tests-btn', 'Run tests button is not shown in new OpenStack environment') .assertElementNotExists('.run-tests-btn',
.assertElementNotExists('.stop-tests-btn', 'Stop tests button is not shown in new OpenStack environment'); 'Run tests button is not shown in new OpenStack environment')
.assertElementNotExists('.stop-tests-btn',
'Stop tests button is not shown in new OpenStack environment');
}, },
//@TODO (morale): imitate tests stop //@TODO (morale): imitate tests stop
'Check Healthcheck tab manipulations after deploy': function() { 'Check Healthcheck tab manipulations after deploy': function() {
@ -86,9 +91,11 @@ define([
.then(function() { .then(function() {
return clusterPage.goToTab('Health Check'); return clusterPage.goToTab('Health Check');
}) })
.assertElementEnabled('.custom-tumbler input[type=checkbox]', 'Test checkbox is enabled after deploy') .assertElementEnabled('.custom-tumbler input[type=checkbox]',
'Test checkbox is enabled after deploy')
// 'run tests' button interactions // 'run tests' button interactions
.assertElementDisabled('.run-tests-btn', 'Run tests button is disabled if no tests checked') .assertElementDisabled('.run-tests-btn',
'Run tests button is disabled if no tests checked')
.assertElementNotExists('.stop-tests-btn', .assertElementNotExists('.stop-tests-btn',
'Stop tests button is not visible if no tests checked') 'Stop tests button is not visible if no tests checked')
.assertElementExists('.toggle-credentials', 'Toggle credentials button is visible') .assertElementExists('.toggle-credentials', 'Toggle credentials button is visible')
@ -120,7 +127,8 @@ define([
.assertElementNotExists('.run-tests-btn', .assertElementNotExists('.run-tests-btn',
'Run tests button is not shown if tests are running') 'Run tests button is not shown if tests are running')
.assertElementEnabled('.stop-tests-btn', 'Stop tests button is enabled during tests run') .assertElementEnabled('.stop-tests-btn', 'Stop tests button is enabled during tests run')
.assertElementsAppear('.glyphicon-refresh.animate-spin', 1000, 'Running status is reflected') .assertElementsAppear('.glyphicon-refresh.animate-spin', 1000,
'Running status is reflected')
.assertElementsAppear('.glyphicon-time', 1000, 'Waiting to run status is reflected') .assertElementsAppear('.glyphicon-time', 1000, 'Waiting to run status is reflected')
.assertElementsAppear('.healthcheck-status-skipped', 1000, 'Skipped status is reflected') .assertElementsAppear('.healthcheck-status-skipped', 1000, 'Skipped status is reflected')
.assertElementsAppear('.healthcheck-status-stopped', 1000, 'Stopped status is reflected'); .assertElementsAppear('.healthcheck-status-stopped', 1000, 'Stopped status is reflected');
@ -140,9 +148,11 @@ define([
.then(function() { .then(function() {
return clusterPage.goToTab('Health Check'); return clusterPage.goToTab('Health Check');
}) })
.assertElementNotExists('.stop-tests-btn', 'Stop tests button is not shown if tests are finished') .assertElementNotExists('.stop-tests-btn',
'Stop tests button is not shown if tests are finished')
.assertElementsAppear('.glyphicon-ok', 1000, 'Success status is reflected') .assertElementsAppear('.glyphicon-ok', 1000, 'Success status is reflected')
.assertElementsAppear('.glyphicon-remove', 1000, 'Error and Failure statuses are reflected'); .assertElementsAppear('.glyphicon-remove', 1000,
'Error and Failure statuses are reflected');
} }
}; };
}); });

View File

@ -50,13 +50,16 @@ define([
'"Show" button availability and logs displaying': function() { '"Show" button availability and logs displaying': function() {
var showLogsButtonSelector = '.sticker button'; var showLogsButtonSelector = '.sticker button';
return this.remote return this.remote
.assertElementsExist('.sticker select[name=source] > option', 'Check if "Source" dropdown exist') .assertElementsExist('.sticker select[name=source] > option',
.assertElementDisabled(showLogsButtonSelector, '"Show" button is disabled until source change') 'Check if "Source" dropdown exist')
.assertElementDisabled(showLogsButtonSelector,
'"Show" button is disabled until source change')
// Change the selected value for the "Source" dropdown to Rest API // Change the selected value for the "Source" dropdown to Rest API
.clickByCssSelector('.sticker select[name=source] option[value=api]') .clickByCssSelector('.sticker select[name=source] option[value=api]')
// Change the selected value for the "Level" dropdown to DEBUG // Change the selected value for the "Level" dropdown to DEBUG
.clickByCssSelector('.sticker select[name=level] option[value=DEBUG]') .clickByCssSelector('.sticker select[name=level] option[value=DEBUG]')
.assertElementEnabled(showLogsButtonSelector, '"Show" button is enabled after source change') .assertElementEnabled(showLogsButtonSelector,
'"Show" button is enabled after source change')
.execute(function() { .execute(function() {
window.fakeServer = sinon.fakeServer.create(); window.fakeServer = sinon.fakeServer.create();
window.fakeServer.autoRespond = true; window.fakeServer.autoRespond = true;
@ -70,7 +73,8 @@ define([
]); ]);
}) })
.clickByCssSelector(showLogsButtonSelector) .clickByCssSelector(showLogsButtonSelector)
.assertElementDisappears('.logs-tab div.progress', 5000, 'Wait till Progress bar disappears') .assertElementDisappears('.logs-tab div.progress', 5000,
'Wait till Progress bar disappears')
.assertElementsAppear('.log-entries > tbody > tr', 5000, 'Log entries are shown') .assertElementsAppear('.log-entries > tbody > tr', 5000, 'Log entries are shown')
.execute(function() { .execute(function() {
window.fakeServer.restore(); window.fakeServer.restore();

View File

@ -100,10 +100,13 @@ define([
return this.remote return this.remote
.clickByCssSelector('.subtab-link-default') .clickByCssSelector('.subtab-link-default')
.assertElementAppears('.storage', 2000, 'Storage network is shown') .assertElementAppears('.storage', 2000, 'Storage network is shown')
.assertElementSelected('.storage .cidr input[type=checkbox]', 'Storage network has "cidr" notation by default') .assertElementSelected('.storage .cidr input[type=checkbox]',
.assertElementNotExists('.storage .ip_ranges input[type=text]:not(:disabled)', 'It is impossible to configure IP ranges for network with "cidr" notation') 'Storage network has "cidr" notation by default')
.assertElementNotExists('.storage .ip_ranges input[type=text]:not(:disabled)',
'It is impossible to configure IP ranges for network with "cidr" notation')
.clickByCssSelector('.storage .cidr input[type=checkbox]') .clickByCssSelector('.storage .cidr input[type=checkbox]')
.assertElementNotExists('.storage .ip_ranges input[type=text]:disabled', 'Network notation was changed to "ip_ranges"'); .assertElementNotExists('.storage .ip_ranges input[type=text]:disabled',
'Network notation was changed to "ip_ranges"');
}, },
'Testing cluster networks: save network changes': function() { 'Testing cluster networks: save network changes': function() {
var cidrElementSelector = '.storage .cidr input[type=text]'; var cidrElementSelector = '.storage .cidr input[type=text]';
@ -112,12 +115,14 @@ define([
.clickByCssSelector(applyButtonSelector) .clickByCssSelector(applyButtonSelector)
.assertElementsAppear('input:not(:disabled)', 2000, 'Inputs are not disabled') .assertElementsAppear('input:not(:disabled)', 2000, 'Inputs are not disabled')
.assertElementNotExists('.alert-error', 'Correct settings were saved successfully') .assertElementNotExists('.alert-error', 'Correct settings were saved successfully')
.assertElementDisabled(applyButtonSelector, 'Save changes button is disabled again after successful settings saving'); .assertElementDisabled(applyButtonSelector,
'Save changes button is disabled again after successful settings saving');
}, },
'Testing cluster networks: verification': function() { 'Testing cluster networks: verification': function() {
return this.remote return this.remote
.clickByCssSelector('.subtab-link-network_verification') .clickByCssSelector('.subtab-link-network_verification')
.assertElementDisabled('.verify-networks-btn', 'Verification button is disabled in case of no nodes') .assertElementDisabled('.verify-networks-btn',
'Verification button is disabled in case of no nodes')
.assertElementTextEquals('.alert-warning', .assertElementTextEquals('.alert-warning',
'At least two online nodes are required to verify environment network configuration', 'At least two online nodes are required to verify environment network configuration',
'Not enough nodes warning is shown') 'Not enough nodes warning is shown')
@ -133,17 +138,20 @@ define([
.clickByCssSelector('.subtab-link-network_verification') .clickByCssSelector('.subtab-link-network_verification')
.clickByCssSelector('.verify-networks-btn') .clickByCssSelector('.verify-networks-btn')
.assertElementAppears('.alert-danger.network-alert', 4000, 'Verification error is shown') .assertElementAppears('.alert-danger.network-alert', 4000, 'Verification error is shown')
.assertElementAppears('.alert-danger.network-alert', 'Address intersection', 'Verification result is shown in case of address intersection') .assertElementAppears('.alert-danger.network-alert', 'Address intersection',
'Verification result is shown in case of address intersection')
// Testing cluster networks: verification task deletion // Testing cluster networks: verification task deletion
.clickByCssSelector('.subtab-link-default') .clickByCssSelector('.subtab-link-default')
.setInputValue('.public input[name=gateway]', '172.16.0.5') .setInputValue('.public input[name=gateway]', '172.16.0.5')
.clickByCssSelector('.subtab-link-network_verification') .clickByCssSelector('.subtab-link-network_verification')
.assertElementNotExists('.page-control-box .alert', 'Verification task was removed after settings has been changed') .assertElementNotExists('.page-control-box .alert',
'Verification task was removed after settings has been changed')
.clickByCssSelector('.btn-revert-changes') .clickByCssSelector('.btn-revert-changes')
.clickByCssSelector('.verify-networks-btn') .clickByCssSelector('.verify-networks-btn')
.waitForElementDeletion('.animation-box .success.connect-1', 6000) .waitForElementDeletion('.animation-box .success.connect-1', 6000)
.assertElementAppears('.alert-success', 6000, 'Success verification message appears') .assertElementAppears('.alert-success', 6000, 'Success verification message appears')
.assertElementContainsText('.alert-success', 'Verification succeeded', 'Success verification message appears with proper text') .assertElementContainsText('.alert-success', 'Verification succeeded',
'Success verification message appears with proper text')
.clickByCssSelector('.btn-revert-changes') .clickByCssSelector('.btn-revert-changes')
.then(function() { .then(function() {
return clusterPage.goToTab('Dashboard'); return clusterPage.goToTab('Dashboard');
@ -185,7 +193,8 @@ define([
var rangeSelector = '.public .ip_ranges '; var rangeSelector = '.public .ip_ranges ';
return this.remote return this.remote
.clickByCssSelector(rangeSelector + '.ip-ranges-add') .clickByCssSelector(rangeSelector + '.ip-ranges-add')
.assertElementsExist(rangeSelector + '.ip-ranges-delete', 2, 'Remove ranges controls appear') .assertElementsExist(rangeSelector + '.ip-ranges-delete', 2,
'Remove ranges controls appear')
.clickByCssSelector(applyButtonSelector) .clickByCssSelector(applyButtonSelector)
.assertElementsExist(rangeSelector + '.range-row', .assertElementsExist(rangeSelector + '.range-row',
'Empty range row is removed after saving changes') 'Empty range row is removed after saving changes')
@ -216,7 +225,8 @@ define([
.then(function() { .then(function() {
return clusterPage.goToTab('Networks'); return clusterPage.goToTab('Networks');
}) })
.assertElementNotExists('.private', 'Private Network is not visible for vlan segmentation type') .assertElementNotExists('.private',
'Private Network is not visible for vlan segmentation type')
.assertElementTextEquals('.segmentation-type', '(Neutron with VLAN segmentation)', .assertElementTextEquals('.segmentation-type', '(Neutron with VLAN segmentation)',
'Segmentation type is correct for VLAN segmentation'); 'Segmentation type is correct for VLAN segmentation');
}, },
@ -277,7 +287,8 @@ define([
.then(function() { .then(function() {
return modal.waitToOpen(); return modal.waitToOpen();
}) })
.assertElementContainsText('h4.modal-title', 'Add New Node Network Group', 'Add New Node Network Group modal expected') .assertElementContainsText('h4.modal-title', 'Add New Node Network Group',
'Add New Node Network Group modal expected')
.setInputValue('[name=node-network-group-name]', 'Node_Network_Group_1') .setInputValue('[name=node-network-group-name]', 'Node_Network_Group_1')
.then(function() { .then(function() {
return modal.clickFooterButton('Add Group'); return modal.clickFooterButton('Add Group');
@ -285,9 +296,11 @@ define([
.then(function() { .then(function() {
return modal.waitToClose(); return modal.waitToClose();
}) })
.assertElementAppears('.node-network-groups-list', 2000, 'Node network groups title appears') .assertElementAppears('.node-network-groups-list', 2000,
'Node network groups title appears')
.assertElementDisplayed('.subtab-link-Node_Network_Group_1', 'New subtab is shown') .assertElementDisplayed('.subtab-link-Node_Network_Group_1', 'New subtab is shown')
.assertElementTextEquals('.network-group-name .btn-link', 'Node_Network_Group_1', 'New Node Network group title is shown'); .assertElementTextEquals('.network-group-name .btn-link', 'Node_Network_Group_1',
'New Node Network group title is shown');
}, },
'Verification is disabled for multirack': function() { 'Verification is disabled for multirack': function() {
return this.remote return this.remote
@ -306,26 +319,30 @@ define([
// Enter // Enter
.type('\uE007') .type('\uE007')
.end() .end()
.assertElementDisplayed('.subtab-link-Node_Network_Group_2', 'Node network group was successfully renamed'); .assertElementDisplayed('.subtab-link-Node_Network_Group_2',
'Node network group was successfully renamed');
}, },
'Node network group deletion': function() { 'Node network group deletion': function() {
return this.remote return this.remote
.clickByCssSelector('.subtab-link-default') .clickByCssSelector('.subtab-link-default')
.assertElementNotExists('.glyphicon-remove', 'It is not possible to delete default node network group') .assertElementNotExists('.glyphicon-remove',
'It is not possible to delete default node network group')
.clickByCssSelector('.subtab-link-Node_Network_Group_2') .clickByCssSelector('.subtab-link-Node_Network_Group_2')
.assertElementAppears('.glyphicon-remove', 1000, 'Remove icon is shown') .assertElementAppears('.glyphicon-remove', 1000, 'Remove icon is shown')
.clickByCssSelector('.glyphicon-remove') .clickByCssSelector('.glyphicon-remove')
.then(function() { .then(function() {
return modal.waitToOpen(); return modal.waitToOpen();
}) })
.assertElementContainsText('h4.modal-title', 'Remove Node Network Group', 'Remove Node Network Group modal expected') .assertElementContainsText('h4.modal-title', 'Remove Node Network Group',
'Remove Node Network Group modal expected')
.then(function() { .then(function() {
return modal.clickFooterButton('Delete'); return modal.clickFooterButton('Delete');
}) })
.then(function() { .then(function() {
return modal.waitToClose(); return modal.waitToClose();
}) })
.assertElementDisappears('.subtab-link-Node_Network_Group_2', 2000, 'Node network groups title disappears'); .assertElementDisappears('.subtab-link-Node_Network_Group_2', 2000,
'Node network groups title disappears');
}, },
'Node network group renaming in deployed environment': function() { 'Node network group renaming in deployed environment': function() {
this.timeout = 100000; this.timeout = 100000;
@ -344,9 +361,11 @@ define([
return clusterPage.goToTab('Networks'); return clusterPage.goToTab('Networks');
}) })
.clickByCssSelector('.subtab-link-default') .clickByCssSelector('.subtab-link-default')
.assertElementNotExists('.glyphicon-pencil', 'Renaming of a node network group is fobidden in deployed environment') .assertElementNotExists('.glyphicon-pencil',
'Renaming of a node network group is fobidden in deployed environment')
.clickByCssSelector('.network-group-name .name') .clickByCssSelector('.network-group-name .name')
.assertElementNotExists('.network-group-name input[type=text]', 'Renaming is not started on a node network group name click'); .assertElementNotExists('.network-group-name input[type=text]',
'Renaming is not started on a node network group name click');
} }
}; };
}); });

View File

@ -49,24 +49,31 @@ define([
}, },
'Add Cluster Nodes': function() { 'Add Cluster Nodes': function() {
return this.remote return this.remote
.assertElementExists('.node-list .alert-warning', 'Node list shows warning if there are no nodes in environment') .assertElementExists('.node-list .alert-warning',
'Node list shows warning if there are no nodes in environment')
.clickByCssSelector('.btn-add-nodes') .clickByCssSelector('.btn-add-nodes')
.assertElementsAppear('.node', 2000, 'Unallocated nodes loaded') .assertElementsAppear('.node', 2000, 'Unallocated nodes loaded')
.assertElementDisabled(applyButtonSelector, 'Apply button is disabled until both roles and nodes chosen') .assertElementDisabled(applyButtonSelector,
.assertElementDisabled('.role-panel [type=checkbox][name=mongo]', 'Unavailable role has locked checkbox') 'Apply button is disabled until both roles and nodes chosen')
.assertElementExists('.role-panel .mongo i.tooltip-icon', 'Unavailable role has warning tooltip') .assertElementDisabled('.role-panel [type=checkbox][name=mongo]',
'Unavailable role has locked checkbox')
.assertElementExists('.role-panel .mongo i.tooltip-icon',
'Unavailable role has warning tooltip')
.then(function() { .then(function() {
return clusterPage.checkNodeRoles(['Controller', 'Storage - Cinder']); return clusterPage.checkNodeRoles(['Controller', 'Storage - Cinder']);
}) })
.assertElementDisabled('.role-panel [type=checkbox][name=compute]', 'Compute role can not be added together with selected roles') .assertElementDisabled('.role-panel [type=checkbox][name=compute]',
.assertElementDisabled(applyButtonSelector, 'Apply button is disabled until both roles and nodes chosen') 'Compute role can not be added together with selected roles')
.assertElementDisabled(applyButtonSelector,
'Apply button is disabled until both roles and nodes chosen')
.then(function() { .then(function() {
return clusterPage.checkNodes(nodesAmount); return clusterPage.checkNodes(nodesAmount);
}) })
.clickByCssSelector(applyButtonSelector) .clickByCssSelector(applyButtonSelector)
.waitForElementDeletion(applyButtonSelector, 2000) .waitForElementDeletion(applyButtonSelector, 2000)
.assertElementAppears('.nodes-group', 2000, 'Cluster node list loaded') .assertElementAppears('.nodes-group', 2000, 'Cluster node list loaded')
.assertElementsExist('.node-list .node', nodesAmount, nodesAmount + ' nodes were successfully added to the cluster') .assertElementsExist('.node-list .node', nodesAmount, nodesAmount +
' nodes were successfully added to the cluster')
.assertElementExists('.nodes-group', 'One node group is present'); .assertElementExists('.nodes-group', 'One node group is present');
}, },
'Edit cluster node roles': function() { 'Edit cluster node roles': function() {
@ -78,17 +85,22 @@ define([
// select all nodes // select all nodes
.clickByCssSelector('.select-all label') .clickByCssSelector('.select-all label')
.clickByCssSelector('.btn-edit-roles') .clickByCssSelector('.btn-edit-roles')
.assertElementDisappears('.btn-edit-roles', 2000, 'Cluster nodes screen unmounted') .assertElementDisappears('.btn-edit-roles', 2000,
.assertElementNotExists('.node-box [type=checkbox]:not(:disabled)', 'Node selection is locked on Edit Roles screen') 'Cluster nodes screen unmounted')
.assertElementNotExists('[name=select-all]:not(:disabled)', 'Select All checkboxes are locked on Edit Roles screen') .assertElementNotExists('.node-box [type=checkbox]:not(:disabled)',
.assertElementExists('.role-panel [type=checkbox][name=controller]:indeterminate', 'Controller role checkbox has indeterminate state') 'Node selection is locked on Edit Roles screen')
.assertElementNotExists('[name=select-all]:not(:disabled)',
'Select All checkboxes are locked on Edit Roles screen')
.assertElementExists('.role-panel [type=checkbox][name=controller]:indeterminate',
'Controller role checkbox has indeterminate state')
.then(function() { .then(function() {
// uncheck Cinder role // uncheck Cinder role
return clusterPage.checkNodeRoles(['Storage - Cinder', 'Storage - Cinder']); return clusterPage.checkNodeRoles(['Storage - Cinder', 'Storage - Cinder']);
}) })
.clickByCssSelector(applyButtonSelector) .clickByCssSelector(applyButtonSelector)
.assertElementDisappears('.btn-apply', 2000, 'Role editing screen unmounted') .assertElementDisappears('.btn-apply', 2000, 'Role editing screen unmounted')
.assertElementsExist('.node-list .node', nodesAmount, 'One node was removed from cluster after editing roles'); .assertElementsExist('.node-list .node', nodesAmount,
'One node was removed from cluster after editing roles');
}, },
'Remove Cluster': function() { 'Remove Cluster': function() {
return this.remote return this.remote

View File

@ -56,7 +56,8 @@ define([
}, },
'Settings tab is rendered correctly': function() { 'Settings tab is rendered correctly': function() {
return this.remote return this.remote
.assertElementNotExists('.nav .subtab-link-network', 'Subtab for Network settings is not presented in navigation') .assertElementNotExists('.nav .subtab-link-network',
'Subtab for Network settings is not presented in navigation')
.assertElementEnabled('.btn-load-defaults', 'Load defaults button is enabled') .assertElementEnabled('.btn-load-defaults', 'Load defaults button is enabled')
.assertElementDisabled('.btn-revert-changes', 'Cancel Changes button is disabled') .assertElementDisabled('.btn-revert-changes', 'Cancel Changes button is disabled')
.assertElementDisabled('.btn-apply-changes', 'Save Settings button is disabled'); .assertElementDisabled('.btn-apply-changes', 'Save Settings button is disabled');
@ -65,10 +66,12 @@ define([
return this.remote return this.remote
// introduce change // introduce change
.clickByCssSelector('input[type=checkbox]') .clickByCssSelector('input[type=checkbox]')
.assertElementAppears('.btn-apply-changes:not(:disabled)', 200, 'Save Settings button is enabled if there are changes') .assertElementAppears('.btn-apply-changes:not(:disabled)', 200,
'Save Settings button is enabled if there are changes')
// reset the change // reset the change
.clickByCssSelector('input[type=checkbox]') .clickByCssSelector('input[type=checkbox]')
.assertElementAppears('.btn-apply-changes:disabled', 200, 'Save Settings button is disabled if there are no changes'); .assertElementAppears('.btn-apply-changes:disabled', 200,
'Save Settings button is disabled if there are no changes');
}, },
'Check Cancel Changes button': function() { 'Check Cancel Changes button': function() {
return this.remote return this.remote
@ -86,7 +89,8 @@ define([
}) })
// reset changes // reset changes
.clickByCssSelector('.btn-revert-changes') .clickByCssSelector('.btn-revert-changes')
.assertElementDisabled('.btn-apply-changes', 'Save Settings button is disabled after changes were cancelled'); .assertElementDisabled('.btn-apply-changes',
'Save Settings button is disabled after changes were cancelled');
}, },
'Check changes saving': function() { 'Check changes saving': function() {
return this.remote return this.remote
@ -97,7 +101,8 @@ define([
.then(function() { .then(function() {
return settingsPage.waitForRequestCompleted(); return settingsPage.waitForRequestCompleted();
}) })
.assertElementDisabled('.btn-revert-changes', 'Cancel Changes button is disabled after changes were saved successfully'); .assertElementDisabled('.btn-revert-changes',
'Cancel Changes button is disabled after changes were saved successfully');
}, },
'Check loading of defaults': function() { 'Check loading of defaults': function() {
return this.remote return this.remote
@ -106,12 +111,15 @@ define([
.then(function() { .then(function() {
return settingsPage.waitForRequestCompleted(); return settingsPage.waitForRequestCompleted();
}) })
.assertElementEnabled('.btn-apply-changes', 'Save Settings button is enabled after defaults were loaded') .assertElementEnabled('.btn-apply-changes',
.assertElementEnabled('.btn-revert-changes', 'Cancel Changes button is enabled after defaults were loaded') 'Save Settings button is enabled after defaults were loaded')
.assertElementEnabled('.btn-revert-changes',
'Cancel Changes button is enabled after defaults were loaded')
// revert the change // revert the change
.clickByCssSelector('.btn-revert-changes'); .clickByCssSelector('.btn-revert-changes');
}, },
'The choice of subgroup is preserved when user navigates through the cluster tabs': function() { 'The choice of subgroup is preserved when user navigates through the cluster tabs':
function() {
return this.remote return this.remote
.clickLinkByText('Logging') .clickLinkByText('Logging')
.then(function() { .then(function() {
@ -120,20 +128,26 @@ define([
.then(function() { .then(function() {
return clusterPage.goToTab('Settings'); return clusterPage.goToTab('Settings');
}) })
.assertElementExists('.nav-pills li.active a.subtab-link-logging', 'The choice of subgroup is preserved when user navigates through the cluster tabs'); .assertElementExists('.nav-pills li.active a.subtab-link-logging',
'The choice of subgroup is preserved when user navigates through the cluster tabs');
}, },
'The page reacts on invalid input': function() { 'The page reacts on invalid input': function() {
return this.remote return this.remote
.clickLinkByText('General') .clickLinkByText('General')
// "nova" is forbidden username // "nova" is forbidden username
.setInputValue('[type=text][name=user]', 'nova') .setInputValue('[type=text][name=user]', 'nova')
.assertElementAppears('.setting-section .form-group.has-error', 200, 'Invalid field marked as error') .assertElementAppears('.setting-section .form-group.has-error', 200,
.assertElementExists('.settings-tab .nav-pills > li.active i.glyphicon-danger-sign', 'Subgroup with invalid field marked as invalid') 'Invalid field marked as error')
.assertElementDisabled('.btn-apply-changes', 'Save Settings button is disabled in case of validation error') .assertElementExists('.settings-tab .nav-pills > li.active i.glyphicon-danger-sign',
'Subgroup with invalid field marked as invalid')
.assertElementDisabled('.btn-apply-changes',
'Save Settings button is disabled in case of validation error')
// revert the change // revert the change
.clickByCssSelector('.btn-revert-changes') .clickByCssSelector('.btn-revert-changes')
.assertElementNotExists('.setting-section .form-group.has-error', 'Validation error is cleared after resetting changes') .assertElementNotExists('.setting-section .form-group.has-error',
.assertElementNotExists('.settings-tab .nav-pills > li.active i.glyphicon-danger-sign', 'Subgroup menu has default layout after resetting changes'); 'Validation error is cleared after resetting changes')
.assertElementNotExists('.settings-tab .nav-pills > li.active i.glyphicon-danger-sign',
'Subgroup menu has default layout after resetting changes');
}, },
'Test repositories custom control': function() { 'Test repositories custom control': function() {
var repoAmount; var repoAmount;
@ -146,18 +160,22 @@ define([
repoAmount = elements.length; repoAmount = elements.length;
}) })
.end() .end()
.assertElementNotExists('.repos .form-inline:nth-of-type(1) .btn-link', 'The first repo can not be deleted') .assertElementNotExists('.repos .form-inline:nth-of-type(1) .btn-link',
'The first repo can not be deleted')
// delete some repo // delete some repo
.clickByCssSelector('.repos .form-inline .btn-link') .clickByCssSelector('.repos .form-inline .btn-link')
.then(function() { .then(function() {
return self.remote.assertElementsExist('.repos .form-inline', repoAmount - 1, 'Repo was deleted'); return self.remote.assertElementsExist('.repos .form-inline', repoAmount - 1,
'Repo was deleted');
}) })
// add new repo // add new repo
.clickByCssSelector('.btn-add-repo') .clickByCssSelector('.btn-add-repo')
.then(function() { .then(function() {
return self.remote.assertElementsExist('.repos .form-inline', repoAmount, 'New repo placeholder was added'); return self.remote.assertElementsExist('.repos .form-inline', repoAmount,
'New repo placeholder was added');
}) })
.assertElementExists('.repos .form-inline .repo-name.has-error', 'Empty repo marked as invalid') .assertElementExists('.repos .form-inline .repo-name.has-error',
'Empty repo marked as invalid')
// revert the change // revert the change
.clickByCssSelector('.btn-revert-changes'); .clickByCssSelector('.btn-revert-changes');
} }

View File

@ -69,18 +69,25 @@ define([
'Check action buttons': function() { 'Check action buttons': function() {
return this.remote return this.remote
.assertElementNotExists('.node .btn-discard', 'No discard changes button on a node') .assertElementNotExists('.node .btn-discard', 'No discard changes button on a node')
.assertElementExists('.node.offline .node-remove-button', 'Removing of offline nodes is available on the page') .assertElementExists('.node.offline .node-remove-button',
'Removing of offline nodes is available on the page')
.clickByCssSelector('.node.pending_addition > label') .clickByCssSelector('.node.pending_addition > label')
.assertElementNotExists('.control-buttons-box .btn', 'No management buttons for selected node') .assertElementNotExists('.control-buttons-box .btn',
.assertElementExists('.node-list-management-buttons .btn-labels:not(:disabled)', 'Nodes can be labelled on the page') 'No management buttons for selected node')
.assertElementsExist('.node.pending_addition .btn-view-logs', 4, 'View logs button is presented for assigned to any environment nodes') .assertElementExists('.node-list-management-buttons .btn-labels:not(:disabled)',
.assertElementNotExists('.node:not(.pending_addition) .btn-view-logs', 'View logs button is not presented for unallocated nodes') 'Nodes can be labelled on the page')
.assertElementsExist('.node.pending_addition .btn-view-logs', 4,
'View logs button is presented for assigned to any environment nodes')
.assertElementNotExists('.node:not(.pending_addition) .btn-view-logs',
'View logs button is not presented for unallocated nodes')
.clickByCssSelector('.node .node-settings') .clickByCssSelector('.node .node-settings')
.then(function() { .then(function() {
return modal.waitToOpen(); return modal.waitToOpen();
}) })
.assertElementNotExists('.btn-edit-disks', 'No disks configuration buttons in node pop-up') .assertElementNotExists('.btn-edit-disks',
.assertElementNotExists('.btn-edit-networks', 'No interfaces configuration buttons in node pop-up') 'No disks configuration buttons in node pop-up')
.assertElementNotExists('.btn-edit-networks',
'No interfaces configuration buttons in node pop-up')
.then(function() { .then(function() {
return modal.close(); return modal.close();
}) })
@ -88,7 +95,8 @@ define([
.then(function() { .then(function() {
return node.openCompactNodeExtendedView(); return node.openCompactNodeExtendedView();
}) })
.assertElementNotExists('.node-popover .node-buttons .btn:not(.btn-view-logs)', 'No action buttons in node extended view in compact mode'); .assertElementNotExists('.node-popover .node-buttons .btn:not(.btn-view-logs)',
'No action buttons in node extended view in compact mode');
} }
}; };
}); });

View File

@ -42,14 +42,16 @@ define([
.then(function() { .then(function() {
return loginPage.login('login', '*****'); return loginPage.login('login', '*****');
}) })
.assertElementAppears('div.login-error', 1000, 'Error message is expected to get displayed'); .assertElementAppears('div.login-error', 1000,
'Error message is expected to get displayed');
}, },
'Login with proper credentials': function() { 'Login with proper credentials': function() {
return this.remote return this.remote
.then(function() { .then(function() {
return loginPage.login(); return loginPage.login();
}) })
.assertElementDisappears('.login-btn', 2000, 'Login button disappears after successful login'); .assertElementDisappears('.login-btn', 2000,
'Login button disappears after successful login');
} }
}; };
}); });

View File

@ -88,9 +88,13 @@ define([
}); });
}) })
.end() .end()
.assertElementExists(sdaDisk + ' .disk-visual [data-volume=image] .close-btn', 'Button Close for Image Storage volume is present') .assertElementExists(sdaDisk + ' .disk-visual [data-volume=image] .close-btn',
.assertElementNotExists(sdaDisk + ' .disk-visual [data-volume=os] .close-btn', 'Button Close for Base system volume is not present') 'Button Close for Image Storage volume is present')
.assertElementExists(sdaDisk + ' .disk-details [data-volume=os] .volume-group-notice.text-info', 'Notice about "Minimal size" is present'); .assertElementNotExists(sdaDisk + ' .disk-visual [data-volume=os] .close-btn',
'Button Close for Base system volume is not present')
.assertElementExists(sdaDisk +
' .disk-details [data-volume=os] .volume-group-notice.text-info',
'Notice about "Minimal size" is present');
}, },
'Testing Apply and Load Defaults buttons behaviour': function() { 'Testing Apply and Load Defaults buttons behaviour': function() {
return this.remote return this.remote
@ -101,7 +105,8 @@ define([
.assertElementDisappears('.btn-load-defaults:disabled', 2000, 'Wait for changes applied') .assertElementDisappears('.btn-load-defaults:disabled', 2000, 'Wait for changes applied')
.clickByCssSelector(loadDefaultsButtonSelector) .clickByCssSelector(loadDefaultsButtonSelector)
.assertElementDisappears('.btn-load-defaults:disabled', 2000, 'Wait for defaults loaded') .assertElementDisappears('.btn-load-defaults:disabled', 2000, 'Wait for defaults loaded')
.assertElementPropertyEquals(sdaDisk + ' input[type=number][name=image]', 'value', initialImageSize, 'Image Storage size restored to default') .assertElementPropertyEquals(sdaDisk + ' input[type=number][name=image]', 'value',
initialImageSize, 'Image Storage size restored to default')
.assertElementEnabled(cancelButtonSelector, 'Cancel button is enabled') .assertElementEnabled(cancelButtonSelector, 'Cancel button is enabled')
.assertElementEnabled(applyButtonSelector, 'Apply button is enabled') .assertElementEnabled(applyButtonSelector, 'Apply button is enabled')
.clickByCssSelector(applyButtonSelector); .clickByCssSelector(applyButtonSelector);
@ -113,7 +118,8 @@ define([
.then(function(element) { .then(function(element) {
return element.getSize() return element.getSize()
.then(function(sizes) { .then(function(sizes) {
assert.isTrue(sizes.width > 0, 'Expected positive width for Image Storage visual'); assert.isTrue(sizes.width > 0,
'Expected positive width for Image Storage visual');
}); });
}) })
.end() .end()
@ -128,18 +134,21 @@ define([
}); });
}) })
.end() .end()
.assertElementPropertyEquals(sdaDisk + ' input[type=number][name=image]', 'value', 0, 'Image Storage volume was removed successfully') .assertElementPropertyEquals(sdaDisk + ' input[type=number][name=image]', 'value', 0,
'Image Storage volume was removed successfully')
.findByCssSelector(sdaDisk + ' .disk-visual [data-volume=unallocated]') .findByCssSelector(sdaDisk + ' .disk-visual [data-volume=unallocated]')
// check that there is unallocated space after Image Storage removal // check that there is unallocated space after Image Storage removal
.then(function(element) { .then(function(element) {
return element.getSize() return element.getSize()
.then(function(sizes) { .then(function(sizes) {
assert.isTrue(sizes.width > 0, 'There is unallocated space after Image Storage removal'); assert.isTrue(sizes.width > 0,
'There is unallocated space after Image Storage removal');
}); });
}) })
.end() .end()
.clickByCssSelector(cancelButtonSelector) .clickByCssSelector(cancelButtonSelector)
.assertElementPropertyEquals(sdaDisk + ' input[type=number][name=image]', 'value', initialImageSize, 'Image Storage volume control contains correct value') .assertElementPropertyEquals(sdaDisk + ' input[type=number][name=image]', 'value',
initialImageSize, 'Image Storage volume control contains correct value')
.assertElementDisabled(applyButtonSelector, 'Apply button is disabled'); .assertElementDisabled(applyButtonSelector, 'Apply button is disabled');
}, },
'Test volume size validation': function() { 'Test volume size validation': function() {
@ -148,8 +157,11 @@ define([
.setInputValue(sdaDisk + ' input[type=number][name=image]', '5') .setInputValue(sdaDisk + ' input[type=number][name=image]', '5')
// set Base OS volume size lower than required // set Base OS volume size lower than required
.setInputValue(sdaDisk + ' input[type=number][name=os]', '5') .setInputValue(sdaDisk + ' input[type=number][name=os]', '5')
.assertElementExists(sdaDisk + ' .disk-details [data-volume=os] .volume-group-notice.text-danger', 'Validation error exists if volume size is less than required.') .assertElementExists(sdaDisk +
.assertElementDisabled(applyButtonSelector, 'Apply button is disabled in case of validation error') ' .disk-details [data-volume=os] .volume-group-notice.text-danger',
'Validation error exists if volume size is less than required.')
.assertElementDisabled(applyButtonSelector,
'Apply button is disabled in case of validation error')
.clickByCssSelector(cancelButtonSelector); .clickByCssSelector(cancelButtonSelector);
} }
}; };

View File

@ -68,7 +68,8 @@ define([
.then(function() { .then(function() {
return interfacesPage.assignNetworkToInterface('Public', 'eth0'); return interfacesPage.assignNetworkToInterface('Public', 'eth0');
}) })
.assertElementExists('div.ifc-error', 'Untagged networks can not be assigned to the same interface message should appear'); .assertElementExists('div.ifc-error',
'Untagged networks can not be assigned to the same interface message should appear');
}, },
'Bond interfaces with different speeds': function() { 'Bond interfaces with different speeds': function() {
return this.remote return this.remote
@ -78,7 +79,8 @@ define([
.then(function() { .then(function() {
return interfacesPage.selectInterface('eth3'); return interfacesPage.selectInterface('eth3');
}) })
.assertElementExists('div.alert.alert-warning', 'Interfaces with different speeds bonding not recommended message should appear') .assertElementExists('div.alert.alert-warning',
'Interfaces with different speeds bonding not recommended message should appear')
.assertElementEnabled('.btn-bond', 'Bonding button should still be enabled'); .assertElementEnabled('.btn-bond', 'Bonding button should still be enabled');
}, },
'Interfaces bonding': function() { 'Interfaces bonding': function() {
@ -137,7 +139,9 @@ define([
return interfacesPage.selectInterface('bond1'); return interfacesPage.selectInterface('bond1');
}) })
.assertElementDisabled('.btn-bond', 'Making sure bond button is disabled') .assertElementDisabled('.btn-bond', 'Making sure bond button is disabled')
.assertElementContainsText('.alert.alert-warning', ' network interface is already bonded with other network interfaces.', 'Warning message should appear when intended to bond bonds'); .assertElementContainsText('.alert.alert-warning',
' network interface is already bonded with other network interfaces.',
'Warning message should appear when intended to bond bonds');
} }
}; };
}); });

View File

@ -50,10 +50,14 @@ define([
}, },
'Test management controls state in new environment': function() { 'Test management controls state in new environment': function() {
return this.remote return this.remote
.assertElementDisabled(searchButtonSelector, 'Search button is locked if there are no nodes in environment') .assertElementDisabled(searchButtonSelector,
.assertElementDisabled(sortingButtonSelector, 'Sorting button is locked if there are no nodes in environment') 'Search button is locked if there are no nodes in environment')
.assertElementDisabled(filtersButtonSelector, 'Filters button is locked if there are no nodes in environment') .assertElementDisabled(sortingButtonSelector,
.assertElementNotExists('.active-sorters-filters', 'Applied sorters and filters are not shown for empty environment'); 'Sorting button is locked if there are no nodes in environment')
.assertElementDisabled(filtersButtonSelector,
'Filters button is locked if there are no nodes in environment')
.assertElementNotExists('.active-sorters-filters',
'Applied sorters and filters are not shown for empty environment');
}, },
'Test management controls behaviour': { 'Test management controls behaviour': {
setup: function() { setup: function() {
@ -87,12 +91,15 @@ define([
.sleep(300) .sleep(300)
.assertElementsExist('.node-list .node', 3, 'Search was successfull') .assertElementsExist('.node-list .node', 3, 'Search was successfull')
.clickByCssSelector('.page-title') .clickByCssSelector('.page-title')
.assertElementNotExists(searchButtonSelector, 'Active search control remains open when clicking outside the input') .assertElementNotExists(searchButtonSelector,
'Active search control remains open when clicking outside the input')
.clickByCssSelector('.node-management-panel .btn-clear-search') .clickByCssSelector('.node-management-panel .btn-clear-search')
.assertElementsExist('.node-list .node', 4, 'Search was reset') .assertElementsExist('.node-list .node', 4, 'Search was reset')
.assertElementNotExists(searchButtonSelector, 'Search input is still shown after search reset') .assertElementNotExists(searchButtonSelector,
'Search input is still shown after search reset')
.clickByCssSelector('.node-list') .clickByCssSelector('.node-list')
.assertElementExists(searchButtonSelector, 'Empty search control is closed when clicking outside the input'); .assertElementExists(searchButtonSelector,
'Empty search control is closed when clicking outside the input');
}, },
'Test node list sorting': function() { 'Test node list sorting': function() {
var activeSortersPanelSelector = '.active-sorters'; var activeSortersPanelSelector = '.active-sorters';
@ -100,13 +107,18 @@ define([
var firstNodeName; var firstNodeName;
var self = this; var self = this;
return this.remote return this.remote
.assertElementExists(activeSortersPanelSelector, 'Active sorters panel is shown if there are nodes in cluster') .assertElementExists(activeSortersPanelSelector,
.assertElementNotExists(activeSortersPanelSelector + '.btn-reset-sorting', 'Default sorting can not be reset from active sorters panel') 'Active sorters panel is shown if there are nodes in cluster')
.assertElementNotExists(activeSortersPanelSelector + '.btn-reset-sorting',
'Default sorting can not be reset from active sorters panel')
.clickByCssSelector(sortingButtonSelector) .clickByCssSelector(sortingButtonSelector)
.assertElementExists('.sorters .sorter-control', 'Cluster node list has one sorting by default') .assertElementExists('.sorters .sorter-control',
'Cluster node list has one sorting by default')
.assertElementExists('.sorters .sort-by-roles-asc', 'Check default sorting by roles') .assertElementExists('.sorters .sort-by-roles-asc', 'Check default sorting by roles')
.assertElementNotExists('.sorters .sorter-control .btn-remove-sorting', 'Node list should have at least one applied sorting') .assertElementNotExists('.sorters .sorter-control .btn-remove-sorting',
.assertElementNotExists('.sorters .btn-reset-sorting', 'Default sorting can not be reset') 'Node list should have at least one applied sorting')
.assertElementNotExists('.sorters .btn-reset-sorting',
'Default sorting can not be reset')
.findByCssSelector('.node-list .node-name .name p') .findByCssSelector('.node-list .node-name .name p')
.getVisibleText().then(function(text) { .getVisibleText().then(function(text) {
firstNodeName = text; firstNodeName = text;
@ -115,20 +127,24 @@ define([
.clickByCssSelector('.sorters .sort-by-roles-asc button') .clickByCssSelector('.sorters .sort-by-roles-asc button')
.findByCssSelector('.node-list .node-name .name p') .findByCssSelector('.node-list .node-name .name p')
.getVisibleText().then(function(text) { .getVisibleText().then(function(text) {
assert.notEqual(text, firstNodeName, 'Order of sorting by roles was changed to desc'); assert.notEqual(text, firstNodeName,
'Order of sorting by roles was changed to desc');
}) })
.end() .end()
.clickByCssSelector('.sorters .sort-by-roles-desc button') .clickByCssSelector('.sorters .sort-by-roles-desc button')
.then(function() { .then(function() {
return self.remote.assertElementTextEquals('.node-list .node-name .name p', firstNodeName, 'Order of sorting by roles was changed to asc (default)'); return self.remote.assertElementTextEquals('.node-list .node-name .name p',
firstNodeName, 'Order of sorting by roles was changed to asc (default)');
}) })
.clickByCssSelector(moreControlSelector + ' button') .clickByCssSelector(moreControlSelector + ' button')
.assertElementsExist(moreControlSelector + ' .popover .checkbox-group', 12, 'Standard node sorters are presented') .assertElementsExist(moreControlSelector + ' .popover .checkbox-group', 12,
'Standard node sorters are presented')
// add sorting by CPU (real) // add sorting by CPU (real)
.clickByCssSelector(moreControlSelector + ' .popover [name=cores]') .clickByCssSelector(moreControlSelector + ' .popover [name=cores]')
// add sorting by manufacturer // add sorting by manufacturer
.clickByCssSelector(moreControlSelector + ' .popover [name=manufacturer]') .clickByCssSelector(moreControlSelector + ' .popover [name=manufacturer]')
.assertElementsExist('.nodes-group', 4, 'New sorting was applied and nodes were grouped') .assertElementsExist('.nodes-group', 4,
'New sorting was applied and nodes were grouped')
// remove sorting by manufacturer // remove sorting by manufacturer
.clickByCssSelector('.sorters .sort-by-manufacturer-asc .btn-remove-sorting') .clickByCssSelector('.sorters .sort-by-manufacturer-asc .btn-remove-sorting')
.assertElementsExist('.nodes-group', 3, 'Particular sorting removal works') .assertElementsExist('.nodes-group', 3, 'Particular sorting removal works')
@ -144,15 +160,20 @@ define([
var activeFiltersPanelSelector = '.active-filters'; var activeFiltersPanelSelector = '.active-filters';
var moreControlSelector = '.filters .more-control'; var moreControlSelector = '.filters .more-control';
return this.remote return this.remote
.assertElementNotExists(activeFiltersPanelSelector, 'Environment has no active filters by default') .assertElementNotExists(activeFiltersPanelSelector,
'Environment has no active filters by default')
.clickByCssSelector(filtersButtonSelector) .clickByCssSelector(filtersButtonSelector)
.assertElementsExist('.filters .filter-control', 2, 'Filters panel has 2 default filters') .assertElementsExist('.filters .filter-control', 2,
'Filters panel has 2 default filters')
.clickByCssSelector('.filter-by-roles') .clickByCssSelector('.filter-by-roles')
.assertElementNotExists('.filter-by-roles [type=checkbox]:checked', 'There are no active options in Roles filter') .assertElementNotExists('.filter-by-roles [type=checkbox]:checked',
.assertElementNotExists('.filters .filter-control .btn-remove-filter', 'Default filters can not be deleted from filters panel') 'There are no active options in Roles filter')
.assertElementNotExists('.filters .filter-control .btn-remove-filter',
'Default filters can not be deleted from filters panel')
.assertElementNotExists('.filters .btn-reset-filters', 'No filters to be reset') .assertElementNotExists('.filters .btn-reset-filters', 'No filters to be reset')
.clickByCssSelector(moreControlSelector + ' button') .clickByCssSelector(moreControlSelector + ' button')
.assertElementsExist(moreControlSelector + ' .popover .checkbox-group', 8, 'Standard node filters are presented') .assertElementsExist(moreControlSelector + ' .popover .checkbox-group', 8,
'Standard node filters are presented')
.clickByCssSelector(moreControlSelector + ' [name=cores]') .clickByCssSelector(moreControlSelector + ' [name=cores]')
.assertElementsExist('.filters .filter-control', 3, 'New Cores (real) filter was added') .assertElementsExist('.filters .filter-control', 3, 'New Cores (real) filter was added')
.assertElementExists('.filter-by-cores .popover-content', 'New filter is open') .assertElementExists('.filter-by-cores .popover-content', 'New filter is open')
@ -160,10 +181,13 @@ define([
.assertElementsExist('.filters .filter-control', 2, 'Particular filter removal works') .assertElementsExist('.filters .filter-control', 2, 'Particular filter removal works')
.clickByCssSelector(moreControlSelector + ' button') .clickByCssSelector(moreControlSelector + ' button')
.clickByCssSelector(moreControlSelector + ' [name=disks_amount]') .clickByCssSelector(moreControlSelector + ' [name=disks_amount]')
.assertElementsExist('.filters .filter-by-disks_amount input[type=number]:not(:disabled)', 2, 'Number filter has 2 fields to set min and max value') .assertElementsExist('.filters ' +
'.filter-by-disks_amount input[type=number]:not(:disabled)', 2,
'Number filter has 2 fields to set min and max value')
// set min value more than max value // set min value more than max value
.setInputValue('.filters .filter-by-disks_amount input[type=number][name=start]', '100') .setInputValue('.filters .filter-by-disks_amount input[type=number][name=start]', '100')
.assertElementsAppear('.filters .filter-by-disks_amount .form-group.has-error', 2000, 'Validation works for Number range filter') .assertElementsAppear('.filters .filter-by-disks_amount .form-group.has-error', 2000,
'Validation works for Number range filter')
.assertElementNotExists('.node-list .node', 'No nodes match invalid filter') .assertElementNotExists('.node-list .node', 'No nodes match invalid filter')
.clickByCssSelector('.filters .btn-reset-filters') .clickByCssSelector('.filters .btn-reset-filters')
.assertElementsExist('.node-list .node', 4, 'Node filtration was successfully reset') .assertElementsExist('.node-list .node', 4, 'Node filtration was successfully reset')
@ -173,8 +197,10 @@ define([
.clickByCssSelector('.filters .filter-by-status [name=pending_addition]') .clickByCssSelector('.filters .filter-by-status [name=pending_addition]')
.assertElementsExist('.node-list .node', 4, 'All nodes shown') .assertElementsExist('.node-list .node', 4, 'All nodes shown')
.clickByCssSelector(filtersButtonSelector) .clickByCssSelector(filtersButtonSelector)
.assertElementExists(activeFiltersPanelSelector, 'Applied filter is reflected in active filters panel') .assertElementExists(activeFiltersPanelSelector,
.assertElementExists('.active-filters .btn-reset-filters', 'Reset filters button exists in active filters panel'); 'Applied filter is reflected in active filters panel')
.assertElementExists('.active-filters .btn-reset-filters',
'Reset filters button exists in active filters panel');
} }
} }
}; };

View File

@ -57,7 +57,8 @@ define([
.clickByCssSelector('.node input[type=checkbox]') .clickByCssSelector('.node input[type=checkbox]')
.assertElementExists('.node.selected', 'Node gets selected upon clicking') .assertElementExists('.node.selected', 'Node gets selected upon clicking')
.assertElementExists('button.btn-delete-nodes:not(:disabled)', 'Delete Nodes and ...') .assertElementExists('button.btn-delete-nodes:not(:disabled)', 'Delete Nodes and ...')
.assertElementExists('button.btn-edit-roles:not(:disabled)', '... Edit Roles buttons appear upon node selection') .assertElementExists('button.btn-edit-roles:not(:disabled)',
'... Edit Roles buttons appear upon node selection')
.then(function() { .then(function() {
return node.renameNode(nodeNewName); return node.renameNode(nodeNewName);
}) })
@ -82,9 +83,11 @@ define([
.then(function() { .then(function() {
return node.openNodePopup(); return node.openNodePopup();
}) })
.assertElementTextEquals('.modal-header h4.modal-title', nodeNewName, 'Node pop-up has updated node name') .assertElementTextEquals('.modal-header h4.modal-title', nodeNewName,
'Node pop-up has updated node name')
.assertElementExists('.modal .btn-edit-disks', 'Disks can be configured for cluster node') .assertElementExists('.modal .btn-edit-disks', 'Disks can be configured for cluster node')
.assertElementExists('.modal .btn-edit-networks', 'Interfaces can be configured for cluster node') .assertElementExists('.modal .btn-edit-networks',
'Interfaces can be configured for cluster node')
.clickByCssSelector('.change-hostname .btn-link') .clickByCssSelector('.change-hostname .btn-link')
// change the hostname // change the hostname
.findByCssSelector('.change-hostname [type=text]') .findByCssSelector('.change-hostname [type=text]')
@ -92,8 +95,10 @@ define([
.type(newHostname) .type(newHostname)
.pressKeys('\uE007') .pressKeys('\uE007')
.end() .end()
.assertElementDisappears('.change-hostname [type=text]', 2000, 'Hostname input disappears after submit') .assertElementDisappears('.change-hostname [type=text]', 2000,
.assertElementTextEquals('span.node-hostname', newHostname, 'Node hostname has been updated') 'Hostname input disappears after submit')
.assertElementTextEquals('span.node-hostname', newHostname,
'Node hostname has been updated')
.then(function() { .then(function() {
return modal.close(); return modal.close();
}); });
@ -107,8 +112,10 @@ define([
.assertElementExists('i.glyphicon-ok', 'Self node is selectable') .assertElementExists('i.glyphicon-ok', 'Self node is selectable')
.end() .end()
.clickByCssSelector('.compact-node .node-name p') .clickByCssSelector('.compact-node .node-name p')
.assertElementNotExists('.compact-node .node-name-input', 'Node can not be renamed from compact panel') .assertElementNotExists('.compact-node .node-name-input',
.assertElementNotExists('.compact-node .role-list', 'Role list is not shown on node compact panel'); 'Node can not be renamed from compact panel')
.assertElementNotExists('.compact-node .role-list',
'Role list is not shown on node compact panel');
}, },
'Compact node extended view': function() { 'Compact node extended view': function() {
var newName = 'Node new new name'; var newName = 'Node new new name';
@ -117,7 +124,8 @@ define([
return node.openCompactNodeExtendedView(); return node.openCompactNodeExtendedView();
}) })
.clickByCssSelector('.node-popover .node-name input[type=checkbox]') .clickByCssSelector('.node-popover .node-name input[type=checkbox]')
.assertElementExists('.compact-node .node-checkbox i.glyphicon-ok', 'Node compact panel is checked') .assertElementExists('.compact-node .node-checkbox i.glyphicon-ok',
'Node compact panel is checked')
.then(function() { .then(function() {
return node.openNodePopup(true); return node.openNodePopup(true);
}) })
@ -131,12 +139,14 @@ define([
}) })
.findByCssSelector('.node-popover') .findByCssSelector('.node-popover')
.assertElementExists('.role-list', 'Role list is shown in cluster node extended view') .assertElementExists('.role-list', 'Role list is shown in cluster node extended view')
.assertElementExists('.node-buttons', 'Cluster node action buttons are presented in extended view') .assertElementExists('.node-buttons',
'Cluster node action buttons are presented in extended view')
.end() .end()
.then(function() { .then(function() {
return node.renameNode(newName, true); return node.renameNode(newName, true);
}) })
.assertElementTextEquals('.node-popover .name p', newName, 'Node name has been updated from extended view') .assertElementTextEquals('.node-popover .name p', newName,
'Node name has been updated from extended view')
.then(function() { .then(function() {
return node.discardNode(true); return node.discardNode(true);
}) })
@ -152,13 +162,17 @@ define([
.then(function() { .then(function() {
return node.openCompactNodeExtendedView(); return node.openCompactNodeExtendedView();
}) })
.assertElementNotExists('.node-popover .role-list', 'Unallocated node does not have roles assigned') .assertElementNotExists('.node-popover .role-list',
.assertElementNotExists('.node-popover .node-buttons .btn', 'There are no action buttons in unallocated node extended view') 'Unallocated node does not have roles assigned')
.assertElementNotExists('.node-popover .node-buttons .btn',
'There are no action buttons in unallocated node extended view')
.then(function() { .then(function() {
return node.openNodePopup(true); return node.openNodePopup(true);
}) })
.assertElementNotExists('.modal .btn-edit-disks', 'Disks can not be configured for unallocated node') .assertElementNotExists('.modal .btn-edit-disks',
.assertElementNotExists('.modal .btn-edit-networks', 'Interfaces can not be configured for unallocated node') 'Disks can not be configured for unallocated node')
.assertElementNotExists('.modal .btn-edit-networks',
'Interfaces can not be configured for unallocated node')
.then(function() { .then(function() {
return modal.close(); return modal.close();
}); });

View File

@ -38,14 +38,18 @@ define([
}, },
'Notification Page': function() { 'Notification Page': function() {
return this.remote return this.remote
.assertElementDisplayed('.notifications-icon .badge', 'Badge notification indicator is shown in navigation') .assertElementDisplayed('.notifications-icon .badge',
'Badge notification indicator is shown in navigation')
// Go to Notification page // Go to Notification page
.clickByCssSelector('.notifications-icon') .clickByCssSelector('.notifications-icon')
.clickLinkByText('View all') .clickLinkByText('View all')
.assertElementAppears('.notifications-page', 2000, 'Notification page is rendered') .assertElementAppears('.notifications-page', 2000, 'Notification page is rendered')
.assertElementExists('.notifications-page .notification', 'There is the start notification on the page') .assertElementExists('.notifications-page .notification',
.assertElementTextEquals('.notification-group .title', 'Today', 'Notification group has "Today" label') 'There is the start notification on the page')
.assertElementNotDisplayed('.notifications-icon .badge', 'Badge notification indicator is hidden'); .assertElementTextEquals('.notification-group .title', 'Today',
'Notification group has "Today" label')
.assertElementNotDisplayed('.notifications-icon .badge',
'Badge notification indicator is hidden');
}, },
'Notification badge behaviour': function() { 'Notification badge behaviour': function() {
var clusterName = common.pickRandomName('Test Cluster'); var clusterName = common.pickRandomName('Test Cluster');
@ -61,9 +65,11 @@ define([
.then(function() { .then(function() {
return common.removeCluster(clusterName); return common.removeCluster(clusterName);
}) })
.assertElementAppears('.notifications-icon .badge.visible', 3000, 'New notification appear after the cluster removal') .assertElementAppears('.notifications-icon .badge.visible', 3000,
'New notification appear after the cluster removal')
.clickByCssSelector('.notifications-icon') .clickByCssSelector('.notifications-icon')
.assertElementAppears('.notifications-popover .notification.clickable', 20000, 'Discovered node notification uploaded') .assertElementAppears('.notifications-popover .notification.clickable', 20000,
'Discovered node notification uploaded')
// Check if Node Information dialog is shown // Check if Node Information dialog is shown
.clickByCssSelector('.notifications-popover .notification.clickable p') .clickByCssSelector('.notifications-popover .notification.clickable p')
.then(function() { .then(function() {

View File

@ -62,9 +62,12 @@ define([
var self = this; var self = this;
var zabbixInitialVersion, zabbixTextInputValue; var zabbixInitialVersion, zabbixTextInputValue;
return this.remote return this.remote
.assertElementEnabled(zabbixSectionSelector + 'h3 input[type=checkbox]', 'Plugin is changeable') .assertElementEnabled(zabbixSectionSelector + 'h3 input[type=checkbox]',
.assertElementNotSelected(zabbixSectionSelector + 'h3 input[type=checkbox]', 'Plugin is not actvated') 'Plugin is changeable')
.assertElementNotExists(zabbixSectionSelector + '> div input:not(:disabled)', 'Inactive plugin attributes can not be changes') .assertElementNotSelected(zabbixSectionSelector + 'h3 input[type=checkbox]',
'Plugin is not actvated')
.assertElementNotExists(zabbixSectionSelector + '> div input:not(:disabled)',
'Inactive plugin attributes can not be changes')
// activate plugin // activate plugin
.clickByCssSelector(zabbixSectionSelector + 'h3 input[type=checkbox]') .clickByCssSelector(zabbixSectionSelector + 'h3 input[type=checkbox]')
// save changes // save changes
@ -85,9 +88,12 @@ define([
}) })
.end() .end()
// change plugin version // change plugin version
.clickByCssSelector(zabbixSectionSelector + '.plugin-versions input[type=radio]:not(:checked)') .clickByCssSelector(zabbixSectionSelector +
.assertElementPropertyNotEquals(zabbixSectionSelector + '[name=zabbix_text_1]', 'value', zabbixTextInputValue, 'Plugin version was changed') '.plugin-versions input[type=radio]:not(:checked)')
.assertElementExists('.subtab-link-other .glyphicon-danger-sign', 'Plugin atributes validation works') .assertElementPropertyNotEquals(zabbixSectionSelector + '[name=zabbix_text_1]', 'value',
zabbixTextInputValue, 'Plugin version was changed')
.assertElementExists('.subtab-link-other .glyphicon-danger-sign',
'Plugin atributes validation works')
// fix validation error // fix validation error
.setInputValue(zabbixSectionSelector + '[name=zabbix_text_with_regex]', 'aa-aa') .setInputValue(zabbixSectionSelector + '[name=zabbix_text_with_regex]', 'aa-aa')
.waitForElementDeletion('.subtab-link-other .glyphicon-danger-sign', 1000) .waitForElementDeletion('.subtab-link-other .glyphicon-danger-sign', 1000)
@ -95,7 +101,9 @@ define([
// reset plugin version change // reset plugin version change
.clickByCssSelector('.btn-revert-changes') .clickByCssSelector('.btn-revert-changes')
.then(function() { .then(function() {
return self.remote.assertElementPropertyEquals(zabbixSectionSelector + '.plugin-versions input[type=radio]:checked', 'value', zabbixInitialVersion, 'Plugin version change can be reset'); return self.remote.assertElementPropertyEquals(zabbixSectionSelector +
'.plugin-versions input[type=radio]:checked', 'value', zabbixInitialVersion,
'Plugin version change can be reset');
}); });
}, },
'Check plugin in deployed environment': function() { 'Check plugin in deployed environment': function() {
@ -124,8 +132,12 @@ define([
.end() .end()
// activate plugin // activate plugin
.clickByCssSelector(zabbixSectionSelector + 'h3 input[type=checkbox]') .clickByCssSelector(zabbixSectionSelector + 'h3 input[type=checkbox]')
.assertElementExists(zabbixSectionSelector + '.plugin-versions input[type=radio]:not(:disabled)', 'Some plugin versions are hotluggable') .assertElementExists(zabbixSectionSelector +
.assertElementPropertyNotEquals(zabbixSectionSelector + '.plugin-versions input[type=radio]:checked', 'value', zabbixInitialVersion, 'Plugin hotpluggable version is automatically chosen') '.plugin-versions input[type=radio]:not(:disabled)',
'Some plugin versions are hotluggable')
.assertElementPropertyNotEquals(zabbixSectionSelector +
'.plugin-versions input[type=radio]:checked', 'value', zabbixInitialVersion,
'Plugin hotpluggable version is automatically chosen')
// fix validation error // fix validation error
.setInputValue(zabbixSectionSelector + '[name=zabbix_text_with_regex]', 'aa-aa') .setInputValue(zabbixSectionSelector + '[name=zabbix_text_with_regex]', 'aa-aa')
.waitForElementDeletion('.subtab-link-other .glyphicon-danger-sign', 1000) .waitForElementDeletion('.subtab-link-other .glyphicon-danger-sign', 1000)
@ -133,7 +145,9 @@ define([
// deactivate plugin // deactivate plugin
.clickByCssSelector(zabbixSectionSelector + 'h3 input[type=checkbox]') .clickByCssSelector(zabbixSectionSelector + 'h3 input[type=checkbox]')
.then(function() { .then(function() {
return self.remote.assertElementPropertyEquals(zabbixSectionSelector + '.plugin-versions input[type=radio]:checked', 'value', zabbixInitialVersion, 'Initial plugin version is set for deactivated plugin'); return self.remote.assertElementPropertyEquals(zabbixSectionSelector +
'.plugin-versions input[type=radio]:checked', 'value', zabbixInitialVersion,
'Initial plugin version is set for deactivated plugin');
}) })
.assertElementDisabled('.btn-apply-changes', 'The change as reset successfully'); .assertElementDisabled('.btn-apply-changes', 'The change as reset successfully');
}, },
@ -144,12 +158,16 @@ define([
.clickByCssSelector(loggingSectionSelector + 'h3 input[type=checkbox]') .clickByCssSelector(loggingSectionSelector + 'h3 input[type=checkbox]')
// activate Zabbix plugin // activate Zabbix plugin
.clickByCssSelector(zabbixSectionSelector + 'h3 input[type=checkbox]') .clickByCssSelector(zabbixSectionSelector + 'h3 input[type=checkbox]')
.assertElementEnabled(loggingSectionSelector + '[name=logging_text]', 'No conflict with default Zabix plugin version') .assertElementEnabled(loggingSectionSelector + '[name=logging_text]',
'No conflict with default Zabix plugin version')
// change Zabbix plugin version // change Zabbix plugin version
.clickByCssSelector(zabbixSectionSelector + '.plugin-versions input[type=radio]:not(:checked)') .clickByCssSelector(zabbixSectionSelector +
.assertElementNotSelected(zabbixSectionSelector + '[name=zabbix_checkbox]', 'Zabbix checkbox is not activated') '.plugin-versions input[type=radio]:not(:checked)')
.assertElementNotSelected(zabbixSectionSelector + '[name=zabbix_checkbox]',
'Zabbix checkbox is not activated')
.clickByCssSelector(zabbixSectionSelector + '[name=zabbix_checkbox]') .clickByCssSelector(zabbixSectionSelector + '[name=zabbix_checkbox]')
.assertElementDisabled(loggingSectionSelector + '[name=logging_text]', 'Conflict with Zabbix checkbox') .assertElementDisabled(loggingSectionSelector + '[name=logging_text]',
'Conflict with Zabbix checkbox')
// reset changes // reset changes
.clickByCssSelector('.btn-revert-changes'); .clickByCssSelector('.btn-revert-changes');
} }

View File

@ -48,7 +48,8 @@ define([
.assertElementExists('.capacity-audit', 'Capacity Audit block is present') .assertElementExists('.capacity-audit', 'Capacity Audit block is present')
.assertElementExists('.tracking', 'Statistics block is present') .assertElementExists('.tracking', 'Statistics block is present')
.assertElementSelected(sendStatisticsCheckbox, 'Save Staticstics checkbox is checked') .assertElementSelected(sendStatisticsCheckbox, 'Save Staticstics checkbox is checked')
.assertElementDisabled(saveStatisticsSettingsButton, '"Save changes" button is disabled until statistics checkbox uncheck'); .assertElementDisabled(saveStatisticsSettingsButton,
'"Save changes" button is disabled until statistics checkbox uncheck');
}, },
'Diagnostic snapshot link generation': function() { 'Diagnostic snapshot link generation': function() {
return this.remote return this.remote
@ -59,9 +60,12 @@ define([
return this.remote return this.remote
// Uncheck "Send usage statistics" checkbox // Uncheck "Send usage statistics" checkbox
.clickByCssSelector(sendStatisticsCheckbox) .clickByCssSelector(sendStatisticsCheckbox)
.assertElementEnabled(saveStatisticsSettingsButton, '"Save changes" button is enabled after changing "Send usage statistics" checkbox value') .assertElementEnabled(saveStatisticsSettingsButton,
'"Save changes" button is enabled after changing "Send usage statistics" ' +
'checkbox value')
.clickByCssSelector(saveStatisticsSettingsButton) .clickByCssSelector(saveStatisticsSettingsButton)
.assertElementDisabled(saveStatisticsSettingsButton, '"Save changes" button is disabled after saving changes'); .assertElementDisabled(saveStatisticsSettingsButton,
'"Save changes" button is disabled after saving changes');
}, },
'Discard changes': function() { 'Discard changes': function() {
return this.remote return this.remote
@ -87,7 +91,8 @@ define([
.assertElementAppears('.clusters-page', 1000, 'Redirecting to Environments') .assertElementAppears('.clusters-page', 1000, 'Redirecting to Environments')
// Go back to Support Page and ... // Go back to Support Page and ...
.clickLinkByText('Support') .clickLinkByText('Support')
.assertElementSelected(sendStatisticsCheckbox, 'Changes saved successfully and save staticstics checkbox is checked') .assertElementSelected(sendStatisticsCheckbox,
'Changes saved successfully and save staticstics checkbox is checked')
// Uncheck the "Send usage statistics" checkbox value // Uncheck the "Send usage statistics" checkbox value
.clickByCssSelector(sendStatisticsCheckbox) .clickByCssSelector(sendStatisticsCheckbox)
// Go to another page with not saved changes // Go to another page with not saved changes
@ -105,7 +110,8 @@ define([
.assertElementAppears('.clusters-page', 1000, 'Redirecting to Environments') .assertElementAppears('.clusters-page', 1000, 'Redirecting to Environments')
// Go back to Support Page and ... // Go back to Support Page and ...
.clickLinkByText('Support') .clickLinkByText('Support')
.assertElementSelected(sendStatisticsCheckbox, 'Changes was not saved and save staticstics checkbox is checked') .assertElementSelected(sendStatisticsCheckbox,
'Changes was not saved and save staticstics checkbox is checked')
// Uncheck the "Send usage statistics" checkbox value // Uncheck the "Send usage statistics" checkbox value
.clickByCssSelector(sendStatisticsCheckbox) .clickByCssSelector(sendStatisticsCheckbox)
// Go to another page with not saved changes // Go to another page with not saved changes
@ -120,7 +126,8 @@ define([
.then(function() { .then(function() {
return modal.waitToClose(); return modal.waitToClose();
}) })
.assertElementNotSelected(sendStatisticsCheckbox, 'We are still on the Support page, and checkbox is unchecked'); .assertElementNotSelected(sendStatisticsCheckbox,
'We are still on the Support page, and checkbox is unchecked');
} }
}; };
}); });

View File

@ -52,7 +52,8 @@ define([
}, },
'Test steps manipulations': function() { 'Test steps manipulations': function() {
return this.remote return this.remote
.assertElementExists('.wizard-step.active', 'There is only one active and available step at the beginning') .assertElementExists('.wizard-step.active',
'There is only one active and available step at the beginning')
// Compute // Compute
.pressKeys('\uE007') .pressKeys('\uE007')
// Network // Network

View File

@ -68,7 +68,8 @@ suite('Expression', () => {
['"unknown-role" in release:roles', false], ['"unknown-role" in release:roles', false],
['settings:common.libvirt_type.value', hypervisor], ['settings:common.libvirt_type.value', hypervisor],
['settings:common.libvirt_type.value == "' + hypervisor + '"', true], ['settings:common.libvirt_type.value == "' + hypervisor + '"', true],
['cluster:mode == "ha_compact" and not (settings:common.libvirt_type.value != "' + hypervisor + '")', true], ['cluster:mode == "ha_compact" and not (settings:common.libvirt_type.value != "' +
hypervisor + '")', true],
// test nonexistent keys // test nonexistent keys
['cluster:nonexistentkey', Error], ['cluster:nonexistentkey', Error],
['cluster:nonexistentkey == null', true, false], ['cluster:nonexistentkey == null', true, false],
@ -86,9 +87,11 @@ suite('Expression', () => {
_.each(testCases, ([expression, result, strict]) => { _.each(testCases, ([expression, result, strict]) => {
var options = {strict}; var options = {strict};
if (result === Error) { if (result === Error) {
assert.throws(_.partial(evaluate, expression, options), Error, '', expression + ' throws an error'); assert.throws(_.partial(evaluate, expression, options), Error, '',
expression + ' throws an error');
} else { } else {
assert.strictEqual(evaluate(expression, options), result, expression + ' evaluates correctly'); assert.strictEqual(evaluate(expression, options), result,
expression + ' evaluates correctly');
} }
}); });
}); });

View File

@ -36,7 +36,8 @@ suite('File Control', () => {
var initialState = input.getInitialState(); var initialState = input.getInitialState();
assert.equal(input.props.type, 'file', 'Input type should be equal to file'); assert.equal(input.props.type, 'file', 'Input type should be equal to file');
assert.equal(initialState.fileName, 'certificate.crt', 'Default file name must correspond to provided one'); assert.equal(initialState.fileName, 'certificate.crt',
'Default file name must correspond to provided one');
assert.equal(initialState.content, 'CERTIFICATE', 'Content should be equal to the default'); assert.equal(initialState.content, 'CERTIFICATE', 'Content should be equal to the default');
}); });
@ -48,7 +49,8 @@ suite('File Control', () => {
}); });
input.pickFile(); input.pickFile();
assert.ok(clickSpy.calledOnce, 'When icon clicked input control should be clicked too to open select file dialog'); assert.ok(clickSpy.calledOnce,
'When icon clicked input control should be clicked too to open select file dialog');
}); });
test('File fetching', () => { test('File fetching', () => {

View File

@ -24,39 +24,48 @@ suite('Test models', () => {
filters = {status: []}; filters = {status: []};
result = ['running', 'pending', 'ready', 'error']; result = ['running', 'pending', 'ready', 'error'];
assert.deepEqual(task.extendStatuses(filters), result, 'All task statuses are acceptable if "status" filter not specified'); assert.deepEqual(task.extendStatuses(filters), result,
'All task statuses are acceptable if "status" filter not specified');
filters = {status: 'ready'}; filters = {status: 'ready'};
result = ['ready']; result = ['ready'];
assert.deepEqual(task.extendStatuses(filters), result, '"status" filter can have string as a value'); assert.deepEqual(task.extendStatuses(filters), result,
'"status" filter can have string as a value');
filters = {status: ['ready', 'running']}; filters = {status: ['ready', 'running']};
result = ['ready', 'running']; result = ['ready', 'running'];
assert.deepEqual(task.extendStatuses(filters), result, '"status" filter can have list of strings as a value'); assert.deepEqual(task.extendStatuses(filters), result,
'"status" filter can have list of strings as a value');
filters = {status: ['ready'], active: true}; filters = {status: ['ready'], active: true};
result = []; result = [];
assert.deepEqual(task.extendStatuses(filters), result, '"status" and "active" filters are not intersected'); assert.deepEqual(task.extendStatuses(filters), result,
'"status" and "active" filters are not intersected');
filters = {status: ['running'], active: true}; filters = {status: ['running'], active: true};
result = ['running']; result = ['running'];
assert.deepEqual(task.extendStatuses(filters), result, '"status" and "active" filters have intersection'); assert.deepEqual(task.extendStatuses(filters), result,
'"status" and "active" filters have intersection');
filters = {status: ['running'], active: false}; filters = {status: ['running'], active: false};
result = []; result = [];
assert.deepEqual(task.extendStatuses(filters), result, '"status" and "active" filters are not intersected'); assert.deepEqual(task.extendStatuses(filters), result,
'"status" and "active" filters are not intersected');
filters = {status: ['ready', 'running'], active: false}; filters = {status: ['ready', 'running'], active: false};
result = ['ready']; result = ['ready'];
assert.deepEqual(task.extendStatuses(filters), result, '"status" and "active" filters have intersection'); assert.deepEqual(task.extendStatuses(filters), result,
'"status" and "active" filters have intersection');
filters = {active: true}; filters = {active: true};
result = ['running', 'pending']; result = ['running', 'pending'];
assert.deepEqual(task.extendStatuses(filters), result, 'True value of "active" filter parsed correctly'); assert.deepEqual(task.extendStatuses(filters), result,
'True value of "active" filter parsed correctly');
filters = {active: false}; filters = {active: false};
result = ['ready', 'error']; result = ['ready', 'error'];
assert.deepEqual(task.extendStatuses(filters), result, 'False value of \'active\' filter parsed correctly'); assert.deepEqual(task.extendStatuses(filters), result,
'False value of \'active\' filter parsed correctly');
}); });
test('Test extendGroups method', () => { test('Test extendGroups method', () => {
@ -66,39 +75,48 @@ suite('Test models', () => {
filters = {name: []}; filters = {name: []};
result = allTaskNames; result = allTaskNames;
assert.deepEqual(task.extendGroups(filters), result, 'All task names are acceptable if "name" filter not specified'); assert.deepEqual(task.extendGroups(filters), result,
'All task names are acceptable if "name" filter not specified');
filters = {group: []}; filters = {group: []};
result = allTaskNames; result = allTaskNames;
assert.deepEqual(task.extendGroups(filters), result, 'All task names are acceptable if "group" filter not specified'); assert.deepEqual(task.extendGroups(filters), result,
'All task names are acceptable if "group" filter not specified');
filters = {name: 'deploy'}; filters = {name: 'deploy'};
result = ['deploy']; result = ['deploy'];
assert.deepEqual(task.extendGroups(filters), result, '"name" filter can have string as a value'); assert.deepEqual(task.extendGroups(filters), result,
'"name" filter can have string as a value');
filters = {name: 'dump'}; filters = {name: 'dump'};
result = ['dump']; result = ['dump'];
assert.deepEqual(task.extendGroups(filters), result, 'Tasks, that are not related to any task group, handled properly'); assert.deepEqual(task.extendGroups(filters), result,
'Tasks, that are not related to any task group, handled properly');
filters = {name: ['deploy', 'check_networks']}; filters = {name: ['deploy', 'check_networks']};
result = ['deploy', 'check_networks']; result = ['deploy', 'check_networks'];
assert.deepEqual(task.extendGroups(filters), result, '"name" filter can have list of strings as a value'); assert.deepEqual(task.extendGroups(filters), result,
'"name" filter can have list of strings as a value');
filters = {group: 'deployment'}; filters = {group: 'deployment'};
result = task.groups.deployment; result = task.groups.deployment;
assert.deepEqual(task.extendGroups(filters), result, '"group" filter can have string as a value'); assert.deepEqual(task.extendGroups(filters), result,
'"group" filter can have string as a value');
filters = {group: ['deployment', 'network']}; filters = {group: ['deployment', 'network']};
result = allTaskNames; result = allTaskNames;
assert.deepEqual(task.extendGroups(filters), result, '"group" filter can have list of strings as a value'); assert.deepEqual(task.extendGroups(filters), result,
'"group" filter can have list of strings as a value');
filters = {name: 'deploy', group: 'deployment'}; filters = {name: 'deploy', group: 'deployment'};
result = ['deploy']; result = ['deploy'];
assert.deepEqual(task.extendGroups(filters), result, '"name" and "group" filters have intersection'); assert.deepEqual(task.extendGroups(filters), result,
'"name" and "group" filters have intersection');
filters = {name: 'deploy', group: 'network'}; filters = {name: 'deploy', group: 'network'};
result = []; result = [];
assert.deepEqual(task.extendGroups(filters), result, '"name" and "group" filters are not intersected'); assert.deepEqual(task.extendGroups(filters), result,
'"name" and "group" filters are not intersected');
}); });
}); });
}); });

View File

@ -13,17 +13,20 @@
* License for the specific language governing permissions and limitations * License for the specific language governing permissions and limitations
* under the License. * under the License.
**/ **/
import OffloadingModes from 'views/cluster_page_tabs/nodes_tab_screens/offloading_modes_control'; import OffloadingModes from
'views/cluster_page_tabs/nodes_tab_screens/offloading_modes_control';
var offloadingModesConrol, TestMode22, TestMode31, fakeOffloadingModes; var offloadingModesConrol, TestMode22, TestMode31, fakeOffloadingModes;
var fakeInterface = { var fakeInterface = {
offloading_modes: fakeOffloadingModes, offloading_modes: fakeOffloadingModes,
get(key) { get(key) {
assert.equal(key, 'offloading_modes', '"offloading_modes" interface property should be used to get data'); assert.equal(key, 'offloading_modes',
'"offloading_modes" interface property should be used to get data');
return fakeOffloadingModes; return fakeOffloadingModes;
}, },
set(key, value) { set(key, value) {
assert.equal(key, 'offloading_modes', '"offloading_modes" interface property should be used to set data'); assert.equal(key, 'offloading_modes',
'"offloading_modes" interface property should be used to set data');
fakeOffloadingModes = value; fakeOffloadingModes = value;
} }
}; };
@ -70,18 +73,21 @@ suite('Offloadning Modes control', () => {
test('Set submodes states logic', () => { test('Set submodes states logic', () => {
var mode = offloadingModesConrol.findMode('TestName1', fakeOffloadingModes); var mode = offloadingModesConrol.findMode('TestName1', fakeOffloadingModes);
offloadingModesConrol.setModeState(mode, false); offloadingModesConrol.setModeState(mode, false);
assert.strictEqual(TestMode31.state, false, 'Parent state changing leads to all child modes states changing'); assert.strictEqual(TestMode31.state, false,
'Parent state changing leads to all child modes states changing');
}); });
test('Disabled reversed logic', () => { test('Disabled reversed logic', () => {
var mode = offloadingModesConrol.findMode('TestName2', fakeOffloadingModes); var mode = offloadingModesConrol.findMode('TestName2', fakeOffloadingModes);
offloadingModesConrol.setModeState(TestMode22, true); offloadingModesConrol.setModeState(TestMode22, true);
offloadingModesConrol.checkModes(null, fakeOffloadingModes); offloadingModesConrol.checkModes(null, fakeOffloadingModes);
assert.strictEqual(mode.state, null, 'Parent state changing leads to all child modes states changing'); assert.strictEqual(mode.state, null,
'Parent state changing leads to all child modes states changing');
}); });
test('All Modes option logic', () => { test('All Modes option logic', () => {
var enableAllModes = offloadingModesConrol.onModeStateChange('All Modes', true); var enableAllModes = offloadingModesConrol.onModeStateChange('All Modes', true);
enableAllModes(); enableAllModes();
var mode = offloadingModesConrol.findMode('TestName2', fakeOffloadingModes); var mode = offloadingModesConrol.findMode('TestName2', fakeOffloadingModes);
assert.strictEqual(mode.state, true, 'All Modes option state changing leads to all parent modes states changing'); assert.strictEqual(mode.state, true,
'All Modes option state changing leads to all parent modes states changing');
}); });
}); });

View File

@ -25,19 +25,24 @@ suite('Test utils', () => {
var serverUnavailableMessage = i18n('dialog.error_dialog.server_unavailable'); var serverUnavailableMessage = i18n('dialog.error_dialog.server_unavailable');
response = {status: 500, responseText: 'Server error occured'}; response = {status: 500, responseText: 'Server error occured'};
assert.equal(getResponseText(response), serverErrorMessage, 'HTTP 500 is treated as a server error'); assert.equal(getResponseText(response), serverErrorMessage,
'HTTP 500 is treated as a server error');
response = {status: 502, responseText: 'Bad gateway'}; response = {status: 502, responseText: 'Bad gateway'};
assert.equal(getResponseText(response), serverUnavailableMessage, 'HTTP 502 is treated as server unavailability'); assert.equal(getResponseText(response), serverUnavailableMessage,
'HTTP 502 is treated as server unavailability');
response = {status: 0, responseText: 'error'}; response = {status: 0, responseText: 'error'};
assert.equal(getResponseText(response), serverUnavailableMessage, 'XHR object with no status is treated as server unavailability'); assert.equal(getResponseText(response), serverUnavailableMessage,
'XHR object with no status is treated as server unavailability');
response = {status: 400, responseText: 'Bad request'}; response = {status: 400, responseText: 'Bad request'};
assert.equal(getResponseText(response), serverErrorMessage, 'HTTP 400 with plain text response is treated as a server error'); assert.equal(getResponseText(response), serverErrorMessage,
'HTTP 400 with plain text response is treated as a server error');
response = {status: 400, responseText: JSON.stringify({message: '123'})}; response = {status: 400, responseText: JSON.stringify({message: '123'})};
assert.equal(getResponseText(response), '123', 'HTTP 400 with JSON response is treated correctly'); assert.equal(getResponseText(response), '123',
'HTTP 400 with JSON response is treated correctly');
}); });
test('Test comparison', () => { test('Test comparison', () => {
@ -67,15 +72,20 @@ suite('Test utils', () => {
assert.equal(compare(model1, model1, {attr: 'number'}), 0, 'Number comparison a=b'); assert.equal(compare(model1, model1, {attr: 'number'}), 0, 'Number comparison a=b');
assert.equal(compare(model1, model2, {attr: 'boolean'}), -1, 'Boolean comparison true and false'); assert.equal(compare(model1, model2, {attr: 'boolean'}), -1,
'Boolean comparison true and false');
assert.equal(compare(model2, model1, {attr: 'boolean'}), 1, 'Boolean comparison false and true'); assert.equal(compare(model2, model1, {attr: 'boolean'}), 1,
'Boolean comparison false and true');
assert.equal(compare(model1, model1, {attr: 'boolean'}), 0, 'Boolean comparison true and true'); assert.equal(compare(model1, model1, {attr: 'boolean'}), 0,
'Boolean comparison true and true');
assert.equal(compare(model2, model2, {attr: 'boolean'}), 0, 'Boolean comparison false and false'); assert.equal(compare(model2, model2, {attr: 'boolean'}), 0,
'Boolean comparison false and false');
assert.equal(compare(model1, model2, {attr: 'booleanFlagWithNull'}), 0, 'Comparison null and false'); assert.equal(compare(model1, model2, {attr: 'booleanFlagWithNull'}), 0,
'Comparison null and false');
}); });
test('Test highlightTestStep', () => { test('Test highlightTestStep', () => {
@ -136,17 +146,22 @@ suite('Test utils', () => {
assert.equal(getGateway('172.16.0.0/24'), '172.16.0.1', 'Getting default gateway for CIDR'); assert.equal(getGateway('172.16.0.0/24'), '172.16.0.1', 'Getting default gateway for CIDR');
assert.equal(getGateway('192.168.0.0/10'), '192.128.0.1', 'Getting default gateway for CIDR'); assert.equal(getGateway('192.168.0.0/10'), '192.128.0.1', 'Getting default gateway for CIDR');
assert.equal(getGateway('172.16.0.0/31'), '', 'No gateway returned for inappropriate CIDR (network is too small)'); assert.equal(getGateway('172.16.0.0/31'), '',
'No gateway returned for inappropriate CIDR (network is too small)');
assert.equal(getGateway('172.16.0.0/'), '', 'No gateway returned for invalid CIDR'); assert.equal(getGateway('172.16.0.0/'), '', 'No gateway returned for invalid CIDR');
}); });
test('Test getDefaultIPRangeForCidr', () => { test('Test getDefaultIPRangeForCidr', () => {
var getRange = utils.getDefaultIPRangeForCidr; var getRange = utils.getDefaultIPRangeForCidr;
assert.deepEqual(getRange('172.16.0.0/24'), [['172.16.0.1', '172.16.0.254']], 'Getting default IP range for CIDR'); assert.deepEqual(getRange('172.16.0.0/24'), [['172.16.0.1', '172.16.0.254']],
assert.deepEqual(getRange('192.168.0.0/10', true), [['192.128.0.2', '192.191.255.254']], 'Gateway address excluded from default IP range'); 'Getting default IP range for CIDR');
assert.deepEqual(getRange('172.16.0.0/31'), [['', '']], 'No IP range returned for inappropriate CIDR (network is too small)'); assert.deepEqual(getRange('192.168.0.0/10', true), [['192.128.0.2', '192.191.255.254']],
assert.deepEqual(getRange('172.16.0.0/', true), [['', '']], 'No IP range returned for invalid CIDR'); 'Gateway address excluded from default IP range');
assert.deepEqual(getRange('172.16.0.0/31'), [['', '']],
'No IP range returned for inappropriate CIDR (network is too small)');
assert.deepEqual(getRange('172.16.0.0/', true), [['', '']],
'No IP range returned for invalid CIDR');
}); });
test('Test validateIpCorrespondsToCIDR', () => { test('Test validateIpCorrespondsToCIDR', () => {
@ -156,6 +171,7 @@ suite('Test utils', () => {
assert.ok(validate('172.16.0.5/24', '172.16.0.2'), 'Check IP, that corresponds to CIDR'); assert.ok(validate('172.16.0.5/24', '172.16.0.2'), 'Check IP, that corresponds to CIDR');
assert.notOk(validate('172.16.0.0/20', '172.16.15.255'), 'Check broadcast address'); assert.notOk(validate('172.16.0.0/20', '172.16.15.255'), 'Check broadcast address');
assert.notOk(validate('172.16.0.0/20', '172.16.0.0'), 'Check network address'); assert.notOk(validate('172.16.0.0/20', '172.16.0.0'), 'Check network address');
assert.notOk(validate('192.168.0.0/10', '192.231.255.254'), 'Check IP, that does not correspond to CIDR'); assert.notOk(validate('192.168.0.0/10', '192.231.255.254'),
'Check IP, that does not correspond to CIDR');
}); });
}); });

View File

@ -26,12 +26,14 @@ import {ErrorDialog} from 'views/dialogs';
import models from 'models'; import models from 'models';
var utils = { var utils = {
/*eslint-disable max-len*/
regexes: { regexes: {
url: /(?:https?:\/\/([\-\w\.]+)+(:\d+)?(\/([\w\/_\-\.]*(\?[\w\/_\-\.&%]*)?(#[\w\/_\-\.&%]*)?)?)?)/, url: /(?:https?:\/\/([\-\w\.]+)+(:\d+)?(\/([\w\/_\-\.]*(\?[\w\/_\-\.&%]*)?(#[\w\/_\-\.&%]*)?)?)?)/,
ip: /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/, ip: /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/,
mac: /^([0-9a-f]{1,2}[\.:-]){5}([0-9a-f]{1,2})$/, mac: /^([0-9a-f]{1,2}[\.:-]){5}([0-9a-f]{1,2})$/,
cidr: /^(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\/([1-9]|[1-2]\d|3[0-2])$/ cidr: /^(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\/([1-9]|[1-2]\d|3[0-2])$/
}, },
/*eslint-enable max-len*/
serializeTabOptions(options) { serializeTabOptions(options) {
return _.map(options, (value, key) => key + ':' + value).join(';'); return _.map(options, (value, key) => key + ':' + value).join(';');
}, },
@ -55,14 +57,16 @@ var utils = {
return '<a target="_blank" href="' + url + '">' + url + '</a>'; return '<a target="_blank" href="' + url + '">' + url + '</a>';
}, },
urlify(text) { urlify(text) {
return utils.linebreaks(text).replace(new RegExp(utils.regexes.url.source, 'g'), utils.composeLink); return utils.linebreaks(text).replace(new RegExp(utils.regexes.url.source, 'g'),
utils.composeLink);
}, },
composeList(value) { composeList(value) {
return _.isUndefined(value) ? [] : _.isArray(value) ? value : [value]; return _.isUndefined(value) ? [] : _.isArray(value) ? value : [value];
}, },
// FIXME(vkramskikh): moved here from healthcheck_tab to make testable // FIXME(vkramskikh): moved here from healthcheck_tab to make testable
highlightTestStep(text, step) { highlightTestStep(text, step) {
return text.replace(new RegExp('(^|\\s*)(' + step + '\\.[\\s\\S]*?)(\\s*\\d+\\.|$)'), '$1<b>$2</b>$3'); return text.replace(new RegExp('(^|\\s*)(' + step + '\\.[\\s\\S]*?)(\\s*\\d+\\.|$)'),
'$1<b>$2</b>$3');
}, },
classNames: classNames, classNames: classNames,
parseModelPath(path, models) { parseModelPath(path, models) {
@ -129,7 +133,8 @@ var utils = {
break; break;
} }
} }
return (result ? result.toFixed(1) : result) + ' ' + i18n('common.size.' + unit, {count: result}); return (result ? result.toFixed(1) : result) + ' ' + i18n('common.size.' + unit,
{count: result});
}, },
showMemorySize(bytes) { showMemorySize(bytes) {
return utils.showSize(bytes, 1024); return utils.showSize(bytes, 1024);
@ -142,7 +147,8 @@ var utils = {
return Math.pow(2, 32 - parseInt(_.last(cidr.split('/')), 10)); return Math.pow(2, 32 - parseInt(_.last(cidr.split('/')), 10));
}, },
formatNumber(n) { formatNumber(n) {
return String(n).replace(/\d/g, (c, i, a) => i > 0 && c !== '.' && (a.length - i) % 3 === 0 ? ',' + c : c); return String(n).replace(/\d/g, (c, i, a) => i > 0 && c !== '.' &&
(a.length - i) % 3 === 0 ? ',' + c : c);
}, },
floor(n, decimals) { floor(n, decimals) {
return Math.floor(n * Math.pow(10, decimals)) / Math.pow(10, decimals); return Math.floor(n * Math.pow(10, decimals)) / Math.pow(10, decimals);
@ -152,7 +158,8 @@ var utils = {
}, },
validateVlan(vlan, forbiddenVlans, field, disallowNullValue) { validateVlan(vlan, forbiddenVlans, field, disallowNullValue) {
var error = {}; var error = {};
if ((_.isNull(vlan) && disallowNullValue) || (!_.isNull(vlan) && (!utils.isNaturalNumber(vlan) || vlan < 1 || vlan > 4094))) { if ((_.isNull(vlan) && disallowNullValue) || (!_.isNull(vlan) &&
(!utils.isNaturalNumber(vlan) || vlan < 1 || vlan > 4094))) {
error[field] = i18n('cluster_page.network_tab.validation.invalid_vlan'); error[field] = i18n('cluster_page.network_tab.validation.invalid_vlan');
return error; return error;
} }
@ -218,7 +225,8 @@ var utils = {
} else if (existingRanges.length) { } else if (existingRanges.length) {
var intersection = utils.checkIPRangesIntersection(range, existingRanges); var intersection = utils.checkIPRangesIntersection(range, existingRanges);
if (intersection) { if (intersection) {
error.start = error.end = warnings.IP_RANGES_INTERSECTION + intersection.join(' - '); error.start = error.end = warnings.IP_RANGES_INTERSECTION +
intersection.join(' - ');
} }
} }
} }
@ -240,13 +248,16 @@ var utils = {
checkIPRangesIntersection([startIP, endIP], existingRanges) { checkIPRangesIntersection([startIP, endIP], existingRanges) {
var startIPInt = IP.toLong(startIP); var startIPInt = IP.toLong(startIP);
var endIPInt = IP.toLong(endIP); var endIPInt = IP.toLong(endIP);
return _.find(existingRanges, ([ip1, ip2]) => IP.toLong(ip2) >= startIPInt && IP.toLong(ip1) <= endIPInt); return _.find(existingRanges, ([ip1, ip2]) => {
return IP.toLong(ip2) >= startIPInt && IP.toLong(ip1) <= endIPInt;
});
}, },
validateIpCorrespondsToCIDR(cidr, ip) { validateIpCorrespondsToCIDR(cidr, ip) {
if (!cidr) return true; if (!cidr) return true;
var networkData = IP.cidrSubnet(cidr); var networkData = IP.cidrSubnet(cidr);
var ipInt = IP.toLong(ip); var ipInt = IP.toLong(ip);
return ipInt >= IP.toLong(networkData.firstAddress) && ipInt <= IP.toLong(networkData.lastAddress); return ipInt >= IP.toLong(networkData.firstAddress) &&
ipInt <= IP.toLong(networkData.lastAddress);
}, },
validateVlanRange(vlanStart, vlanEnd, vlan) { validateVlanRange(vlanStart, vlanEnd, vlan) {
return vlan >= vlanStart && vlan <= vlanEnd; return vlan >= vlanStart && vlan <= vlanEnd;

View File

@ -62,10 +62,15 @@ var ClusterPage = React.createClass({
['home', '#'], ['home', '#'],
['environments', '#clusters'], ['environments', '#clusters'],
[cluster.get('name'), '#cluster/' + cluster.get('id'), {skipTranslation: true}], [cluster.get('name'), '#cluster/' + cluster.get('id'), {skipTranslation: true}],
[i18n('cluster_page.tabs.' + pageOptions.activeTab), '#cluster/' + cluster.get('id') + '/' + pageOptions.activeTab, {active: !addScreenBreadcrumb}] [
i18n('cluster_page.tabs.' + pageOptions.activeTab),
'#cluster/' + cluster.get('id') + '/' + pageOptions.activeTab,
{active: !addScreenBreadcrumb}
]
]; ];
if (addScreenBreadcrumb) { if (addScreenBreadcrumb) {
breadcrumbs.push([i18n('cluster_page.nodes_tab.breadcrumbs.' + tabOptions), null, {active: true}]); breadcrumbs.push([i18n('cluster_page.nodes_tab.breadcrumbs.' + tabOptions), null,
{active: true}]);
} }
return breadcrumbs; return breadcrumbs;
}, },
@ -94,7 +99,8 @@ var ClusterPage = React.createClass({
if (currentClusterId == id) { if (currentClusterId == id) {
// just another tab has been chosen, do not load cluster again // just another tab has been chosen, do not load cluster again
cluster = app.page.props.cluster; cluster = app.page.props.cluster;
promise = tab.fetchData ? tab.fetchData({cluster: cluster, tabOptions: tabOptions}) : $.Deferred().resolve(); promise = tab.fetchData ? tab.fetchData({cluster: cluster, tabOptions: tabOptions}) :
$.Deferred().resolve();
} else { } else {
cluster = new models.Cluster({id: id}); cluster = new models.Cluster({id: id});
@ -111,7 +117,8 @@ var ClusterPage = React.createClass({
cluster.set({pluginLinks: pluginLinks}); cluster.set({pluginLinks: pluginLinks});
cluster.get('nodes').fetch = function(options) { cluster.get('nodes').fetch = function(options) {
return this.constructor.__super__.fetch.call(this, _.extend({data: {cluster_id: id}}, options)); return this.constructor.__super__.fetch.call(this,
_.extend({data: {cluster_id: id}}, options));
}; };
promise = $.when( promise = $.when(
cluster.fetch(), cluster.fetch(),
@ -124,12 +131,16 @@ var ClusterPage = React.createClass({
) )
.then(() => { .then(() => {
var networkConfiguration = new models.NetworkConfiguration(); var networkConfiguration = new models.NetworkConfiguration();
networkConfiguration.url = _.result(cluster, 'url') + '/network_configuration/' + cluster.get('net_provider'); networkConfiguration.url = _.result(cluster, 'url') + '/network_configuration/' +
cluster.get('net_provider');
cluster.set({ cluster.set({
networkConfiguration: networkConfiguration, networkConfiguration: networkConfiguration,
release: new models.Release({id: cluster.get('release_id')}) release: new models.Release({id: cluster.get('release_id')})
}); });
return $.when(cluster.get('networkConfiguration').fetch(), cluster.get('release').fetch()); return $.when(
cluster.get('networkConfiguration').fetch(),
cluster.get('release').fetch()
);
}) })
.then(() => { .then(() => {
var useVcenter = cluster.get('settings').get('common.use_vcenter.value'); var useVcenter = cluster.get('settings').get('common.use_vcenter.value');
@ -141,7 +152,8 @@ var ClusterPage = React.createClass({
return vcenter.fetch(); return vcenter.fetch();
}) })
.then(() => { .then(() => {
return tab.fetchData ? tab.fetchData({cluster: cluster, tabOptions: tabOptions}) : $.Deferred().resolve(); return tab.fetchData ? tab.fetchData({cluster: cluster, tabOptions: tabOptions}) :
$.Deferred().resolve();
}); });
} }
return promise.then((data) => { return promise.then((data) => {
@ -232,7 +244,8 @@ var ClusterPage = React.createClass({
var selectedLogs; var selectedLogs;
if (props.tabOptions[0]) { if (props.tabOptions[0]) {
selectedLogs = utils.deserializeTabOptions(_.compact(props.tabOptions).join('/')); selectedLogs = utils.deserializeTabOptions(_.compact(props.tabOptions).join('/'));
selectedLogs.level = selectedLogs.level ? selectedLogs.level.toUpperCase() : props.defaultLogLevel; selectedLogs.level = selectedLogs.level ? selectedLogs.level.toUpperCase() :
props.defaultLogLevel;
this.setState({selectedLogs: selectedLogs}); this.setState({selectedLogs: selectedLogs});
} }
} }
@ -283,7 +296,11 @@ var ClusterPage = React.createClass({
<div className='page-title'> <div className='page-title'>
<h1 className='title'> <h1 className='title'>
{cluster.get('name')} {cluster.get('name')}
<div className='title-node-count'>({i18n('common.node', {count: cluster.get('nodes').length})})</div> <div
className='title-node-count'
>
({i18n('common.node', {count: cluster.get('nodes').length})})
</div>
</h1> </h1>
</div> </div>
<div className='tabs-box'> <div className='tabs-box'>
@ -292,7 +309,12 @@ var ClusterPage = React.createClass({
return ( return (
<a <a
key={url} key={url}
className={url + ' ' + utils.classNames({'cluster-tab': true, active: this.props.activeTab == url})} className={
url + ' ' + utils.classNames({
'cluster-tab': true,
active: this.props.activeTab == url
})
}
href={'#cluster/' + cluster.id + '/' + url} href={'#cluster/' + cluster.id + '/' + url}
> >
<div className='icon'></div> <div className='icon'></div>

View File

@ -21,14 +21,18 @@ import ReactDOM from 'react-dom';
import utils from 'utils'; import utils from 'utils';
import dispatcher from 'dispatcher'; import dispatcher from 'dispatcher';
import {Input, ProgressBar, Tooltip} from 'views/controls'; import {Input, ProgressBar, Tooltip} from 'views/controls';
import {DiscardNodeChangesDialog, DeployChangesDialog, ProvisionVMsDialog, RemoveClusterDialog, ResetEnvironmentDialog, StopDeploymentDialog} from 'views/dialogs'; import {
DiscardNodeChangesDialog, DeployChangesDialog, ProvisionVMsDialog,
RemoveClusterDialog, ResetEnvironmentDialog, StopDeploymentDialog
} from 'views/dialogs';
import {backboneMixin, pollingMixin, renamingMixin} from 'component_mixins'; import {backboneMixin, pollingMixin, renamingMixin} from 'component_mixins';
var namespace = 'cluster_page.dashboard_tab.'; var namespace = 'cluster_page.dashboard_tab.';
var DashboardTab = React.createClass({ var DashboardTab = React.createClass({
mixins: [ mixins: [
// this is needed to somehow handle the case when verification is in progress and user pressed Deploy // this is needed to somehow handle the case when verification
// is in progress and user pressed Deploy
backboneMixin({ backboneMixin({
modelOrCollection: (props) => props.cluster.get('tasks'), modelOrCollection: (props) => props.cluster.get('tasks'),
renderOn: 'update change' renderOn: 'update change'
@ -254,9 +258,13 @@ var DeploymentResult = React.createClass({
<br /> <br />
<span dangerouslySetInnerHTML={{__html: utils.urlify(summary)}} /> <span dangerouslySetInnerHTML={{__html: utils.urlify(summary)}} />
<div className={utils.classNames({'task-result-details': true, hidden: !details})}> <div className={utils.classNames({'task-result-details': true, hidden: !details})}>
<pre className='collapse result-details' dangerouslySetInnerHTML={{__html: utils.urlify(details)}} /> <pre
className='collapse result-details'
dangerouslySetInnerHTML={{__html: utils.urlify(details)}}
/>
<button className='btn-link' data-toggle='collapse' data-target='.result-details'> <button className='btn-link' data-toggle='collapse' data-target='.result-details'>
{this.state.collapsed ? i18n('cluster_page.hide_details_button') : i18n('cluster_page.show_details_button')} {this.state.collapsed ? i18n('cluster_page.hide_details_button') :
i18n('cluster_page.show_details_button')}
</button> </button>
</div> </div>
</div> </div>
@ -288,14 +296,29 @@ var DocumentationLinks = React.createClass({
<div className='documentation col-xs-12'> <div className='documentation col-xs-12'>
{isMirantisIso ? {isMirantisIso ?
[ [
this.renderDocumentationLinks('https://www.mirantis.com/openstack-documentation/', 'mos_documentation'), this.renderDocumentationLinks(
this.renderDocumentationLinks(utils.composeDocumentationLink('plugin-dev.html#plugin-dev'), 'plugin_documentation'), 'https://www.mirantis.com/openstack-documentation/',
this.renderDocumentationLinks('https://software.mirantis.com/mirantis-openstack-technical-bulletins/', 'technical_bulletins') 'mos_documentation'
),
this.renderDocumentationLinks(
utils.composeDocumentationLink('plugin-dev.html#plugin-dev'),
'plugin_documentation'
),
this.renderDocumentationLinks(
'https://software.mirantis.com/mirantis-openstack-technical-bulletins/',
'technical_bulletins'
)
] ]
: :
[ [
this.renderDocumentationLinks('http://docs.openstack.org/', 'openstack_documentation'), this.renderDocumentationLinks(
this.renderDocumentationLinks('https://wiki.openstack.org/wiki/Fuel/Plugins', 'plugin_documentation') 'http://docs.openstack.org/',
'openstack_documentation'
),
this.renderDocumentationLinks(
'https://wiki.openstack.org/wiki/Fuel/Plugins',
'plugin_documentation'
)
] ]
} }
</div> </div>
@ -308,7 +331,8 @@ var DocumentationLinks = React.createClass({
// it should be refactored to provide proper logics separation and decoupling // it should be refactored to provide proper logics separation and decoupling
var DeployReadinessBlock = React.createClass({ var DeployReadinessBlock = React.createClass({
mixins: [ mixins: [
// this is needed to somehow handle the case when verification is in progress and user pressed Deploy // this is needed to somehow handle the case when verification
// is in progress and user pressed Deploy
backboneMixin({ backboneMixin({
modelOrCollection(props) { modelOrCollection(props) {
return props.cluster.get('tasks'); return props.cluster.get('tasks');
@ -332,7 +356,8 @@ var DeployReadinessBlock = React.createClass({
validate(cluster) { validate(cluster) {
return _.reduce( return _.reduce(
this.validations, this.validations,
(accumulator, validator) => _.merge(accumulator, validator.call(this, cluster), (a, b) => a.concat(_.compact(b))), (accumulator, validator) => _.merge(accumulator, validator.call(this, cluster), (a, b) =>
a.concat(_.compact(b))),
{blocker: [], error: [], warning: []} {blocker: [], error: [], warning: []}
); );
}, },
@ -398,9 +423,16 @@ var DeployReadinessBlock = React.createClass({
function(cluster) { function(cluster) {
var configModels = this.getConfigModels(); var configModels = this.getConfigModels();
var roleModels = cluster.get('roles'); var roleModels = cluster.get('roles');
var validRoleModels = roleModels.filter((role) => !role.checkRestrictions(configModels).result); var validRoleModels = roleModels.filter((role) => {
var limitValidations = _.zipObject(validRoleModels.map((role) => [role.get('name'), role.checkLimits(configModels, cluster.get('nodes'))])); return !role.checkRestrictions(configModels).result;
var limitRecommendations = _.zipObject(validRoleModels.map((role) => [role.get('name'), role.checkLimits(configModels, cluster.get('nodes'), true, ['recommended'])])); });
var limitValidations = _.zipObject(validRoleModels.map((role) => {
return [role.get('name'), role.checkLimits(configModels, cluster.get('nodes'))];
}));
var limitRecommendations = _.zipObject(validRoleModels.map((role) => {
return [role.get('name'), role.checkLimits(configModels, cluster.get('nodes'), true,
['recommended'])];
}));
return { return {
blocker: roleModels.map((role) => { blocker: roleModels.map((role) => {
var name = role.get('name'); var name = role.get('name');
@ -461,7 +493,9 @@ var DeployReadinessBlock = React.createClass({
var nodes = cluster.get('nodes'); var nodes = cluster.get('nodes');
var alerts = this.validate(cluster); var alerts = this.validate(cluster);
var isDeploymentPossible = cluster.isDeploymentPossible() && !alerts.blocker.length; var isDeploymentPossible = cluster.isDeploymentPossible() && !alerts.blocker.length;
var isVMsProvisioningAvailable = nodes.any((node) => node.get('pending_addition') && node.hasRole('virt')); var isVMsProvisioningAvailable = nodes.any((node) => {
return node.get('pending_addition') && node.hasRole('virt');
});
return ( return (
<div className='row'> <div className='row'>
@ -471,9 +505,18 @@ var DeployReadinessBlock = React.createClass({
<div> <div>
<h4>{i18n(namespace + 'changes_header')}</h4> <h4>{i18n(namespace + 'changes_header')}</h4>
<ul> <ul>
{this.renderChangedNodesAmount(nodes.where({pending_addition: true}), 'added_node')} {this.renderChangedNodesAmount(
{this.renderChangedNodesAmount(nodes.where({status: 'provisioned'}), 'provisioned_node')} nodes.where({pending_addition: true}),
{this.renderChangedNodesAmount(nodes.where({pending_deletion: true}), 'deleted_node')} 'added_node'
)}
{this.renderChangedNodesAmount(
nodes.where({status: 'provisioned'}),
'provisioned_node'
)}
{this.renderChangedNodesAmount(
nodes.where({pending_deletion: true}),
'deleted_node'
)}
</ul> </ul>
</div> </div>
} }
@ -489,7 +532,10 @@ var DeployReadinessBlock = React.createClass({
<button <button
className={utils.classNames({ className={utils.classNames({
'btn btn-primary deploy-btn': true, 'btn btn-primary deploy-btn': true,
'btn-warning': _.isEmpty(alerts.blocker) && (!_.isEmpty(alerts.error) || !_.isEmpty(alerts.warning)) 'btn-warning': (
_.isEmpty(alerts.blocker) &&
(!_.isEmpty(alerts.error) || !_.isEmpty(alerts.warning))
)
})} })}
onClick={_.partial(this.showDialog, DeployChangesDialog)} onClick={_.partial(this.showDialog, DeployChangesDialog)}
disabled={!isDeploymentPossible} disabled={!isDeploymentPossible}
@ -652,7 +698,8 @@ var ClusterInfo = React.createClass({
}, },
renderLegend(fieldsData, isRole) { renderLegend(fieldsData, isRole) {
var result = _.map(fieldsData, (field) => { var result = _.map(fieldsData, (field) => {
var numberOfNodes = isRole ? this.getNumberOfNodesWithRole(field) : this.getNumberOfNodesWithStatus(field); var numberOfNodes = isRole ? this.getNumberOfNodesWithRole(field) :
this.getNumberOfNodesWithStatus(field);
return numberOfNodes ? return numberOfNodes ?
<div key={field}> <div key={field}>
<div className='col-xs-10'> <div className='col-xs-10'>
@ -683,8 +730,10 @@ var ClusterInfo = React.createClass({
renderStatistics() { renderStatistics() {
var hasNodes = !!this.props.cluster.get('nodes').length; var hasNodes = !!this.props.cluster.get('nodes').length;
var fieldRoles = _.union(['total'], this.props.cluster.get('roles').pluck('name')); var fieldRoles = _.union(['total'], this.props.cluster.get('roles').pluck('name'));
var fieldStatuses = ['offline', 'error', 'pending_addition', 'pending_deletion', 'ready', 'provisioned', var fieldStatuses = [
'provisioning', 'deploying', 'removing']; 'offline', 'error', 'pending_addition', 'pending_deletion', 'ready',
'provisioned', 'provisioning', 'deploying', 'removing'
];
return ( return (
<div className='row statistics-block'> <div className='row statistics-block'>
<div className='title'>{i18n(namespace + 'cluster_info_fields.statistics')}</div> <div className='title'>{i18n(namespace + 'cluster_info_fields.statistics')}</div>
@ -905,7 +954,8 @@ var ResetEnvironmentAction = React.createClass({
<Tooltip <Tooltip
key='reset-tooltip' key='reset-tooltip'
placement='right' placement='right'
text={!isLocked ? i18n(namespace + 'reset_environment_warning') : i18n(namespace + this.getDescriptionKey())} text={!isLocked ? i18n(namespace + 'reset_environment_warning') :
i18n(namespace + this.getDescriptionKey())}
> >
<i className='glyphicon glyphicon-info-sign' /> <i className='glyphicon glyphicon-info-sign' />
</Tooltip> </Tooltip>

View File

@ -95,14 +95,14 @@ var HealthcheckTabContent = React.createClass({
return { return {
actionInProgress: false, actionInProgress: false,
credentialsVisible: null, credentialsVisible: null,
credentials: _.transform(this.props.cluster.get('settings').get('access'), (result, value, key) => { credentials: _.transform(this.props.cluster.get('settings').get('access'),
result[key] = value.value; (result, value, key) => result[key] = value.value)
})
}; };
}, },
isLocked() { isLocked() {
var cluster = this.props.cluster; var cluster = this.props.cluster;
return cluster.get('status') != 'operational' || !!cluster.task({group: 'deployment', active: true}); return cluster.get('status') != 'operational' || !!cluster.task({group: 'deployment',
active: true});
}, },
getNumberOfCheckedTests() { getNumberOfCheckedTests() {
return this.props.tests.where({checked: true}).length; return this.props.tests.where({checked: true}).length;
@ -248,14 +248,17 @@ var HealthcheckTabContent = React.createClass({
} }
<div> <div>
{(this.props.cluster.get('status') == 'new') && {(this.props.cluster.get('status') == 'new') &&
<div className='alert alert-warning'>{i18n('cluster_page.healthcheck_tab.deploy_alert')}</div> <div className='alert alert-warning'>
{i18n('cluster_page.healthcheck_tab.deploy_alert')}
</div>
} }
<div key='testsets'> <div key='testsets'>
{this.props.testsets.map((testset) => { {this.props.testsets.map((testset) => {
return <TestSet return <TestSet
key={testset.id} key={testset.id}
testset={testset} testset={testset}
testrun={this.props.testruns.findWhere({testset: testset.id}) || new models.TestRun({testset: testset.id})} testrun={this.props.testruns.findWhere({testset: testset.id}) ||
new models.TestRun({testset: testset.id})}
tests={new Backbone.Collection(this.props.tests.where({testset: testset.id}))} tests={new Backbone.Collection(this.props.tests.where({testset: testset.id}))}
disabled={disabledState || hasRunningTests} disabled={disabledState || hasRunningTests}
/>; />;
@ -312,7 +315,10 @@ var TestSet = React.createClass({
this.props.tests.invoke('on', 'change:checked', this.updateTestsetCheckbox, this); this.props.tests.invoke('on', 'change:checked', this.updateTestsetCheckbox, this);
}, },
updateTestsetCheckbox() { updateTestsetCheckbox() {
this.props.testset.set('checked', this.props.tests.where({checked: true}).length == this.props.tests.length); this.props.testset.set(
'checked',
this.props.tests.where({checked: true}).length == this.props.tests.length
);
}, },
render() { render() {
var classes = { var classes = {
@ -434,7 +440,8 @@ var Test = React.createClass({
</td> </td>
<td className='healthcheck-col-status'> <td className='healthcheck-col-status'>
<div className={currentStatusClassName}> <div className={currentStatusClassName}>
{iconClasses[status] ? <i className={iconClasses[status]} /> : String.fromCharCode(0x2014)} {iconClasses[status] ? <i className={iconClasses[status]} /> :
String.fromCharCode(0x2014)}
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -67,9 +67,11 @@ var LogsTab = React.createClass({
}, },
showLogs(params) { showLogs(params) {
this.stopPolling(); this.stopPolling();
var logOptions = this.props.selectedLogs.type == 'remote' ? _.extend({}, this.props.selectedLogs) : _.omit(this.props.selectedLogs, 'node'); var logOptions = this.props.selectedLogs.type == 'remote' ?
_.extend({}, this.props.selectedLogs) : _.omit(this.props.selectedLogs, 'node');
logOptions.level = logOptions.level.toLowerCase(); logOptions.level = logOptions.level.toLowerCase();
app.navigate('#cluster/' + this.props.cluster.id + '/logs/' + utils.serializeTabOptions(logOptions), {trigger: false, replace: true}); app.navigate('#cluster/' + this.props.cluster.id + '/logs/' +
utils.serializeTabOptions(logOptions), {trigger: false, replace: true});
params = params || {}; params = params || {};
this.fetchLogs(params) this.fetchLogs(params)
.done((data) => { .done((data) => {
@ -131,7 +133,8 @@ var LogsTab = React.createClass({
}); });
var LogFilterBar = React.createClass({ var LogFilterBar = React.createClass({
// PureRenderMixin added for prevention the rerender LogFilterBar (because of polling) in Mozilla browser // PureRenderMixin added for prevention the rerender LogFilterBar
// (because of polling) in Mozilla browser
mixins: [PureRenderMixin], mixins: [PureRenderMixin],
getInitialState() { getInitialState() {
return _.extend({}, this.props.selectedLogs, { return _.extend({}, this.props.selectedLogs, {
@ -150,9 +153,13 @@ var LogFilterBar = React.createClass({
: :
this.sources.fetch(); this.sources.fetch();
this.sources.deferred.done(() => { this.sources.deferred.done(() => {
var filteredSources = this.sources.filter((source) => source.get('remote') == (type != 'local')); var filteredSources = this.sources.filter((source) => {
var chosenSource = _.findWhere(filteredSources, {id: this.state.source}) || _.first(filteredSources); return source.get('remote') == (type != 'local');
var chosenLevelId = chosenSource ? _.contains(chosenSource.get('levels'), this.state.level) ? this.state.level : _.first(chosenSource.get('levels')) : null; });
var chosenSource = _.findWhere(filteredSources, {id: this.state.source}) ||
_.first(filteredSources);
var chosenLevelId = chosenSource ? _.contains(chosenSource.get('levels'), this.state.level) ?
this.state.level : _.first(chosenSource.get('levels')) : null;
this.setState({ this.setState({
type: type, type: type,
sources: this.sources, sources: this.sources,
@ -168,7 +175,8 @@ var LogFilterBar = React.createClass({
type: type, type: type,
sources: {}, sources: {},
sourcesLoadingState: 'fail', sourcesLoadingState: 'fail',
sourcesLoadingError: utils.getResponseText(response, i18n('cluster_page.logs_tab.source_alert')), sourcesLoadingError: utils.getResponseText(response,
i18n('cluster_page.logs_tab.source_alert')),
locked: false locked: false
}); });
}); });
@ -310,7 +318,8 @@ var LogFilterBar = React.createClass({
</div>; </div>;
}, },
renderSourceSelect() { renderSourceSelect() {
var sourceOptions = this.state.type == 'local' ? this.getLocalSources() : this.getRemoteSources(); var sourceOptions = this.state.type == 'local' ? this.getLocalSources() :
this.getRemoteSources();
return <div className='col-md-2 col-sm-3'> return <div className='col-md-2 col-sm-3'>
<Input <Input
type='select' type='select'
@ -397,7 +406,13 @@ var LogsTable = React.createClass({
<span>{i18n('cluster_page.logs_tab.bottom_text')}</span>: <span>{i18n('cluster_page.logs_tab.bottom_text')}</span>:
{ {
[100, 500, 1000, 5000].map((count) => { [100, 500, 1000, 5000].map((count) => {
return <button className='btn btn-link show-more-entries' onClick={_.bind(this.handleShowMoreClick, this, count)} key={count}>{count}</button>; return <button
key={count}
className='btn btn-link show-more-entries'
onClick={_.bind(this.handleShowMoreClick, this, count)}
>
{count}
</button>;
}) })
} }
</div> </div>

View File

@ -30,7 +30,10 @@ import CSSTransitionGroup from 'react-addons-transition-group';
var parametersNS = 'cluster_page.network_tab.networking_parameters.'; var parametersNS = 'cluster_page.network_tab.networking_parameters.';
var networkTabNS = 'cluster_page.network_tab.'; var networkTabNS = 'cluster_page.network_tab.';
var defaultNetworkSubtabs = ['neutron_l2', 'neutron_l3', 'network_settings', 'network_verification', 'nova_configuration']; var defaultNetworkSubtabs = [
'neutron_l2', 'neutron_l3', 'network_settings',
'network_verification', 'nova_configuration'
];
var NetworkModelManipulationMixin = { var NetworkModelManipulationMixin = {
setValue(attribute, value, options) { setValue(attribute, value, options) {
@ -98,7 +101,8 @@ var NetworkInputsMixin = {
var error; var error;
if (this.props.network) { if (this.props.network) {
try { try {
error = validationError.networks[this.props.currentNodeNetworkGroup.id][this.props.network.id][attribute]; error = validationError
.networks[this.props.currentNodeNetworkGroup.id][this.props.network.id][attribute];
} catch (e) {} } catch (e) {}
return error || null; return error || null;
} }
@ -139,7 +143,11 @@ var Range = React.createClass({
componentDidUpdate() { componentDidUpdate() {
// this glitch is needed to fix // this glitch is needed to fix
// when pressing '+' or '-' buttons button remains focused // when pressing '+' or '-' buttons button remains focused
if (this.props.extendable && this.state.elementToFocus && this.getModel().get(this.props.name).length) { if (
this.props.extendable &&
this.state.elementToFocus &&
this.getModel().get(this.props.name).length
) {
$(this.refs[this.state.elementToFocus].getInputDOMNode()).focus(); $(this.refs[this.state.elementToFocus].getInputDOMNode()).focus();
this.setState({elementToFocus: null}); this.setState({elementToFocus: null});
} }
@ -244,7 +252,10 @@ var Range = React.createClass({
onFocus={_.partial(this.autoCompleteIPRange, rangeError && rangeError.start, range[0])} onFocus={_.partial(this.autoCompleteIPRange, rangeError && rangeError.start, range[0])}
disabled={this.props.disabled || !!this.props.autoIncreaseWith} disabled={this.props.disabled || !!this.props.autoIncreaseWith}
placeholder={rangeError.end ? '' : this.props.placeholder} placeholder={rangeError.end ? '' : this.props.placeholder}
extraContent={!this.props.hiddenControls && this.renderRangeControls(attributeName, index, ranges.length)} extraContent={
!this.props.hiddenControls &&
this.renderRangeControls(attributeName, index, ranges.length)
}
/> />
<div className='validation-error text-danger pull-left'> <div className='validation-error text-danger pull-left'>
<span className='help-inline'> <span className='help-inline'>
@ -306,7 +317,8 @@ var Range = React.createClass({
<div className='col-xs-12'> <div className='col-xs-12'>
<label>{this.props.label}</label> <label>{this.props.label}</label>
{ {
// TODO: renderExtendableRanges & renderRanges methods should be refactored to avoid copy-paste // TODO: renderExtendableRanges & renderRanges methods
// should be refactored to avoid copy-paste
this.props.extendable ? this.props.extendable ?
this.renderExtendableRanges({error, attributeName, ranges, verificationError}) this.renderExtendableRanges({error, attributeName, ranges, verificationError})
: :
@ -546,7 +558,8 @@ var NetworkTab = React.createClass({
configModels: { configModels: {
cluster: this.props.cluster, cluster: this.props.cluster,
settings: settings, settings: settings,
networking_parameters: this.props.cluster.get('networkConfiguration').get('networking_parameters'), networking_parameters:
this.props.cluster.get('networkConfiguration').get('networking_parameters'),
version: app.version, version: app.version,
release: this.props.cluster.get('release'), release: this.props.cluster.get('release'),
default: settings default: settings
@ -560,7 +573,11 @@ var NetworkTab = React.createClass({
componentDidMount() { componentDidMount() {
this.props.cluster.get('networkConfiguration').isValid(); this.props.cluster.get('networkConfiguration').isValid();
this.props.cluster.get('settings').isValid({models: this.state.configModels}); this.props.cluster.get('settings').isValid({models: this.state.configModels});
this.props.cluster.get('tasks').on('change:status change:unsaved', this.destroyUnsavedNetworkVerificationTask, this); this.props.cluster.get('tasks').on(
'change:status change:unsaved',
this.destroyUnsavedNetworkVerificationTask,
this
);
}, },
componentWillUnmount() { componentWillUnmount() {
this.loadInitialConfiguration(); this.loadInitialConfiguration();
@ -583,10 +600,16 @@ var NetworkTab = React.createClass({
clusterTasks.each((task) => task.get('unsaved') && clusterTasks.remove(task)); clusterTasks.each((task) => task.get('unsaved') && clusterTasks.remove(task));
}, },
isNetworkConfigurationChanged() { isNetworkConfigurationChanged() {
return !_.isEqual(this.state.initialConfiguration, this.props.cluster.get('networkConfiguration').toJSON()); return !_.isEqual(
this.state.initialConfiguration,
this.props.cluster.get('networkConfiguration').toJSON()
);
}, },
isNetworkSettingsChanged() { isNetworkSettingsChanged() {
return this.props.cluster.get('settings').hasChanges(this.state.initialSettingsAttributes, this.state.configModels); return this.props.cluster.get('settings').hasChanges(
this.state.initialSettingsAttributes,
this.state.configModels
);
}, },
hasChanges() { hasChanges() {
return this.isNetworkConfigurationChanged() || this.isNetworkSettingsChanged(); return this.isNetworkConfigurationChanged() || this.isNetworkSettingsChanged();
@ -602,17 +625,26 @@ var NetworkTab = React.createClass({
}, },
loadInitialConfiguration() { loadInitialConfiguration() {
var networkConfiguration = this.props.cluster.get('networkConfiguration'); var networkConfiguration = this.props.cluster.get('networkConfiguration');
networkConfiguration.get('networks').reset(_.cloneDeep(this.state.initialConfiguration.networks)); networkConfiguration.get('networks').reset(
networkConfiguration.get('networking_parameters').set(_.cloneDeep(this.state.initialConfiguration.networking_parameters)); _.cloneDeep(this.state.initialConfiguration.networks)
);
networkConfiguration.get('networking_parameters').set(
_.cloneDeep(this.state.initialConfiguration.networking_parameters)
);
}, },
loadInitialSettings() { loadInitialSettings() {
var settings = this.props.cluster.get('settings'); var settings = this.props.cluster.get('settings');
settings.set(_.cloneDeep(this.state.initialSettingsAttributes), {silent: true, validate: false}); settings.set(
_.cloneDeep(this.state.initialSettingsAttributes),
{silent: true, validate: false}
);
settings.mergePluginSettings(); settings.mergePluginSettings();
settings.isValid({models: this.state.configModels}); settings.isValid({models: this.state.configModels});
}, },
updateInitialConfiguration() { updateInitialConfiguration() {
this.setState({initialConfiguration: _.cloneDeep(this.props.cluster.get('networkConfiguration').toJSON())}); this.setState({
initialConfiguration: _.cloneDeep(this.props.cluster.get('networkConfiguration').toJSON())
});
}, },
isLocked() { isLocked() {
return !!this.props.cluster.task({group: ['deployment', 'network'], active: true}) || return !!this.props.cluster.task({group: ['deployment', 'network'], active: true}) ||
@ -630,13 +662,16 @@ var NetworkTab = React.createClass({
}); });
var floatingRanges = networkConfiguration.get('networking_parameters').get('floating_ranges'); var floatingRanges = networkConfiguration.get('networking_parameters').get('floating_ranges');
if (floatingRanges) { if (floatingRanges) {
networkConfiguration.get('networking_parameters').set({floating_ranges: removeEmptyRanges(floatingRanges)}); networkConfiguration.get('networking_parameters').set({
floating_ranges: removeEmptyRanges(floatingRanges)
});
} }
}, },
onManagerChange(name, value) { onManagerChange(name, value) {
var networkConfiguration = this.props.cluster.get('networkConfiguration'); var networkConfiguration = this.props.cluster.get('networkConfiguration');
var networkingParameters = networkConfiguration.get('networking_parameters'); var networkingParameters = networkConfiguration.get('networking_parameters');
var fixedAmount = networkConfiguration.get('networking_parameters').get('fixed_networks_amount') || 1; var fixedAmount =
networkConfiguration.get('networking_parameters').get('fixed_networks_amount') || 1;
networkingParameters.set({ networkingParameters.set({
net_manager: value, net_manager: value,
fixed_networks_amount: value == 'FlatDHCPManager' ? 1 : fixedAmount fixed_networks_amount: value == 'FlatDHCPManager' ? 1 : fixedAmount
@ -770,7 +805,8 @@ var NetworkTab = React.createClass({
_.isNull(this.props.cluster.get('settings').validationError); _.isNull(this.props.cluster.get('settings').validationError);
}, },
renderButtons() { renderButtons() {
var isCancelChangesDisabled = this.state.actionInProgress || !!this.props.cluster.task({group: 'deployment', active: true}) || !this.hasChanges(); var isCancelChangesDisabled = this.state.actionInProgress ||
!!this.props.cluster.task({group: 'deployment', active: true}) || !this.hasChanges();
return ( return (
<div className='well clearfix'> <div className='well clearfix'>
<div className='btn-group pull-right'> <div className='btn-group pull-right'>
@ -795,7 +831,8 @@ var NetworkTab = React.createClass({
); );
}, },
getVerificationErrors() { getVerificationErrors() {
var task = this.state.hideVerificationResult ? null : this.props.cluster.task({group: 'network', status: 'error'}); var task = this.state.hideVerificationResult ? null :
this.props.cluster.task({group: 'network', status: 'error'});
var fieldsWithVerificationErrors = []; var fieldsWithVerificationErrors = [];
// @TODO(morale): soon response format will be changed and this part should be rewritten // @TODO(morale): soon response format will be changed and this part should be rewritten
if (task && task.get('result').length) { if (task && task.get('result').length) {
@ -816,7 +853,9 @@ var NetworkTab = React.createClass({
showUnsavedChangesWarning: this.hasChanges() showUnsavedChangesWarning: this.hasChanges()
}) })
.done(() => { .done(() => {
this.props.setActiveNetworkSectionName(this.nodeNetworkGroups.find({is_default: true}).get('name')); this.props.setActiveNetworkSectionName(
this.nodeNetworkGroups.find({is_default: true}).get('name')
);
return nodeNetworkGroup return nodeNetworkGroup
.destroy({wait: true}) .destroy({wait: true})
.then( .then(
@ -833,7 +872,11 @@ var NetworkTab = React.createClass({
if (hasChanges) { if (hasChanges) {
utils.showErrorDialog({ utils.showErrorDialog({
title: i18n(networkTabNS + 'node_network_group_creation_error'), title: i18n(networkTabNS + 'node_network_group_creation_error'),
message: <div><i className='glyphicon glyphicon-danger-sign' /> {i18n(networkTabNS + 'save_changes_warning')}</div> message: <div>
<i className='glyphicon glyphicon-danger-sign' />
{' '}
{i18n(networkTabNS + 'save_changes_warning')}
</div>
}); });
return; return;
} }
@ -879,7 +922,8 @@ var NetworkTab = React.createClass({
row: true, row: true,
'changes-locked': isLocked 'changes-locked': isLocked
}; };
var nodeNetworkGroups = this.nodeNetworkGroups = new models.NodeNetworkGroups(this.props.nodeNetworkGroups.where({cluster_id: cluster.id})); var nodeNetworkGroups = this.nodeNetworkGroups =
new models.NodeNetworkGroups(this.props.nodeNetworkGroups.where({cluster_id: cluster.id}));
var isNovaEnvironment = cluster.get('net_provider') == 'nova_network'; var isNovaEnvironment = cluster.get('net_provider') == 'nova_network';
var networks = networkConfiguration.get('networks'); var networks = networkConfiguration.get('networks');
var isMultiRack = nodeNetworkGroups.length > 1; var isMultiRack = nodeNetworkGroups.length > 1;
@ -922,7 +966,10 @@ var NetworkTab = React.createClass({
key='add_node_group' key='add_node_group'
className='btn btn-default add-nodegroup-btn pull-right' className='btn btn-default add-nodegroup-btn pull-right'
onClick={_.partial(this.addNodeNetworkGroup, hasChanges)} onClick={_.partial(this.addNodeNetworkGroup, hasChanges)}
disabled={!!cluster.task({group: ['deployment', 'network'], active: true}) || this.state.actionInProgress} disabled={
!!cluster.task({group: ['deployment', 'network'], active: true}) ||
this.state.actionInProgress
}
> >
{hasChanges && <i className='glyphicon glyphicon-danger-sign'/>} {hasChanges && <i className='glyphicon glyphicon-danger-sign'/>}
{i18n(networkTabNS + 'add_node_network_group')} {i18n(networkTabNS + 'add_node_network_group')}
@ -1008,7 +1055,8 @@ var NetworkTab = React.createClass({
</div> </div>
</div> </div>
</div> </div>
{!this.state.hideVerificationResult && networkCheckTask && networkCheckTask.match({status: 'error'}) && {!this.state.hideVerificationResult && networkCheckTask &&
networkCheckTask.match({status: 'error'}) &&
<div className='col-xs-12'> <div className='col-xs-12'>
<div className='alert alert-danger enable-selection col-xs-12 network-alert'> <div className='alert alert-danger enable-selection col-xs-12 network-alert'>
{utils.renderMultilineText(networkCheckTask.get('message'))} {utils.renderMultilineText(networkCheckTask.get('message'))}
@ -1025,7 +1073,10 @@ var NetworkTab = React.createClass({
var NodeNetworkGroup = React.createClass({ var NodeNetworkGroup = React.createClass({
render() { render() {
var {cluster, networks, nodeNetworkGroup, nodeNetworkGroups, verificationErrors, validationError} = this.props; var {
cluster, networks, nodeNetworkGroup, nodeNetworkGroups,
verificationErrors, validationError
} = this.props;
return ( return (
<div> <div>
<NodeNetworkGroupTitle <NodeNetworkGroupTitle
@ -1044,7 +1095,9 @@ var NodeNetworkGroup = React.createClass({
cluster={cluster} cluster={cluster}
validationError={(validationError || {}).networks} validationError={(validationError || {}).networks}
disabled={this.props.locked} disabled={this.props.locked}
verificationErrorField={_.pluck(_.where(verificationErrors, {network: network.id}), 'field')} verificationErrorField={
_.pluck(_.where(verificationErrors, {network: network.id}), 'field')
}
currentNodeNetworkGroup={nodeNetworkGroup} currentNodeNetworkGroup={nodeNetworkGroup}
/> />
); );
@ -1069,11 +1122,20 @@ var NetworkSubtabs = React.createClass({
// is one of predefined sections selected (networking_parameters) // is one of predefined sections selected (networking_parameters)
if (groupName == 'neutron_l2') { if (groupName == 'neutron_l2') {
isInvalid = !!_.intersection(NetworkingL2Parameters.renderedParameters, _.keys(networkParametersErrors)).length; isInvalid = !!_.intersection(
NetworkingL2Parameters.renderedParameters,
_.keys(networkParametersErrors)
).length;
} else if (groupName == 'neutron_l3') { } else if (groupName == 'neutron_l3') {
isInvalid = !!_.intersection(NetworkingL3Parameters.renderedParameters, _.keys(networkParametersErrors)).length; isInvalid = !!_.intersection(
NetworkingL3Parameters.renderedParameters,
_.keys(networkParametersErrors)
).length;
} else if (groupName == 'nova_configuration') { } else if (groupName == 'nova_configuration') {
isInvalid = !!_.intersection(NovaParameters.renderedParameters, _.keys(networkParametersErrors)).length; isInvalid = !!_.intersection(
NovaParameters.renderedParameters,
_.keys(networkParametersErrors)
).length;
} else if (groupName == 'network_settings') { } else if (groupName == 'network_settings') {
var settings = cluster.get('settings'); var settings = cluster.get('settings');
isInvalid = _.any(_.keys(settings.validationError), (settingPath) => { isInvalid = _.any(_.keys(settings.validationError), (settingPath) => {
@ -1084,7 +1146,8 @@ var NetworkSubtabs = React.createClass({
} }
if (isNetworkGroupPill) { if (isNetworkGroupPill) {
isInvalid = networksErrors && (isNovaEnvironment || !!networksErrors[nodeNetworkGroups.findWhere({name: groupName}).id]); isInvalid = networksErrors && (isNovaEnvironment ||
!!networksErrors[nodeNetworkGroups.findWhere({name: groupName}).id]);
} else { } else {
tabLabel = i18n(networkTabNS + 'tabs.' + groupName); tabLabel = i18n(networkTabNS + 'tabs.' + groupName);
} }
@ -1233,10 +1296,15 @@ var NodeNetworkGroupTitle = React.createClass({
} }
{isDeletionPossible && ( {isDeletionPossible && (
currentNodeNetworkGroup.get('is_default') ? currentNodeNetworkGroup.get('is_default') ?
<span className='explanation'>{i18n(networkTabNS + 'default_node_network_group_info')}</span> <span className='explanation'>
{i18n(networkTabNS + 'default_node_network_group_info')}
</span>
: :
!this.state.isRenaming && !this.state.isRenaming &&
<i className='glyphicon glyphicon-remove' onClick={this.props.removeNodeNetworkGroup} /> <i
className='glyphicon glyphicon-remove'
onClick={this.props.removeNodeNetworkGroup}
/>
)} )}
</div> </div>
); );
@ -1339,7 +1407,9 @@ var NovaParameters = React.createClass({
wrapperClassName='clearfix vlan-id-range' wrapperClassName='clearfix vlan-id-range'
label={i18n(parametersNS + 'fixed_vlan_range')} label={i18n(parametersNS + 'fixed_vlan_range')}
extendable={false} extendable={false}
autoIncreaseWith={parseInt(networkingParameters.get('fixed_networks_amount'), 10) || 0} autoIncreaseWith={
parseInt(networkingParameters.get('fixed_networks_amount'), 10) || 0
}
integerValue integerValue
placeholder='' placeholder=''
mini mini
@ -1368,12 +1438,18 @@ var NetworkingL2Parameters = React.createClass({
] ]
}, },
render() { render() {
var networkParameters = this.props.cluster.get('networkConfiguration').get('networking_parameters'); var networkParameters =
this.props.cluster.get('networkConfiguration').get('networking_parameters');
var idRangePrefix = networkParameters.get('segmentation_type') == 'vlan' ? 'vlan' : 'gre_id'; var idRangePrefix = networkParameters.get('segmentation_type') == 'vlan' ? 'vlan' : 'gre_id';
return ( return (
<div className='forms-box' key='neutron-l2'> <div className='forms-box' key='neutron-l2'>
<h3 className='networks'>{i18n(parametersNS + 'l2_configuration')}</h3> <h3 className='networks'>{i18n(parametersNS + 'l2_configuration')}</h3>
<div className='network-description'>{i18n(networkTabNS + 'networking_parameters.l2_' + networkParameters.get('segmentation_type') + '_description')}</div> <div className='network-description'>
{
i18n(networkTabNS + 'networking_parameters.l2_' +
networkParameters.get('segmentation_type') + '_description')
}
</div>
<div> <div>
<Range <Range
{...this.composeProps(idRangePrefix + '_range', true)} {...this.composeProps(idRangePrefix + '_range', true)}
@ -1429,9 +1505,13 @@ var NetworkingL3Parameters = React.createClass({
{networks.findWhere({name: 'baremetal'}) && {networks.findWhere({name: 'baremetal'}) &&
<div className='forms-box' key='baremetal-net'> <div className='forms-box' key='baremetal-net'>
<h3> <h3>
<span className='subtab-group-baremetal-net'>{i18n(networkTabNS + 'baremetal_net')}</span> <span className='subtab-group-baremetal-net'>
{i18n(networkTabNS + 'baremetal_net')}
</span>
</h3> </h3>
<div className='network-description'>{i18n(networkTabNS + 'networking_parameters.baremetal_parameters_description')}</div> <div className='network-description'>
{i18n(networkTabNS + 'networking_parameters.baremetal_parameters_description')}
</div>
<Range <Range
key='baremetal_range' key='baremetal_range'
{...this.composeProps('baremetal_range', true)} {...this.composeProps('baremetal_range', true)}
@ -1443,9 +1523,13 @@ var NetworkingL3Parameters = React.createClass({
} }
<div className='forms-box' key='dns-nameservers'> <div className='forms-box' key='dns-nameservers'>
<h3> <h3>
<span className='subtab-group-dns-nameservers'>{i18n(networkTabNS + 'dns_nameservers')}</span> <span className='subtab-group-dns-nameservers'>
{i18n(networkTabNS + 'dns_nameservers')}
</span>
</h3> </h3>
<div className='network-description'>{i18n(networkTabNS + 'networking_parameters.dns_servers_description')}</div> <div className='network-description'>
{i18n(networkTabNS + 'networking_parameters.dns_servers_description')}
</div>
<MultipleValuesInput {...this.composeProps('dns_nameservers', true)} /> <MultipleValuesInput {...this.composeProps('dns_nameservers', true)} />
</div> </div>
</div> </div>
@ -1465,14 +1549,17 @@ var NetworkSettings = React.createClass({
settings.isValid({models: this.props.configModels}); settings.isValid({models: this.props.configModels});
}, },
checkRestrictions(action, setting) { checkRestrictions(action, setting) {
return this.props.cluster.get('settings').checkRestrictions(this.props.configModels, action, setting); return this.props.cluster.get('settings')
.checkRestrictions(this.props.configModels, action, setting);
}, },
render() { render() {
var cluster = this.props.cluster; var cluster = this.props.cluster;
var settings = cluster.get('settings'); var settings = cluster.get('settings');
var locked = this.props.locked || !!cluster.task({group: ['deployment', 'network'], active: true}); var locked = this.props.locked ||
!!cluster.task({group: ['deployment', 'network'], active: true});
var lockedCluster = !cluster.isAvailableForSettingsChanges(); var lockedCluster = !cluster.isAvailableForSettingsChanges();
var allocatedRoles = _.uniq(_.flatten(_.union(cluster.get('nodes').pluck('roles'), cluster.get('nodes').pluck('pending_roles')))); var allocatedRoles = _.uniq(_.flatten(_.union(cluster.get('nodes').pluck('roles'),
cluster.get('nodes').pluck('pending_roles'))));
return ( return (
<div className='forms-box network'> <div className='forms-box network'>
{ {
@ -1481,7 +1568,8 @@ var NetworkSettings = React.createClass({
.filter( .filter(
(sectionName) => { (sectionName) => {
var section = settings.get(sectionName); var section = settings.get(sectionName);
return (section.metadata.group == 'network' || _.any(section, {group: 'network'})) && return (section.metadata.group == 'network' ||
_.any(section, {group: 'network'})) &&
!this.checkRestrictions('hide', section.metadata).result; !this.checkRestrictions('hide', section.metadata).result;
} }
) )
@ -1501,7 +1589,10 @@ var NetworkSettings = React.createClass({
})); }));
if (_.isEmpty(settingsToDisplay) && !settings.isPlugin(section)) return null; if (_.isEmpty(settingsToDisplay) && !settings.isPlugin(section)) return null;
return <SettingSection return <SettingSection
{... _.pick(this.props, 'cluster', 'initialAttributes', 'settingsForChecks', 'configModels')} {... _.pick(
this.props,
'cluster', 'initialAttributes', 'settingsForChecks', 'configModels'
)}
key={sectionName} key={sectionName}
sectionName={sectionName} sectionName={sectionName}
settingsToDisplay={settingsToDisplay} settingsToDisplay={settingsToDisplay}
@ -1558,7 +1649,11 @@ var NetworkVerificationResult = React.createClass({
<div className='animation-box'> <div className='animation-box'>
{_.times(3, (index) => { {_.times(3, (index) => {
++index; ++index;
return <div key={index} className={this.getConnectionStatus(task, index == 1) + ' connect-' + index}></div>; return <div
key={index}
className={this.getConnectionStatus(task, index == 1) + ' connect-' + index}
>
</div>;
})} })}
</div> </div>
<div className='nodes-box'> <div className='nodes-box'>
@ -1620,7 +1715,12 @@ var NetworkVerificationResult = React.createClass({
var absentVlans = _.map(node.absent_vlans, (vlan) => { var absentVlans = _.map(node.absent_vlans, (vlan) => {
return vlan || i18n(networkTabNS + 'untagged'); return vlan || i18n(networkTabNS + 'untagged');
}); });
return [node.name || 'N/A', node.mac || 'N/A', node.interface, absentVlans.join(', ')]; return [
node.name || 'N/A',
node.mac || 'N/A',
node.interface,
absentVlans.join(', ')
];
}) })
} }
/> />

View File

@ -23,7 +23,8 @@ import ClusterNodesScreen from 'views/cluster_page_tabs/nodes_tab_screens/cluste
import AddNodesScreen from 'views/cluster_page_tabs/nodes_tab_screens/add_nodes_screen'; import AddNodesScreen from 'views/cluster_page_tabs/nodes_tab_screens/add_nodes_screen';
import EditNodesScreen from 'views/cluster_page_tabs/nodes_tab_screens/edit_nodes_screen'; import EditNodesScreen from 'views/cluster_page_tabs/nodes_tab_screens/edit_nodes_screen';
import EditNodeDisksScreen from 'views/cluster_page_tabs/nodes_tab_screens/edit_node_disks_screen'; import EditNodeDisksScreen from 'views/cluster_page_tabs/nodes_tab_screens/edit_node_disks_screen';
import EditNodeInterfacesScreen from 'views/cluster_page_tabs/nodes_tab_screens/edit_node_interfaces_screen'; import EditNodeInterfacesScreen from
'views/cluster_page_tabs/nodes_tab_screens/edit_node_interfaces_screen';
import ReactTransitionGroup from 'react-addons-transition-group'; import ReactTransitionGroup from 'react-addons-transition-group';
var NodesTab = React.createClass({ var NodesTab = React.createClass({
@ -65,7 +66,10 @@ var NodesTab = React.createClass({
}); });
}) })
.fail(() => { .fail(() => {
app.navigate('#cluster/' + this.props.cluster.id + '/nodes', {trigger: true, replace: true}); app.navigate(
'#cluster/' + this.props.cluster.id + '/nodes',
{trigger: true, replace: true}
);
}); });
}, },
getScreen(props) { getScreen(props) {
@ -111,7 +115,10 @@ var NodesTab = React.createClass({
> >
<Screen <Screen
{...this.state.screenData} {...this.state.screenData}
{... _.pick(this.props, 'cluster', 'nodeNetworkGroups', 'selectedNodeIds', 'selectNodes')} {..._.pick(
this.props,
'cluster', 'nodeNetworkGroups', 'selectedNodeIds', 'selectNodes'
)}
ref='screen' ref='screen'
screenOptions={this.state.screenOptions} screenOptions={this.state.screenOptions}
/> />

View File

@ -24,7 +24,8 @@ var AddNodesScreen = React.createClass({
fetchData(options) { fetchData(options) {
var nodes = new models.Nodes(); var nodes = new models.Nodes();
nodes.fetch = function(options) { nodes.fetch = function(options) {
return this.constructor.__super__.fetch.call(this, _.extend({data: {cluster_id: ''}}, options)); return this.constructor.__super__.fetch.call(this, _.extend({data: {cluster_id: ''}},
options));
}; };
return $.when(nodes.fetch(), options.cluster.get('roles').fetch(), return $.when(nodes.fetch(), options.cluster.get('roles').fetch(),
options.cluster.get('settings').fetch({cache: true})).then(() => ({nodes: nodes})); options.cluster.get('settings').fetch({cache: true})).then(() => ({nodes: nodes}));

View File

@ -69,7 +69,10 @@ var EditNodeDisksScreen = React.createClass({
this.setState({initialDisks: _.cloneDeep(this.props.nodes.at(0).disks.toJSON())}); this.setState({initialDisks: _.cloneDeep(this.props.nodes.at(0).disks.toJSON())});
}, },
hasChanges() { hasChanges() {
return !this.isLocked() && !_.isEqual(_.pluck(this.props.disks.toJSON(), 'volumes'), _.pluck(this.state.initialDisks, 'volumes')); return !this.isLocked() && !_.isEqual(
_.pluck(this.props.disks.toJSON(), 'volumes'),
_.pluck(this.state.initialDisks, 'volumes')
);
}, },
loadDefaults() { loadDefaults() {
this.setState({actionInProgress: true}); this.setState({actionInProgress: true});
@ -168,7 +171,13 @@ var EditNodeDisksScreen = React.createClass({
<div className='edit-node-disks-screen'> <div className='edit-node-disks-screen'>
<div className='row'> <div className='row'>
<div className='title'> <div className='title'>
{i18n('cluster_page.nodes_tab.configure_disks.' + (locked ? 'read_only_' : '') + 'title', {count: this.props.nodes.length, name: this.props.nodes.length && this.props.nodes.at(0).get('name')})} {i18n(
'cluster_page.nodes_tab.configure_disks.' + (locked ? 'read_only_' : '') + 'title',
{
count: this.props.nodes.length,
name: this.props.nodes.length && this.props.nodes.at(0).get('name')
}
)}
</div> </div>
<div className='col-xs-12 node-disks'> <div className='col-xs-12 node-disks'>
{this.props.disks.length ? {this.props.disks.length ?
@ -184,22 +193,44 @@ var EditNodeDisksScreen = React.createClass({
}) })
: :
<div className='alert alert-warning'> <div className='alert alert-warning'>
{i18n('cluster_page.nodes_tab.configure_disks.no_disks', {count: this.props.nodes.length})} {i18n('cluster_page.nodes_tab.configure_disks.no_disks',
{count: this.props.nodes.length})}
</div> </div>
} }
</div> </div>
<div className='col-xs-12 page-buttons content-elements'> <div className='col-xs-12 page-buttons content-elements'>
<div className='well clearfix'> <div className='well clearfix'>
<div className='btn-group'> <div className='btn-group'>
<a className='btn btn-default' href={'#cluster/' + this.props.cluster.id + '/nodes'} disabled={this.state.actionInProgress}> <a className='btn btn-default'
href={'#cluster/' + this.props.cluster.id + '/nodes'}
disabled={this.state.actionInProgress}
>
{i18n('cluster_page.nodes_tab.back_to_nodes_button')} {i18n('cluster_page.nodes_tab.back_to_nodes_button')}
</a> </a>
</div> </div>
{!locked && !!this.props.disks.length && {!locked && !!this.props.disks.length &&
<div className='btn-group pull-right'> <div className='btn-group pull-right'>
<button className='btn btn-default btn-defaults' onClick={this.loadDefaults} disabled={loadDefaultsDisabled}>{i18n('common.load_defaults_button')}</button> <button
<button className='btn btn-default btn-revert-changes' onClick={this.revertChanges} disabled={revertChangesDisabled}>{i18n('common.cancel_changes_button')}</button> className='btn btn-default btn-defaults'
<button className='btn btn-success btn-apply' onClick={this.applyChanges} disabled={!this.isSavingPossible()}>{i18n('common.apply_button')}</button> onClick={this.loadDefaults}
disabled={loadDefaultsDisabled}
>
{i18n('common.load_defaults_button')}
</button>
<button
className='btn btn-default btn-revert-changes'
onClick={this.revertChanges}
disabled={revertChangesDisabled}
>
{i18n('common.cancel_changes_button')}
</button>
<button
className='btn btn-success btn-apply'
onClick={this.applyChanges}
disabled={!this.isSavingPossible()}
>
{i18n('common.apply_button')}
</button>
</div> </div>
} }
</div> </div>
@ -225,7 +256,8 @@ var NodeDisk = React.createClass({
if (size > volumeInfo.max) { if (size > volumeInfo.max) {
size = volumeInfo.max; size = volumeInfo.max;
} }
this.props.disk.get('volumes').findWhere({name: name}).set({size: size}).isValid({minimum: volumeInfo.min}); this.props.disk.get('volumes').findWhere({name: name}).set({size: size})
.isValid({minimum: volumeInfo.min});
this.props.disk.trigger('change', this.props.disk); this.props.disk.trigger('change', this.props.disk);
}, },
toggleDisk(name) { toggleDisk(name) {
@ -236,7 +268,8 @@ var NodeDisk = React.createClass({
var volumesInfo = this.props.volumesInfo; var volumesInfo = this.props.volumesInfo;
var diskMetaData = this.props.diskMetaData; var diskMetaData = this.props.diskMetaData;
var requiredDiskSize = _.sum(disk.get('volumes').map((volume) => { var requiredDiskSize = _.sum(disk.get('volumes').map((volume) => {
return volume.getMinimalSize(this.props.volumes.findWhere({name: volume.get('name')}).get('min_size')); return volume
.getMinimalSize(this.props.volumes.findWhere({name: volume.get('name')}).get('min_size'));
})); }));
var diskError = disk.get('size') < requiredDiskSize; var diskError = disk.get('size') < requiredDiskSize;
var sortOrder = ['name', 'model', 'size']; var sortOrder = ['name', 'model', 'size'];
@ -263,26 +296,44 @@ var NodeDisk = React.createClass({
data-volume={volumeName} data-volume={volumeName}
style={{width: volumesInfo[volumeName].width + '%'}} style={{width: volumesInfo[volumeName].width + '%'}}
> >
<div className='text-center toggle' onClick={_.partial(this.toggleDisk, disk.get('name'))}> <div
className='text-center toggle'
onClick={_.partial(this.toggleDisk, disk.get('name'))}
>
<div>{volume.get('label')}</div> <div>{volume.get('label')}</div>
<div className='volume-group-size'> <div className='volume-group-size'>
{utils.showDiskSize(volumesInfo[volumeName].size, 2)} {utils.showDiskSize(volumesInfo[volumeName].size, 2)}
</div> </div>
</div> </div>
{!this.props.disabled && volumesInfo[volumeName].min <= 0 && this.state.collapsed && {!this.props.disabled && volumesInfo[volumeName].min <= 0 && this.state.collapsed &&
<div className='close-btn' onClick={_.partial(this.updateDisk, volumeName, 0)}>&times;</div> <div
className='close-btn'
onClick={_.partial(this.updateDisk, volumeName, 0)}
>
&times;
</div>
} }
</div> </div>
); );
})} })}
<div className='volume-group pull-left' data-volume='unallocated' style={{width: volumesInfo.unallocated.width + '%'}}> <div
<div className='text-center toggle' onClick={_.partial(this.toggleDisk, disk.get('name'))}> className='volume-group pull-left'
data-volume='unallocated'
style={{width: volumesInfo.unallocated.width + '%'}}
>
<div
className='text-center toggle'
onClick={_.partial(this.toggleDisk, disk.get('name'))}
>
<div className='volume-group-name'>{i18n(ns + 'unallocated')}</div> <div className='volume-group-name'>{i18n(ns + 'unallocated')}</div>
<div className='volume-group-size'>{utils.showDiskSize(volumesInfo.unallocated.size, 2)}</div> <div className='volume-group-size'>
{utils.showDiskSize(volumesInfo.unallocated.size, 2)}
</div>
</div> </div>
</div> </div>
</div> </div>
<div className='row collapse disk-details' id={disk.get('name')} key='diskDetails' ref={disk.get('name')}> <div className='row collapse disk-details' id={disk.get('name')} key='diskDetails'
ref={disk.get('name')}>
<div className='col-xs-5'> <div className='col-xs-5'>
{diskMetaData && {diskMetaData &&
<div> <div>
@ -294,7 +345,10 @@ var NodeDisk = React.createClass({
<label className='col-xs-2'>{propertyName.replace(/_/g, ' ')}</label> <label className='col-xs-2'>{propertyName.replace(/_/g, ' ')}</label>
<div className='col-xs-10'> <div className='col-xs-10'>
<p className='form-control-static'> <p className='form-control-static'>
{propertyName == 'size' ? utils.showDiskSize(diskMetaData[propertyName]) : diskMetaData[propertyName]} {propertyName == 'size' ?
utils.showDiskSize(diskMetaData[propertyName]) :
diskMetaData[propertyName]
}
</p> </p>
</div> </div>
</div> </div>
@ -325,7 +379,12 @@ var NodeDisk = React.createClass({
<div key={'edit_' + volumeName} data-volume={volumeName}> <div key={'edit_' + volumeName} data-volume={volumeName}>
<div className='form-group volume-group row'> <div className='form-group volume-group row'>
<label className='col-xs-4 volume-group-label'> <label className='col-xs-4 volume-group-label'>
<span ref={'volume-group-flag ' + volumeName} className={'volume-type-' + (index + 1)}> &nbsp; </span> <span
ref={'volume-group-flag ' + volumeName}
className={'volume-type-' + (index + 1)}
>
&nbsp;
</span>
{volume.get('label')} {volume.get('label')}
</label> </label>
<div className='col-xs-4 volume-group-range'> <div className='col-xs-4 volume-group-range'>
@ -343,10 +402,14 @@ var NodeDisk = React.createClass({
error={validationError && ''} error={validationError && ''}
value={value} value={value}
/> />
<div className='col-xs-1 volume-group-size-label'>{i18n('common.size.mb')}</div> <div className='col-xs-1 volume-group-size-label'>
{i18n('common.size.mb')}
</div>
</div> </div>
{!!value && value == currentMinSize && {!!value && value == currentMinSize &&
<div className='volume-group-notice text-info'>{i18n(ns + 'minimum_reached')}</div> <div className='volume-group-notice text-info'>
{i18n(ns + 'minimum_reached')}
</div>
} }
{validationError && {validationError &&
<div className='volume-group-notice text-danger'>{validationError}</div> <div className='volume-group-notice text-danger'>{validationError}</div>
@ -355,7 +418,9 @@ var NodeDisk = React.createClass({
); );
})} })}
{diskError && {diskError &&
<div className='volume-group-notice text-danger'>{i18n(ns + 'not_enough_space')}</div> <div className='volume-group-notice text-danger'>
{i18n(ns + 'not_enough_space')}
</div>
} }
</div> </div>
</div> </div>

View File

@ -92,7 +92,10 @@ var EditNodeInterfacesScreen = React.createClass({
}, },
interfacesPickFromJSON(json) { interfacesPickFromJSON(json) {
// Pick certain interface fields that have influence on hasChanges. // Pick certain interface fields that have influence on hasChanges.
return _.pick(json, ['assigned_networks', 'mode', 'type', 'slaves', 'bond_properties', 'interface_properties', 'offloading_modes']); return _.pick(json, [
'assigned_networks', 'mode', 'type', 'slaves', 'bond_properties',
'interface_properties', 'offloading_modes'
]);
}, },
interfacesToJSON(interfaces, remainingNodesMode) { interfacesToJSON(interfaces, remainingNodesMode) {
// Sometimes 'state' is sent from the API and sometimes not // Sometimes 'state' is sent from the API and sometimes not
@ -179,7 +182,9 @@ var EditNodeInterfacesScreen = React.createClass({
node.interfaces.each((ifc, index) => { node.interfaces.each((ifc, index) => {
var updatedIfc = ifc.isBond() ? bondsByName[ifc.get('name')] : interfaces.at(index); var updatedIfc = ifc.isBond() ? bondsByName[ifc.get('name')] : interfaces.at(index);
ifc.set({ ifc.set({
assigned_networks: new models.InterfaceNetworks(updatedIfc.get('assigned_networks').toJSON()), assigned_networks: new models.InterfaceNetworks(
updatedIfc.get('assigned_networks').toJSON()
),
interface_properties: updatedIfc.get('interface_properties') interface_properties: updatedIfc.get('interface_properties')
}); });
if (ifc.isBond()) { if (ifc.isBond()) {
@ -197,7 +202,8 @@ var EditNodeInterfacesScreen = React.createClass({
return Backbone.sync('update', node.interfaces, {url: _.result(node, 'url') + '/interfaces'}); return Backbone.sync('update', node.interfaces, {url: _.result(node, 'url') + '/interfaces'});
})) }))
.done(() => { .done(() => {
this.setState({initialInterfaces: _.cloneDeep(this.interfacesToJSON(this.props.interfaces))}); this.setState({initialInterfaces:
_.cloneDeep(this.interfacesToJSON(this.props.interfaces))});
dispatcher.trigger('networkConfigurationUpdated'); dispatcher.trigger('networkConfigurationUpdated');
}) })
.fail((response) => { .fail((response) => {
@ -213,19 +219,21 @@ var EditNodeInterfacesScreen = React.createClass({
}); });
}, },
configurationTemplateExists() { configurationTemplateExists() {
return !_.isEmpty(this.props.cluster.get('networkConfiguration').get('networking_parameters').get('configuration_template')); return !_.isEmpty(this.props.cluster.get('networkConfiguration')
.get('networking_parameters').get('configuration_template'));
}, },
bondingAvailable() { bondingAvailable() {
var availableBondTypes = this.getBondType(); var availableBondTypes = this.getBondType();
return !!availableBondTypes && !this.configurationTemplateExists(); return !!availableBondTypes && !this.configurationTemplateExists();
}, },
getBondType() { getBondType() {
return _.compact(_.flatten(_.map(this.props.bondingConfig.availability, (modeAvailabilityData) => { return _.compact(_.flatten(_.map(this.props.bondingConfig.availability,
return _.map(modeAvailabilityData, (condition, name) => { (modeAvailabilityData) => {
var result = utils.evaluateExpression(condition, this.props.configModels).value; return _.map(modeAvailabilityData, (condition, name) => {
return result && name; var result = utils.evaluateExpression(condition, this.props.configModels).value;
}); return result && name;
})))[0]; });
})))[0];
}, },
findOffloadingModesIntersection(set1, set2) { findOffloadingModesIntersection(set1, set2) {
return _.map( return _.map(
@ -248,7 +256,9 @@ var EditNodeInterfacesScreen = React.createClass({
var offloadingModes = interfaces.map((ifc) => ifc.get('offloading_modes') || []); var offloadingModes = interfaces.map((ifc) => ifc.get('offloading_modes') || []);
if (!offloadingModes.length) return []; if (!offloadingModes.length) return [];
return offloadingModes.reduce((result, modes) => this.findOffloadingModesIntersection(result, modes)); return offloadingModes.reduce((result, modes) => {
return this.findOffloadingModesIntersection(result, modes);
});
}, },
bondInterfaces() { bondInterfaces() {
this.setState({actionInProgress: true}); this.setState({actionInProgress: true});
@ -261,7 +271,8 @@ var EditNodeInterfacesScreen = React.createClass({
var bondMode = _.flatten(_.pluck(bondingProperties[this.getBondType()].mode, 'values'))[0]; var bondMode = _.flatten(_.pluck(bondingProperties[this.getBondType()].mode, 'values'))[0];
bonds = new models.Interface({ bonds = new models.Interface({
type: 'bond', type: 'bond',
name: this.props.interfaces.generateBondName(this.getBondType() == 'linux' ? 'bond' : 'ovs-bond'), name: this.props.interfaces.generateBondName(this.getBondType() ==
'linux' ? 'bond' : 'ovs-bond'),
mode: bondMode, mode: bondMode,
assigned_networks: new models.InterfaceNetworks(), assigned_networks: new models.InterfaceNetworks(),
slaves: _.invoke(interfaces, 'pick', 'name'), slaves: _.invoke(interfaces, 'pick', 'name'),
@ -293,7 +304,9 @@ var EditNodeInterfacesScreen = React.createClass({
}, },
unbondInterfaces() { unbondInterfaces() {
this.setState({actionInProgress: true}); this.setState({actionInProgress: true});
_.each(this.props.interfaces.where({checked: true}), (bond) => this.removeInterfaceFromBond(bond.get('name'))); _.each(this.props.interfaces.where({checked: true}), (bond) => {
return this.removeInterfaceFromBond(bond.get('name'));
});
this.setState({actionInProgress: false}); this.setState({actionInProgress: false});
}, },
removeInterfaceFromBond(bondName, slaveInterfaceName) { removeInterfaceFromBond(bondName, slaveInterfaceName) {
@ -320,7 +333,9 @@ var EditNodeInterfacesScreen = React.createClass({
if (slaveInterfaceName) { if (slaveInterfaceName) {
var slavesUpdated = _.reject(slaves, {name: slaveInterfaceName}); var slavesUpdated = _.reject(slaves, {name: slaveInterfaceName});
var names = _.pluck(slavesUpdated, 'name'); var names = _.pluck(slavesUpdated, 'name');
var bondSlaveInterfaces = this.props.interfaces.filter((ifc) => _.contains(names, ifc.get('name'))); var bondSlaveInterfaces = this.props.interfaces.filter((ifc) => {
return _.contains(names, ifc.get('name'));
});
bond.set({ bond.set({
slaves: slavesUpdated, slaves: slavesUpdated,
@ -377,7 +392,8 @@ var EditNodeInterfacesScreen = React.createClass({
return _.uniq(speeds).length > 1 || !_.compact(speeds).length; return _.uniq(speeds).length > 1 || !_.compact(speeds).length;
}, },
isSavingPossible() { isSavingPossible() {
return !_.chain(this.state.interfaceErrors).values().some().value() && !this.state.actionInProgress && this.hasChanges(); return !_.chain(this.state.interfaceErrors).values().some().value() &&
!this.state.actionInProgress && this.hasChanges();
}, },
getIfcProperty(property) { getIfcProperty(property) {
var {interfaces, nodes} = this.props; var {interfaces, nodes} = this.props;
@ -416,20 +432,25 @@ var EditNodeInterfacesScreen = React.createClass({
var slaveInterfaceNames = _.pluck(_.flatten(_.filter(interfaces.pluck('slaves'))), 'name'); var slaveInterfaceNames = _.pluck(_.flatten(_.filter(interfaces.pluck('slaves'))), 'name');
var loadDefaultsEnabled = !this.state.actionInProgress; var loadDefaultsEnabled = !this.state.actionInProgress;
var revertChangesEnabled = !this.state.actionInProgress && hasChanges; var revertChangesEnabled = !this.state.actionInProgress && hasChanges;
var invalidSpeedsForBonding = bondingPossible && this.validateSpeedsForBonding(checkedBonds.concat(checkedInterfaces)) || interfaces.any((ifc) => { var invalidSpeedsForBonding = bondingPossible &&
return ifc.isBond() && this.validateSpeedsForBonding([ifc]); this.validateSpeedsForBonding(checkedBonds.concat(checkedInterfaces)) ||
}); interfaces.any((ifc) => {
return ifc.isBond() && this.validateSpeedsForBonding([ifc]);
});
var interfaceSpeeds = this.getIfcProperty('current_speed'); var interfaceSpeeds = this.getIfcProperty('current_speed');
var interfaceNames = this.getIfcProperty('name'); var interfaceNames = this.getIfcProperty('name');
return ( return (
<div className='row'> <div className='row'>
<div className='title'> <div className='title'>
{i18n(ns + (locked ? 'read_only_' : '') + 'title', {count: nodes.length, name: nodeNames.join(', ')})} {i18n(ns + (locked ? 'read_only_' : '') + 'title',
{count: nodes.length, name: nodeNames.join(', ')})}
</div> </div>
{configurationTemplateExists && {configurationTemplateExists &&
<div className='col-xs-12'> <div className='col-xs-12'>
<div className='alert alert-warning'>{i18n(ns + 'configuration_template_warning')}</div> <div className='alert alert-warning'>
{i18n(ns + 'configuration_template_warning')}
</div>
</div> </div>
} }
{bondingAvailable && !locked && {bondingAvailable && !locked &&
@ -437,10 +458,18 @@ var EditNodeInterfacesScreen = React.createClass({
<div className='page-buttons'> <div className='page-buttons'>
<div className='well clearfix'> <div className='well clearfix'>
<div className='btn-group pull-right'> <div className='btn-group pull-right'>
<button className='btn btn-default btn-bond' onClick={this.bondInterfaces} disabled={!bondingPossible}> <button
className='btn btn-default btn-bond'
onClick={this.bondInterfaces}
disabled={!bondingPossible}
>
{i18n(ns + 'bond_button')} {i18n(ns + 'bond_button')}
</button> </button>
<button className='btn btn-default btn-unbond' onClick={this.unbondInterfaces} disabled={!unbondingPossible}> <button
className='btn btn-default btn-unbond'
onClick={this.unbondInterfaces}
disabled={!unbondingPossible}
>
{i18n(ns + 'unbond_button')} {i18n(ns + 'unbond_button')}
</button> </button>
</div> </div>
@ -478,19 +507,35 @@ var EditNodeInterfacesScreen = React.createClass({
<div className='col-xs-12 page-buttons content-elements'> <div className='col-xs-12 page-buttons content-elements'>
<div className='well clearfix'> <div className='well clearfix'>
<div className='btn-group'> <div className='btn-group'>
<a className='btn btn-default' href={'#cluster/' + this.props.cluster.id + '/nodes'} disabled={this.state.actionInProgress}> <a
className='btn btn-default'
href={'#cluster/' + this.props.cluster.id + '/nodes'}
disabled={this.state.actionInProgress}
>
{i18n('cluster_page.nodes_tab.back_to_nodes_button')} {i18n('cluster_page.nodes_tab.back_to_nodes_button')}
</a> </a>
</div> </div>
{!locked && {!locked &&
<div className='btn-group pull-right'> <div className='btn-group pull-right'>
<button className='btn btn-default btn-defaults' onClick={this.loadDefaults} disabled={!loadDefaultsEnabled}> <button
className='btn btn-default btn-defaults'
onClick={this.loadDefaults}
disabled={!loadDefaultsEnabled}
>
{i18n('common.load_defaults_button')} {i18n('common.load_defaults_button')}
</button> </button>
<button className='btn btn-default btn-revert-changes' onClick={this.revertChanges} disabled={!revertChangesEnabled}> <button
className='btn btn-default btn-revert-changes'
onClick={this.revertChanges}
disabled={!revertChangesEnabled}
>
{i18n('common.cancel_changes_button')} {i18n('common.cancel_changes_button')}
</button> </button>
<button className='btn btn-success btn-apply' onClick={this.applyChanges} disabled={!this.isSavingPossible()}> <button
className='btn btn-success btn-apply'
onClick={this.applyChanges}
disabled={!this.isSavingPossible()}
>
{i18n('common.apply_button')} {i18n('common.apply_button')}
</button> </button>
</div> </div>
@ -508,7 +553,8 @@ var NodeInterface = React.createClass({
drop(props, monitor) { drop(props, monitor) {
var targetInterface = props.interface; var targetInterface = props.interface;
var sourceInterface = props.interfaces.findWhere({name: monitor.getItem().interfaceName}); var sourceInterface = props.interfaces.findWhere({name: monitor.getItem().interfaceName});
var network = sourceInterface.get('assigned_networks').findWhere({name: monitor.getItem().networkName}); var network = sourceInterface.get('assigned_networks')
.findWhere({name: monitor.getItem().networkName});
sourceInterface.get('assigned_networks').remove(network); sourceInterface.get('assigned_networks').remove(network);
targetInterface.get('assigned_networks').add(network); targetInterface.get('assigned_networks').add(network);
// trigger 'change' event to update screen buttons state // trigger 'change' event to update screen buttons state
@ -542,7 +588,8 @@ var NodeInterface = React.createClass({
return _.contains(this.getBondPropertyValues('lacp_rate', 'for_modes'), this.getBondMode()); return _.contains(this.getBondPropertyValues('lacp_rate', 'for_modes'), this.getBondMode());
}, },
isHashPolicyNeeded() { isHashPolicyNeeded() {
return _.contains(this.getBondPropertyValues('xmit_hash_policy', 'for_modes'), this.getBondMode()); return _.contains(this.getBondPropertyValues('xmit_hash_policy', 'for_modes'),
this.getBondMode());
}, },
getBondMode() { getBondMode() {
var ifc = this.props.interface; var ifc = this.props.interface;
@ -552,11 +599,13 @@ var NodeInterface = React.createClass({
var modes = this.props.bondingProperties[this.props.bondType].mode; var modes = this.props.bondingProperties[this.props.bondType].mode;
var configModels = _.clone(this.props.configModels); var configModels = _.clone(this.props.configModels);
var availableModes = []; var availableModes = [];
var interfaces = this.props.interface.isBond() ? this.props.interface.getSlaveInterfaces() : [this.props.interface]; var interfaces = this.props.interface.isBond() ? this.props.interface.getSlaveInterfaces() :
[this.props.interface];
_.each(interfaces, (ifc) => { _.each(interfaces, (ifc) => {
configModels.interface = ifc; configModels.interface = ifc;
availableModes.push(_.reduce(modes, (result, modeSet) => { availableModes.push(_.reduce(modes, (result, modeSet) => {
if (modeSet.condition && !utils.evaluateExpression(modeSet.condition, configModels).value) return result; if (modeSet.condition &&
!utils.evaluateExpression(modeSet.condition, configModels).value) return result;
return result.concat(modeSet.values); return result.concat(modeSet.values);
}, [])); }, []));
}); });
@ -583,7 +632,8 @@ var NodeInterface = React.createClass({
this.props.interface.set({mode: value}); this.props.interface.set({mode: value});
this.updateBondProperties({mode: value}); this.updateBondProperties({mode: value});
if (this.isHashPolicyNeeded()) { if (this.isHashPolicyNeeded()) {
this.updateBondProperties({xmit_hash_policy: this.getBondPropertyValues('xmit_hash_policy', 'values')[0]}); this.updateBondProperties({xmit_hash_policy: this.getBondPropertyValues('xmit_hash_policy',
'values')[0]});
} }
if (this.isLacpRateAvailable()) { if (this.isLacpRateAvailable()) {
this.updateBondProperties({lacp_rate: this.getBondPropertyValues('lacp_rate', 'values')[0]}); this.updateBondProperties({lacp_rate: this.getBondPropertyValues('lacp_rate', 'values')[0]});
@ -684,7 +734,10 @@ var NodeInterface = React.createClass({
disabled={!bondingPossible} disabled={!bondingPossible}
onChange={this.onPolicyChange} onChange={this.onPolicyChange}
label={i18n(ns + 'bonding_policy')} label={i18n(ns + 'bonding_policy')}
children={this.getBondingOptions(this.getBondPropertyValues('xmit_hash_policy', 'values'), 'hash_policy')} children={this.getBondingOptions(
this.getBondPropertyValues('xmit_hash_policy', 'values'),
'hash_policy'
)}
wrapperClassName='pull-right' wrapperClassName='pull-right'
/> />
} }
@ -695,7 +748,10 @@ var NodeInterface = React.createClass({
disabled={!bondingPossible} disabled={!bondingPossible}
onChange={this.onLacpChange} onChange={this.onLacpChange}
label={i18n(ns + 'lacp_rate')} label={i18n(ns + 'lacp_rate')}
children={this.getBondingOptions(this.getBondPropertyValues('lacp_rate', 'values'), 'lacp_rates')} children={this.getBondingOptions(
this.getBondPropertyValues('lacp_rate', 'values'),
'lacp_rates'
)}
wrapperClassName='pull-right' wrapperClassName='pull-right'
/> />
} }
@ -718,14 +774,21 @@ var NodeInterface = React.createClass({
<div className='pull-left'> <div className='pull-left'>
{_.map(slaveInterfaces, (slaveInterface, index) => { {_.map(slaveInterfaces, (slaveInterface, index) => {
return ( return (
<div key={'info-' + slaveInterface.get('name')} className='ifc-info-block clearfix'> <div
key={'info-' + slaveInterface.get('name')}
className='ifc-info-block clearfix'
>
<div className='ifc-connection pull-left'> <div className='ifc-connection pull-left'>
<div className={utils.classNames(connectionStatusClasses(slaveInterface))} /> <div
className={utils.classNames(connectionStatusClasses(slaveInterface))}
/>
</div> </div>
<div className='ifc-info pull-left'> <div className='ifc-info pull-left'>
{this.props.interfaceNames[index].length == 1 && {this.props.interfaceNames[index].length == 1 &&
<div> <div>
{i18n(ns + 'name')}: <span className='ifc-name'>{this.props.interfaceNames[index]}</span> {i18n(ns + 'name')}:
{' '}
<span className='ifc-name'>{this.props.interfaceNames[index]}</span>
</div> </div>
} }
{this.props.nodes.length == 1 && {this.props.nodes.length == 1 &&
@ -735,7 +798,13 @@ var NodeInterface = React.createClass({
{i18n(ns + 'speed')}: {this.props.interfaceSpeeds[index].join(', ')} {i18n(ns + 'speed')}: {this.props.interfaceSpeeds[index].join(', ')}
</div> </div>
{(bondingPossible && slaveInterfaces.length >= 3) && {(bondingPossible && slaveInterfaces.length >= 3) &&
<button className='btn btn-link' onClick={_.partial(this.props.removeInterfaceFromBond, ifc.get('name'), slaveInterface.get('name'))}> <button
className='btn btn-link'
onClick={_.partial(
this.props.removeInterfaceFromBond,
ifc.get('name'), slaveInterface.get('name')
)}
>
{i18n('common.remove_button')} {i18n('common.remove_button')}
</button> </button>
} }
@ -789,7 +858,8 @@ var NodeInterface = React.createClass({
onClick={this.toggleOffloading} onClick={this.toggleOffloading}
disabled={locked} disabled={locked}
className='btn btn-default toggle-offloading'> className='btn btn-default toggle-offloading'>
{i18n(ns + (interfaceProperties.disable_offloading ? 'disable_offloading' : 'default_offloading'))} {i18n(ns + (interfaceProperties.disable_offloading ? 'disable_offloading' :
'default_offloading'))}
</button> </button>
} }
</div> </div>
@ -802,7 +872,11 @@ var NodeInterface = React.createClass({
} }
}); });
var NodeInterfaceDropTarget = DropTarget('network', NodeInterface.target, NodeInterface.collect)(NodeInterface); var NodeInterfaceDropTarget = DropTarget(
'network',
NodeInterface.target,
NodeInterface.collect
)(NodeInterface);
var Network = React.createClass({ var Network = React.createClass({
statics: { statics: {
@ -838,7 +912,10 @@ var Network = React.createClass({
return this.props.connectDragSource( return this.props.connectDragSource(
<div className={utils.classNames(classes)}> <div className={utils.classNames(classes)}>
<div className='network-name'> <div className='network-name'>
{i18n('network.' + interfaceNetwork.get('name'), {defaultValue: interfaceNetwork.get('name')})} {i18n(
'network.' + interfaceNetwork.get('name'),
{defaultValue: interfaceNetwork.get('name')}
)}
</div> </div>
{vlanRange && {vlanRange &&
<div className='vlan-id'> <div className='vlan-id'>

View File

@ -30,7 +30,8 @@ var EditNodesScreen = React.createClass({
} }
nodes.fetch = function(options) { nodes.fetch = function(options) {
return this.constructor.__super__.fetch.call(this, _.extend({data: {cluster_id: cluster.id}}, options)); return this.constructor.__super__.fetch.call(this,
_.extend({data: {cluster_id: cluster.id}}, options));
}; };
nodes.parse = function() { nodes.parse = function() {
return this.getByIds(nodes.pluck('id')); return this.getByIds(nodes.pluck('id'));

View File

@ -44,12 +44,21 @@ var Node = React.createClass({
var options = {type: 'remote', node: this.props.node.id}; var options = {type: 'remote', node: this.props.node.id};
if (status == 'discover') { if (status == 'discover') {
options.source = 'bootstrap/messages'; options.source = 'bootstrap/messages';
} else if (status == 'provisioning' || status == 'provisioned' || (status == 'error' && error == 'provision')) { } else if (
status == 'provisioning' ||
status == 'provisioned' ||
(status == 'error' && error == 'provision')
) {
options.source = 'install/fuel-agent'; options.source = 'install/fuel-agent';
} else if (status == 'deploying' || status == 'ready' || (status == 'error' && error == 'deploy')) { } else if (
status == 'deploying' ||
status == 'ready' ||
(status == 'error' && error == 'deploy')
) {
options.source = 'install/puppet'; options.source = 'install/puppet';
} }
return '#cluster/' + this.props.node.get('cluster') + '/logs/' + utils.serializeTabOptions(options); return '#cluster/' + this.props.node.get('cluster') + '/logs/' +
utils.serializeTabOptions(options);
}, },
applyNewNodeName(newName) { applyNewNodeName(newName) {
if (newName && newName != this.props.node.get('name')) { if (newName && newName != this.props.node.get('name')) {
@ -100,7 +109,8 @@ var Node = React.createClass({
.sync('delete', this.props.node) .sync('delete', this.props.node)
.then( .then(
(task) => { (task) => {
dispatcher.trigger('networkConfigurationUpdated updateNodeStats updateNotifications labelsConfigurationUpdated'); dispatcher.trigger('networkConfigurationUpdated updateNodeStats ' +
'updateNotifications labelsConfigurationUpdated');
if (task.status == 'ready') { if (task.status == 'ready') {
// Do not send the 'DELETE' request again, just get rid // Do not send the 'DELETE' request again, just get rid
// of this node. // of this node.
@ -129,7 +139,8 @@ var Node = React.createClass({
}); });
}, },
toggleExtendedNodePanel() { toggleExtendedNodePanel() {
var states = this.state.extendedView ? {extendedView: false, isRenaming: false} : {extendedView: true}; var states = this.state.extendedView ?
{extendedView: false, isRenaming: false} : {extendedView: true};
this.setState(states); this.setState(states);
}, },
renderNameControl() { renderNameControl() {
@ -159,7 +170,8 @@ var Node = React.createClass({
return ( return (
<span> <span>
{i18n('cluster_page.nodes_tab.node.status.' + status, { {i18n('cluster_page.nodes_tab.node.status.' + status, {
os: this.props.cluster && this.props.cluster.get('release').get('operating_system') || 'OS' os: this.props.cluster && this.props.cluster.get('release').get('operating_system')
|| 'OS'
})} })}
</span> </span>
); );
@ -174,7 +186,12 @@ var Node = React.createClass({
{': ' + nodeProgress + '%'} {': ' + nodeProgress + '%'}
</div> </div>
} }
<div className='progress-bar' role='progressbar' style={{width: _.max([nodeProgress, 3]) + '%'}}></div> <div
className='progress-bar'
role='progressbar'
style={{width: _.max([nodeProgress, 3]) + '%'}}
>
</div>
</div> </div>
); );
}, },
@ -184,16 +201,34 @@ var Node = React.createClass({
var ram = this.props.node.resource('ram'); var ram = this.props.node.resource('ram');
return ( return (
<div className='node-hardware'> <div className='node-hardware'>
<span>{i18n('node_details.cpu')}: {this.props.node.resource('cores') || '0'} ({_.isUndefined(htCores) ? '?' : htCores})</span> <span>
<span>{i18n('node_details.hdd')}: {_.isUndefined(hdd) ? '?' + i18n('common.size.gb') : utils.showDiskSize(hdd)}</span> {i18n('node_details.cpu')}
<span>{i18n('node_details.ram')}: {_.isUndefined(ram) ? '?' + i18n('common.size.gb') : utils.showMemorySize(ram)}</span> {': '}
{this.props.node.resource('cores') || '0'} ({_.isUndefined(htCores) ? '?' : htCores})
</span>
<span>
{i18n('node_details.hdd')}
{': '}
{_.isUndefined(hdd) ? '?' + i18n('common.size.gb') : utils.showDiskSize(hdd)}
</span>
<span>
{i18n('node_details.ram')}
{': '}
{_.isUndefined(ram) ? '?' + i18n('common.size.gb') : utils.showMemorySize(ram)}
</span>
</div> </div>
); );
}, },
renderLogsLink(iconRepresentation) { renderLogsLink(iconRepresentation) {
return ( return (
<Tooltip key='logs' text={iconRepresentation ? i18n('cluster_page.nodes_tab.node.view_logs') : null}> <Tooltip
<a className={'btn-view-logs ' + (iconRepresentation ? 'icon icon-logs' : 'btn')} href={this.getNodeLogsLink()}> key='logs'
text={iconRepresentation ? i18n('cluster_page.nodes_tab.node.view_logs') : null}
>
<a
className={'btn-view-logs ' + (iconRepresentation ? 'icon icon-logs' : 'btn')}
href={this.getNodeLogsLink()}
>
{!iconRepresentation && i18n('cluster_page.nodes_tab.node.view_logs')} {!iconRepresentation && i18n('cluster_page.nodes_tab.node.view_logs')}
</a> </a>
</Tooltip> </Tooltip>
@ -269,7 +304,9 @@ var Node = React.createClass({
<label className='node-box'> <label className='node-box'>
<div <div
className='node-box-inner clearfix' className='node-box-inner clearfix'
onClick={isSelectable && _.partial(this.props.onNodeSelection, null, !this.props.checked)} onClick={isSelectable &&
_.partial(this.props.onNodeSelection, null, !this.props.checked)
}
> >
<div className='node-checkbox'> <div className='node-checkbox'>
{this.props.checked && <i className='glyphicon glyphicon-ok' />} {this.props.checked && <i className='glyphicon glyphicon-ok' />}
@ -290,9 +327,13 @@ var Node = React.createClass({
<span> <span>
{node.resource('cores') || '0'} ({node.resource('ht_cores') || '?'}) {node.resource('cores') || '0'} ({node.resource('ht_cores') || '?'})
</span> / <span> </span> / <span>
{node.resource('hdd') ? utils.showDiskSize(node.resource('hdd')) : '?' + i18n('common.size.gb')} {node.resource('hdd') ? utils.showDiskSize(node.resource('hdd')) : '?' +
i18n('common.size.gb')
}
</span> / <span> </span> / <span>
{node.resource('ram') ? utils.showMemorySize(node.resource('ram')) : '?' + i18n('common.size.gb')} {node.resource('ram') ? utils.showMemorySize(node.resource('ram')) : '?' +
i18n('common.size.gb')
}
</span> </span>
</p> </p>
<p className='btn btn-link' onClick={this.toggleExtendedNodePanel}> <p className='btn btn-link' onClick={this.toggleExtendedNodePanel}>
@ -340,13 +381,21 @@ var Node = React.createClass({
{status == 'offline' && this.renderRemoveButton()} {status == 'offline' && this.renderRemoveButton()}
{[ {[
!!node.get('cluster') && this.renderLogsLink(), !!node.get('cluster') && this.renderLogsLink(),
this.props.renderActionButtons && node.hasChanges() && !this.props.locked && this.props.renderActionButtons && node.hasChanges() &&
!this.props.locked &&
<button <button
className='btn btn-discard' className='btn btn-discard'
key='btn-discard' key='btn-discard'
onClick={node.get('pending_deletion') ? this.discardNodeDeletion : this.showDeleteNodesDialog} onClick={
node.get('pending_deletion') ?
this.discardNodeDeletion : this.showDeleteNodesDialog
}
> >
{i18n(ns + (node.get('pending_deletion') ? 'discard_deletion' : 'delete_node'))} {i18n(
ns + (node.get('pending_deletion') ?
'discard_deletion' : 'delete_node'
)
)}
</button> </button>
]} ]}
</div> </div>
@ -404,11 +453,15 @@ var Node = React.createClass({
this.props.renderActionButtons && node.hasChanges() && !this.props.locked && this.props.renderActionButtons && node.hasChanges() && !this.props.locked &&
<Tooltip <Tooltip
key={'pending_addition_' + node.id} key={'pending_addition_' + node.id}
text={i18n(ns + (node.get('pending_deletion') ? 'discard_deletion' : 'delete_node'))} text={
i18n(ns + (node.get('pending_deletion') ? 'discard_deletion' : 'delete_node'))
}
> >
<div <div
className='icon btn-discard' className='icon btn-discard'
onClick={node.get('pending_deletion') ? this.discardNodeDeletion : this.showDeleteNodesDialog} onClick={node.get('pending_deletion') ?
this.discardNodeDeletion : this.showDeleteNodesDialog
}
/> />
</Tooltip> </Tooltip>
]} ]}
@ -439,7 +492,9 @@ var Node = React.createClass({
var node = this.props.node; var node = this.props.node;
var isSelectable = node.isSelectable() && !this.props.locked && this.props.mode != 'edit'; var isSelectable = node.isSelectable() && !this.props.locked && this.props.mode != 'edit';
var status = node.getStatusSummary(); var status = node.getStatusSummary();
var roles = this.props.cluster ? node.sortedRoles(this.props.cluster.get('roles').pluck('name')) : []; var roles = this.props.cluster ? node.sortedRoles(
this.props.cluster.get('roles').pluck('name')
) : [];
// compose classes // compose classes
var nodePanelClasses = { var nodePanelClasses = {
@ -470,9 +525,13 @@ var Node = React.createClass({
}[status]; }[status];
statusClasses[statusClass] = true; statusClasses[statusClass] = true;
var renderMethod = this.props.viewMode == 'compact' ? this.renderCompactNode : this.renderStandardNode; var renderMethod = this.props.viewMode == 'compact' ? this.renderCompactNode :
this.renderStandardNode;
return renderMethod({ns, status, roles, nodePanelClasses, logoClasses, statusClasses, isSelectable}); return renderMethod({
ns, status, roles, nodePanelClasses,
logoClasses, statusClasses, isSelectable
});
} }
}); });

View File

@ -27,13 +27,17 @@ import {DeleteNodesDialog} from 'views/dialogs';
import {backboneMixin, pollingMixin, dispatcherMixin, unsavedChangesMixin} from 'component_mixins'; import {backboneMixin, pollingMixin, dispatcherMixin, unsavedChangesMixin} from 'component_mixins';
import Node from 'views/cluster_page_tabs/nodes_tab_screens/node'; import Node from 'views/cluster_page_tabs/nodes_tab_screens/node';
var NodeListScreen, MultiSelectControl, NumberRangeControl, ManagementPanel, NodeLabelsPanel, RolePanel, SelectAllMixin, NodeList, NodeGroup; var NodeListScreen, MultiSelectControl, NumberRangeControl, ManagementPanel,
NodeLabelsPanel, RolePanel, SelectAllMixin, NodeList, NodeGroup;
class Sorter { class Sorter {
constructor(name, order, isLabel) { constructor(name, order, isLabel) {
this.name = name; this.name = name;
this.order = order; this.order = order;
this.title = isLabel ? name : i18n('cluster_page.nodes_tab.sorters.' + name, {defaultValue: name}); this.title = isLabel ? name : i18n(
'cluster_page.nodes_tab.sorters.' + name,
{defaultValue: name}
);
this.isLabel = isLabel; this.isLabel = isLabel;
return this; return this;
} }
@ -52,9 +56,13 @@ class Filter {
constructor(name, values, isLabel) { constructor(name, values, isLabel) {
this.name = name; this.name = name;
this.values = values; this.values = values;
this.title = isLabel ? name : i18n('cluster_page.nodes_tab.filters.' + name, {defaultValue: name}); this.title = isLabel ? name : i18n(
'cluster_page.nodes_tab.filters.' + name,
{defaultValue: name}
);
this.isLabel = isLabel; this.isLabel = isLabel;
this.isNumberRange = !isLabel && !_.contains(['roles', 'status', 'manufacturer', 'group_id', 'cluster'], name); this.isNumberRange = !isLabel &&
!_.contains(['roles', 'status', 'manufacturer', 'group_id', 'cluster'], name);
return this; return this;
} }
@ -76,7 +84,10 @@ class Filter {
var resources = nodes.invoke('resource', this.name); var resources = nodes.invoke('resource', this.name);
limits = [_.min(resources), _.max(resources)]; limits = [_.min(resources), _.max(resources)];
if (this.name == 'hdd' || this.name == 'ram') { if (this.name == 'hdd' || this.name == 'ram') {
limits = [Math.floor(limits[0] / Math.pow(1024, 3)), Math.ceil(limits[1] / Math.pow(1024, 3))]; limits = [
Math.floor(limits[0] / Math.pow(1024, 3)),
Math.ceil(limits[1] / Math.pow(1024, 3))
];
} }
} }
this.limits = limits; this.limits = limits;
@ -133,15 +144,20 @@ NodeListScreen = React.createClass({
var viewMode = uiSettings.view_mode; var viewMode = uiSettings.view_mode;
var isLabelsPanelOpen = false; var isLabelsPanelOpen = false;
var states = {search, activeSorters, activeFilters, availableSorters, availableFilters, viewMode, isLabelsPanelOpen}; var states = {search, activeSorters, activeFilters, availableSorters, availableFilters,
viewMode, isLabelsPanelOpen};
// Equipment page // Equipment page
if (!cluster) return states; if (!cluster) return states;
// additonal Nodes tab states (Cluster page) // additonal Nodes tab states (Cluster page)
var roles = cluster.get('roles').pluck('name'); var roles = cluster.get('roles').pluck('name');
var selectedRoles = nodes.length ? _.filter(roles, (role) => !nodes.any((node) => !node.hasRole(role))) : []; var selectedRoles = nodes.length ? _.filter(roles, (role) => !nodes.any((node) => {
var indeterminateRoles = nodes.length ? _.filter(roles, (role) => !_.contains(selectedRoles, role) && nodes.any((node) => node.hasRole(role))) : []; return !node.hasRole(role);
})) : [];
var indeterminateRoles = nodes.length ? _.filter(roles, (role) => {
return !_.contains(selectedRoles, role) && nodes.any((node) => node.hasRole(role));
}) : [];
var configModels = { var configModels = {
cluster: cluster, cluster: cluster,
@ -180,8 +196,12 @@ NodeListScreen = React.createClass({
var filter = _.clone(activeFilter); var filter = _.clone(activeFilter);
if (filter.values.length) { if (filter.values.length) {
if (filter.isLabel) { if (filter.isLabel) {
filter.values = _.intersection(filter.values, this.props.nodes.getLabelValues(filter.name)); filter.values = _.intersection(
} else if (checkStandardNodeFilters && _.contains(['manufacturer', 'group_id', 'cluster'], filter.name)) { filter.values,
this.props.nodes.getLabelValues(filter.name)
);
} else if (checkStandardNodeFilters &&
_.contains(['manufacturer', 'group_id', 'cluster'], filter.name)) {
filter.values = _.filter(filter.values, (value) => { filter.values = _.filter(filter.values, (value) => {
return this.props.nodes.any((node) => node.get(filter.name) == value); return this.props.nodes.any((node) => node.get(filter.name) == value);
}, this); }, this);
@ -189,7 +209,12 @@ NodeListScreen = React.createClass({
} }
return filter; return filter;
}, this); }, this);
if (!_.isEqual(_.pluck(normalizedFilters, 'values'), _.pluck(this.state.activeFilters, 'values'))) { if (
!_.isEqual(
_.pluck(normalizedFilters, 'values'),
_.pluck(this.state.activeFilters, 'values')
)
) {
this.updateFilters(normalizedFilters); this.updateFilters(normalizedFilters);
} }
} }
@ -218,14 +243,21 @@ NodeListScreen = React.createClass({
var processedRoleLimits = {}; var processedRoleLimits = {};
var selectedNodes = this.props.nodes.filter((node) => this.props.selectedNodeIds[node.id]); var selectedNodes = this.props.nodes.filter((node) => this.props.selectedNodeIds[node.id]);
var clusterNodes = this.props.cluster.get('nodes').filter((node) => !_.contains(this.props.selectedNodeIds, node.id)); var clusterNodes = this.props.cluster.get('nodes').filter((node) => {
return !_.contains(this.props.selectedNodeIds, node.id);
});
var nodesForLimitCheck = new models.Nodes(_.union(selectedNodes, clusterNodes)); var nodesForLimitCheck = new models.Nodes(_.union(selectedNodes, clusterNodes));
cluster.get('roles').each((role) => { cluster.get('roles').each((role) => {
if ((role.get('limits') || {}).max) { if ((role.get('limits') || {}).max) {
var roleName = role.get('name'); var roleName = role.get('name');
var isRoleAlreadyAssigned = nodesForLimitCheck.any((node) => node.hasRole(roleName)); var isRoleAlreadyAssigned = nodesForLimitCheck.any((node) => node.hasRole(roleName));
processedRoleLimits[roleName] = role.checkLimits(this.state.configModels, nodesForLimitCheck, !isRoleAlreadyAssigned, ['max']); processedRoleLimits[roleName] = role.checkLimits(
this.state.configModels,
nodesForLimitCheck,
!isRoleAlreadyAssigned,
['max']
);
} }
}); });
@ -238,11 +270,13 @@ NodeListScreen = React.createClass({
// need to cache roles with limits in order to avoid calculating this twice on the RolePanel // need to cache roles with limits in order to avoid calculating this twice on the RolePanel
processedRoleLimits: processedRoleLimits, processedRoleLimits: processedRoleLimits,
// real number of nodes to add used by Select All controls // real number of nodes to add used by Select All controls
maxNumberOfNodes: maxNumberOfNodes.length ? _.min(maxNumberOfNodes) - _.size(this.props.selectedNodeIds) : null maxNumberOfNodes: maxNumberOfNodes.length ?
_.min(maxNumberOfNodes) - _.size(this.props.selectedNodeIds) : null
}; };
}, },
updateInitialRoles() { updateInitialRoles() {
this.initialRoles = _.zipObject(this.props.nodes.pluck('id'), this.props.nodes.pluck('pending_roles')); this.initialRoles = _.zipObject(this.props.nodes.pluck('id'),
this.props.nodes.pluck('pending_roles'));
}, },
checkRoleAssignment(node, roles, options) { checkRoleAssignment(node, roles, options) {
if (!options.assign) node.set({pending_roles: node.previous('pending_roles')}, {assign: true}); if (!options.assign) node.set({pending_roles: node.previous('pending_roles')}, {assign: true});
@ -309,7 +343,8 @@ NodeListScreen = React.createClass({
return values.map((value) => { return values.map((value) => {
return { return {
name: value, name: value,
label: _.isNull(value) ? i18n(ns + 'label_value_not_specified') : value === false ? i18n(ns + 'label_not_assigned') : value label: _.isNull(value) ? i18n(ns + 'label_value_not_specified') : value === false ?
i18n(ns + 'label_not_assigned') : value
}; };
}); });
} }
@ -317,7 +352,8 @@ NodeListScreen = React.createClass({
var options; var options;
switch (filter.name) { switch (filter.name) {
case 'status': case 'status':
var os = this.props.cluster && this.props.cluster.get('release').get('operating_system') || 'OS'; var os = this.props.cluster && this.props.cluster.get('release').get('operating_system') ||
'OS';
options = this.props.statusesToFilter.map((status) => { options = this.props.statusesToFilter.map((status) => {
return { return {
name: status, name: status,
@ -343,7 +379,12 @@ NodeListScreen = React.createClass({
return { return {
name: groupId, name: groupId,
label: nodeNetworkGroup ? label: nodeNetworkGroup ?
nodeNetworkGroup.get('name') + (this.props.cluster ? '' : ' (' + this.props.clusters.get(nodeNetworkGroup.get('cluster_id')).get('name') + ')') nodeNetworkGroup.get('name') +
(
this.props.cluster ?
'' :
' (' + this.props.clusters.get(nodeNetworkGroup.get('cluster_id')).get('name') + ')'
)
: :
i18n('common.not_specified') i18n('common.not_specified')
}; };
@ -353,7 +394,8 @@ NodeListScreen = React.createClass({
options = _.uniq(this.props.nodes.pluck('cluster')).map((clusterId) => { options = _.uniq(this.props.nodes.pluck('cluster')).map((clusterId) => {
return { return {
name: clusterId, name: clusterId,
label: clusterId ? this.props.clusters.get(clusterId).get('name') : i18n('cluster_page.nodes_tab.node.unallocated') label: clusterId ? this.props.clusters.get(clusterId).get('name') :
i18n('cluster_page.nodes_tab.node.unallocated')
}; };
}); });
break; break;
@ -433,8 +475,11 @@ NodeListScreen = React.createClass({
default: default:
// handle number ranges // handle number ranges
var currentValue = node.resource(filter.name); var currentValue = node.resource(filter.name);
if (filter.name == 'hdd' || filter.name == 'ram') currentValue = currentValue / Math.pow(1024, 3); if (filter.name == 'hdd' || filter.name == 'ram') {
result = currentValue >= filter.values[0] && (_.isUndefined(filter.values[1]) || currentValue <= filter.values[1]); currentValue = currentValue / Math.pow(1024, 3);
}
result = currentValue >= filter.values[0] &&
(_.isUndefined(filter.values[1]) || currentValue <= filter.values[1]);
break; break;
} }
return result; return result;
@ -446,15 +491,24 @@ NodeListScreen = React.createClass({
var processedRoleData = cluster ? this.processRoleLimits() : {}; var processedRoleData = cluster ? this.processRoleLimits() : {};
// labels to manage in labels panel // labels to manage in labels panel
var selectedNodes = new models.Nodes(this.props.nodes.filter((node) => this.props.selectedNodeIds[node.id])); var selectedNodes = new models.Nodes(this.props.nodes.filter((node) => {
var selectedNodeLabels = _.chain(selectedNodes.pluck('labels')).flatten().map(_.keys).flatten().uniq().value(); return this.props.selectedNodeIds[node.id];
}));
var selectedNodeLabels = _.chain(selectedNodes.pluck('labels'))
.flatten()
.map(_.keys)
.flatten()
.uniq()
.value();
// filter nodes // filter nodes
var filteredNodes = nodes.filter((node) => { var filteredNodes = nodes.filter((node) => {
// search field // search field
if (this.state.search) { if (this.state.search) {
var search = this.state.search.toLowerCase(); var search = this.state.search.toLowerCase();
if (!_.any(node.pick('name', 'mac', 'ip'), (attribute) => _.contains((attribute || '').toLowerCase(), search))) { if (!_.any(node.pick('name', 'mac', 'ip'), (attribute) => {
return _.contains((attribute || '').toLowerCase(), search);
})) {
return false; return false;
} }
} }
@ -480,13 +534,21 @@ NodeListScreen = React.createClass({
</div> </div>
} }
<ManagementPanel <ManagementPanel
{... _.pick(this.state, 'viewMode', 'search', 'activeSorters', 'activeFilters', 'availableSorters', 'availableFilters', 'isLabelsPanelOpen')} {... _.pick(
{... _.pick(this.props, 'cluster', 'mode', 'defaultSorting', 'statusesToFilter', 'defaultFilters')} this.state,
{... _.pick(this, 'addSorting', 'removeSorting', 'resetSorters', 'changeSortingOrder')} 'viewMode', 'search', 'activeSorters', 'activeFilters', 'availableSorters',
{... _.pick(this, 'addFilter', 'changeFilter', 'removeFilter', 'resetFilters', 'getFilterOptions')} 'availableFilters', 'isLabelsPanelOpen'
{... _.pick(this, 'toggleLabelsPanel')} )}
{... _.pick(this, 'changeSearch', 'clearSearchField')} {... _.pick(
{... _.pick(this, 'changeViewMode')} this.props,
'cluster', 'mode', 'defaultSorting', 'statusesToFilter', 'defaultFilters'
)}
{... _.pick(
this,
'addSorting', 'removeSorting', 'resetSorters', 'changeSortingOrder',
'addFilter', 'changeFilter', 'removeFilter', 'resetFilters', 'getFilterOptions',
'toggleLabelsPanel', 'changeSearch', 'clearSearchField', 'changeViewMode'
)}
labelSorters={screenNodesLabels.map((name) => new Sorter(name, 'asc', true))} labelSorters={screenNodesLabels.map((name) => new Sorter(name, 'asc', true))}
labelFilters={screenNodesLabels.map((name) => new Filter(name, [], true))} labelFilters={screenNodesLabels.map((name) => new Filter(name, [], true))}
nodes={selectedNodes} nodes={selectedNodes}
@ -508,7 +570,9 @@ NodeListScreen = React.createClass({
} }
<NodeList <NodeList
{... _.pick(this.state, 'viewMode', 'activeSorters', 'selectedRoles')} {... _.pick(this.state, 'viewMode', 'activeSorters', 'selectedRoles')}
{... _.pick(this.props, 'cluster', 'mode', 'statusesToFilter', 'selectedNodeIds', 'clusters', 'roles', 'nodeNetworkGroups')} {... _.pick(this.props, 'cluster', 'mode', 'statusesToFilter', 'selectedNodeIds',
'clusters', 'roles', 'nodeNetworkGroups')
}
{... _.pick(processedRoleData, 'maxNumberOfNodes', 'processedRoleLimits')} {... _.pick(processedRoleData, 'maxNumberOfNodes', 'processedRoleLimits')}
nodes={filteredNodes} nodes={filteredNodes}
totalNodesLength={nodes.length} totalNodesLength={nodes.length}
@ -524,7 +588,10 @@ MultiSelectControl = React.createClass({
propTypes: { propTypes: {
name: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.bool]), name: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.bool]),
options: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, options: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
values: React.PropTypes.arrayOf(React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.bool])), values: React.PropTypes.arrayOf(React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.bool
])),
label: React.PropTypes.node.isRequired, label: React.PropTypes.node.isRequired,
dynamicValues: React.PropTypes.bool, dynamicValues: React.PropTypes.bool,
onChange: React.PropTypes.func, onChange: React.PropTypes.func,
@ -559,7 +626,10 @@ MultiSelectControl = React.createClass({
var label = this.props.label; var label = this.props.label;
if (!this.props.dynamicValues && valuesAmount) { if (!this.props.dynamicValues && valuesAmount) {
label = this.props.label + ': ' + (valuesAmount > 3 ? label = this.props.label + ': ' + (valuesAmount > 3 ?
i18n('cluster_page.nodes_tab.node_management_panel.selected_options', {label: this.props.label, count: valuesAmount}) i18n(
'cluster_page.nodes_tab.node_management_panel.selected_options',
{label: this.props.label, count: valuesAmount}
)
: :
_.map(this.props.values, (itemName) => { _.map(this.props.values, (itemName) => {
return _.find(this.props.options, {name: itemName}).label; return _.find(this.props.options, {name: itemName}).label;
@ -592,7 +662,9 @@ MultiSelectControl = React.createClass({
return ( return (
<div className={utils.classNames(classNames)} tabIndex='-1' onKeyDown={this.closeOnEscapeKey}> <div className={utils.classNames(classNames)} tabIndex='-1' onKeyDown={this.closeOnEscapeKey}>
<button <button
className={'btn dropdown-toggle ' + ((this.props.dynamicValues && !this.props.isOpen) ? 'btn-link' : 'btn-default')} className={'btn dropdown-toggle ' + ((this.props.dynamicValues && !this.props.isOpen) ?
'btn-link' : 'btn-default')
}
onClick={this.props.toggle} onClick={this.props.toggle}
> >
{label} <span className='caret'></span> {label} <span className='caret'></span>
@ -627,7 +699,9 @@ MultiSelectControl = React.createClass({
onChange={_.partialRight(this.onChange, false)} onChange={_.partialRight(this.onChange, false)}
/>; />;
})} })}
{!!attributes.length && !!labels.length && <div key='divider' className='divider' />} {!!attributes.length && !!labels.length &&
<div key='divider' className='divider' />
}
{_.map(labels, (option) => { {_.map(labels, (option) => {
return <Input {...optionProps(option)} return <Input {...optionProps(option)}
key={'label-' + option.name} key={'label-' + option.name}
@ -687,7 +761,9 @@ NumberRangeControl = React.createClass({
return ( return (
<div className={utils.classNames(classNames)} tabIndex='-1' onKeyDown={this.closeOnEscapeKey}> <div className={utils.classNames(classNames)} tabIndex='-1' onKeyDown={this.closeOnEscapeKey}>
<button className='btn btn-default dropdown-toggle' onClick={this.props.toggle}> <button className='btn btn-default dropdown-toggle' onClick={this.props.toggle}>
{this.props.label + ': ' + _.uniq(this.props.values).join(' - ')} <span className='caret'></span> {this.props.label + ': ' + _.uniq(this.props.values).join(' - ')}
{' '}
<span className='caret' />
</button> </button>
{this.props.isOpen && {this.props.isOpen &&
<Popover toggle={this.props.toggle}> <Popover toggle={this.props.toggle}>
@ -735,7 +811,10 @@ ManagementPanel = React.createClass({
var ns = 'cluster_page.nodes_tab.node_management_panel.node_management_error.'; var ns = 'cluster_page.nodes_tab.node_management_panel.node_management_error.';
utils.showErrorDialog({ utils.showErrorDialog({
title: i18n(ns + 'title'), title: i18n(ns + 'title'),
message: <div><i className='glyphicon glyphicon-danger-sign' /> {i18n(ns + action + '_configuration_warning')}</div> message: <div>
<i className='glyphicon glyphicon-danger-sign' />
{i18n(ns + action + '_configuration_warning')}
</div>
}); });
return; return;
} }
@ -743,7 +822,9 @@ ManagementPanel = React.createClass({
}, },
showDeleteNodesDialog() { showDeleteNodesDialog() {
DeleteNodesDialog.show({nodes: this.props.nodes, cluster: this.props.cluster}) DeleteNodesDialog.show({nodes: this.props.nodes, cluster: this.props.cluster})
.done(_.partial(this.props.selectNodes, _.pluck(this.props.nodes.where({status: 'ready'}), 'id'), null, true)); .done(_.partial(this.props.selectNodes,
_.pluck(this.props.nodes.where({status: 'ready'}), 'id'), null, true)
);
}, },
hasChanges() { hasChanges() {
return this.props.hasChanges; return this.props.hasChanges;
@ -761,7 +842,9 @@ ManagementPanel = React.createClass({
var nodes = new models.Nodes(this.props.nodes.map((node) => { var nodes = new models.Nodes(this.props.nodes.map((node) => {
var data = {id: node.id, pending_roles: node.get('pending_roles')}; var data = {id: node.id, pending_roles: node.get('pending_roles')};
if (node.get('pending_roles').length) { if (node.get('pending_roles').length) {
if (this.props.mode == 'add') return _.extend(data, {cluster_id: this.props.cluster.id, pending_addition: true}); if (this.props.mode == 'add') {
return _.extend(data, {cluster_id: this.props.cluster.id, pending_addition: true});
}
} else if (node.get('pending_addition')) { } else if (node.get('pending_addition')) {
return _.extend(data, {cluster_id: null, pending_addition: false}); return _.extend(data, {cluster_id: null, pending_addition: false});
} }
@ -771,7 +854,8 @@ ManagementPanel = React.createClass({
.done(() => { .done(() => {
$.when(this.props.cluster.fetch(), this.props.cluster.fetchRelated('nodes')).always(() => { $.when(this.props.cluster.fetch(), this.props.cluster.fetchRelated('nodes')).always(() => {
if (this.props.mode == 'add') { if (this.props.mode == 'add') {
dispatcher.trigger('updateNodeStats networkConfigurationUpdated labelsConfigurationUpdated'); dispatcher.trigger('updateNodeStats networkConfigurationUpdated ' +
'labelsConfigurationUpdated');
this.props.selectNodes(); this.props.selectNodes();
} }
}); });
@ -779,7 +863,8 @@ ManagementPanel = React.createClass({
.fail((response) => { .fail((response) => {
this.setState({actionInProgress: false}); this.setState({actionInProgress: false});
utils.showErrorDialog({ utils.showErrorDialog({
message: i18n('cluster_page.nodes_tab.node_management_panel.node_management_error.saving_warning'), message: i18n('cluster_page.nodes_tab.node_management_panel.' +
'node_management_error.saving_warning'),
response: response response: response
}); });
}); });
@ -800,7 +885,8 @@ ManagementPanel = React.createClass({
activateSearch() { activateSearch() {
this.setState({activeSearch: true}); this.setState({activeSearch: true});
$('html').on('click.search', (e) => { $('html').on('click.search', (e) => {
if (!this.props.search && this.refs.search && !$(e.target).closest(ReactDOM.findDOMNode(this.refs.search)).length) { if (!this.props.search && this.refs.search &&
!$(e.target).closest(ReactDOM.findDOMNode(this.refs.search)).length) {
this.setState({activeSearch: false}); this.setState({activeSearch: false});
} }
}); });
@ -838,17 +924,20 @@ ManagementPanel = React.createClass({
}, },
toggleMoreFilterControl(visible) { toggleMoreFilterControl(visible) {
this.setState({ this.setState({
isMoreFilterControlVisible: _.isBoolean(visible) ? visible : !this.state.isMoreFilterControlVisible, isMoreFilterControlVisible: _.isBoolean(visible) ? visible :
!this.state.isMoreFilterControlVisible,
openFilter: null openFilter: null
}); });
}, },
toggleMoreSorterControl(visible) { toggleMoreSorterControl(visible) {
this.setState({ this.setState({
isMoreSorterControlVisible: _.isBoolean(visible) ? visible : !this.state.isMoreSorterControlVisible isMoreSorterControlVisible: _.isBoolean(visible) ? visible :
!this.state.isMoreSorterControlVisible
}); });
}, },
isFilterOpen(filter) { isFilterOpen(filter) {
return !_.isNull(this.state.openFilter) && this.state.openFilter.name == filter.name && this.state.openFilter.isLabel == filter.isLabel; return !_.isNull(this.state.openFilter) &&
this.state.openFilter.name == filter.name && this.state.openFilter.isLabel == filter.isLabel;
}, },
addFilter(filter) { addFilter(filter) {
this.props.addFilter(filter); this.props.addFilter(filter);
@ -889,7 +978,10 @@ ManagementPanel = React.createClass({
renderDeleteFilterButton(filter) { renderDeleteFilterButton(filter) {
if (!filter.isLabel && _.contains(_.keys(this.props.defaultFilters), filter.name)) return null; if (!filter.isLabel && _.contains(_.keys(this.props.defaultFilters), filter.name)) return null;
return ( return (
<i className='btn btn-link glyphicon glyphicon-minus-sign btn-remove-filter' onClick={_.partial(this.removeFilter, filter)} /> <i
className='btn btn-link glyphicon glyphicon-minus-sign btn-remove-filter'
onClick={_.partial(this.removeFilter, filter)}
/>
); );
}, },
toggleLabelsPanel() { toggleLabelsPanel() {
@ -902,7 +994,10 @@ ManagementPanel = React.createClass({
}, },
renderDeleteSorterButton(sorter) { renderDeleteSorterButton(sorter) {
return ( return (
<i className='btn btn-link glyphicon glyphicon-minus-sign btn-remove-sorting' onClick={_.partial(this.removeSorting, sorter)} /> <i
className='btn btn-link glyphicon glyphicon-minus-sign btn-remove-sorting'
onClick={_.partial(this.removeSorting, sorter)}
/>
); );
}, },
render() { render() {
@ -925,14 +1020,32 @@ ManagementPanel = React.createClass({
}; };
if (this.props.mode != 'edit') { if (this.props.mode != 'edit') {
var checkSorter = (sorter, isLabel) => !_.any(this.props.activeSorters, {name: sorter.name, isLabel: isLabel}); var checkSorter = (sorter, isLabel) => {
inactiveSorters = _.union(_.filter(this.props.availableSorters, _.partial(checkSorter, _, false)), _.filter(this.props.labelSorters, _.partial(checkSorter, _, true))) return !_.any(this.props.activeSorters, {name: sorter.name, isLabel: isLabel});
.sort((sorter1, sorter2) => utils.natsort(sorter1.title, sorter2.title, {insensitive: true})); };
canResetSorters = _.any(this.props.activeSorters, {isLabel: true}) || !_(this.props.activeSorters).where({isLabel: false}).map(Sorter.toObject).isEqual(this.props.defaultSorting); inactiveSorters = _.union(
_.filter(this.props.availableSorters, _.partial(checkSorter, _, false)),
_.filter(this.props.labelSorters, _.partial(checkSorter, _, true))
)
.sort((sorter1, sorter2) => {
return utils.natsort(sorter1.title, sorter2.title, {insensitive: true});
});
canResetSorters = _.any(this.props.activeSorters, {isLabel: true}) ||
!_(this.props.activeSorters)
.where({isLabel: false})
.map(Sorter.toObject)
.isEqual(this.props.defaultSorting);
var checkFilter = (filter, isLabel) => !_.any(this.props.activeFilters, {name: filter.name, isLabel: isLabel}); var checkFilter = (filter, isLabel) => {
inactiveFilters = _.union(_.filter(this.props.availableFilters, _.partial(checkFilter, _, false)), _.filter(this.props.labelFilters, _.partial(checkFilter, _, true))) return !_.any(this.props.activeFilters, {name: filter.name, isLabel: isLabel});
.sort((filter1, filter2) => utils.natsort(filter1.title, filter2.title, {insensitive: true})); };
inactiveFilters = _.union(
_.filter(this.props.availableFilters, _.partial(checkFilter, _, false)),
_.filter(this.props.labelFilters, _.partial(checkFilter, _, true))
)
.sort((filter1, filter2) => {
return utils.natsort(filter1.title, filter2.title, {insensitive: true});
});
appliedFilters = _.reject(this.props.activeFilters, (filter) => !filter.values.length); appliedFilters = _.reject(this.props.activeFilters, (filter) => !filter.values.length);
} }
@ -948,8 +1061,12 @@ ManagementPanel = React.createClass({
return ( return (
<Tooltip key={mode + '-view'} text={i18n(ns + mode + '_mode_tooltip')}> <Tooltip key={mode + '-view'} text={i18n(ns + mode + '_mode_tooltip')}>
<label <label
className={utils.classNames(managementButtonClasses(mode == this.props.viewMode, mode))} className={utils.classNames(
onClick={mode != this.props.viewMode && _.partial(this.props.changeViewMode, 'view_mode', mode)} managementButtonClasses(mode == this.props.viewMode, mode)
)}
onClick={mode != this.props.viewMode &&
_.partial(this.props.changeViewMode, 'view_mode', mode)
}
> >
<input type='radio' name='view_mode' value={mode} /> <input type='radio' name='view_mode' value={mode} />
<i <i
@ -970,7 +1087,9 @@ ManagementPanel = React.createClass({
<button <button
disabled={!this.props.nodes.length} disabled={!this.props.nodes.length}
onClick={this.props.nodes.length && this.toggleLabelsPanel} onClick={this.props.nodes.length && this.toggleLabelsPanel}
className={utils.classNames(managementButtonClasses(this.props.isLabelsPanelOpen, 'btn-labels'))} className={utils.classNames(
managementButtonClasses(this.props.isLabelsPanelOpen, 'btn-labels')
)}
> >
<i className='glyphicon glyphicon-tag' /> <i className='glyphicon glyphicon-tag' />
</button> </button>
@ -979,7 +1098,9 @@ ManagementPanel = React.createClass({
<button <button
disabled={!this.props.screenNodes.length} disabled={!this.props.screenNodes.length}
onClick={this.toggleSorters} onClick={this.toggleSorters}
className={utils.classNames(managementButtonClasses(this.state.areSortersVisible, 'btn-sorters'))} className={utils.classNames(
managementButtonClasses(this.state.areSortersVisible, 'btn-sorters')
)}
> >
<i className='glyphicon glyphicon-sort' /> <i className='glyphicon glyphicon-sort' />
</button> </button>
@ -988,7 +1109,9 @@ ManagementPanel = React.createClass({
<button <button
disabled={!this.props.screenNodes.length} disabled={!this.props.screenNodes.length}
onClick={this.toggleFilters} onClick={this.toggleFilters}
className={utils.classNames(managementButtonClasses(this.state.areFiltersVisible, 'btn-filters'))} className={utils.classNames(
managementButtonClasses(this.state.areFiltersVisible, 'btn-filters')
)}
> >
<i className='glyphicon glyphicon-filter' /> <i className='glyphicon glyphicon-filter' />
</button> </button>
@ -1018,7 +1141,12 @@ ManagementPanel = React.createClass({
autoFocus autoFocus
/> />
{this.state.isSearchButtonVisible && {this.state.isSearchButtonVisible &&
<button className='close btn-clear-search' onClick={this.clearSearchField}>&times;</button> <button
className='close btn-clear-search'
onClick={this.clearSearchField}
>
&times;
</button>
} }
</div> </div>
) )
@ -1055,19 +1183,27 @@ ManagementPanel = React.createClass({
onClick={_.bind(this.goToConfigurationScreen, this, 'disks', disksConflict)} onClick={_.bind(this.goToConfigurationScreen, this, 'disks', disksConflict)}
> >
{disksConflict && <i className='glyphicon glyphicon-danger-sign' />} {disksConflict && <i className='glyphicon glyphicon-danger-sign' />}
{i18n('dialog.show_node.disk_configuration' + (_.all(this.props.nodes.invoke('areDisksConfigurable')) ? '_action' : ''))} {i18n('dialog.show_node.disk_configuration' +
(_.all(this.props.nodes.invoke('areDisksConfigurable')) ? '_action' : ''))
}
</button> </button>
<button <button
className='btn btn-default btn-configure-interfaces' className='btn btn-default btn-configure-interfaces'
disabled={!this.props.nodes.length} disabled={!this.props.nodes.length}
onClick={_.bind(this.goToConfigurationScreen, this, 'interfaces', interfaceConflict)} onClick={_.bind(this.goToConfigurationScreen, this, 'interfaces',
interfaceConflict)
}
> >
{interfaceConflict && <i className='glyphicon glyphicon-danger-sign' />} {interfaceConflict && <i className='glyphicon glyphicon-danger-sign' />}
{i18n('dialog.show_node.network_configuration' + (_.all(this.props.nodes.invoke('areInterfacesConfigurable')) ? '_action' : ''))} {i18n('dialog.show_node.network_configuration' +
(_.all(this.props.nodes.invoke('areInterfacesConfigurable')) ?
'_action' : ''))
}
</button> </button>
</div>, </div>,
<div className='btn-group' role='group' key='role-management-buttons'> <div className='btn-group' role='group' key='role-management-buttons'>
{!this.props.locked && !!this.props.nodes.length && this.props.nodes.any({pending_deletion: false}) && {!this.props.locked && !!this.props.nodes.length &&
this.props.nodes.any({pending_deletion: false}) &&
<button <button
className='btn btn-danger btn-delete-nodes' className='btn btn-danger btn-delete-nodes'
onClick={this.showDeleteNodesDialog} onClick={this.showDeleteNodesDialog}
@ -1076,7 +1212,8 @@ ManagementPanel = React.createClass({
{i18n('common.delete_button')} {i18n('common.delete_button')}
</button> </button>
} }
{!!this.props.nodes.length && !this.props.nodes.any({pending_addition: false}) && {!!this.props.nodes.length &&
!this.props.nodes.any({pending_addition: false}) &&
<button <button
className='btn btn-success btn-edit-roles' className='btn btn-success btn-edit-roles'
onClick={_.bind(this.changeScreen, this, 'edit', true)} onClick={_.bind(this.changeScreen, this, 'edit', true)}
@ -1113,7 +1250,10 @@ ManagementPanel = React.createClass({
<div className='well-heading'> <div className='well-heading'>
<i className='glyphicon glyphicon-sort' /> {i18n(ns + 'sort_by')} <i className='glyphicon glyphicon-sort' /> {i18n(ns + 'sort_by')}
{canResetSorters && {canResetSorters &&
<button className='btn btn-link pull-right btn-reset-sorting' onClick={this.resetSorters}> <button
className='btn btn-link pull-right btn-reset-sorting'
onClick={this.resetSorters}
>
<i className='glyphicon glyphicon-remove-sign' /> {i18n(ns + 'reset')} <i className='glyphicon glyphicon-remove-sign' /> {i18n(ns + 'reset')}
</button> </button>
} }
@ -1128,7 +1268,10 @@ ManagementPanel = React.createClass({
['sort-by-' + sorter.name + '-' + sorter.order]: !sorter.isLabel ['sort-by-' + sorter.name + '-' + sorter.order]: !sorter.isLabel
})} })}
> >
<button className='btn btn-default' onClick={_.partial(this.props.changeSortingOrder, sorter)}> <button
className='btn btn-default'
onClick={_.partial(this.props.changeSortingOrder, sorter)}
>
{sorter.title} {sorter.title}
<i <i
className={utils.classNames({ className={utils.classNames({
@ -1138,7 +1281,9 @@ ManagementPanel = React.createClass({
})} })}
/> />
</button> </button>
{this.props.activeSorters.length > 1 && this.renderDeleteSorterButton(sorter)} {this.props.activeSorters.length > 1 &&
this.renderDeleteSorterButton(sorter)
}
</div> </div>
); );
})} })}
@ -1160,7 +1305,10 @@ ManagementPanel = React.createClass({
<div className='well-heading'> <div className='well-heading'>
<i className='glyphicon glyphicon-filter' /> {i18n(ns + 'filter_by')} <i className='glyphicon glyphicon-filter' /> {i18n(ns + 'filter_by')}
{!!appliedFilters.length && {!!appliedFilters.length &&
<button className='btn btn-link pull-right btn-reset-filters' onClick={this.resetFilters}> <button
className='btn btn-link pull-right btn-reset-filters'
onClick={this.resetFilters}
>
<i className='glyphicon glyphicon-remove-sign' /> {i18n(ns + 'reset')} <i className='glyphicon glyphicon-remove-sign' /> {i18n(ns + 'reset')}
</button> </button>
} }
@ -1178,15 +1326,25 @@ ManagementPanel = React.createClass({
label: filter.title, label: filter.title,
extraContent: this.renderDeleteFilterButton(filter), extraContent: this.renderDeleteFilterButton(filter),
onChange: _.partial(this.props.changeFilter, filter), onChange: _.partial(this.props.changeFilter, filter),
prefix: i18n('cluster_page.nodes_tab.filters.prefixes.' + filter.name, {defaultValue: ''}), prefix: i18n(
'cluster_page.nodes_tab.filters.prefixes.' + filter.name,
{defaultValue: ''}
),
isOpen: this.isFilterOpen(filter), isOpen: this.isFilterOpen(filter),
toggle: _.partial(this.toggleFilter, filter) toggle: _.partial(this.toggleFilter, filter)
}; };
if (filter.isNumberRange) { if (filter.isNumberRange) {
return <NumberRangeControl {...props} min={filter.limits[0]} max={filter.limits[1]} />; return <NumberRangeControl
{...props}
min={filter.limits[0]}
max={filter.limits[1]}
/>;
} }
return <MultiSelectControl {...props} options={this.props.getFilterOptions(filter)} />; return <MultiSelectControl
{...props}
options={this.props.getFilterOptions(filter)}
/>;
})} })}
<MultiSelectControl <MultiSelectControl
name='filter-more' name='filter-more'
@ -1203,7 +1361,8 @@ ManagementPanel = React.createClass({
]} ]}
{this.props.mode != 'edit' && !!this.props.screenNodes.length && {this.props.mode != 'edit' && !!this.props.screenNodes.length &&
<div className='col-xs-12'> <div className='col-xs-12'>
{(!this.state.areSortersVisible || !this.state.areFiltersVisible && !!appliedFilters.length) && {(!this.state.areSortersVisible || !this.state.areFiltersVisible &&
!!appliedFilters.length) &&
<div className='active-sorters-filters'> <div className='active-sorters-filters'>
{!this.state.areFiltersVisible && !!appliedFilters.length && {!this.state.areFiltersVisible && !!appliedFilters.length &&
<div className='active-filters row' onClick={this.toggleFilters}> <div className='active-filters row' onClick={this.toggleFilters}>
@ -1214,7 +1373,8 @@ ManagementPanel = React.createClass({
total: this.props.screenNodes.length total: this.props.screenNodes.length
})} })}
{_.map(appliedFilters, (filter) => { {_.map(appliedFilters, (filter) => {
var options = filter.isNumberRange ? null : this.props.getFilterOptions(filter); var options = filter.isNumberRange ? null :
this.props.getFilterOptions(filter);
return ( return (
<div key={filter.name}> <div key={filter.name}>
<strong>{filter.title}{!!filter.values.length && ':'} </strong> <strong>{filter.title}{!!filter.values.length && ':'} </strong>
@ -1223,7 +1383,9 @@ ManagementPanel = React.createClass({
_.uniq(filter.values).join(' - ') _.uniq(filter.values).join(' - ')
: :
_.pluck( _.pluck(
_.filter(options, (option) => _.contains(filter.values, option.name)) _.filter(options, (option) => {
return _.contains(filter.values, option.name);
})
, 'label').join(', ') , 'label').join(', ')
} }
</span> </span>
@ -1231,7 +1393,10 @@ ManagementPanel = React.createClass({
); );
}, this)} }, this)}
</div> </div>
<button className='btn btn-link btn-reset-filters' onClick={this.resetFilters}> <button
className='btn btn-link btn-reset-filters'
onClick={this.resetFilters}
>
<i className='glyphicon glyphicon-remove-sign' /> <i className='glyphicon glyphicon-remove-sign' />
</button> </button>
</div> </div>
@ -1258,7 +1423,10 @@ ManagementPanel = React.createClass({
})} })}
</div> </div>
{canResetSorters && {canResetSorters &&
<button className='btn btn-link btn-reset-sorting' onClick={this.resetSorters}> <button
className='btn btn-link btn-reset-sorting'
onClick={this.resetSorters}
>
<i className='glyphicon glyphicon-remove-sign' /> <i className='glyphicon glyphicon-remove-sign' />
</button> </button>
} }
@ -1355,7 +1523,8 @@ NodeLabelsPanel = React.createClass({
}); });
}, },
isSavingPossible() { isSavingPossible() {
return !this.state.actionInProgress && this.hasChanges() && _.all(_.pluck(this.state.labels, 'error'), _.isNull); return !this.state.actionInProgress && this.hasChanges() &&
_.all(_.pluck(this.state.labels, 'error'), _.isNull);
}, },
revertChanges() { revertChanges() {
return this.props.toggleLabelsPanel(); return this.props.toggleLabelsPanel();
@ -1409,7 +1578,10 @@ NodeLabelsPanel = React.createClass({
}) })
.fail((response) => { .fail((response) => {
utils.showErrorDialog({ utils.showErrorDialog({
message: i18n('cluster_page.nodes_tab.node_management_panel.node_management_error.labels_warning'), message: i18n(
'cluster_page.nodes_tab.node_management_panel.' +
'node_management_error.labels_warning'
),
response: response response: response
}); });
}); });
@ -1443,7 +1615,10 @@ NodeLabelsPanel = React.createClass({
var showControlLabels = index == 0; var showControlLabels = index == 0;
return ( return (
<div className={utils.classNames({clearfix: true, 'has-label': showControlLabels})} key={index}> <div
className={utils.classNames({clearfix: true, 'has-label': showControlLabels})}
key={index}
>
<Input <Input
type='checkbox' type='checkbox'
ref={labelData.key} ref={labelData.key}
@ -1547,12 +1722,19 @@ RolePanel = React.createClass({
.value(); .value();
var messages = []; var messages = [];
if (restrictionsCheck.result && restrictionsCheck.message) messages.push(restrictionsCheck.message); if (restrictionsCheck.result && restrictionsCheck.message) {
if (roleLimitsCheckResults && !roleLimitsCheckResults.valid && roleLimitsCheckResults.message) messages.push(roleLimitsCheckResults.message); messages.push(restrictionsCheck.message);
}
if (roleLimitsCheckResults && !roleLimitsCheckResults.valid && roleLimitsCheckResults.message) {
messages.push(roleLimitsCheckResults.message);
}
if (_.contains(conflicts, name)) messages.push(i18n('cluster_page.nodes_tab.role_conflict')); if (_.contains(conflicts, name)) messages.push(i18n('cluster_page.nodes_tab.role_conflict'));
return { return {
result: restrictionsCheck.result || _.contains(conflicts, name) || (roleLimitsCheckResults && !roleLimitsCheckResults.valid && !_.contains(this.props.selectedRoles, name)), result: restrictionsCheck.result || _.contains(conflicts, name) ||
(roleLimitsCheckResults && !roleLimitsCheckResults.valid &&
!_.contains(this.props.selectedRoles, name)
),
message: messages.join(' ') message: messages.join(' ')
}; };
}, },
@ -1563,7 +1745,8 @@ RolePanel = React.createClass({
{this.props.cluster.get('roles').map((role) => { {this.props.cluster.get('roles').map((role) => {
if (!role.checkRestrictions(this.props.configModels, 'hide').result) { if (!role.checkRestrictions(this.props.configModels, 'hide').result) {
var name = role.get('name'); var name = role.get('name');
var processedRestrictions = this.props.nodes.length ? this.processRestrictions(role, this.props.configModels) : {}; var processedRestrictions = this.props.nodes.length ?
this.processRestrictions(role, this.props.configModels) : {};
return ( return (
<Input <Input
key={name} key={name}
@ -1590,11 +1773,14 @@ SelectAllMixin = {
componentDidUpdate() { componentDidUpdate() {
if (this.refs['select-all']) { if (this.refs['select-all']) {
var input = this.refs['select-all'].getInputDOMNode(); var input = this.refs['select-all'].getInputDOMNode();
input.indeterminate = !input.checked && _.any(this.props.nodes, (node) => this.props.selectedNodeIds[node.id]); input.indeterminate = !input.checked && _.any(this.props.nodes, (node) => {
return this.props.selectedNodeIds[node.id];
});
} }
}, },
renderSelectAllCheckbox() { renderSelectAllCheckbox() {
var checked = this.props.mode == 'edit' || (this.props.nodes.length && !_.any(this.props.nodes, (node) => !this.props.selectedNodeIds[node.id])); var checked = this.props.mode == 'edit' || (this.props.nodes.length &&
!_.any(this.props.nodes, (node) => !this.props.selectedNodeIds[node.id]));
return ( return (
<Input <Input
ref='select-all' ref='select-all'
@ -1603,7 +1789,8 @@ SelectAllMixin = {
checked={checked} checked={checked}
disabled={ disabled={
this.props.mode == 'edit' || this.props.locked || !this.props.nodes.length || this.props.mode == 'edit' || this.props.locked || !this.props.nodes.length ||
!checked && !_.isNull(this.props.maxNumberOfNodes) && this.props.maxNumberOfNodes < this.props.nodes.length !checked && !_.isNull(this.props.maxNumberOfNodes) &&
this.props.maxNumberOfNodes < this.props.nodes.length
} }
label={i18n('common.select_all')} label={i18n('common.select_all')}
wrapperClassName='select-all pull-right' wrapperClassName='select-all pull-right'
@ -1622,7 +1809,8 @@ NodeList = React.createClass({
var diskSizes = node.resource('disks'); var diskSizes = node.resource('disks');
return i18n('node_details.disks_amount', { return i18n('node_details.disks_amount', {
count: diskSizes.length, count: diskSizes.length,
size: diskSizes.map((size) => utils.showDiskSize(size) + ' ' + i18n('node_details.hdd')).join(', ') size: diskSizes.map((size) => utils.showDiskSize(size) + ' ' +
i18n('node_details.hdd')).join(', ')
}); });
}; };
@ -1655,14 +1843,27 @@ NodeList = React.createClass({
group_id: () => { group_id: () => {
var nodeNetworkGroup = this.props.nodeNetworkGroups.get(node.get('group_id')); var nodeNetworkGroup = this.props.nodeNetworkGroups.get(node.get('group_id'));
return nodeNetworkGroup && i18n(ns + 'node_network_group', { return nodeNetworkGroup && i18n(ns + 'node_network_group', {
group: nodeNetworkGroup.get('name') + (this.props.cluster ? '' : ' (' + cluster.get('name') + ')') group: nodeNetworkGroup.get('name') +
(this.props.cluster ? '' : ' (' + cluster.get('name') + ')')
}) || i18n(ns + 'no_node_network_group'); }) || i18n(ns + 'no_node_network_group');
}, },
cluster: () => cluster && i18n(ns + 'cluster', {cluster: cluster.get('name')}) || i18n(ns + 'unallocated'), cluster: () => cluster && i18n(
hdd: () => i18n('node_details.total_hdd', {total: utils.showDiskSize(node.resource('hdd'))}), ns + 'cluster',
{cluster: cluster.get('name')}
) || i18n(ns + 'unallocated'),
hdd: () => i18n(
'node_details.total_hdd',
{total: utils.showDiskSize(node.resource('hdd'))}
),
disks: () => composeNodeDiskSizesLabel(node), disks: () => composeNodeDiskSizesLabel(node),
ram: () => i18n('node_details.total_ram', {total: utils.showMemorySize(node.resource('ram'))}), ram: () => i18n(
interfaces: () => i18n('node_details.interfaces_amount', {count: node.resource('interfaces')}), 'node_details.total_ram',
{total: utils.showMemorySize(node.resource('ram'))}
),
interfaces: () => i18n(
'node_details.interfaces_amount',
{count: node.resource('interfaces')}
),
default: () => i18n('node_details.' + sorter.name, {count: node.resource(sorter.name)}) default: () => i18n('node_details.' + sorter.name, {count: node.resource(sorter.name)})
}; };
return (sorterNameFormatters[sorter.name] || sorterNameFormatters.default)(); return (sorterNameFormatters[sorter.name] || sorterNameFormatters.default)();
@ -1698,7 +1899,8 @@ NodeList = React.createClass({
if (node1Label && node2Label) { if (node1Label && node2Label) {
result = utils.natsort(node1Label, node2Label, {insensitive: true}); result = utils.natsort(node1Label, node2Label, {insensitive: true});
} else { } else {
result = node1Label === node2Label ? 0 : _.isString(node1Label) ? -1 : _.isNull(node1Label) ? -1 : 1; result = node1Label === node2Label ? 0 : _.isString(node1Label) ? -1 :
_.isNull(node1Label) ? -1 : 1;
} }
} else { } else {
var comparators = { var comparators = {
@ -1711,19 +1913,22 @@ NodeList = React.createClass({
else if (!roles2.length) result = -1; else if (!roles2.length) result = -1;
else { else {
while (!order && roles1.length && roles2.length) { while (!order && roles1.length && roles2.length) {
order = _.indexOf(preferredRolesOrder, roles1.shift()) - _.indexOf(preferredRolesOrder, roles2.shift()); order = _.indexOf(preferredRolesOrder, roles1.shift()) -
_.indexOf(preferredRolesOrder, roles2.shift());
} }
result = order || roles1.length - roles2.length; result = order || roles1.length - roles2.length;
} }
}, },
status: () => { status: () => {
result = _.indexOf(this.props.statusesToFilter, node1.getStatusSummary()) - _.indexOf(this.props.statusesToFilter, node2.getStatusSummary()); result = _.indexOf(this.props.statusesToFilter, node1.getStatusSummary()) -
_.indexOf(this.props.statusesToFilter, node2.getStatusSummary());
}, },
manufacturer: () => { manufacturer: () => {
result = utils.compare(node1, node2, {attr: sorter.name}); result = utils.compare(node1, node2, {attr: sorter.name});
}, },
disks: () => { disks: () => {
result = utils.natsort(composeNodeDiskSizesLabel(node1), composeNodeDiskSizesLabel(node2)); result = utils.natsort(composeNodeDiskSizesLabel(node1),
composeNodeDiskSizesLabel(node2));
}, },
group_id: () => { group_id: () => {
var nodeGroup1 = node1.get('group_id'); var nodeGroup1 = node1.get('group_id');
@ -1735,7 +1940,9 @@ NodeList = React.createClass({
var cluster1 = node1.get('cluster'); var cluster1 = node1.get('cluster');
var cluster2 = node2.get('cluster'); var cluster2 = node2.get('cluster');
result = cluster1 == cluster2 ? 0 : result = cluster1 == cluster2 ? 0 :
!cluster1 ? 1 : !cluster2 ? -1 : utils.natsort(this.props.clusters.get(cluster1).get('name'), this.props.clusters.get(cluster2).get('name')); !cluster1 ? 1 : !cluster2 ? -1 :
utils.natsort(this.props.clusters.get(cluster1).get('name'),
this.props.clusters.get(cluster2).get('name'));
}, },
default: () => { default: () => {
result = node1.resource(sorter.name) - node2.resource(sorter.name); result = node1.resource(sorter.name) - node2.resource(sorter.name);
@ -1754,11 +1961,15 @@ NodeList = React.createClass({
}, },
render() { render() {
var groups = this.groupNodes(); var groups = this.groupNodes();
var rolesWithLimitReached = _.keys(_.omit(this.props.processedRoleLimits, (roleLimit, roleName) => { var rolesWithLimitReached = _.keys(_.omit(this.props.processedRoleLimits,
return roleLimit.valid || !_.contains(this.props.selectedRoles, roleName); (roleLimit, roleName) => {
})); return roleLimit.valid || !_.contains(this.props.selectedRoles, roleName);
}
));
return ( return (
<div className={utils.classNames({'node-list row': true, compact: this.props.viewMode == 'compact'})}> <div className={utils.classNames({
'node-list row': true, compact: this.props.viewMode == 'compact'
})}>
{groups.length > 1 && {groups.length > 1 &&
<div className='col-xs-12 node-list-header'> <div className='col-xs-12 node-list-header'>
{this.renderSelectAllCheckbox()} {this.renderSelectAllCheckbox()}
@ -1783,7 +1994,11 @@ NodeList = React.createClass({
: :
<div className='alert alert-warning'> <div className='alert alert-warning'>
{utils.renderMultilineText( {utils.renderMultilineText(
i18n('cluster_page.nodes_tab.' + (this.props.mode == 'add' ? 'no_nodes_in_fuel' : 'no_nodes_in_environment')) i18n(
'cluster_page.nodes_tab.' + (this.props.mode == 'add' ?
'no_nodes_in_fuel' : 'no_nodes_in_environment'
)
)
)} )}
</div> </div>
} }

View File

@ -30,7 +30,8 @@ var SettingSection = React.createClass({
// FIXME: hack for #1442475 to lock images_ceph in env with controllers // FIXME: hack for #1442475 to lock images_ceph in env with controllers
if (settingName == 'images_ceph') { if (settingName == 'images_ceph') {
if (_.contains(_.flatten(this.props.cluster.get('nodes').pluck('pending_roles')), 'controller')) { if (_.contains(_.flatten(this.props.cluster.get('nodes').pluck('pending_roles')),
'controller')) {
result = true; result = true;
messages.push(i18n('cluster_page.settings_tab.images_ceph_warning')); messages.push(i18n('cluster_page.settings_tab.images_ceph_warning'));
} }
@ -46,8 +47,22 @@ var SettingSection = React.createClass({
var dependentRoles = this.checkDependentRoles(sectionName, settingName); var dependentRoles = this.checkDependentRoles(sectionName, settingName);
var dependentSettings = this.checkDependentSettings(sectionName, settingName); var dependentSettings = this.checkDependentSettings(sectionName, settingName);
if (dependentRoles.length) messages.push(i18n('cluster_page.settings_tab.dependent_role_warning', {roles: dependentRoles.join(', '), count: dependentRoles.length})); if (dependentRoles.length) {
if (dependentSettings.length) messages.push(i18n('cluster_page.settings_tab.dependent_settings_warning', {settings: dependentSettings.join(', '), count: dependentSettings.length})); messages.push(
i18n(
'cluster_page.settings_tab.dependent_role_warning',
{roles: dependentRoles.join(', '), count: dependentRoles.length}
)
);
}
if (dependentSettings.length) {
messages.push(
i18n(
'cluster_page.settings_tab.dependent_settings_warning',
{settings: dependentSettings.join(', '), count: dependentSettings.length}
)
);
}
return { return {
result: !!dependentRoles.length || !!dependentSettings.length, result: !!dependentRoles.length || !!dependentSettings.length,
@ -58,13 +73,18 @@ var SettingSection = React.createClass({
return setting.toggleable || _.contains(['checkbox', 'radio'], setting.type); return setting.toggleable || _.contains(['checkbox', 'radio'], setting.type);
}, },
getValuesToCheck(setting, valueAttribute) { getValuesToCheck(setting, valueAttribute) {
return setting.values ? _.without(_.pluck(setting.values, 'data'), setting[valueAttribute]) : [!setting[valueAttribute]]; return setting.values ? _.without(_.pluck(setting.values, 'data'), setting[valueAttribute]) :
[!setting[valueAttribute]];
}, },
checkValues(values, path, currentValue, restriction) { checkValues(values, path, currentValue, restriction) {
var extraModels = {settings: this.props.settingsForChecks}; var extraModels = {settings: this.props.settingsForChecks};
var result = _.all(values, (value) => { var result = _.all(values, (value) => {
this.props.settingsForChecks.set(path, value); this.props.settingsForChecks.set(path, value);
return new Expression(restriction.condition, this.props.configModels, restriction).evaluate(extraModels); return new Expression(
restriction.condition,
this.props.configModels,
restriction
).evaluate(extraModels);
}); });
this.props.settingsForChecks.set(path, currentValue); this.props.settingsForChecks.set(path, currentValue);
return result; return result;
@ -82,7 +102,12 @@ var SettingSection = React.createClass({
var role = roles.findWhere({name: roleName}); var role = roles.findWhere({name: roleName});
if (_.any(role.get('restrictions'), (restriction) => { if (_.any(role.get('restrictions'), (restriction) => {
restriction = utils.expandRestriction(restriction); restriction = utils.expandRestriction(restriction);
if (_.contains(restriction.condition, 'settings:' + path) && !(new Expression(restriction.condition, this.props.configModels, restriction).evaluate())) { if (_.contains(restriction.condition, 'settings:' + path) &&
!(new Expression(
restriction.condition,
this.props.configModels,
restriction
).evaluate())) {
return this.checkValues(valuesToCheck, pathToCheck, setting[valueAttribute], restriction); return this.checkValues(valuesToCheck, pathToCheck, setting[valueAttribute], restriction);
} }
})) return role.get('label'); })) return role.get('label');
@ -95,7 +120,10 @@ var SettingSection = React.createClass({
var dependentRestrictions = {}; var dependentRestrictions = {};
var addDependentRestrictions = (setting, label) => { var addDependentRestrictions = (setting, label) => {
var result = _.filter(_.map(setting.restrictions, utils.expandRestriction), var result = _.filter(_.map(setting.restrictions, utils.expandRestriction),
(restriction) => restriction.action == 'disable' && _.contains(restriction.condition, 'settings:' + path) (restriction) => {
return restriction.action == 'disable' &&
_.contains(restriction.condition, 'settings:' + path);
}
); );
if (result.length) { if (result.length) {
dependentRestrictions[label] = result.concat(dependentRestrictions[label] || []); dependentRestrictions[label] = result.concat(dependentRestrictions[label] || []);
@ -106,7 +134,8 @@ var SettingSection = React.createClass({
// don't take into account hidden dependent settings // don't take into account hidden dependent settings
if (this.props.checkRestrictions('hide', section.metadata).result) return; if (this.props.checkRestrictions('hide', section.metadata).result) return;
_.each(section, (setting, settingName) => { _.each(section, (setting, settingName) => {
// we support dependecies on checkboxes, toggleable setting groups, dropdowns and radio groups // we support dependecies on checkboxes,
// toggleable setting groups, dropdowns and radio groups
if (!this.areCalculationsPossible(setting) || if (!this.areCalculationsPossible(setting) ||
this.props.makePath(sectionName, settingName) == path || this.props.makePath(sectionName, settingName) == path ||
this.props.checkRestrictions('hide', setting).result this.props.checkRestrictions('hide', setting).result
@ -124,7 +153,10 @@ var SettingSection = React.createClass({
var valueAttribute = this.props.getValueAttribute(settingName); var valueAttribute = this.props.getValueAttribute(settingName);
var pathToCheck = this.props.makePath(path, valueAttribute); var pathToCheck = this.props.makePath(path, valueAttribute);
var valuesToCheck = this.getValuesToCheck(currentSetting, valueAttribute); var valuesToCheck = this.getValuesToCheck(currentSetting, valueAttribute);
var checkValues = _.partial(this.checkValues, valuesToCheck, pathToCheck, currentSetting[valueAttribute]); var checkValues = _.partial(
this.checkValues,
valuesToCheck, pathToCheck, currentSetting[valueAttribute]
);
return _.compact(_.map(dependentRestrictions, (restrictions, label) => { return _.compact(_.map(dependentRestrictions, (restrictions, label) => {
if (_.any(restrictions, checkValues)) return label; if (_.any(restrictions, checkValues)) return label;
})); }));
@ -155,18 +187,25 @@ var SettingSection = React.createClass({
var pluginMetadata = this.props.settings.get(pluginName).metadata; var pluginMetadata = this.props.settings.get(pluginName).metadata;
if (enabled) { if (enabled) {
// check for editable plugin version // check for editable plugin version
var chosenVersionData = _.find(pluginMetadata.versions, (version) => version.metadata.plugin_id == pluginMetadata.chosen_id); var chosenVersionData = _.find(pluginMetadata.versions, (version) => {
return version.metadata.plugin_id == pluginMetadata.chosen_id;
});
if (this.props.lockedCluster && !chosenVersionData.metadata.always_editable) { if (this.props.lockedCluster && !chosenVersionData.metadata.always_editable) {
var editableVersion = _.find(pluginMetadata.versions, (version) => version.metadata.always_editable).metadata.plugin_id; var editableVersion = _.find(pluginMetadata.versions, (version) => {
return version.metadata.always_editable;
}).metadata.plugin_id;
this.onPluginVersionChange(pluginName, editableVersion); this.onPluginVersionChange(pluginName, editableVersion);
} }
} else { } else {
var initialVersion = this.props.initialAttributes[pluginName].metadata.chosen_id; var initialVersion = this.props.initialAttributes[pluginName].metadata.chosen_id;
if (pluginMetadata.chosen_id !== initialVersion) this.onPluginVersionChange(pluginName, initialVersion); if (pluginMetadata.chosen_id !== initialVersion) {
this.onPluginVersionChange(pluginName, initialVersion);
}
} }
}, },
renderTitle(options) { renderTitle(options) {
var {metadata, sectionName, isGroupDisabled, processedGroupDependencies, showSettingGroupWarning, groupWarning, isPlugin} = options; var {metadata, sectionName, isGroupDisabled, processedGroupDependencies,
showSettingGroupWarning, groupWarning, isPlugin} = options;
return metadata.toggleable ? return metadata.toggleable ?
<Input <Input
type='checkbox' type='checkbox'
@ -178,10 +217,20 @@ var SettingSection = React.createClass({
onChange={isPlugin ? _.partial(this.togglePlugin, sectionName) : this.props.onChange} onChange={isPlugin ? _.partial(this.togglePlugin, sectionName) : this.props.onChange}
/> />
: :
<span className={'subtab-group-' + sectionName}>{sectionName == 'common' ? i18n('cluster_page.settings_tab.groups.common') : metadata.label || sectionName}</span>; <span
className={'subtab-group-' + sectionName}
>
{
sectionName == 'common' ?
i18n('cluster_page.settings_tab.groups.common') : metadata.label || sectionName
}
</span>;
}, },
renderCustomControl(options) { renderCustomControl(options) {
var {setting, settingKey, error, isSettingDisabled, showSettingWarning, settingWarning, CustomControl, path} = options; var {
setting, settingKey, error, isSettingDisabled, showSettingWarning,
settingWarning, CustomControl, path
} = options;
return <CustomControl return <CustomControl
{...setting} {...setting}
{... _.pick(this.props, 'cluster', 'settings', 'configModels')} {... _.pick(this.props, 'cluster', 'settings', 'configModels')}
@ -193,7 +242,8 @@ var SettingSection = React.createClass({
/>; />;
}, },
renderRadioGroup(options) { renderRadioGroup(options) {
var {setting, settingKey, error, isSettingDisabled, showSettingWarning, settingWarning, settingName} = options; var {setting, settingKey, error, isSettingDisabled, showSettingWarning, settingWarning,
settingName} = options;
var values = _.chain(_.cloneDeep(setting.values)) var values = _.chain(_.cloneDeep(setting.values))
.map((value) => { .map((value) => {
var processedValueRestrictions = this.props.checkRestrictions('disable', value); var processedValueRestrictions = this.props.checkRestrictions('disable', value);
@ -218,7 +268,8 @@ var SettingSection = React.createClass({
); );
}, },
renderInput(options) { renderInput(options) {
var {setting, settingKey, error, isSettingDisabled, showSettingWarning, settingWarning, settingName} = options; var {setting, settingKey, error, isSettingDisabled, showSettingWarning, settingWarning,
settingName} = options;
var settingDescription = setting.description && var settingDescription = setting.description &&
<span dangerouslySetInnerHTML={{__html: utils.urlify(_.escape(setting.description))}} />; <span dangerouslySetInnerHTML={{__html: utils.urlify(_.escape(setting.description))}} />;
return <Input return <Input
@ -242,18 +293,25 @@ var SettingSection = React.createClass({
var section = settings.get(sectionName); var section = settings.get(sectionName);
var isPlugin = settings.isPlugin(section); var isPlugin = settings.isPlugin(section);
var metadata = section.metadata; var metadata = section.metadata;
var sortedSettings = _.sortBy(this.props.settingsToDisplay, (settingName) => section[settingName].weight); var sortedSettings = _.sortBy(this.props.settingsToDisplay, (settingName) => {
return section[settingName].weight;
});
var processedGroupRestrictions = this.processRestrictions(metadata); var processedGroupRestrictions = this.processRestrictions(metadata);
var processedGroupDependencies = this.checkDependencies(sectionName, 'metadata'); var processedGroupDependencies = this.checkDependencies(sectionName, 'metadata');
var isGroupAlwaysEditable = isPlugin ? _.any(metadata.versions, (version) => version.metadata.always_editable) : metadata.always_editable; var isGroupAlwaysEditable = isPlugin ? _.any(metadata.versions, (version) => {
var isGroupDisabled = this.props.locked || (this.props.lockedCluster && !isGroupAlwaysEditable) || processedGroupRestrictions.result; return version.metadata.always_editable;
}) : metadata.always_editable;
var isGroupDisabled = this.props.locked ||
(this.props.lockedCluster && !isGroupAlwaysEditable) || processedGroupRestrictions.result;
var showSettingGroupWarning = !this.props.lockedCluster || metadata.always_editable; var showSettingGroupWarning = !this.props.lockedCluster || metadata.always_editable;
var groupWarning = _.compact([processedGroupRestrictions.message, processedGroupDependencies.message]).join(' '); var groupWarning = _.compact([processedGroupRestrictions.message,
processedGroupDependencies.message]).join(' ');
return ( return (
<div className={'setting-section setting-section-' + sectionName}> <div className={'setting-section setting-section-' + sectionName}>
<h3> <h3>
{this.renderTitle({metadata, sectionName, isGroupDisabled, processedGroupDependencies, showSettingGroupWarning, groupWarning, isPlugin})} {this.renderTitle({metadata, sectionName, isGroupDisabled, processedGroupDependencies,
showSettingGroupWarning, groupWarning, isPlugin})}
</h3> </h3>
<div> <div>
{isPlugin && {isPlugin &&
@ -267,7 +325,9 @@ var SettingSection = React.createClass({
data: version.metadata.plugin_id, data: version.metadata.plugin_id,
label: version.metadata.plugin_version, label: version.metadata.plugin_version,
defaultChecked: version.metadata.plugin_id == metadata.chosen_id, defaultChecked: version.metadata.plugin_id == metadata.chosen_id,
disabled: this.props.locked || (this.props.lockedCluster && !version.metadata.always_editable) || processedGroupRestrictions.result || (metadata.toggleable && !metadata.enabled) disabled: this.props.locked || (this.props.lockedCluster &&
!version.metadata.always_editable) || processedGroupRestrictions.result ||
(metadata.toggleable && !metadata.enabled)
}; };
}, this)} }, this)}
onChange={this.onPluginVersionChange} onChange={this.onPluginVersionChange}
@ -281,11 +341,16 @@ var SettingSection = React.createClass({
var error = (settings.validationError || {})[path]; var error = (settings.validationError || {})[path];
var processedSettingRestrictions = this.processRestrictions(setting, settingName); var processedSettingRestrictions = this.processRestrictions(setting, settingName);
var processedSettingDependencies = this.checkDependencies(sectionName, settingName); var processedSettingDependencies = this.checkDependencies(sectionName, settingName);
var isSettingDisabled = isGroupDisabled || (metadata.toggleable && !metadata.enabled) || processedSettingRestrictions.result || processedSettingDependencies.result; var isSettingDisabled = isGroupDisabled ||
var showSettingWarning = showSettingGroupWarning && !isGroupDisabled && (!metadata.toggleable || metadata.enabled); (metadata.toggleable && !metadata.enabled) ||
var settingWarning = _.compact([processedSettingRestrictions.message, processedSettingDependencies.message]).join(' '); processedSettingRestrictions.result || processedSettingDependencies.result;
var showSettingWarning = showSettingGroupWarning && !isGroupDisabled &&
(!metadata.toggleable || metadata.enabled);
var settingWarning = _.compact([processedSettingRestrictions.message,
processedSettingDependencies.message]).join(' ');
var renderOptions = {setting, settingKey, error, isSettingDisabled, showSettingWarning, settingWarning}; var renderOptions = {setting, settingKey, error, isSettingDisabled,
showSettingWarning, settingWarning};
// support of custom controls // support of custom controls
var CustomControl = customControls[setting.type]; var CustomControl = customControls[setting.type];

View File

@ -52,7 +52,8 @@ var SettingsTab = React.createClass({
configModels: { configModels: {
cluster: this.props.cluster, cluster: this.props.cluster,
settings: settings, settings: settings,
networking_parameters: this.props.cluster.get('networkConfiguration').get('networking_parameters'), networking_parameters: this.props.cluster.get('networkConfiguration')
.get('networking_parameters'),
version: app.version, version: app.version,
release: this.props.cluster.get('release'), release: this.props.cluster.get('release'),
default: settings default: settings
@ -69,7 +70,10 @@ var SettingsTab = React.createClass({
this.loadInitialSettings(); this.loadInitialSettings();
}, },
hasChanges() { hasChanges() {
return this.props.cluster.get('settings').hasChanges(this.state.initialAttributes, this.state.configModels); return this.props.cluster.get('settings').hasChanges(
this.state.initialAttributes,
this.state.configModels
);
}, },
applyChanges() { applyChanges() {
if (!this.isSavingPossible()) return $.Deferred().reject(); if (!this.isSavingPossible()) return $.Deferred().reject();
@ -110,14 +114,16 @@ var SettingsTab = React.createClass({
var settings = this.props.cluster.get('settings'); var settings = this.props.cluster.get('settings');
var lockedCluster = !this.props.cluster.isAvailableForSettingsChanges(); var lockedCluster = !this.props.cluster.isAvailableForSettingsChanges();
var defaultSettings = new models.Settings(); var defaultSettings = new models.Settings();
var deferred = defaultSettings.fetch({url: _.result(this.props.cluster, 'url') + '/attributes/defaults'}); var deferred = defaultSettings.fetch({url: _.result(this.props.cluster, 'url') +
'/attributes/defaults'});
if (deferred) { if (deferred) {
this.setState({actionInProgress: true}); this.setState({actionInProgress: true});
deferred deferred
.done(() => { .done(() => {
_.each(settings.attributes, (section, sectionName) => { _.each(settings.attributes, (section, sectionName) => {
if ((!lockedCluster || section.metadata.always_editable) && section.metadata.group != 'network') { if ((!lockedCluster || section.metadata.always_editable) &&
section.metadata.group != 'network') {
_.each(section, (setting, settingName) => { _.each(section, (setting, settingName) => {
// do not update hidden settings (hack for #1442143), // do not update hidden settings (hack for #1442143),
// the same for settings with group network // the same for settings with group network
@ -164,11 +170,18 @@ var SettingsTab = React.createClass({
settings.isValid({models: this.state.configModels}); settings.isValid({models: this.state.configModels});
}, },
checkRestrictions(action, setting) { checkRestrictions(action, setting) {
return this.props.cluster.get('settings').checkRestrictions(this.state.configModels, action, setting); return this.props.cluster.get('settings').checkRestrictions(
this.state.configModels,
action,
setting
);
}, },
isSavingPossible() { isSavingPossible() {
var settings = this.props.cluster.get('settings'); var settings = this.props.cluster.get('settings');
var locked = this.state.actionInProgress || !!this.props.cluster.task({group: 'deployment', active: true}); var locked = this.state.actionInProgress || !!this.props.cluster.task({
group: 'deployment',
active: true
});
// network settings are shown on Networks tab, so they should not block // network settings are shown on Networks tab, so they should not block
// saving of changes on Settings tab // saving of changes on Settings tab
var areSettingsValid = !_.any(_.keys(settings.validationError), (settingPath) => { var areSettingsValid = !_.any(_.keys(settings.validationError), (settingPath) => {
@ -184,9 +197,15 @@ var SettingsTab = React.createClass({
var settingsGroupList = settings.getGroupList(); var settingsGroupList = settings.getGroupList();
var locked = this.state.actionInProgress || !!cluster.task({group: 'deployment', active: true}); var locked = this.state.actionInProgress || !!cluster.task({group: 'deployment', active: true});
var lockedCluster = !cluster.isAvailableForSettingsChanges(); var lockedCluster = !cluster.isAvailableForSettingsChanges();
var someSettingsEditable = _.any(settings.attributes, (group) => group.metadata.always_editable); var someSettingsEditable = _.any(
settings.attributes,
(group) => group.metadata.always_editable
);
var hasChanges = this.hasChanges(); var hasChanges = this.hasChanges();
var allocatedRoles = _.uniq(_.flatten(_.union(cluster.get('nodes').pluck('roles'), cluster.get('nodes').pluck('pending_roles')))); var allocatedRoles = _.uniq(_.flatten(_.union(
cluster.get('nodes').pluck('roles'),
cluster.get('nodes').pluck('pending_roles')
)));
var classes = { var classes = {
row: true, row: true,
'changes-locked': lockedCluster 'changes-locked': lockedCluster
@ -237,7 +256,10 @@ var SettingsTab = React.createClass({
return (settings.validationError || {})[settings.makePath(sectionName, settingName)]; return (settings.validationError || {})[settings.makePath(sectionName, settingName)];
}); });
if (!_.isEmpty(pickedSettings)) { if (!_.isEmpty(pickedSettings)) {
groupedSettings[calculatedGroup][sectionName] = {settings: pickedSettings, invalid: hasErrors}; groupedSettings[calculatedGroup][sectionName] = {
settings: pickedSettings,
invalid: hasErrors
};
} }
}); });
} }
@ -261,7 +283,9 @@ var SettingsTab = React.createClass({
{_.map(groupedSettings, (selectedGroup, groupName) => { {_.map(groupedSettings, (selectedGroup, groupName) => {
if (groupName != this.props.activeSettingsSectionName) return null; if (groupName != this.props.activeSettingsSectionName) return null;
var sortedSections = _.sortBy(_.keys(selectedGroup), (name) => settings.get(name + '.metadata.weight')); var sortedSections = _.sortBy(
_.keys(selectedGroup), (name) => settings.get(name + '.metadata.weight')
);
return ( return (
<div className={'col-xs-10 forms-box ' + groupName} key={groupName}> <div className={'col-xs-10 forms-box ' + groupName} key={groupName}>
{_.map(sortedSections, (sectionName) => { {_.map(sortedSections, (sectionName) => {
@ -295,13 +319,25 @@ var SettingsTab = React.createClass({
<div className='col-xs-12 page-buttons content-elements'> <div className='col-xs-12 page-buttons content-elements'>
<div className='well clearfix'> <div className='well clearfix'>
<div className='btn-group pull-right'> <div className='btn-group pull-right'>
<button className='btn btn-default btn-load-defaults' onClick={this.loadDefaults} disabled={locked || (lockedCluster && !someSettingsEditable)}> <button
className='btn btn-default btn-load-defaults'
onClick={this.loadDefaults}
disabled={locked || (lockedCluster && !someSettingsEditable)}
>
{i18n('common.load_defaults_button')} {i18n('common.load_defaults_button')}
</button> </button>
<button className='btn btn-default btn-revert-changes' onClick={this.revertChanges} disabled={locked || !hasChanges}> <button
className='btn btn-default btn-revert-changes'
onClick={this.revertChanges}
disabled={locked || !hasChanges}
>
{i18n('common.cancel_changes_button')} {i18n('common.cancel_changes_button')}
</button> </button>
<button className='btn btn-success btn-apply-changes' onClick={this.applyChanges} disabled={!this.isSavingPossible()}> <button
className='btn btn-success btn-apply-changes'
onClick={this.applyChanges}
disabled={!this.isSavingPossible()}
>
{i18n('common.save_settings_button')} {i18n('common.save_settings_button')}
</button> </button>
</div> </div>
@ -316,7 +352,11 @@ var SettingSubtabs = React.createClass({
render() { render() {
return ( return (
<div className='col-xs-2'> <div className='col-xs-2'>
<CSSTransitionGroup component='ul' transitionName='subtab-item' className='nav nav-pills nav-stacked'> <CSSTransitionGroup
component='ul'
transitionName='subtab-item'
className='nav nav-pills nav-stacked'
>
{ {
this.props.settingsGroupList.map((groupName) => { this.props.settingsGroupList.map((groupName) => {
if (!this.props.groupedSettings[groupName]) return null; if (!this.props.groupedSettings[groupName]) return null;
@ -326,7 +366,9 @@ var SettingSubtabs = React.createClass({
<li <li
key={groupName} key={groupName}
role='presentation' role='presentation'
className={utils.classNames({active: groupName == this.props.activeSettingsSectionName})} className={utils.classNames({
active: groupName == this.props.activeSettingsSectionName
})}
onClick={_.partial(this.props.setActiveSettingsGroupName, groupName)} onClick={_.partial(this.props.setActiveSettingsGroupName, groupName)}
> >
<a className={'subtab-link-' + groupName}> <a className={'subtab-link-' + groupName}>

View File

@ -58,7 +58,11 @@ ClustersPage = React.createClass({
ClusterList = React.createClass({ ClusterList = React.createClass({
mixins: [backboneMixin('clusters')], mixins: [backboneMixin('clusters')],
createCluster() { createCluster() {
CreateClusterWizard.show({clusters: this.props.clusters, modalClass: 'wizard', backdrop: 'static'}); CreateClusterWizard.show({
clusters: this.props.clusters,
modalClass: 'wizard',
backdrop: 'static'
});
}, },
render() { render() {
return ( return (
@ -126,7 +130,8 @@ Cluster = React.createClass({
var cluster = this.props.cluster; var cluster = this.props.cluster;
var status = cluster.get('status'); var status = cluster.get('status');
var nodes = cluster.get('nodes'); var nodes = cluster.get('nodes');
var isClusterDeleting = !!cluster.task({name: 'cluster_deletion', active: true}) || !!cluster.task({name: 'cluster_deletion', status: 'ready'}); var isClusterDeleting = !!cluster.task({name: 'cluster_deletion', active: true}) ||
!!cluster.task({name: 'cluster_deletion', status: 'ready'});
var deploymentTask = cluster.task({group: 'deployment', active: true}); var deploymentTask = cluster.task({group: 'deployment', active: true});
var Tag = isClusterDeleting ? 'div' : 'a'; var Tag = isClusterDeleting ? 'div' : 'a';
return ( return (
@ -140,15 +145,29 @@ Cluster = React.createClass({
> >
<div className='name'>{cluster.get('name')}</div> <div className='name'>{cluster.get('name')}</div>
<div className='tech-info'> <div className='tech-info'>
<div key='nodes-title' className='item'>{i18n('clusters_page.cluster_hardware_nodes')}</div> <div key='nodes-title' className='item'>
{i18n('clusters_page.cluster_hardware_nodes')}
</div>
<div key='nodes-value' className='value'>{nodes.length}</div> <div key='nodes-value' className='value'>{nodes.length}</div>
{!!nodes.length && [ {!!nodes.length && [
<div key='cpu-title' className='item'>{i18n('clusters_page.cluster_hardware_cpu')}</div>, <div key='cpu-title' className='item'>
<div key='cpu-value' className='value'>{nodes.resources('cores')} ({nodes.resources('ht_cores')})</div>, {i18n('clusters_page.cluster_hardware_cpu')}
<div key='hdd-title' className='item'>{i18n('clusters_page.cluster_hardware_hdd')}</div>, </div>,
<div key='hdd-value' className='value'>{nodes.resources('hdd') ? utils.showDiskSize(nodes.resources('hdd')) : '?GB'}</div>, <div key='cpu-value' className='value'>
<div key='ram-title' className='item'>{i18n('clusters_page.cluster_hardware_ram')}</div>, {nodes.resources('cores')} ({nodes.resources('ht_cores')})
<div key='ram-value' className='value'>{nodes.resources('ram') ? utils.showMemorySize(nodes.resources('ram')) : '?GB'}</div> </div>,
<div key='hdd-title' className='item'>
{i18n('clusters_page.cluster_hardware_hdd')}
</div>,
<div key='hdd-value' className='value'>
{nodes.resources('hdd') ? utils.showDiskSize(nodes.resources('hdd')) : '?GB'}
</div>,
<div key='ram-title' className='item'>
{i18n('clusters_page.cluster_hardware_ram')}
</div>,
<div key='ram-value' className='value'>
{nodes.resources('ram') ? utils.showMemorySize(nodes.resources('ram')) : '?GB'}
</div>
]} ]}
</div> </div>
<div className='status text-info'> <div className='status text-info'>
@ -157,9 +176,14 @@ Cluster = React.createClass({
<div <div
className={utils.classNames({ className={utils.classNames({
'progress-bar': true, 'progress-bar': true,
'progress-bar-warning': _.contains(['stop_deployment', 'reset_environment'], deploymentTask.get('name')) 'progress-bar-warning': _.contains(
['stop_deployment', 'reset_environment'],
deploymentTask.get('name')
)
})} })}
style={{width: (deploymentTask.get('progress') > 3 ? deploymentTask.get('progress') : 3) + '%'}} style={{width: (
deploymentTask.get('progress') > 3 ? deploymentTask.get('progress') : 3
) + '%'}}
></div> ></div>
</div> </div>
: :

View File

@ -30,7 +30,10 @@ import {outerClickMixin} from 'component_mixins';
export var Input = React.createClass({ export var Input = React.createClass({
propTypes: { propTypes: {
type: React.PropTypes.oneOf(['text', 'password', 'textarea', 'checkbox', 'radio', 'select', 'hidden', 'number', 'range', 'file']).isRequired, type: React.PropTypes.oneOf([
'text', 'password', 'textarea', 'checkbox', 'radio',
'select', 'hidden', 'number', 'range', 'file'
]).isRequired,
name: React.PropTypes.node, name: React.PropTypes.node,
label: React.PropTypes.node, label: React.PropTypes.node,
debounce: React.PropTypes.bool, debounce: React.PropTypes.bool,
@ -147,19 +150,35 @@ export var Input = React.createClass({
className='form-control file-name' className='form-control file-name'
type='text' type='text'
placeholder={i18n('file.placeholder')} placeholder={i18n('file.placeholder')}
value={this.state.fileName && '[' + utils.showSize(this.state.content.length) + '] ' + this.state.fileName} value={
this.state.fileName && (
'[' + utils.showSize(this.state.content.length) + '] ' + this.state.fileName
)
}
onClick={this.pickFile} onClick={this.pickFile}
disabled={this.props.disabled} disabled={this.props.disabled}
readOnly readOnly
/> />
<div className='input-group-addon' onClick={this.state.fileName ? this.removeFile : this.pickFile}> <div
<i className={this.state.fileName && !this.props.disabled ? 'glyphicon glyphicon-remove' : 'glyphicon glyphicon-file'} /> className='input-group-addon'
onClick={this.state.fileName ? this.removeFile : this.pickFile}
>
<i
className={this.state.fileName && !this.props.disabled ?
'glyphicon glyphicon-remove' : 'glyphicon glyphicon-file'
}
/>
</div> </div>
</div> </div>
} }
{this.props.toggleable && {this.props.toggleable &&
<div className='input-group-addon' onClick={this.togglePassword}> <div className='input-group-addon' onClick={this.togglePassword}>
<i className={this.state.visible ? 'glyphicon glyphicon-eye-close' : 'glyphicon glyphicon-eye-open'} /> <i
className={
this.state.visible ?
'glyphicon glyphicon-eye-close' : 'glyphicon glyphicon-eye-open'
}
/>
</div> </div>
} }
{isCheckboxOrRadio && <span>&nbsp;</span>} {isCheckboxOrRadio && <span>&nbsp;</span>}
@ -182,7 +201,8 @@ export var Input = React.createClass({
); );
}, },
renderDescription() { renderDescription() {
var text = !_.isUndefined(this.props.error) && !_.isNull(this.props.error) ? this.props.error : this.props.description || ''; var text = !_.isUndefined(this.props.error) && !_.isNull(this.props.error) ? this.props.error :
this.props.description || '';
return <span key='description' className='help-block'>{text}</span>; return <span key='description' className='help-block'>{text}</span>;
}, },
renderWrapper(children) { renderWrapper(children) {
@ -372,7 +392,9 @@ export var Tooltip = React.createClass({
$(ReactDOM.findDOMNode(this.refs.tooltip)).tooltip('destroy'); $(ReactDOM.findDOMNode(this.refs.tooltip)).tooltip('destroy');
}, },
render() { render() {
if (!this.props.wrap) return React.cloneElement(React.Children.only(this.props.children), {ref: 'tooltip'}); if (!this.props.wrap) {
return React.cloneElement(React.Children.only(this.props.children), {ref: 'tooltip'});
}
return ( return (
<div className={this.props.wrapperClassName} ref='tooltip'> <div className={this.props.wrapperClassName} ref='tooltip'>
{this.props.children} {this.props.children}

View File

@ -40,7 +40,13 @@ customControls.custom_repo_configuration = React.createClass({
error.uri = i18n(ns + 'invalid_repo'); error.uri = i18n(ns + 'invalid_repo');
} }
var priority = repo.priority; var priority = repo.priority;
if (_.isNaN(priority) || !_.isNull(priority) && (!(priority == _.parseInt(priority, 10)) || os == 'CentOS' && (priority < 1 || priority > 99))) { if (
_.isNaN(priority) ||
!_.isNull(priority) && (
!(priority == _.parseInt(priority, 10)) ||
os == 'CentOS' && (priority < 1 || priority > 99)
)
) {
error.priority = i18n(ns + 'invalid_priority'); error.priority = i18n(ns + 'invalid_priority');
} }
return _.isEmpty(error) ? null : error; return _.isEmpty(error) ? null : error;
@ -48,7 +54,9 @@ customControls.custom_repo_configuration = React.createClass({
return _.compact(errors).length ? errors : null; return _.compact(errors).length ? errors : null;
}, },
repoToString(repo, os) { repoToString(repo, os) {
var repoData = _.compact(this.defaultProps.repoAttributes[os].map((attribute) => repo[attribute])); var repoData = _.compact(this.defaultProps.repoAttributes[os].map(
(attribute) => repo[attribute]
));
if (!repoData.length) return ''; // in case of new repo if (!repoData.length) return ''; // in case of new repo
return repoData.join(' '); return repoData.join(' ');
} }
@ -58,10 +66,12 @@ customControls.custom_repo_configuration = React.createClass({
}, },
getDefaultProps() { getDefaultProps() {
return { return {
/* eslint-disable max-len */
repoRegexes: { repoRegexes: {
Ubuntu: /^(deb|deb-src)\s+(\w+:\/\/[\w\-.\/]+(?::\d+)?[\w\-.\/]+)\s+([\w\-.\/]+)(?:\s+([\w\-.\/\s]+))?$/i, Ubuntu: /^(deb|deb-src)\s+(\w+:\/\/[\w\-.\/]+(?::\d+)?[\w\-.\/]+)\s+([\w\-.\/]+)(?:\s+([\w\-.\/\s]+))?$/i,
CentOS: /^(\w+:\/\/[\w\-.\/]+(?::\d+)?[\w\-.\/]+)\s*$/i CentOS: /^(\w+:\/\/[\w\-.\/]+(?::\d+)?[\w\-.\/]+)\s*$/i
}, },
/* eslint-enable max-len */
repoAttributes: { repoAttributes: {
Ubuntu: ['type', 'uri', 'suite', 'section'], Ubuntu: ['type', 'uri', 'suite', 'section'],
CentOS: ['uri'] CentOS: ['uri']
@ -130,7 +140,12 @@ customControls.custom_repo_configuration = React.createClass({
return ( return (
<div className='repos' key={this.state.key}> <div className='repos' key={this.state.key}>
{this.props.description && {this.props.description &&
<span className='help-block' dangerouslySetInnerHTML={{__html: utils.urlify(utils.linebreaks(_.escape(this.props.description)))}} /> <span
className='help-block'
dangerouslySetInnerHTML={{
__html: utils.urlify(utils.linebreaks(_.escape(this.props.description)))
}}
/>
} }
{this.props.value.map((repo, index) => { {this.props.value.map((repo, index) => {
var error = (this.props.error || {})[index]; var error = (this.props.error || {})[index];
@ -162,7 +177,9 @@ customControls.custom_repo_configuration = React.createClass({
<Input <Input
{...props} {...props}
defaultValue={repo.priority} defaultValue={repo.priority}
error={error && (error.priority ? (error.name || error.uri) ? '' : error.priority : null)} error={
error && (error.priority ? (error.name || error.uri) ? '' : error.priority : null)
}
wrapperClassName='repo-priority' wrapperClassName='repo-priority'
onChange={this.changeRepos.bind(this, 'change_priority')} onChange={this.changeRepos.bind(this, 'change_priority')}
extraContent={index > 0 && this.renderDeleteButton(index)} extraContent={index > 0 && this.renderDeleteButton(index)}
@ -174,7 +191,12 @@ customControls.custom_repo_configuration = React.createClass({
); );
})} })}
<div className='buttons'> <div className='buttons'>
<button key='addExtraRepo' className='btn btn-default btn-add-repo' onClick={this.changeRepos.bind(this, 'add')} disabled={this.props.disabled}> <button
key='addExtraRepo'
className='btn btn-default btn-add-repo'
onClick={this.changeRepos.bind(this, 'add')}
disabled={this.props.disabled}
>
{i18n(ns + 'add_repo_button')} {i18n(ns + 'add_repo_button')}
</button> </button>
</div> </div>

View File

@ -67,14 +67,20 @@ export var dialogMixin = {
} }
return result; return result;
} else { } else {
return ReactDOM.render(React.createElement(this, dialogOptions), $('#modal-container')[0]).getResult(); return ReactDOM.render(
React.createElement(this, dialogOptions),
$('#modal-container')[0]
).getResult();
} }
} }
}, },
updateProps(partialProps) { updateProps(partialProps) {
var props; var props;
props = _.extend({}, this.props, partialProps); props = _.extend({}, this.props, partialProps);
ReactDOM.render(React.createElement(this.constructor, props), ReactDOM.findDOMNode(this).parentNode); ReactDOM.render(
React.createElement(this.constructor, props),
ReactDOM.findDOMNode(this).parentNode
);
}, },
getInitialState() { getInitialState() {
return { return {
@ -118,7 +124,11 @@ export var dialogMixin = {
if (e.target.tagName == 'A' && !e.target.target && e.target.href) this.close(); if (e.target.tagName == 'A' && !e.target.target && e.target.href) this.close();
}, },
closeOnEscapeKey(e) { closeOnEscapeKey(e) {
if (this.props.keyboard !== false && this.props.closeable !== false && e.key == 'Escape') this.close(); if (
this.props.keyboard !== false &&
this.props.closeable !== false &&
e.key == 'Escape'
) this.close();
if (_.isFunction(this.onKeyDown)) this.onKeyDown(e); if (_.isFunction(this.onKeyDown)) this.onKeyDown(e);
}, },
showError(response, message) { showError(response, message) {
@ -137,7 +147,12 @@ export var dialogMixin = {
var classes = {'modal fade': true}; var classes = {'modal fade': true};
classes[this.props.modalClass] = this.props.modalClass; classes[this.props.modalClass] = this.props.modalClass;
return ( return (
<div className={utils.classNames(classes)} tabIndex='-1' onClick={this.closeOnLinkClick} onKeyDown={this.closeOnEscapeKey}> <div
className={utils.classNames(classes)}
tabIndex='-1'
onClick={this.closeOnLinkClick}
onKeyDown={this.closeOnEscapeKey}
>
<div className='modal-dialog'> <div className='modal-dialog'>
<div className='modal-content'> <div className='modal-content'>
<div className='modal-header'> <div className='modal-header'>
@ -146,7 +161,13 @@ export var dialogMixin = {
<span aria-hidden='true'>&times;</span> <span aria-hidden='true'>&times;</span>
</button> </button>
} }
<h4 className='modal-title'>{this.props.title || this.state.title || (this.props.error ? i18n('dialog.error_dialog.title') : '')}</h4> <h4 className='modal-title'>
{
this.props.title ||
this.state.title ||
(this.props.error ? i18n('dialog.error_dialog.title') : '')
}
</h4>
</div> </div>
<div className='modal-body'> <div className='modal-body'>
{this.props.error ? {this.props.error ?
@ -159,7 +180,9 @@ export var dialogMixin = {
{this.renderFooter && !this.props.error ? {this.renderFooter && !this.props.error ?
this.renderFooter() this.renderFooter()
: :
<button className='btn btn-default' onClick={this.close}>{i18n('common.close_button')}</button> <button className='btn btn-default' onClick={this.close}>
{i18n('common.close_button')}
</button>
} }
</div> </div>
</div> </div>
@ -219,7 +242,9 @@ export var NailgunUnavailabilityDialog = React.createClass({
this.startCountdown(); this.startCountdown();
}, },
componentDidMount() { componentDidMount() {
$(ReactDOM.findDOMNode(this)).on('shown.bs.modal', () => $(ReactDOM.findDOMNode(this.refs['retry-button'])).focus()); $(ReactDOM.findDOMNode(this)).on('shown.bs.modal', () => {
return $(ReactDOM.findDOMNode(this.refs['retry-button'])).focus();
});
}, },
startCountdown() { startCountdown() {
this.activeTimeout = _.delay(this.countdown, 1000); this.activeTimeout = _.delay(this.countdown, 1000);
@ -242,7 +267,9 @@ export var NailgunUnavailabilityDialog = React.createClass({
reinitializeUI() { reinitializeUI() {
app.initialize().then(this.close, () => { app.initialize().then(this.close, () => {
var {retryDelayIntervals} = this.props; var {retryDelayIntervals} = this.props;
var nextDelay = retryDelayIntervals[retryDelayIntervals.indexOf(this.state.currentDelayInterval) + 1] || _.last(retryDelayIntervals); var nextDelay = retryDelayIntervals[
retryDelayIntervals.indexOf(this.state.currentDelayInterval) + 1
] || _.last(retryDelayIntervals);
_.defer(() => this.setState({ _.defer(() => this.setState({
actionInProgress: false, actionInProgress: false,
currentDelay: nextDelay, currentDelay: nextDelay,
@ -266,7 +293,10 @@ export var NailgunUnavailabilityDialog = React.createClass({
{i18n('dialog.nailgun_unavailability.unavailability_message')} {i18n('dialog.nailgun_unavailability.unavailability_message')}
{' '} {' '}
{this.state.currentDelay ? {this.state.currentDelay ?
i18n('dialog.nailgun_unavailability.retry_delay_message', {count: this.state.currentDelay}) i18n(
'dialog.nailgun_unavailability.retry_delay_message',
{count: this.state.currentDelay}
)
: :
i18n('dialog.nailgun_unavailability.retrying') i18n('dialog.nailgun_unavailability.retrying')
} }
@ -315,7 +345,8 @@ export var DiscardNodeChangesDialog = React.createClass({
Backbone.sync('update', nodes) Backbone.sync('update', nodes)
.then(() => this.props.cluster.fetchRelated('nodes')) .then(() => this.props.cluster.fetchRelated('nodes'))
.done(() => { .done(() => {
dispatcher.trigger('updateNodeStats networkConfigurationUpdated labelsConfigurationUpdated'); dispatcher
.trigger('updateNodeStats networkConfigurationUpdated labelsConfigurationUpdated');
this.state.result.resolve(); this.state.result.resolve();
this.close(); this.close();
}) })
@ -333,8 +364,22 @@ export var DiscardNodeChangesDialog = React.createClass({
}, },
renderFooter() { renderFooter() {
return ([ return ([
<button key='cancel' className='btn btn-default' onClick={this.close} disabled={this.state.actionInProgress}>{i18n('common.cancel_button')}</button>, <button
<button key='discard' className='btn btn-danger' disabled={this.state.actionInProgress} onClick={this.discardNodeChanges}>{i18n('dialog.discard_changes.discard_button')}</button> key='cancel'
className='btn btn-default'
onClick={this.close}
disabled={this.state.actionInProgress}
>
{i18n('common.cancel_button')}
</button>,
<button
key='discard'
className='btn btn-danger'
disabled={this.state.actionInProgress}
onClick={this.discardNodeChanges}
>
{i18n('dialog.discard_changes.discard_button')}
</button>
]); ]);
} }
}); });
@ -342,7 +387,8 @@ export var DiscardNodeChangesDialog = React.createClass({
export var DeployChangesDialog = React.createClass({ export var DeployChangesDialog = React.createClass({
mixins: [ mixins: [
dialogMixin, dialogMixin,
// this is needed to somehow handle the case when verification is in progress and user pressed Deploy // this is needed to somehow handle the case when
// verification is in progress and user pressed Deploy
backboneMixin({ backboneMixin({
modelOrCollection(props) { modelOrCollection(props) {
return props.cluster.get('tasks'); return props.cluster.get('tasks');
@ -400,7 +446,14 @@ export var DeployChangesDialog = React.createClass({
}, },
renderFooter() { renderFooter() {
return ([ return ([
<button key='cancel' className='btn btn-default' onClick={this.close} disabled={this.state.actionInProgress}>{i18n('common.cancel_button')}</button>, <button
key='cancel'
className='btn btn-default'
onClick={this.close}
disabled={this.state.actionInProgress}
>
{i18n('common.cancel_button')}
</button>,
<button key='deploy' <button key='deploy'
className='btn start-deployment-btn btn-success' className='btn start-deployment-btn btn-success'
disabled={this.state.actionInProgress || this.state.isInvalid} disabled={this.state.actionInProgress || this.state.isInvalid}
@ -435,8 +488,22 @@ export var ProvisionVMsDialog = React.createClass({
}, },
renderFooter() { renderFooter() {
return ([ return ([
<button key='cancel' className='btn btn-default' onClick={this.close} disabled={this.state.actionInProgress}>{i18n('common.cancel_button')}</button>, <button
<button key='provision' className='btn btn-success' disabled={this.state.actionInProgress} onClick={this.startProvisioning}>{i18n('common.start_button')}</button> key='cancel'
className='btn btn-default'
onClick={this.close}
disabled={this.state.actionInProgress}
>
{i18n('common.cancel_button')}
</button>,
<button
key='provision'
className='btn btn-success'
disabled={this.state.actionInProgress}
onClick={this.startProvisioning}
>
{i18n('common.start_button')}
</button>
]); ]);
} }
}); });
@ -455,21 +522,40 @@ export var StopDeploymentDialog = React.createClass({
dispatcher.trigger('deploymentTaskStarted'); dispatcher.trigger('deploymentTaskStarted');
}) })
.fail((response) => { .fail((response) => {
this.showError(response, i18n('dialog.stop_deployment.stop_deployment_error.stop_deployment_warning')); this.showError(
response,
i18n('dialog.stop_deployment.stop_deployment_error.stop_deployment_warning')
);
}); });
}, },
renderBody() { renderBody() {
return ( return (
<div className='text-danger'> <div className='text-danger'>
{this.renderImportantLabel()} {this.renderImportantLabel()}
{i18n('dialog.stop_deployment.' + (this.props.cluster.get('nodes').where({status: 'provisioning'}).length ? 'provisioning_warning' : 'text'))} {i18n('dialog.stop_deployment.' +
(this.props.cluster.get('nodes').where({status: 'provisioning'}).length ?
'provisioning_warning' : 'text'))}
</div> </div>
); );
}, },
renderFooter() { renderFooter() {
return ([ return ([
<button key='cancel' className='btn btn-default' onClick={this.close} disabled={this.state.actionInProgress}>{i18n('common.cancel_button')}</button>, <button
<button key='deploy' className='btn stop-deployment-btn btn-danger' disabled={this.state.actionInProgress} onClick={this.stopDeployment}>{i18n('common.stop_button')}</button> key='cancel'
className='btn btn-default'
onClick={this.close}
disabled={this.state.actionInProgress}
>
{i18n('common.cancel_button')}
</button>,
<button
key='deploy'
className='btn stop-deployment-btn btn-danger'
disabled={this.state.actionInProgress}
onClick={this.stopDeployment}
>
{i18n('common.stop_button')}
</button>
]); ]);
} }
}); });
@ -537,12 +623,21 @@ export var RemoveClusterDialog = React.createClass({
}, },
renderFooter() { renderFooter() {
return ([ return ([
<button key='cancel' className='btn btn-default' onClick={this.close} disabled={this.state.actionInProgress}>{i18n('common.cancel_button')}</button>, <button
key='cancel'
className='btn btn-default'
onClick={this.close}
disabled={this.state.actionInProgress}
>
{i18n('common.cancel_button')}
</button>,
<button <button
key='remove' key='remove'
className='btn btn-danger remove-cluster-btn' className='btn btn-danger remove-cluster-btn'
disabled={this.state.actionInProgress || this.state.confirmation && _.isUndefined(this.state.confirmationError) || this.state.confirmationError} disabled={this.state.actionInProgress || this.state.confirmation &&
onClick={this.props.cluster.get('status') == 'new' || this.state.confirmation ? this.removeCluster : this.showConfirmationForm} _.isUndefined(this.state.confirmationError) || this.state.confirmationError}
onClick={this.props.cluster.get('status') == 'new' || this.state.confirmation ?
this.removeCluster : this.showConfirmationForm}
> >
{i18n('common.delete_button')} {i18n('common.delete_button')}
</button> </button>
@ -602,11 +697,19 @@ export var ResetEnvironmentDialog = React.createClass({
}, },
renderFooter() { renderFooter() {
return ([ return ([
<button key='cancel' className='btn btn-default' disabled={this.state.actionInProgress} onClick={this.close}>{i18n('common.cancel_button')}</button>, <button
key='cancel'
className='btn btn-default'
disabled={this.state.actionInProgress}
onClick={this.close}
>
{i18n('common.cancel_button')}
</button>,
<button <button
key='reset' key='reset'
className='btn btn-danger reset-environment-btn' className='btn btn-danger reset-environment-btn'
disabled={this.state.actionInProgress || this.state.confirmation && _.isUndefined(this.state.confirmationError) || this.state.confirmationError} disabled={this.state.actionInProgress || this.state.confirmation &&
_.isUndefined(this.state.confirmationError) || this.state.confirmationError}
onClick={this.state.confirmation ? this.resetEnvironment : this.showConfirmationForm} onClick={this.state.confirmation ? this.resetEnvironment : this.showConfirmationForm}
> >
{i18n('common.reset_button')} {i18n('common.reset_button')}
@ -634,7 +737,11 @@ export var ShowNodeInfoDialog = React.createClass({
}, },
goToConfigurationScreen(url) { goToConfigurationScreen(url) {
this.close(); this.close();
app.navigate('#cluster/' + this.props.node.get('cluster') + '/nodes/' + url + '/' + utils.serializeTabOptions({nodes: this.props.node.id}), {trigger: true}); app.navigate(
'#cluster/' + this.props.node.get('cluster') + '/nodes/' + url + '/' +
utils.serializeTabOptions({nodes: this.props.node.id}),
{trigger: true}
);
}, },
showSummary(meta, group) { showSummary(meta, group) {
var summary = ''; var summary = '';
@ -647,21 +754,27 @@ export var ShowNodeInfoDialog = React.createClass({
if (_.isArray(meta.memory.devices) && meta.memory.devices.length) { if (_.isArray(meta.memory.devices) && meta.memory.devices.length) {
var sizes = _.countBy(_.pluck(meta.memory.devices, 'size'), utils.showMemorySize); var sizes = _.countBy(_.pluck(meta.memory.devices, 'size'), utils.showMemorySize);
summary = _.map(_.keys(sizes).sort(), (size) => sizes[size] + ' x ' + size).join(', '); summary = _.map(_.keys(sizes).sort(), (size) => sizes[size] + ' x ' + size).join(', ');
summary += ', ' + utils.showMemorySize(meta.memory.total) + ' ' + i18n('dialog.show_node.total'); summary += ', ' + utils.showMemorySize(meta.memory.total) + ' ' +
} else summary = utils.showMemorySize(meta.memory.total) + ' ' + i18n('dialog.show_node.total'); i18n('dialog.show_node.total');
} else summary = utils.showMemorySize(meta.memory.total) + ' ' +
i18n('dialog.show_node.total');
break; break;
case 'disks': case 'disks':
summary = meta.disks.length + ' '; summary = meta.disks.length + ' ';
summary += i18n('dialog.show_node.drive', {count: meta.disks.length}); summary += i18n('dialog.show_node.drive', {count: meta.disks.length});
summary += ', ' + utils.showDiskSize(_.reduce(_.pluck(meta.disks, 'size'), (sum, n) => sum + n, 0)) + ' ' + i18n('dialog.show_node.total'); summary += ', ' + utils.showDiskSize(_.reduce(_.pluck(meta.disks, 'size'), (sum, n) =>
sum + n, 0)) + ' ' + i18n('dialog.show_node.total');
break; break;
case 'cpu': case 'cpu':
var frequencies = _.countBy(_.pluck(meta.cpu.spec, 'frequency'), utils.showFrequency); var frequencies = _.countBy(_.pluck(meta.cpu.spec, 'frequency'), utils.showFrequency);
summary = _.map(_.keys(frequencies).sort(), (frequency) => frequencies[frequency] + ' x ' + frequency).join(', '); summary = _.map(_.keys(frequencies).sort(), (frequency) => frequencies[frequency] +
' x ' + frequency).join(', ');
break; break;
case 'interfaces': case 'interfaces':
var bandwidths = _.countBy(_.pluck(meta.interfaces, 'current_speed'), utils.showBandwidth); var bandwidths = _.countBy(_.pluck(meta.interfaces, 'current_speed'),
summary = _.map(_.keys(bandwidths).sort(), (bandwidth) => bandwidths[bandwidth] + ' x ' + bandwidth).join(', '); utils.showBandwidth);
summary = _.map(_.keys(bandwidths).sort(), (bandwidth) => bandwidths[bandwidth] +
' x ' + bandwidth).join(', ');
break; break;
} }
} catch (ignore) {} } catch (ignore) {}
@ -714,8 +827,10 @@ export var ShowNodeInfoDialog = React.createClass({
}, },
assignAccordionEvents() { assignAccordionEvents() {
$('.panel-collapse', ReactDOM.findDOMNode(this)) $('.panel-collapse', ReactDOM.findDOMNode(this))
.on('show.bs.collapse', (e) => $(e.currentTarget).siblings('.panel-heading').find('i').removeClass('glyphicon-plus').addClass('glyphicon-minus')) .on('show.bs.collapse', (e) => $(e.currentTarget).siblings('.panel-heading').find('i')
.on('hide.bs.collapse', (e) => $(e.currentTarget).siblings('.panel-heading').find('i').removeClass('glyphicon-minus').addClass('glyphicon-plus')) .removeClass('glyphicon-plus').addClass('glyphicon-minus'))
.on('hide.bs.collapse', (e) => $(e.currentTarget).siblings('.panel-heading').find('i')
.removeClass('glyphicon-minus').addClass('glyphicon-plus'))
.on('hidden.bs.collapse', (e) => e.stopPropagation()); .on('hidden.bs.collapse', (e) => e.stopPropagation());
}, },
toggle(groupIndex) { toggle(groupIndex) {
@ -777,7 +892,8 @@ export var ShowNodeInfoDialog = React.createClass({
var groups = _.sortBy(_.keys(meta), (group) => _.indexOf(groupOrder, group)); var groups = _.sortBy(_.keys(meta), (group) => _.indexOf(groupOrder, group));
var sortOrder = { var sortOrder = {
disks: ['name', 'model', 'size'], disks: ['name', 'model', 'size'],
interfaces: ['name', 'mac', 'state', 'ip', 'netmask', 'current_speed', 'max_speed', 'driver', 'bus_info'] interfaces: ['name', 'mac', 'state', 'ip', 'netmask', 'current_speed', 'max_speed',
'driver', 'bus_info']
}; };
if (this.state.VMsConf) groups.push('config'); if (this.state.VMsConf) groups.push('config');
@ -787,17 +903,26 @@ export var ShowNodeInfoDialog = React.createClass({
<div className='col-xs-5'><div className='node-image-outline' /></div> <div className='col-xs-5'><div className='node-image-outline' /></div>
<div className='col-xs-7 node-summary'> <div className='col-xs-7 node-summary'>
{this.props.cluster && {this.props.cluster &&
<div><strong>{i18n('dialog.show_node.cluster')}: </strong>{this.props.cluster.get('name')}</div> <div><strong>{i18n('dialog.show_node.cluster')}: </strong>
{this.props.cluster.get('name')}
</div>
} }
<div><strong>{i18n('dialog.show_node.manufacturer_label')}: </strong>{node.get('manufacturer') || i18n('common.not_available')}</div> <div><strong>{i18n('dialog.show_node.manufacturer_label')}: </strong>
{node.get('manufacturer') || i18n('common.not_available')}
</div>
{this.props.nodeNetworkGroup && {this.props.nodeNetworkGroup &&
<div> <div>
<strong>{i18n('dialog.show_node.node_network_group')}: </strong> <strong>{i18n('dialog.show_node.node_network_group')}: </strong>
{this.props.nodeNetworkGroup.get('name')} {this.props.nodeNetworkGroup.get('name')}
</div> </div>
} }
<div><strong>{i18n('dialog.show_node.mac_address_label')}: </strong>{node.get('mac') || i18n('common.not_available')}</div> <div><strong>{i18n('dialog.show_node.mac_address_label')}: </strong>
<div><strong>{i18n('dialog.show_node.fqdn_label')}: </strong>{(node.get('meta').system || {}).fqdn || node.get('fqdn') || i18n('common.not_available')}</div> {node.get('mac') || i18n('common.not_available')}
</div>
<div><strong>{i18n('dialog.show_node.fqdn_label')}: </strong>
{(node.get('meta').system || {}).fqdn || node.get('fqdn') ||
i18n('common.not_available')}
</div>
<div className='change-hostname'> <div className='change-hostname'>
<strong>{i18n('dialog.show_node.hostname_label')}: </strong> <strong>{i18n('dialog.show_node.hostname_label')}: </strong>
{this.state.isRenaming ? {this.state.isRenaming ?
@ -832,28 +957,57 @@ export var ShowNodeInfoDialog = React.createClass({
{_.map(groups, (group, groupIndex) => { {_.map(groups, (group, groupIndex) => {
var groupEntries = meta[group]; var groupEntries = meta[group];
var subEntries = []; var subEntries = [];
if (group == 'interfaces' || group == 'disks') groupEntries = _.sortBy(groupEntries, 'name'); if (group == 'interfaces' || group == 'disks') {
if (_.isPlainObject(groupEntries)) subEntries = _.find(_.values(groupEntries), _.isArray); groupEntries = _.sortBy(groupEntries, 'name');
}
if (_.isPlainObject(groupEntries)) {
subEntries = _.find(_.values(groupEntries), _.isArray);
}
return ( return (
<div className='panel panel-default' key={group}> <div className='panel panel-default' key={group}>
<div className='panel-heading' role='tab' id={'heading' + group} onClick={this.toggle.bind(this, groupIndex)}> <div
className='panel-heading'
role='tab'
id={'heading' + group}
onClick={this.toggle.bind(this, groupIndex)}
>
<div className='panel-title'> <div className='panel-title'>
<div data-parent='#accordion' aria-expanded='true' aria-controls={'body' + group}> <div
<strong>{i18n('node_details.' + group, {defaultValue: group})}</strong> {this.showSummary(meta, group)} data-parent='#accordion'
aria-expanded='true'
aria-controls={'body' + group}
>
<strong>{i18n('node_details.' + group, {defaultValue: group})}</strong>
{this.showSummary(meta, group)}
<i className='glyphicon glyphicon-plus pull-right' /> <i className='glyphicon glyphicon-plus pull-right' />
</div> </div>
</div> </div>
</div> </div>
<div className='panel-collapse collapse' role='tabpanel' aria-labelledby={'heading' + group} ref={'togglable_' + groupIndex}> <div
className='panel-collapse collapse'
role='tabpanel'
aria-labelledby={'heading' + group}
ref={'togglable_' + groupIndex}
>
<div className='panel-body enable-selection'> <div className='panel-body enable-selection'>
{_.isArray(groupEntries) && {_.isArray(groupEntries) &&
<div> <div>
{_.map(groupEntries, (entry, entryIndex) => { {_.map(groupEntries, (entry, entryIndex) => {
return ( return (
<div className='nested-object' key={'entry_' + groupIndex + entryIndex}> <div className='nested-object' key={'entry_' + groupIndex + entryIndex}>
{_.map(utils.sortEntryProperties(entry, sortOrder[group]), (propertyName) => { {_.map(utils.sortEntryProperties(entry, sortOrder[group]),
if (!_.isPlainObject(entry[propertyName]) && !_.isArray(entry[propertyName])) return this.renderNodeInfo(propertyName, this.showPropertyValue(group, propertyName, entry[propertyName])); (propertyName) => {
})} if (!_.isPlainObject(entry[propertyName]) &&
!_.isArray(entry[propertyName])) {
return this.renderNodeInfo(
propertyName,
this.showPropertyValue(
group, propertyName, entry[propertyName]
)
);
}
}
)}
</div> </div>
); );
})} })}
@ -862,15 +1016,25 @@ export var ShowNodeInfoDialog = React.createClass({
{_.isPlainObject(groupEntries) && {_.isPlainObject(groupEntries) &&
<div> <div>
{_.map(groupEntries, (propertyValue, propertyName) => { {_.map(groupEntries, (propertyValue, propertyName) => {
if (!_.isPlainObject(propertyValue) && !_.isArray(propertyValue) && !_.isNumber(propertyName)) return this.renderNodeInfo(propertyName, this.showPropertyValue(group, propertyName, propertyValue)); if (!_.isPlainObject(propertyValue) && !_.isArray(propertyValue) &&
!_.isNumber(propertyName)) return this.renderNodeInfo(propertyName,
this.showPropertyValue(group, propertyName, propertyValue));
})} })}
{!_.isEmpty(subEntries) && {!_.isEmpty(subEntries) &&
<div> <div>
{_.map(subEntries, (subentry, subentrysIndex) => { {_.map(subEntries, (subentry, subentrysIndex) => {
return ( return (
<div className='nested-object' key={'subentries_' + groupIndex + subentrysIndex}> <div
className='nested-object'
key={'subentries_' + groupIndex + subentrysIndex}
>
{_.map(utils.sortEntryProperties(subentry), (propertyName) => { {_.map(utils.sortEntryProperties(subentry), (propertyName) => {
if (!_.isPlainObject(subentry[propertyName]) && !_.isArray(subentry[propertyName])) return this.renderNodeInfo(propertyName, this.showPropertyValue(group, propertyName, subentry[propertyName])); return this.renderNodeInfo(
propertyName,
this.showPropertyValue(
group, propertyName, subentry[propertyName]
)
);
})} })}
</div> </div>
); );
@ -879,7 +1043,8 @@ export var ShowNodeInfoDialog = React.createClass({
} }
</div> </div>
} }
{(!_.isPlainObject(groupEntries) && !_.isArray(groupEntries) && !_.isUndefined(groupEntries)) && {(!_.isPlainObject(groupEntries) && !_.isArray(groupEntries) &&
!_.isUndefined(groupEntries)) &&
<div>{groupEntries}</div> <div>{groupEntries}</div>
} }
{group == 'config' && {group == 'config' &&
@ -895,7 +1060,8 @@ export var ShowNodeInfoDialog = React.createClass({
<button <button
className='btn btn-success' className='btn btn-success'
onClick={this.saveVMsConf} onClick={this.saveVMsConf}
disabled={this.state.VMsConfValidationError || this.state.actionInProgress} disabled={this.state.VMsConfValidationError ||
this.state.actionInProgress}
> >
{i18n('common.save_settings_button')} {i18n('common.save_settings_button')}
</button> </button>
@ -915,16 +1081,29 @@ export var ShowNodeInfoDialog = React.createClass({
<div> <div>
{this.props.renderActionButtons && this.props.node.get('cluster') && {this.props.renderActionButtons && this.props.node.get('cluster') &&
<div className='btn-group' role='group'> <div className='btn-group' role='group'>
<button className='btn btn-default btn-edit-disks' onClick={_.partial(this.goToConfigurationScreen, 'disks')}> <button
{i18n('dialog.show_node.disk_configuration' + (this.props.node.areDisksConfigurable() ? '_action' : ''))} className='btn btn-default btn-edit-disks'
onClick={_.partial(this.goToConfigurationScreen, 'disks')}
>
{i18n('dialog.show_node.disk_configuration' +
(this.props.node.areDisksConfigurable() ? '_action' : ''))}
</button> </button>
<button className='btn btn-default btn-edit-networks' onClick={_.partial(this.goToConfigurationScreen, 'interfaces')}> <button
{i18n('dialog.show_node.network_configuration' + (this.props.node.areInterfacesConfigurable() ? '_action' : ''))} className='btn btn-default btn-edit-networks'
onClick={_.partial(this.goToConfigurationScreen, 'interfaces')}
>
{i18n('dialog.show_node.network_configuration' +
(this.props.node.areInterfacesConfigurable() ? '_action' : ''))}
</button> </button>
</div> </div>
} }
<div className='btn-group' role='group'> <div className='btn-group' role='group'>
<button className='btn btn-default' onClick={this.close}>{i18n('common.close_button')}</button> <button
className='btn btn-default'
onClick={this.close}
>
{i18n('common.close_button')}
</button>
</div> </div>
</div> </div>
); );
@ -932,7 +1111,9 @@ export var ShowNodeInfoDialog = React.createClass({
renderNodeInfo(name, value) { renderNodeInfo(name, value) {
return ( return (
<div key={name + value} className='node-details-row'> <div key={name + value} className='node-details-row'>
<label>{i18n('dialog.show_node.' + name, {defaultValue: this.showPropertyName(name)})}</label> <label>
{i18n('dialog.show_node.' + name, {defaultValue: this.showPropertyName(name)})}
</label>
{value} {value}
</div> </div>
); );
@ -1042,7 +1223,8 @@ export var DeleteNodesDialog = React.createClass({
{this.renderImportantLabel()} {this.renderImportantLabel()}
{i18n(ns + 'common_message', {count: this.props.nodes.length})} {i18n(ns + 'common_message', {count: this.props.nodes.length})}
<br/> <br/>
{!!notDeployedNodesAmount && i18n(ns + 'not_deployed_nodes_message', {count: notDeployedNodesAmount})} {!!notDeployedNodesAmount && i18n(ns + 'not_deployed_nodes_message',
{count: notDeployedNodesAmount})}
{' '} {' '}
{!!deployedNodesAmount && i18n(ns + 'deployed_nodes_message', {count: deployedNodesAmount})} {!!deployedNodesAmount && i18n(ns + 'deployed_nodes_message', {count: deployedNodesAmount})}
</div> </div>
@ -1050,8 +1232,18 @@ export var DeleteNodesDialog = React.createClass({
}, },
renderFooter() { renderFooter() {
return [ return [
<button key='cancel' className='btn btn-default' onClick={this.close}>{i18n('common.cancel_button')}</button>, <button
<button key='delete' className='btn btn-danger btn-delete' onClick={this.deleteNodes} disabled={this.state.actionInProgress}>{i18n('common.delete_button')}</button> key='cancel'
className='btn btn-default'
onClick={this.close}>{i18n('common.cancel_button')}
</button>,
<button
key='delete'
className='btn btn-danger btn-delete'
onClick={this.deleteNodes} disabled={this.state.actionInProgress}
>
{i18n('common.delete_button')}
</button>
]; ];
}, },
deleteNodes() { deleteNodes() {
@ -1075,12 +1267,14 @@ export var DeleteNodesDialog = React.createClass({
return this.props.cluster.fetchRelated('nodes'); return this.props.cluster.fetchRelated('nodes');
}) })
.done(() => { .done(() => {
dispatcher.trigger('updateNodeStats networkConfigurationUpdated labelsConfigurationUpdated'); dispatcher.trigger('updateNodeStats networkConfigurationUpdated ' +
'labelsConfigurationUpdated');
this.state.result.resolve(); this.state.result.resolve();
this.close(); this.close();
}) })
.fail((response) => { .fail((response) => {
this.showError(response, i18n('cluster_page.nodes_tab.node_deletion_error.node_deletion_warning')); this.showError(response, i18n('cluster_page.nodes_tab.node_deletion_error.' +
'node_deletion_warning'));
}); });
} }
}); });
@ -1106,7 +1300,9 @@ export var ChangePasswordDialog = React.createClass({
}, },
getError(name) { getError(name) {
var ns = 'dialog.change_password.'; var ns = 'dialog.change_password.';
if (name == 'currentPassword' && this.state.validationError) return i18n(ns + 'wrong_current_password'); if (name == 'currentPassword' && this.state.validationError) {
return i18n(ns + 'wrong_current_password');
}
if (this.state.newPassword != this.state.confirmationPassword) { if (this.state.newPassword != this.state.confirmationPassword) {
if (name == 'confirmationPassword') return i18n(ns + 'new_password_mismatch'); if (name == 'confirmationPassword') return i18n(ns + 'new_password_mismatch');
if (name == 'newPassword') return ''; if (name == 'newPassword') return '';
@ -1140,7 +1336,12 @@ export var ChangePasswordDialog = React.createClass({
}, },
renderFooter() { renderFooter() {
return [ return [
<button key='cancel' className='btn btn-default' onClick={this.close} disabled={this.state.actionInProgress}> <button
key='cancel'
className='btn btn-default'
onClick={this.close}
disabled={this.state.actionInProgress}
>
{i18n('common.cancel_button')} {i18n('common.cancel_button')}
</button>, </button>,
<button key='apply' className='btn btn-success' onClick={this.changePassword} <button key='apply' className='btn btn-success' onClick={this.changePassword}
@ -1177,7 +1378,8 @@ export var ChangePasswordDialog = React.createClass({
this.setState({actionInProgress: true}); this.setState({actionInProgress: true});
keystoneClient.changePassword(this.state.currentPassword, this.state.newPassword) keystoneClient.changePassword(this.state.currentPassword, this.state.newPassword)
.done(() => { .done(() => {
dispatcher.trigger(this.state.newPassword == keystoneClient.DEFAULT_PASSWORD ? 'showDefaultPasswordWarning' : 'hideDefaultPasswordWarning'); dispatcher.trigger(this.state.newPassword == keystoneClient.DEFAULT_PASSWORD ?
'showDefaultPasswordWarning' : 'hideDefaultPasswordWarning');
app.user.set({token: keystoneClient.token}); app.user.set({token: keystoneClient.token});
this.close(); this.close();
}) })
@ -1225,7 +1427,9 @@ export var RegistrationDialog = React.createClass({
onChange(inputName, value) { onChange(inputName, value) {
var registrationForm = this.props.registrationForm; var registrationForm = this.props.registrationForm;
var name = registrationForm.makePath('credentials', inputName, 'value'); var name = registrationForm.makePath('credentials', inputName, 'value');
if (registrationForm.validationError) delete registrationForm.validationError['credentials.' + inputName]; if (registrationForm.validationError) {
delete registrationForm.validationError['credentials.' + inputName];
}
registrationForm.set(name, value); registrationForm.set(name, value);
}, },
composeOptions(values) { composeOptions(values) {
@ -1238,14 +1442,21 @@ export var RegistrationDialog = React.createClass({
}); });
}, },
getAgreementLink(link) { getAgreementLink(link) {
return (<span>{i18n('dialog.registration.i_agree')} <a href={link} target='_blank'>{i18n('dialog.registration.terms_and_conditions')}</a></span>); return (
<span>
{i18n('dialog.registration.i_agree')}
<a href={link} target='_blank'>
{i18n('dialog.registration.terms_and_conditions')}
</a>
</span>);
}, },
validateRegistrationForm() { validateRegistrationForm() {
var registrationForm = this.props.registrationForm; var registrationForm = this.props.registrationForm;
var isValid = registrationForm.isValid(); var isValid = registrationForm.isValid();
if (!registrationForm.attributes.credentials.agree.value) { if (!registrationForm.attributes.credentials.agree.value) {
if (!registrationForm.validationError) registrationForm.validationError = {}; if (!registrationForm.validationError) registrationForm.validationError = {};
registrationForm.validationError['credentials.agree'] = i18n('dialog.registration.agree_error'); registrationForm.validationError['credentials.agree'] =
i18n('dialog.registration.agree_error');
isValid = false; isValid = false;
} }
this.setState({ this.setState({
@ -1263,7 +1474,10 @@ export var RegistrationDialog = React.createClass({
var collector = (path) => { var collector = (path) => {
return (name) => { return (name) => {
this.props.settings.set(this.props.settings.makePath(path, name, 'value'), response[name]); this.props.settings.set(
this.props.settings.makePath(path, name, 'value'),
response[name]
);
}; };
}; };
_.each(['company', 'name', 'email'], collector('statistics')); _.each(['company', 'name', 'email'], collector('statistics'));
@ -1346,7 +1560,12 @@ export var RegistrationDialog = React.createClass({
</button> </button>
]; ];
if (!this.state.loading) buttons.push( if (!this.state.loading) buttons.push(
<button key='apply' className='btn btn-success' disabled={this.state.actionInProgress || this.state.connectionError} onClick={this.validateRegistrationForm}> <button
key='apply'
className='btn btn-success'
disabled={this.state.actionInProgress || this.state.connectionError}
onClick={this.validateRegistrationForm}
>
{i18n('welcome_page.register.create_account')} {i18n('welcome_page.register.create_account')}
</button> </button>
); );
@ -1386,7 +1605,9 @@ export var RetrievePasswordDialog = React.createClass({
}, },
onChange(inputName, value) { onChange(inputName, value) {
var remoteRetrievePasswordForm = this.props.remoteRetrievePasswordForm; var remoteRetrievePasswordForm = this.props.remoteRetrievePasswordForm;
if (remoteRetrievePasswordForm.validationError) delete remoteRetrievePasswordForm.validationError['credentials.email']; if (remoteRetrievePasswordForm.validationError) {
delete remoteRetrievePasswordForm.validationError['credentials.email'];
}
remoteRetrievePasswordForm.set('credentials.email.value', value); remoteRetrievePasswordForm.set('credentials.email.value', value);
}, },
retrievePassword() { retrievePassword() {
@ -1411,7 +1632,8 @@ export var RetrievePasswordDialog = React.createClass({
var error = this.state.error; var error = this.state.error;
var actionInProgress = this.state.actionInProgress; var actionInProgress = this.state.actionInProgress;
var input = (remoteRetrievePasswordForm.get('credentials') || {}).email; var input = (remoteRetrievePasswordForm.get('credentials') || {}).email;
var inputError = remoteRetrievePasswordForm ? (remoteRetrievePasswordForm.validationError || {})['credentials.email'] : null; var inputError = remoteRetrievePasswordForm ? (remoteRetrievePasswordForm.validationError ||
{})['credentials.email'] : null;
return ( return (
<div className='retrieve-password-content'> <div className='retrieve-password-content'>
{!this.state.passwordSent ? {!this.state.passwordSent ?
@ -1457,7 +1679,12 @@ export var RetrievePasswordDialog = React.createClass({
</button> </button>
]; ];
if (!this.state.loading) buttons.push( if (!this.state.loading) buttons.push(
<button key='apply' className='btn btn-success' disabled={this.state.actionInProgress || this.state.connectionError} onClick={this.retrievePassword}> <button
key='apply'
className='btn btn-success'
disabled={this.state.actionInProgress || this.state.connectionError}
onClick={this.retrievePassword}
>
{i18n('dialog.retrieve_password.send_new_password')} {i18n('dialog.retrieve_password.send_new_password')}
</button> </button>
); );
@ -1498,10 +1725,20 @@ export var CreateNodeNetworkGroupDialog = React.createClass({
}, },
renderFooter() { renderFooter() {
return [ return [
<button key='cancel' className='btn btn-default' onClick={this.close} disabled={this.state.actionInProgress}> <button
key='cancel'
className='btn btn-default'
onClick={this.close}
disabled={this.state.actionInProgress}
>
{i18n('common.cancel_button')} {i18n('common.cancel_button')}
</button>, </button>,
<button key='apply' className='btn btn-success' onClick={this.createNodeNetworkGroup} disabled={this.state.actionInProgress || this.state.error}> <button
key='apply'
className='btn btn-success'
onClick={this.createNodeNetworkGroup}
disabled={this.state.actionInProgress || this.state.error}
>
{i18n(this.props.ns + 'add')} {i18n(this.props.ns + 'add')}
</button> </button>
]; ];
@ -1556,7 +1793,9 @@ export var RemoveNodeNetworkGroupDialog = React.createClass({
<div> <div>
<div className='text-danger'> <div className='text-danger'>
{this.renderImportantLabel()} {this.renderImportantLabel()}
{this.props.showUnsavedChangesWarning && (i18n('dialog.remove_node_network_group.unsaved_changes_alert') + ' ')} {this.props.showUnsavedChangesWarning &&
(i18n('dialog.remove_node_network_group.unsaved_changes_alert') + ' ')
}
{i18n('dialog.remove_node_network_group.confirmation')} {i18n('dialog.remove_node_network_group.confirmation')}
</div> </div>
</div> </div>

View File

@ -52,7 +52,10 @@ export var Navbar = React.createClass({
return this.props.user.get('authenticated'); return this.props.user.get('authenticated');
}, },
fetchData() { fetchData() {
return $.when(this.props.statistics.fetch(), this.props.notifications.fetch({limit: this.props.notificationsDisplayCount})); return $.when(
this.props.statistics.fetch(),
this.props.notifications.fetch({limit: this.props.notificationsDisplayCount})
);
}, },
updateNodeStats() { updateNodeStats() {
return this.props.statistics.fetch(); return this.props.statistics.fetch();
@ -91,7 +94,8 @@ export var Navbar = React.createClass({
}, },
render() { render() {
var unreadNotificationsCount = this.props.notifications.where({status: 'unread'}).length; var unreadNotificationsCount = this.props.notifications.where({status: 'unread'}).length;
var authenticationEnabled = this.props.version.get('auth_required') && this.props.user.get('authenticated'); var authenticationEnabled = this.props.version.get('auth_required') &&
this.props.user.get('authenticated');
return ( return (
<div className='navigation-box'> <div className='navigation-box'>
<div className='navbar-bg'></div> <div className='navbar-bg'></div>
@ -104,7 +108,12 @@ export var Navbar = React.createClass({
<ul className='nav navbar-nav pull-left'> <ul className='nav navbar-nav pull-left'>
{_.map(this.props.elements, (element) => { {_.map(this.props.elements, (element) => {
return ( return (
<li className={utils.classNames({active: this.props.activeElement == element.url.slice(1)})} key={element.label}> <li
className={utils.classNames({
active: this.props.activeElement == element.url.slice(1)
})}
key={element.label}
>
<a href={element.url}> <a href={element.url}>
{i18n('navbar.' + element.label, {defaultValue: element.label})} {i18n('navbar.' + element.label, {defaultValue: element.label})}
</a> </a>
@ -128,7 +137,10 @@ export var Navbar = React.createClass({
</li> </li>
<li <li
key='statistics-icon' key='statistics-icon'
className={'statistics-icon ' + (this.props.statistics.get('unallocated') ? '' : 'no-unallocated')} className={
'statistics-icon ' +
(this.props.statistics.get('unallocated') ? '' : 'no-unallocated')
}
onClick={this.togglePopover('statistics')} onClick={this.togglePopover('statistics')}
> >
{!!this.props.statistics.get('unallocated') && {!!this.props.statistics.get('unallocated') &&
@ -148,7 +160,9 @@ export var Navbar = React.createClass({
className='notifications-icon' className='notifications-icon'
onClick={this.togglePopover('notifications')} onClick={this.togglePopover('notifications')}
> >
<span className={utils.classNames({badge: true, visible: unreadNotificationsCount})}> <span
className={utils.classNames({badge: true, visible: unreadNotificationsCount})}
>
{unreadNotificationsCount} {unreadNotificationsCount}
</span> </span>
</li> </li>
@ -258,7 +272,10 @@ var UserPopover = React.createClass({
<div className='username'>{i18n('common.username')}:</div> <div className='username'>{i18n('common.username')}:</div>
<h3 className='name'>{this.props.user.get('username')}</h3> <h3 className='name'>{this.props.user.get('username')}</h3>
<div className='clearfix'> <div className='clearfix'>
<button className='btn btn-default btn-sm pull-left' onClick={this.showChangePasswordDialog}> <button
className='btn btn-default btn-sm pull-left'
onClick={this.showChangePasswordDialog}
>
<i className='glyphicon glyphicon-user'></i> <i className='glyphicon glyphicon-user'></i>
{i18n('common.change_password')} {i18n('common.change_password')}
</button> </button>
@ -281,7 +298,9 @@ var NotificationsPopover = React.createClass({
ShowNodeInfoDialog.show({node: node}); ShowNodeInfoDialog.show({node: node});
}, },
markAsRead() { markAsRead() {
var notificationsToMark = new models.Notifications(this.props.notifications.where({status: 'unread'})); var notificationsToMark = new models.Notifications(
this.props.notifications.where({status: 'unread'})
);
if (notificationsToMark.length) { if (notificationsToMark.length) {
this.setState({unreadNotificationsIds: notificationsToMark.pluck('id')}); this.setState({unreadNotificationsIds: notificationsToMark.pluck('id')});
notificationsToMark.toJSON = function() { notificationsToMark.toJSON = function() {
@ -307,7 +326,8 @@ var NotificationsPopover = React.createClass({
'text-danger': topic == 'error', 'text-danger': topic == 'error',
'text-warning': topic == 'warning', 'text-warning': topic == 'warning',
clickable: nodeId, clickable: nodeId,
unread: notification.get('status') == 'unread' || _.contains(this.state.unreadNotificationsIds, notification.id) unread: notification.get('status') == 'unread' ||
_.contains(this.state.unreadNotificationsIds, notification.id)
}; };
var iconClass = { var iconClass = {
error: 'glyphicon-exclamation-sign', error: 'glyphicon-exclamation-sign',
@ -347,7 +367,12 @@ export var Footer = React.createClass({
return ( return (
<div className='footer'> <div className='footer'>
{_.contains(version.get('feature_groups'), 'mirantis') && [ {_.contains(version.get('feature_groups'), 'mirantis') && [
<a key='logo' className='mirantis-logo-white' href='http://www.mirantis.com/' target='_blank'></a>, <a
key='logo'
className='mirantis-logo-white'
href='http://www.mirantis.com/'
target='_blank'
/>,
<div key='copyright'>{i18n('common.copyright')}</div> <div key='copyright'>{i18n('common.copyright')}</div>
]} ]}
<div key='version'>{i18n('common.version')}: {version.get('release')}</div> <div key='version'>{i18n('common.version')}: {version.get('release')}</div>
@ -365,7 +390,8 @@ export var Breadcrumbs = React.createClass({
}, },
getBreadcrumbsPath() { getBreadcrumbsPath() {
var page = this.props.Page; var page = this.props.Page;
return _.isFunction(page.breadcrumbsPath) ? page.breadcrumbsPath(this.props.pageOptions) : page.breadcrumbsPath; return _.isFunction(page.breadcrumbsPath) ? page.breadcrumbsPath(this.props.pageOptions) :
page.breadcrumbsPath;
}, },
refresh() { refresh() {
this.setState({path: this.getBreadcrumbsPath()}); this.setState({path: this.getBreadcrumbsPath()});

View File

@ -62,7 +62,8 @@ var LoginForm = React.createClass({
var error = 'login_error'; var error = 'login_error';
if (status == 401) { if (status == 401) {
error = 'credentials_error'; error = 'credentials_error';
} else if (!status || String(status)[0] == '5') { // no status (connection refused) or 5xx error // no status (connection refused) or 5xx error
} else if (!status || String(status)[0] == '5') {
error = 'keystone_unavailable_error'; error = 'keystone_unavailable_error';
} }
this.setState({error: i18n('login_page.' + error)}); this.setState({error: i18n('login_page.' + error)});
@ -126,7 +127,14 @@ var LoginForm = React.createClass({
<i className='glyphicon glyphicon-user'></i> <i className='glyphicon glyphicon-user'></i>
</label> </label>
<div className='col-xs-8'> <div className='col-xs-8'>
<input className='form-control input-sm' type='text' name='username' ref='username' placeholder={i18n('login_page.username')} onChange={this.onChange} /> <input
className='form-control input-sm'
type='text'
name='username'
ref='username'
placeholder={i18n('login_page.username')}
onChange={this.onChange}
/>
</div> </div>
</div> </div>
<div className='form-group'> <div className='form-group'>
@ -134,7 +142,14 @@ var LoginForm = React.createClass({
<i className='glyphicon glyphicon-lock'></i> <i className='glyphicon glyphicon-lock'></i>
</label> </label>
<div className='col-xs-8'> <div className='col-xs-8'>
<input className='form-control input-sm' type='password' name='password' ref='password' placeholder={i18n('login_page.password')} onChange={this.onChange} /> <input
className='form-control input-sm'
type='password'
name='password'
ref='password'
placeholder={i18n('login_page.password')}
onChange={this.onChange}
/>
</div> </div>
</div> </div>
{!httpsUsed && {!httpsUsed &&

View File

@ -110,7 +110,12 @@ Notification = React.createClass({
<div className='notification-time'>{this.props.notification.get('time')}</div> <div className='notification-time'>{this.props.notification.get('time')}</div>
<div className='notification-type'><i className={'glyphicon ' + iconClass} /></div> <div className='notification-type'><i className={'glyphicon ' + iconClass} /></div>
<div className='notification-message'> <div className='notification-message'>
<span className={this.props.notification.get('node_id') && 'btn btn-link'} dangerouslySetInnerHTML={{__html: utils.urlify(this.props.notification.escape('message'))}}></span> <span
className={this.props.notification.get('node_id') && 'btn btn-link'}
dangerouslySetInnerHTML={{
__html: utils.urlify(this.props.notification.escape('message'))
}}
/>
</div> </div>
</div> </div>
); );

View File

@ -103,7 +103,9 @@ var PluginsPage = React.createClass({
render() { render() {
var isMirantisIso = _.contains(app.version.get('feature_groups'), 'mirantis'); var isMirantisIso = _.contains(app.version.get('feature_groups'), 'mirantis');
var links = { var links = {
catalog: isMirantisIso ? 'https://www.mirantis.com/products/openstack-drivers-and-plugins/fuel-plugins/' : 'http://stackalytics.com/report/driverlog?project_id=openstack%2Ffuel', catalog: isMirantisIso ?
'https://www.mirantis.com/products/openstack-drivers-and-plugins/fuel-plugins/' :
'http://stackalytics.com/report/driverlog?project_id=openstack%2Ffuel',
documentation: utils.composeDocumentationLink('plugin-dev.html') documentation: utils.composeDocumentationLink('plugin-dev.html')
}; };
return ( return (

View File

@ -68,10 +68,23 @@ var RootComponent = React.createClass({
<div id='content-wrapper'> <div id='content-wrapper'>
<div className={utils.classNames(layoutClasses)}> <div className={utils.classNames(layoutClasses)}>
{!Page.hiddenLayout && [ {!Page.hiddenLayout && [
<Navbar key='navbar' ref='navbar' activeElement={Page.navbarActiveElement} {...this.props} />, <Navbar
key='navbar'
ref='navbar'
activeElement={Page.navbarActiveElement}
{...this.props}
/>,
<Breadcrumbs key='breadcrumbs' ref='breadcrumbs' {...this.state} />, <Breadcrumbs key='breadcrumbs' ref='breadcrumbs' {...this.state} />,
showDefaultPasswordWarning && <DefaultPasswordWarning key='password-warning' close={this.hideDefaultPasswordWarning} />, showDefaultPasswordWarning &&
fuelSettings.get('bootstrap.error.value') && <BootstrapError key='bootstrap-error' text={fuelSettings.get('bootstrap.error.value')} /> <DefaultPasswordWarning
key='password-warning'
close={this.hideDefaultPasswordWarning}
/>,
fuelSettings.get('bootstrap.error.value') &&
<BootstrapError
key='bootstrap-error'
text={fuelSettings.get('bootstrap.error.value')}
/>
]} ]}
<div id='content'> <div id='content'>
<Page ref='page' {...this.state.pageOptions} /> <Page ref='page' {...this.state.pageOptions} />

View File

@ -109,7 +109,11 @@ export default {
} }
}, },
checkRestrictions(name, action = 'disable') { checkRestrictions(name, action = 'disable') {
return this.props.settings.checkRestrictions(this.configModels, action, this.props.settings.get('statistics').name); return this.props.settings.checkRestrictions(
this.configModels,
action,
this.props.settings.get('statistics').name
);
}, },
componentWillMount() { componentWillMount() {
var model = this.props.statistics || this.props.tracking; var model = this.props.statistics || this.props.tracking;
@ -129,9 +133,15 @@ export default {
renderInput(settingName, wrapperClassName, disabledState) { renderInput(settingName, wrapperClassName, disabledState) {
var model = this.props.statistics || this.props.tracking; var model = this.props.statistics || this.props.tracking;
var setting = model.get(model.makePath('statistics', settingName)); var setting = model.get(model.makePath('statistics', settingName));
if (this.checkRestrictions('metadata', 'hide').result || this.checkRestrictions(settingName, 'hide').result || setting.type == 'hidden') return null; if (
this.checkRestrictions('metadata', 'hide').result ||
this.checkRestrictions(settingName, 'hide').result ||
setting.type == 'hidden'
) return null;
var error = this.getError(model, settingName); var error = this.getError(model, settingName);
var disabled = this.checkRestrictions('metadata').result || this.checkRestrictions(settingName).result || disabledState; var disabled = this.checkRestrictions('metadata').result ||
this.checkRestrictions(settingName).result ||
disabledState;
return <Input return <Input
key={settingName} key={settingName}
type={setting.type} type={setting.type}
@ -198,8 +208,16 @@ export default {
return ( return (
<div> <div>
<div className='statistics-text-box'> <div className='statistics-text-box'>
<div className={utils.classNames({notice: isMirantisIso})}>{this.getText(ns + 'help_to_improve')}</div> <div className={utils.classNames({notice: isMirantisIso})}>
<button className='btn-link' data-toggle='collapse' data-target='.statistics-disclaimer-box'>{i18n(ns + 'learn_whats_collected')}</button> {this.getText(ns + 'help_to_improve')}
</div>
<button
className='btn-link'
data-toggle='collapse'
data-target='.statistics-disclaimer-box'
>
{i18n(ns + 'learn_whats_collected')}
</button>
<div className='collapse statistics-disclaimer-box'> <div className='collapse statistics-disclaimer-box'>
<p>{i18n(ns + 'statistics_includes_full')}</p> <p>{i18n(ns + 'statistics_includes_full')}</p>
{_.map(lists, this.renderList)} {_.map(lists, this.renderList)}
@ -258,10 +276,16 @@ export default {
/>; />;
})} })}
<div className='links-container'> <div className='links-container'>
<button className='btn btn-link create-account pull-left' onClick={this.showRegistrationDialog}> <button
className='btn btn-link create-account pull-left'
onClick={this.showRegistrationDialog}
>
{i18n('welcome_page.register.create_account')} {i18n('welcome_page.register.create_account')}
</button> </button>
<button className='btn btn-link retrive-password pull-right' onClick={this.showRetrievePasswordDialog}> <button
className='btn btn-link retrive-password pull-right'
onClick={this.showRetrievePasswordDialog}
>
{i18n('welcome_page.register.retrieve_password')} {i18n('welcome_page.register.retrieve_password')}
</button> </button>
</div> </div>

View File

@ -46,17 +46,33 @@ var SupportPage = React.createClass({
render() { render() {
var elements = [ var elements = [
<DocumentationLink key='DocumentationLink' />, <DocumentationLink key='DocumentationLink' />,
<DiagnosticSnapshot key='DiagnosticSnapshot' tasks={this.props.tasks} task={this.props.tasks.findTask({name: 'dump'})} />, <DiagnosticSnapshot
key='DiagnosticSnapshot'
tasks={this.props.tasks}
task={this.props.tasks.findTask({name: 'dump'})}
/>,
<CapacityAudit key='CapacityAudit' /> <CapacityAudit key='CapacityAudit' />
]; ];
if (_.contains(app.version.get('feature_groups'), 'mirantis')) { if (_.contains(app.version.get('feature_groups'), 'mirantis')) {
elements.unshift( elements.unshift(
<RegistrationInfo key='RegistrationInfo' settings={this.props.settings} tracking={this.props.tracking}/>, <RegistrationInfo
<StatisticsSettings key='StatisticsSettings' settings={this.props.settings} statistics={this.props.statistics}/>, key='RegistrationInfo'
settings={this.props.settings}
tracking={this.props.tracking}
/>,
<StatisticsSettings
key='StatisticsSettings'
settings={this.props.settings}
statistics={this.props.statistics}
/>,
<SupportContacts key='SupportContacts' /> <SupportContacts key='SupportContacts' />
); );
} else { } else {
elements.push(<StatisticsSettings key='StatisticsSettings' settings={this.props.settings} statistics={this.props.statistics}/>); elements.push(<StatisticsSettings
key='StatisticsSettings'
settings={this.props.settings}
statistics={this.props.statistics}
/>);
} }
return ( return (
<div className='support-page'> <div className='support-page'>
@ -92,7 +108,8 @@ var SupportPageElement = React.createClass({
var DocumentationLink = React.createClass({ var DocumentationLink = React.createClass({
render() { render() {
var ns = 'support_page.' + (_.contains(app.version.get('feature_groups'), 'mirantis') ? 'mirantis' : 'community') + '_'; var ns = 'support_page.' + (_.contains(app.version.get('feature_groups'), 'mirantis') ?
'mirantis' : 'community') + '_';
return ( return (
<SupportPageElement <SupportPageElement
className='img-documentation-link' className='img-documentation-link'
@ -100,7 +117,11 @@ var DocumentationLink = React.createClass({
text={i18n(ns + 'text')} text={i18n(ns + 'text')}
> >
<p> <p>
<a className='btn btn-default documentation-link' href='https://www.mirantis.com/openstack-documentation/' target='_blank'> <a
className='btn btn-default documentation-link'
href='https://www.mirantis.com/openstack-documentation/'
target='_blank'
>
{i18n('support_page.documentation_link')} {i18n('support_page.documentation_link')}
</a> </a>
</p> </p>
@ -124,12 +145,26 @@ var RegistrationInfo = React.createClass({
> >
<div className='registeredData enable-selection'> <div className='registeredData enable-selection'>
{_.map(['name', 'email', 'company'], (value) => { {_.map(['name', 'email', 'company'], (value) => {
return <div key={value}><b>{i18n('statistics.setting_labels.' + value)}:</b> {this.props.tracking.get('statistics')[value].value}</div>; return (
<div key={value}>
<b>{i18n('statistics.setting_labels.' + value)}:</b>
{' '}
{this.props.tracking.get('statistics')[value].value}
</div>
);
})} })}
<div><b>{i18n('support_page.master_node_uuid')}:</b> {this.props.tracking.get('master_node_uid')}</div> <div>
<b>{i18n('support_page.master_node_uuid')}:</b>
{' '}
{this.props.tracking.get('master_node_uid')}
</div>
</div> </div>
<p> <p>
<a className='btn btn-default' href='https://software.mirantis.com/account/' target='_blank'> <a
className='btn btn-default'
href='https://software.mirantis.com/account/'
target='_blank'
>
{i18n('support_page.manage_account')} {i18n('support_page.manage_account')}
</a> </a>
</p> </p>
@ -142,9 +177,18 @@ var RegistrationInfo = React.createClass({
text={i18n('support_page.register_fuel_content')} text={i18n('support_page.register_fuel_content')}
> >
<div className='tracking'> <div className='tracking'>
{this.renderRegistrationForm(this.props.tracking, this.state.actionInProgress, this.state.error, this.state.actionInProgress)} {this.renderRegistrationForm(
this.props.tracking,
this.state.actionInProgress,
this.state.error,
this.state.actionInProgress
)}
<p> <p>
<button className='btn btn-default' onClick={this.connectToMirantis} disabled={this.state.actionInProgress} target='_blank'> <button
className='btn btn-default'
onClick={this.connectToMirantis}
disabled={this.state.actionInProgress} target='_blank'
>
{i18n('support_page.register_fuel_title')} {i18n('support_page.register_fuel_title')}
</button> </button>
</p> </p>
@ -212,9 +256,17 @@ var SupportContacts = React.createClass({
title={i18n('support_page.contact_support')} title={i18n('support_page.contact_support')}
text={i18n('support_page.contact_text')} text={i18n('support_page.contact_text')}
> >
<p>{i18n('support_page.irc_text')} <strong>#fuel</strong> on <a href='http://freenode.net' target='_blank'>freenode.net</a>.</p>
<p> <p>
<a className='btn btn-default' href='http://support.mirantis.com/requests/new' target='_blank'> {i18n('support_page.irc_text')}
{' '}
<strong>#fuel</strong> on <a href='http://freenode.net' target='_blank'>freenode.net</a>.
</p>
<p>
<a
className='btn btn-default'
href='http://support.mirantis.com/requests/new'
target='_blank'
>
{i18n('support_page.contact_support')} {i18n('support_page.contact_support')}
</a> </a>
</p> </p>
@ -244,7 +296,7 @@ var DiagnosticSnapshot = React.createClass({
}, },
downloadLogs() { downloadLogs() {
this.setState({generating: true}); this.setState({generating: true});
(new models.LogsPackage()).save({}, {method: 'PUT'}).always(_.bind(this.props.tasks.fetch, this.props.tasks)); (new models.LogsPackage()).save({}, {method: 'PUT'}).always(() => this.props.tasks.fetch());
}, },
componentDidUpdate() { componentDidUpdate() {
this.startPolling(); this.startPolling();
@ -260,7 +312,8 @@ var DiagnosticSnapshot = React.createClass({
> >
<p className='snapshot'> <p className='snapshot'>
<button className='btn btn-default' disabled={generating} onClick={this.downloadLogs}> <button className='btn btn-default' disabled={generating} onClick={this.downloadLogs}>
{generating ? i18n('support_page.gen_logs_snapshot_text') : i18n('support_page.gen_diagnostic_snapshot_text')} {generating ? i18n('support_page.gen_logs_snapshot_text') :
i18n('support_page.gen_diagnostic_snapshot_text')}
</button> </button>
{' '} {' '}
{!generating && task && {!generating && task &&

View File

@ -80,13 +80,25 @@ var WelcomePage = React.createClass({
<div className='happy-cloud'> <div className='happy-cloud'>
<div className='cloud-smile' /> <div className='cloud-smile' />
<div className='row'> <div className='row'>
<div className='col-xs-8 col-xs-offset-2'>{i18n(ns + 'register.welcome_phrase.thanks')}{username ? ' ' + username : ''}, {i18n(ns + 'register.welcome_phrase.content')}</div> <div className='col-xs-8 col-xs-offset-2'>
{i18n(ns + 'register.welcome_phrase.thanks')}
{username ? ' ' + username : ''}
{', '}
{i18n(ns + 'register.welcome_phrase.content')}
</div>
</div> </div>
</div> </div>
: :
<div> <div>
<p className='register_installation'>{i18n(ns + 'register.register_installation')}</p> <p className='register_installation'>
{this.renderRegistrationForm(this.props.tracking, disabled, this.state.error, this.state.actionInProgress && !this.state.locked)} {i18n(ns + 'register.register_installation')}
</p>
{this.renderRegistrationForm(
this.props.tracking,
disabled,
this.state.error,
this.state.actionInProgress && !this.state.locked
)}
</div> </div>
} }
</div> </div>
@ -96,7 +108,9 @@ var WelcomePage = React.createClass({
{this.renderInput('send_user_info', 'welcome-checkbox-box', disabled)} {this.renderInput('send_user_info', 'welcome-checkbox-box', disabled)}
<div> <div>
<div className='notice'>{i18n(ns + 'privacy_policy')}</div> <div className='notice'>{i18n(ns + 'privacy_policy')}</div>
<div><a href={privacyPolicyLink} target='_blank'>{i18n(ns + 'privacy_policy_link')}</a></div> <div>
<a href={privacyPolicyLink} target='_blank'>{i18n(ns + 'privacy_policy_link')}</a>
</div>
</div> </div>
</div> </div>
: :

View File

@ -111,7 +111,10 @@ var ComponentRadioGroup = React.createClass({
var ClusterWizardPanesMixin = { var ClusterWizardPanesMixin = {
componentWillMount() { componentWillMount() {
if (this.props.allComponents) { if (this.props.allComponents) {
this.components = this.props.allComponents.getComponentsByType(this.constructor.componentType, {sorted: true}); this.components = this.props.allComponents.getComponentsByType(
this.constructor.componentType,
{sorted: true}
);
this.processRestrictions(this.components); this.processRestrictions(this.components);
} }
}, },
@ -167,7 +170,8 @@ var ClusterWizardPanesMixin = {
}); });
component.set({ component.set({
isCompatible: isCompatible, isCompatible: isCompatible,
warnings: isCompatible ? i18n('dialog.create_cluster_wizard.compatible') : i18n('dialog.create_cluster_wizard.incompatible_list') + warnings.join(', '), warnings: isCompatible ? i18n('dialog.create_cluster_wizard.compatible') :
i18n('dialog.create_cluster_wizard.incompatible_list') + warnings.join(', '),
availability: (isCompatible ? 'compatible' : 'available') availability: (isCompatible ? 'compatible' : 'available')
}); });
}); });
@ -181,7 +185,10 @@ var ClusterWizardPanesMixin = {
var warnings = []; var warnings = [];
_.each(incompatibles, (incompatible) => { _.each(incompatibles, (incompatible) => {
var type = incompatible.component.get('type'); var type = incompatible.component.get('type');
var isInStopList = _.find(stopList, (component) => component.id == incompatible.component.id); var isInStopList = _.find(
stopList,
(component) => component.id == incompatible.component.id
);
if (!_.contains(types, type) || isInStopList) { if (!_.contains(types, type) || isInStopList) {
// ignore forward incompatibilities // ignore forward incompatibilities
return; return;
@ -261,10 +268,15 @@ var NameAndRelease = React.createClass({
}, },
isValid() { isValid() {
var wizard = this.props.wizard; var wizard = this.props.wizard;
var [name, cluster, clusters] = [wizard.get('name'), wizard.get('cluster'), wizard.get('clusters')]; var [name, cluster, clusters] = [
wizard.get('name'),
wizard.get('cluster'),
wizard.get('clusters')
];
// test cluster name is already taken // test cluster name is already taken
if (clusters.findWhere({name: name})) { if (clusters.findWhere({name: name})) {
var error = i18n('dialog.create_cluster_wizard.name_release.existing_environment', {name: name}); var error = i18n('dialog.create_cluster_wizard.name_release.existing_environment',
{name: name});
wizard.set({name_error: error}); wizard.set({name_error: error});
return false; return false;
} }
@ -286,7 +298,9 @@ var NameAndRelease = React.createClass({
return null; return null;
} }
var os = release.get('operating_system'); var os = release.get('operating_system');
var connectivityAlert = i18n('dialog.create_cluster_wizard.name_release.' + os + '_connectivity_alert'); var connectivityAlert = i18n(
'dialog.create_cluster_wizard.name_release.' + os + '_connectivity_alert'
);
return ( return (
<div className='create-cluster-form name-and-release'> <div className='create-cluster-form name-and-release'>
<Input <Input
@ -345,7 +359,9 @@ var Compute = React.createClass({
return _.contains(this.constructor.vCenterNetworkBackends, component.id); return _.contains(this.constructor.vCenterNetworkBackends, component.id);
}); });
if (!hasCompatibleBackends) { if (!hasCompatibleBackends) {
var vCenter = _.find(allComponents.models, (component) => component.id == this.constructor.vCenterPath); var vCenter = _.find(allComponents.models, (component) => {
return component.id == this.constructor.vCenterPath;
});
vCenter.set({ vCenter.set({
disabled: true, disabled: true,
warnings: i18n('dialog.create_cluster_wizard.compute.vcenter_requires_network_backend') warnings: i18n('dialog.create_cluster_wizard.compute.vcenter_requires_network_backend')
@ -363,7 +379,9 @@ var Compute = React.createClass({
onChange={this.props.onChange} onChange={this.props.onChange}
/> />
{this.constructor.hasErrors(this.props.wizard) && {this.constructor.hasErrors(this.props.wizard) &&
<div className='alert alert-warning'>{i18n('dialog.create_cluster_wizard.compute.empty_choice')}</div> <div className='alert alert-warning'>
{i18n('dialog.create_cluster_wizard.compute.empty_choice')}
</div>
} }
</div> </div>
); );
@ -405,10 +423,17 @@ var Network = React.createClass({
var monolithic = _.filter(this.components, (component) => !component.isML2Driver()); var monolithic = _.filter(this.components, (component) => !component.isML2Driver());
var hasMl2 = _.any(this.components, (component) => component.isML2Driver()); var hasMl2 = _.any(this.components, (component) => component.isML2Driver());
if (!hasMl2) { if (!hasMl2) {
monolithic = _.filter(monolithic, (component) => component.id != this.constructor.ml2CorePath); monolithic = _.filter(monolithic, (component) => {
return component.id != this.constructor.ml2CorePath;
});
} }
this.processRestrictions(monolithic, this.constructor.panesForRestrictions); this.processRestrictions(monolithic, this.constructor.panesForRestrictions);
this.processCompatible(this.props.allComponents, monolithic, this.constructor.panesForRestrictions, monolithic); this.processCompatible(
this.props.allComponents,
monolithic,
this.constructor.panesForRestrictions,
monolithic
);
this.selectActiveComponent(monolithic); this.selectActiveComponent(monolithic);
return ( return (
<ComponentRadioGroup <ComponentRadioGroup
@ -438,7 +463,9 @@ var Network = React.createClass({
{this.renderML2DriverControls()} {this.renderML2DriverControls()}
</div> </div>
{this.constructor.hasErrors(this.props.wizard) && {this.constructor.hasErrors(this.props.wizard) &&
<div className='alert alert-warning'>{i18n('dialog.create_cluster_wizard.network.ml2_empty_choice')}</div> <div className='alert alert-warning'>
{i18n('dialog.create_cluster_wizard.network.ml2_empty_choice')}
</div>
} }
</div> </div>
); );
@ -456,8 +483,17 @@ var Storage = React.createClass({
renderSection(components, type) { renderSection(components, type) {
var sectionComponents = _.filter(components, (component) => component.get('subtype') == type); var sectionComponents = _.filter(components, (component) => component.get('subtype') == type);
var isRadio = this.areComponentsMutuallyExclusive(sectionComponents); var isRadio = this.areComponentsMutuallyExclusive(sectionComponents);
this.processRestrictions(sectionComponents, this.constructor.panesForRestrictions, (isRadio ? sectionComponents : [])); this.processRestrictions(
this.processCompatible(this.props.allComponents, sectionComponents, this.constructor.panesForRestrictions, isRadio ? sectionComponents : []); sectionComponents,
this.constructor.panesForRestrictions,
(isRadio ? sectionComponents : [])
);
this.processCompatible(
this.props.allComponents,
sectionComponents,
this.constructor.panesForRestrictions,
isRadio ? sectionComponents : []
);
return ( return (
React.createElement((isRadio ? ComponentRadioGroup : ComponentCheckboxGroup), { React.createElement((isRadio ? ComponentRadioGroup : ComponentCheckboxGroup), {
groupName: type, groupName: type,
@ -468,7 +504,11 @@ var Storage = React.createClass({
}, },
render() { render() {
this.processRestrictions(this.components, this.constructor.panesForRestrictions); this.processRestrictions(this.components, this.constructor.panesForRestrictions);
this.processCompatible(this.props.allComponents, this.components, this.constructor.panesForRestrictions); this.processCompatible(
this.props.allComponents,
this.components,
this.constructor.panesForRestrictions
);
return ( return (
<div className='wizard-storage-pane'> <div className='wizard-storage-pane'>
<div className='row'> <div className='row'>
@ -506,7 +546,11 @@ var AdditionalServices = React.createClass({
}, },
render() { render() {
this.processRestrictions(this.components, this.constructor.panesForRestrictions); this.processRestrictions(this.components, this.constructor.panesForRestrictions);
this.processCompatible(this.props.allComponents, this.components, this.constructor.panesForRestrictions); this.processCompatible(
this.props.allComponents,
this.components,
this.constructor.panesForRestrictions
);
return ( return (
<div className='wizard-compute-pane'> <div className='wizard-compute-pane'>
<ComponentCheckboxGroup <ComponentCheckboxGroup
@ -591,7 +635,8 @@ var CreateClusterWizard = React.createClass({
}, },
updateState(nextState) { updateState(nextState) {
var numberOfPanes = this.getEnabledPanes().length; var numberOfPanes = this.getEnabledPanes().length;
var nextActivePaneIndex = _.isNumber(nextState.activePaneIndex) ? nextState.activePaneIndex : this.state.activePaneIndex; var nextActivePaneIndex = _.isNumber(nextState.activePaneIndex) ? nextState.activePaneIndex :
this.state.activePaneIndex;
var pane = clusterWizardPanes[nextActivePaneIndex]; var pane = clusterWizardPanes[nextActivePaneIndex];
var paneHasErrors = _.isFunction(pane.hasErrors) ? pane.hasErrors(this.wizard) : false; var paneHasErrors = _.isFunction(pane.hasErrors) ? pane.hasErrors(this.wizard) : false;
@ -714,7 +759,10 @@ var CreateClusterWizard = React.createClass({
break; break;
default: default:
maxAvailablePaneIndex = this.state.activePaneIndex; maxAvailablePaneIndex = this.state.activePaneIndex;
var panesToRestore = this.getListOfTypesToRestore(this.state.activePaneIndex, this.state.maxAvailablePaneIndex); var panesToRestore = this.getListOfTypesToRestore(
this.state.activePaneIndex,
this.state.maxAvailablePaneIndex
);
if (panesToRestore.length > 0) { if (panesToRestore.length > 0) {
this.components.restoreDefaultValues(panesToRestore); this.components.restoreDefaultValues(panesToRestore);
} }
@ -790,11 +838,17 @@ var CreateClusterWizard = React.createClass({
var actionInProgress = this.state.actionInProgress; var actionInProgress = this.state.actionInProgress;
return ( return (
<div className='wizard-footer'> <div className='wizard-footer'>
<button className={utils.classNames('btn btn-default pull-left', {disabled: actionInProgress})} data-dismiss='modal'> <button
className={utils.classNames('btn btn-default pull-left', {disabled: actionInProgress})}
data-dismiss='modal'
>
{i18n('common.cancel_button')} {i18n('common.cancel_button')}
</button> </button>
<button <button
className={utils.classNames('btn btn-default prev-pane-btn', {disabled: !this.state.previousEnabled || actionInProgress})} className={utils.classNames(
'btn btn-default prev-pane-btn',
{disabled: !this.state.previousEnabled || actionInProgress}
)}
onClick={this.prevPane} onClick={this.prevPane}
> >
<i className='glyphicon glyphicon-arrow-left' aria-hidden='true'></i> <i className='glyphicon glyphicon-arrow-left' aria-hidden='true'></i>
@ -803,7 +857,10 @@ var CreateClusterWizard = React.createClass({
</button> </button>
{this.state.nextVisible && {this.state.nextVisible &&
<button <button
className={utils.classNames('btn btn-default btn-success next-pane-btn', {disabled: !this.state.nextEnabled || actionInProgress})} className={utils.classNames(
'btn btn-default btn-success next-pane-btn',
{disabled: !this.state.nextEnabled || actionInProgress}
)}
onClick={this.nextPane} onClick={this.nextPane}
> >
<span>{i18n('dialog.create_cluster_wizard.next')}</span> <span>{i18n('dialog.create_cluster_wizard.next')}</span>
@ -813,7 +870,10 @@ var CreateClusterWizard = React.createClass({
} }
{this.state.createVisible && {this.state.createVisible &&
<button <button
className={utils.classNames('btn btn-default btn-success finish-btn', {disabled: actionInProgress})} className={utils.classNames(
'btn btn-default btn-success finish-btn',
{disabled: actionInProgress}
)}
onClick={this.saveCluster} onClick={this.saveCluster}
autoFocus autoFocus
> >

View File

@ -22,7 +22,10 @@ module.exports = {
}, },
{test: /\/expression\/parser\.js$/, loader: 'exports?parser'}, {test: /\/expression\/parser\.js$/, loader: 'exports?parser'},
{test: require.resolve('jquery'), loader: 'expose?jQuery!expose?$'}, {test: require.resolve('jquery'), loader: 'expose?jQuery!expose?$'},
{test: /\/sinon\.js$/, loader: 'imports?this=>window,define=>false,exports=>false,module=>false,require=>false'}, {
test: /\/sinon\.js$/,
loader: 'imports?this=>window,define=>false,exports=>false,module=>false,require=>false'
},
{test: /\.css$/, loader: 'style!css!postcss'}, {test: /\.css$/, loader: 'style!css!postcss'},
{test: /\.less$/, loader: 'style!css!postcss!less'}, {test: /\.less$/, loader: 'style!css!postcss!less'},
{test: /\.html$/, loader: 'raw'}, {test: /\.html$/, loader: 'raw'},