Use 2 spaces instead of 4 for indentation of JS code
Implements: blueprint converge-to-eslint-config-openstack Change-Id: I91c258706dea064d25e78efd39a9df2d4d4de3ed
This commit is contained in:
parent
87d0779a55
commit
c45545695b
|
@ -32,7 +32,6 @@
|
|||
complexity: 0
|
||||
eqeqeq: 0
|
||||
no-script-url: 0
|
||||
indent: [0, 2, {SwitchCase: 1}]
|
||||
one-var: [0, {uninitialized: always, initialized: never}]
|
||||
max-len: [0, 120]
|
||||
|
||||
|
@ -81,7 +80,8 @@
|
|||
react/jsx-boolean-value: [2, never]
|
||||
react/jsx-closing-bracket-location: [2, {nonEmpty: false, selfClosing: line-aligned}]
|
||||
react/jsx-curly-spacing: [2, never]
|
||||
react/jsx-indent-props: [2, 4]
|
||||
react/jsx-indent: [2, 2]
|
||||
react/jsx-indent-props: [2, 2]
|
||||
react/jsx-key: 2
|
||||
react/jsx-no-duplicate-props: 2
|
||||
react/jsx-no-literals: 0
|
||||
|
|
44
gulp/i18n.js
44
gulp/i18n.js
|
@ -5,33 +5,33 @@ var gutil = require('gulp-util');
|
|||
var _ = require('lodash');
|
||||
|
||||
function validate(translations, locales) {
|
||||
var processedTranslations = {};
|
||||
var baseLocale = 'en-US';
|
||||
var existingLocales = _.keys(translations);
|
||||
if (!locales) locales = existingLocales;
|
||||
var processedTranslations = {};
|
||||
var baseLocale = 'en-US';
|
||||
var existingLocales = _.keys(translations);
|
||||
if (!locales) locales = existingLocales;
|
||||
|
||||
function processTranslations(translations) {
|
||||
function processPiece(base, piece) {
|
||||
return _.map(piece, function(value, key) {
|
||||
var localBase = base ? base + '.' + key : key;
|
||||
return _.isPlainObject(value) ? processPiece(localBase, value) : localBase;
|
||||
});
|
||||
}
|
||||
return _.uniq(_.flatten(processPiece(null, translations.translation), true)).sort();
|
||||
function processTranslations(translations) {
|
||||
function processPiece(base, piece) {
|
||||
return _.map(piece, function(value, key) {
|
||||
var localBase = base ? base + '.' + key : key;
|
||||
return _.isPlainObject(value) ? processPiece(localBase, value) : localBase;
|
||||
});
|
||||
}
|
||||
return _.uniq(_.flatten(processPiece(null, translations.translation), true)).sort();
|
||||
}
|
||||
|
||||
_.each(_.union(locales, [baseLocale]), function(locale) {
|
||||
processedTranslations[locale] = processTranslations(translations[locale]);
|
||||
});
|
||||
_.each(_.union(locales, [baseLocale]), function(locale) {
|
||||
processedTranslations[locale] = processTranslations(translations[locale]);
|
||||
});
|
||||
|
||||
function compareLocales(locale1, locale2) {
|
||||
return _.without.apply(null, [processedTranslations[locale1]].concat(processedTranslations[locale2]));
|
||||
}
|
||||
function compareLocales(locale1, locale2) {
|
||||
return _.without.apply(null, [processedTranslations[locale1]].concat(processedTranslations[locale2]));
|
||||
}
|
||||
|
||||
_.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 missing in', baseLocale, ':\n') + compareLocales(locale, baseLocale).join('\n'));
|
||||
});
|
||||
_.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 missing in', baseLocale, ':\n') + compareLocales(locale, baseLocale).join('\n'));
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {validate: validate};
|
||||
|
|
466
gulpfile.js
466
gulpfile.js
|
@ -14,7 +14,9 @@
|
|||
* under the License.
|
||||
**/
|
||||
|
||||
/*eslint-disable strict*/
|
||||
/*eslint-env node*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var argv = require('minimist')(process.argv.slice(2));
|
||||
|
||||
|
@ -37,307 +39,293 @@ var jison = require('gulp-jison');
|
|||
|
||||
var validateTranslations = require('./gulp/i18n').validate;
|
||||
gulp.task('i18n:validate', function() {
|
||||
var tranlations = JSON.parse(fs.readFileSync('static/translations/core.json'));
|
||||
var locales = argv.locales ? argv.locales.split(',') : null;
|
||||
validateTranslations(tranlations, locales);
|
||||
var tranlations = JSON.parse(fs.readFileSync('static/translations/core.json'));
|
||||
var locales = argv.locales ? argv.locales.split(',') : null;
|
||||
validateTranslations(tranlations, locales);
|
||||
});
|
||||
|
||||
var seleniumProcess = null;
|
||||
function shutdownSelenium() {
|
||||
if (seleniumProcess) {
|
||||
seleniumProcess.kill();
|
||||
seleniumProcess = null;
|
||||
}
|
||||
if (seleniumProcess) {
|
||||
seleniumProcess.kill();
|
||||
seleniumProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
var SELENIUM_VERSION = '2.45.0';
|
||||
var SELENIUM_DRIVERS = {chrome: {version: '2.20'}};
|
||||
|
||||
gulp.task('selenium:fetch', function(cb) {
|
||||
var selenium = require('selenium-standalone');
|
||||
selenium.install({
|
||||
version: process.env.SELENIUM_VERSION || SELENIUM_VERSION,
|
||||
dirvers: SELENIUM_DRIVERS
|
||||
}, cb);
|
||||
var selenium = require('selenium-standalone');
|
||||
selenium.install({
|
||||
version: process.env.SELENIUM_VERSION || SELENIUM_VERSION,
|
||||
dirvers: SELENIUM_DRIVERS
|
||||
}, cb);
|
||||
});
|
||||
|
||||
gulp.task('selenium', ['selenium:fetch'], function(cb) {
|
||||
var selenium = require('selenium-standalone');
|
||||
var port = process.env.SELENIUM_SERVER_PORT || 4444;
|
||||
selenium.start(
|
||||
{
|
||||
version: process.env.SELENIUM_VERSION || SELENIUM_VERSION,
|
||||
dirvers: SELENIUM_DRIVERS,
|
||||
seleniumArgs: ['--port', port],
|
||||
spawnOptions: {stdio: 'pipe'}
|
||||
},
|
||||
function(err, child) {
|
||||
if (err) throw err;
|
||||
child.on('exit', function() {
|
||||
if (seleniumProcess) {
|
||||
gutil.log(gutil.colors.yellow('Selenium process died unexpectedly. Probably port', port, 'is already in use.'));
|
||||
}
|
||||
});
|
||||
['exit', 'uncaughtException', 'SIGTERM', 'SIGINT'].forEach(function(event) {
|
||||
process.on(event, shutdownSelenium);
|
||||
});
|
||||
seleniumProcess = child;
|
||||
cb();
|
||||
var selenium = require('selenium-standalone');
|
||||
var port = process.env.SELENIUM_SERVER_PORT || 4444;
|
||||
selenium.start(
|
||||
{
|
||||
version: process.env.SELENIUM_VERSION || SELENIUM_VERSION,
|
||||
dirvers: SELENIUM_DRIVERS,
|
||||
seleniumArgs: ['--port', port],
|
||||
spawnOptions: {stdio: 'pipe'}
|
||||
},
|
||||
function(err, child) {
|
||||
if (err) throw err;
|
||||
child.on('exit', function() {
|
||||
if (seleniumProcess) {
|
||||
gutil.log(gutil.colors.yellow('Selenium process died unexpectedly. Probably port', port, 'is already in use.'));
|
||||
}
|
||||
);
|
||||
});
|
||||
['exit', 'uncaughtException', 'SIGTERM', 'SIGINT'].forEach(function(event) {
|
||||
process.on(event, shutdownSelenium);
|
||||
});
|
||||
seleniumProcess = child;
|
||||
cb();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
gulp.task('karma', function(cb) {
|
||||
var Server = require('karma').Server;
|
||||
new Server({
|
||||
configFile: __dirname + '/karma.config.js',
|
||||
browsers: [argv.browser || process.env.BROWSER || 'firefox']
|
||||
}, cb).start();
|
||||
var Server = require('karma').Server;
|
||||
new Server({
|
||||
configFile: __dirname + '/karma.config.js',
|
||||
browsers: [argv.browser || process.env.BROWSER || 'firefox']
|
||||
}, cb).start();
|
||||
});
|
||||
|
||||
function runIntern(params) {
|
||||
return function() {
|
||||
var baseDir = 'static';
|
||||
var runner = './node_modules/.bin/intern-runner';
|
||||
var browser = argv.browser || process.env.BROWSER || 'firefox';
|
||||
var options = [['config', 'tests/functional/config/intern-' + browser + '.js']];
|
||||
var suiteOptions = [];
|
||||
['suites', 'functionalSuites'].forEach(function(suiteType) {
|
||||
if (params[suiteType]) {
|
||||
var suiteFiles = glob.sync(path.relative(baseDir, params[suiteType]), {cwd: baseDir});
|
||||
suiteOptions = suiteOptions.concat(suiteFiles.map(function(suiteFile) {
|
||||
return [suiteType, suiteFile.replace(/\.js$/, '')];
|
||||
}));
|
||||
}
|
||||
});
|
||||
if (!suiteOptions.length) {
|
||||
throw new Error('No matching suites');
|
||||
}
|
||||
options = options.concat(suiteOptions);
|
||||
var command = [path.relative(baseDir, runner)].concat(options.map(function(o) {
|
||||
return o.join('=');
|
||||
})).join(' ');
|
||||
gutil.log('Executing', command);
|
||||
return shell.task(command, {cwd: baseDir})();
|
||||
};
|
||||
return function() {
|
||||
var baseDir = 'static';
|
||||
var runner = './node_modules/.bin/intern-runner';
|
||||
var browser = argv.browser || process.env.BROWSER || 'firefox';
|
||||
var options = [['config', 'tests/functional/config/intern-' + browser + '.js']];
|
||||
var suiteOptions = [];
|
||||
['suites', 'functionalSuites'].forEach(function(suiteType) {
|
||||
if (params[suiteType]) {
|
||||
var suiteFiles = glob.sync(path.relative(baseDir, params[suiteType]), {cwd: baseDir});
|
||||
suiteOptions = suiteOptions.concat(suiteFiles.map(function(suiteFile) {
|
||||
return [suiteType, suiteFile.replace(/\.js$/, '')];
|
||||
}));
|
||||
}
|
||||
});
|
||||
if (!suiteOptions.length) {
|
||||
throw new Error('No matching suites');
|
||||
}
|
||||
options = options.concat(suiteOptions);
|
||||
var command = [path.relative(baseDir, runner)].concat(options.map(function(o) {
|
||||
return o.join('=');
|
||||
})).join(' ');
|
||||
gutil.log('Executing', command);
|
||||
return shell.task(command, {cwd: baseDir})();
|
||||
};
|
||||
}
|
||||
|
||||
gulp.task('intern:functional', runIntern({functionalSuites: argv.suites || 'static/tests/functional/**/test_*.js'}));
|
||||
|
||||
gulp.task('unit-tests', function(cb) {
|
||||
runSequence('selenium', 'karma', function(err) {
|
||||
shutdownSelenium();
|
||||
cb(err);
|
||||
});
|
||||
runSequence('selenium', 'karma', function(err) {
|
||||
shutdownSelenium();
|
||||
cb(err);
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task('functional-tests', function(cb) {
|
||||
runSequence('selenium', 'intern:functional', function(err) {
|
||||
shutdownSelenium();
|
||||
cb(err);
|
||||
});
|
||||
runSequence('selenium', 'intern:functional', function(err) {
|
||||
shutdownSelenium();
|
||||
cb(err);
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task('jison', function() {
|
||||
return gulp.src('static/expression/parser.jison')
|
||||
.pipe(jison({moduleType: 'js'}))
|
||||
.pipe(gulp.dest('static/expression/'));
|
||||
return gulp.src('static/expression/parser.jison')
|
||||
.pipe(jison({moduleType: 'js'}))
|
||||
.pipe(gulp.dest('static/expression/'));
|
||||
});
|
||||
|
||||
gulp.task('license', function(cb) {
|
||||
require('nlf').find({production: true, depth: 0}, function(err, data) {
|
||||
if (err) cb(err);
|
||||
// https://github.com/openstack/requirements#for-new-requirements
|
||||
// Is the library license compatible?
|
||||
// Preferably Apache2, BSD, MIT licensed. LGPL is ok.
|
||||
var licenseRegexp = /(Apache.*?2)|\bBSD\b|\bMIT\b|\bLGPL\b/i;
|
||||
require('nlf').find({production: true, depth: 0}, function(err, data) {
|
||||
if (err) cb(err);
|
||||
// https://github.com/openstack/requirements#for-new-requirements
|
||||
// Is the library license compatible?
|
||||
// Preferably Apache2, BSD, MIT licensed. LGPL is ok.
|
||||
var licenseRegexp = /(Apache.*?2)|\bBSD\b|\bMIT\b|\bLGPL\b/i;
|
||||
|
||||
var errors = [];
|
||||
_.each(data, function(moduleInfo) {
|
||||
var name = moduleInfo.name;
|
||||
var version = moduleInfo.version;
|
||||
var license = _.pluck(moduleInfo.licenseSources.package.sources, 'license').join(', ') || 'unknown';
|
||||
var licenseOk = license.match(licenseRegexp);
|
||||
if (!licenseOk) errors.push({libraryName: name, license: license});
|
||||
gutil.log(
|
||||
gutil.colors.cyan(name),
|
||||
gutil.colors.yellow(version),
|
||||
gutil.colors[licenseOk ? 'green' : 'red'](license)
|
||||
);
|
||||
});
|
||||
if (errors.length) {
|
||||
_.each(errors, function(error) {
|
||||
gutil.log(gutil.colors.red(error.libraryName, 'has', error.license, 'license'));
|
||||
});
|
||||
cb('Issues with licenses found');
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
var errors = [];
|
||||
_.each(data, function(moduleInfo) {
|
||||
var name = moduleInfo.name;
|
||||
var version = moduleInfo.version;
|
||||
var license = _.pluck(moduleInfo.licenseSources.package.sources, 'license').join(', ') || 'unknown';
|
||||
var licenseOk = license.match(licenseRegexp);
|
||||
if (!licenseOk) errors.push({libraryName: name, license: license});
|
||||
gutil.log(
|
||||
gutil.colors.cyan(name),
|
||||
gutil.colors.yellow(version),
|
||||
gutil.colors[licenseOk ? 'green' : 'red'](license)
|
||||
);
|
||||
});
|
||||
if (errors.length) {
|
||||
_.each(errors, function(error) {
|
||||
gutil.log(gutil.colors.red(error.libraryName, 'has', error.license, 'license'));
|
||||
});
|
||||
cb('Issues with licenses found');
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var jsFiles = [
|
||||
'static/**/*.js',
|
||||
'!static/build/**',
|
||||
'!static/vendor/**',
|
||||
'!static/expression/parser.js',
|
||||
'static/tests/**/*.js'
|
||||
'static/**/*.js',
|
||||
'!static/build/**',
|
||||
'!static/vendor/**',
|
||||
'!static/expression/parser.js',
|
||||
'static/tests/**/*.js'
|
||||
];
|
||||
var styleFiles = [
|
||||
'static/**/*.less',
|
||||
'static/**/*.css',
|
||||
'!static/build/**',
|
||||
'!static/vendor/**'
|
||||
'static/**/*.less',
|
||||
'static/**/*.css',
|
||||
'!static/build/**',
|
||||
'!static/vendor/**'
|
||||
];
|
||||
|
||||
gulp.task('eslint', function() {
|
||||
var eslint = require('gulp-eslint');
|
||||
return gulp.src(jsFiles)
|
||||
.pipe(eslint())
|
||||
.pipe(eslint.format())
|
||||
.pipe(eslint.failAfterError());
|
||||
});
|
||||
|
||||
var lintspacesConfig = {
|
||||
showValid: true,
|
||||
newline: true,
|
||||
trailingspaces: true,
|
||||
indentation: 'spaces'
|
||||
};
|
||||
|
||||
gulp.task('lintspaces:js', function() {
|
||||
var lintspaces = require('gulp-lintspaces');
|
||||
return gulp.src(jsFiles)
|
||||
.pipe(lintspaces(_.extend({}, lintspacesConfig, {
|
||||
ignores: ['js-comments'],
|
||||
spaces: 4
|
||||
})))
|
||||
.pipe(lintspaces.reporter());
|
||||
var eslint = require('gulp-eslint');
|
||||
return gulp.src(jsFiles)
|
||||
.pipe(eslint())
|
||||
.pipe(eslint.format())
|
||||
.pipe(eslint.failAfterError());
|
||||
});
|
||||
|
||||
gulp.task('lintspaces:styles', function() {
|
||||
var lintspaces = require('gulp-lintspaces');
|
||||
return gulp.src(styleFiles)
|
||||
.pipe(lintspaces(_.extend({}, lintspacesConfig, {
|
||||
ignores: ['js-comments'],
|
||||
spaces: 2,
|
||||
newlineMaximum: 2
|
||||
})))
|
||||
.pipe(lintspaces.reporter());
|
||||
var lintspaces = require('gulp-lintspaces');
|
||||
return gulp.src(styleFiles)
|
||||
.pipe(lintspaces({
|
||||
showValid: true,
|
||||
newline: true,
|
||||
trailingspaces: true,
|
||||
indentation: 'spaces',
|
||||
ignores: ['js-comments'],
|
||||
spaces: 2,
|
||||
newlineMaximum: 2
|
||||
}))
|
||||
.pipe(lintspaces.reporter());
|
||||
});
|
||||
|
||||
gulp.task('lint', [
|
||||
'eslint',
|
||||
'lintspaces:js',
|
||||
'lintspaces:styles'
|
||||
'eslint',
|
||||
'lintspaces:styles'
|
||||
]);
|
||||
|
||||
var WEBPACK_STATS_OPTIONS = {
|
||||
colors: true,
|
||||
hash: false,
|
||||
version: false,
|
||||
assets: false,
|
||||
chunks: false
|
||||
colors: true,
|
||||
hash: false,
|
||||
version: false,
|
||||
assets: false,
|
||||
chunks: false
|
||||
};
|
||||
|
||||
gulp.task('dev-server', function() {
|
||||
var devServerHost = argv['dev-server-host'] || '127.0.0.1';
|
||||
var devServerPort = argv['dev-server-port'] || 8080;
|
||||
var devServerUrl = 'http://' + devServerHost + ':' + devServerPort;
|
||||
var nailgunHost = argv['nailgun-host'] || '127.0.0.1';
|
||||
var nailgunPort = argv['nailgun-port'] || 8000;
|
||||
var nailgunUrl = 'http://' + nailgunHost + ':' + nailgunPort;
|
||||
var hotReload = !argv['no-hot'];
|
||||
var devServerHost = argv['dev-server-host'] || '127.0.0.1';
|
||||
var devServerPort = argv['dev-server-port'] || 8080;
|
||||
var devServerUrl = 'http://' + devServerHost + ':' + devServerPort;
|
||||
var nailgunHost = argv['nailgun-host'] || '127.0.0.1';
|
||||
var nailgunPort = argv['nailgun-port'] || 8000;
|
||||
var nailgunUrl = 'http://' + nailgunHost + ':' + nailgunPort;
|
||||
var hotReload = !argv['no-hot'];
|
||||
|
||||
var config = require('./webpack.config');
|
||||
config.entry.push('webpack-dev-server/client?' + devServerUrl);
|
||||
if (hotReload) {
|
||||
config.entry.push('webpack/hot/dev-server');
|
||||
config.plugins.push(new webpack.HotModuleReplacementPlugin());
|
||||
config.plugins.push(new webpack.NoErrorsPlugin());
|
||||
}
|
||||
var config = require('./webpack.config');
|
||||
config.entry.push('webpack-dev-server/client?' + devServerUrl);
|
||||
if (hotReload) {
|
||||
config.entry.push('webpack/hot/dev-server');
|
||||
config.plugins.push(new webpack.HotModuleReplacementPlugin());
|
||||
config.plugins.push(new webpack.NoErrorsPlugin());
|
||||
}
|
||||
|
||||
var WebpackDevServer = require('webpack-dev-server');
|
||||
var options = {
|
||||
hot: hotReload,
|
||||
stats: WEBPACK_STATS_OPTIONS,
|
||||
proxy: [
|
||||
{path: '/', target: devServerUrl, rewrite: function(req) {
|
||||
req.url = '/static/index.html';
|
||||
}},
|
||||
{path: /^\/(?!static\/).+/, target: nailgunUrl}
|
||||
]
|
||||
};
|
||||
_.extend(options, config.output);
|
||||
new WebpackDevServer(webpack(config), options).listen(devServerPort, devServerHost, function(err) {
|
||||
if (err) throw err;
|
||||
gutil.log('Development server started at ' + devServerUrl);
|
||||
});
|
||||
var WebpackDevServer = require('webpack-dev-server');
|
||||
var options = {
|
||||
hot: hotReload,
|
||||
stats: WEBPACK_STATS_OPTIONS,
|
||||
proxy: [
|
||||
{path: '/', target: devServerUrl, rewrite: function(req) {
|
||||
req.url = '/static/index.html';
|
||||
}},
|
||||
{path: /^\/(?!static\/).+/, target: nailgunUrl}
|
||||
]
|
||||
};
|
||||
_.extend(options, config.output);
|
||||
new WebpackDevServer(webpack(config), options).listen(devServerPort, devServerHost, function(err) {
|
||||
if (err) throw err;
|
||||
gutil.log('Development server started at ' + devServerUrl);
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task('build', function(cb) {
|
||||
var sourceDir = path.resolve('static');
|
||||
var targetDir = argv['static-dir'] ? path.resolve(argv['static-dir']) : sourceDir;
|
||||
var sourceDir = path.resolve('static');
|
||||
var targetDir = argv['static-dir'] ? path.resolve(argv['static-dir']) : sourceDir;
|
||||
|
||||
var config = require('./webpack.config');
|
||||
config.output.path = path.join(targetDir, 'build');
|
||||
if (!argv.dev) {
|
||||
config.plugins.push(
|
||||
new webpack.DefinePlugin({'process.env': {NODE_ENV: '"production"'}})
|
||||
);
|
||||
var config = require('./webpack.config');
|
||||
config.output.path = path.join(targetDir, 'build');
|
||||
if (!argv.dev) {
|
||||
config.plugins.push(
|
||||
new webpack.DefinePlugin({'process.env': {NODE_ENV: '"production"'}})
|
||||
);
|
||||
}
|
||||
if (argv['extra-entries']) {
|
||||
config.entry = config.entry.concat(argv['extra-entries'].split(','));
|
||||
}
|
||||
|
||||
if (argv.uglify !== false) {
|
||||
config.devtool = 'source-map';
|
||||
config.plugins.push(
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
sourceMap: true,
|
||||
mangle: false,
|
||||
compress: {warnings: false}
|
||||
})
|
||||
);
|
||||
}
|
||||
if (argv.sourcemaps === false) {
|
||||
delete config.devtool;
|
||||
}
|
||||
if (argv.watch) {
|
||||
config.watch = true;
|
||||
}
|
||||
|
||||
rimraf.sync(config.output.path);
|
||||
|
||||
var compiler = webpack(config);
|
||||
var run = config.watch ? compiler.watch.bind(compiler, config.watchOptions) : compiler.run.bind(compiler);
|
||||
|
||||
run(function(err, stats) {
|
||||
if (err) return cb(err);
|
||||
|
||||
gutil.log(stats.toString(WEBPACK_STATS_OPTIONS));
|
||||
|
||||
if (stats.hasErrors()) return cb('Build failed');
|
||||
|
||||
if (targetDir != sourceDir) {
|
||||
var indexFilter = filter('index.html');
|
||||
gulp
|
||||
.src([
|
||||
'index.html',
|
||||
'favicon.ico',
|
||||
'img/loader-bg.svg',
|
||||
'img/loader-logo.svg',
|
||||
'styles/layout.css'
|
||||
], {cwd: sourceDir, base: sourceDir})
|
||||
.pipe(indexFilter)
|
||||
.pipe(replace('__CACHE_BUST__', Date.now()))
|
||||
.pipe(indexFilter.restore())
|
||||
.pipe(gulp.dest(targetDir))
|
||||
.on('end', cb);
|
||||
} else if (!config.watch) {
|
||||
cb();
|
||||
}
|
||||
if (argv['extra-entries']) {
|
||||
config.entry = config.entry.concat(argv['extra-entries'].split(','));
|
||||
}
|
||||
|
||||
if (argv.uglify !== false) {
|
||||
config.devtool = 'source-map';
|
||||
config.plugins.push(
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
sourceMap: true,
|
||||
mangle: false,
|
||||
compress: {warnings: false}
|
||||
})
|
||||
);
|
||||
}
|
||||
if (argv.sourcemaps === false) {
|
||||
delete config.devtool;
|
||||
}
|
||||
if (argv.watch) {
|
||||
config.watch = true;
|
||||
}
|
||||
|
||||
rimraf.sync(config.output.path);
|
||||
|
||||
var compiler = webpack(config);
|
||||
var run = config.watch ? compiler.watch.bind(compiler, config.watchOptions) : compiler.run.bind(compiler);
|
||||
|
||||
run(function(err, stats) {
|
||||
if (err) return cb(err);
|
||||
|
||||
gutil.log(stats.toString(WEBPACK_STATS_OPTIONS));
|
||||
|
||||
if (stats.hasErrors()) return cb('Build failed');
|
||||
|
||||
if (targetDir != sourceDir) {
|
||||
var indexFilter = filter('index.html');
|
||||
gulp
|
||||
.src([
|
||||
'index.html',
|
||||
'favicon.ico',
|
||||
'img/loader-bg.svg',
|
||||
'img/loader-logo.svg',
|
||||
'styles/layout.css'
|
||||
], {cwd: sourceDir, base: sourceDir})
|
||||
.pipe(indexFilter)
|
||||
.pipe(replace('__CACHE_BUST__', Date.now()))
|
||||
.pipe(indexFilter.restore())
|
||||
.pipe(gulp.dest(targetDir))
|
||||
.on('end', cb);
|
||||
} else if (!config.watch) {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task('default', ['build']);
|
||||
|
|
434
static/app.js
434
static/app.js
|
@ -38,242 +38,242 @@ import 'backbone.routefilter';
|
|||
import 'bootstrap';
|
||||
import './styles/main.less';
|
||||
|
||||
class Router extends Backbone.Router {
|
||||
routes() {
|
||||
return {
|
||||
login: 'login',
|
||||
logout: 'logout',
|
||||
welcome: 'welcome',
|
||||
clusters: 'listClusters',
|
||||
'cluster/:id(/:tab)(/:opt1)(/:opt2)': 'showCluster',
|
||||
equipment: 'showEquipmentPage',
|
||||
releases: 'listReleases',
|
||||
plugins: 'listPlugins',
|
||||
notifications: 'showNotifications',
|
||||
support: 'showSupportPage',
|
||||
capacity: 'showCapacityPage',
|
||||
'*default': 'default'
|
||||
};
|
||||
}
|
||||
class Router extends Backbone.Router {
|
||||
routes() {
|
||||
return {
|
||||
login: 'login',
|
||||
logout: 'logout',
|
||||
welcome: 'welcome',
|
||||
clusters: 'listClusters',
|
||||
'cluster/:id(/:tab)(/:opt1)(/:opt2)': 'showCluster',
|
||||
equipment: 'showEquipmentPage',
|
||||
releases: 'listReleases',
|
||||
plugins: 'listPlugins',
|
||||
notifications: 'showNotifications',
|
||||
support: 'showSupportPage',
|
||||
capacity: 'showCapacityPage',
|
||||
'*default': 'default'
|
||||
};
|
||||
}
|
||||
|
||||
// pre-route hook
|
||||
before(currentRouteName) {
|
||||
var currentUrl = Backbone.history.getHash();
|
||||
var preventRouting = false;
|
||||
// remove trailing slash
|
||||
if (_.endsWith(currentUrl, '/')) {
|
||||
this.navigate(currentUrl.substr(0, currentUrl.length - 1), {trigger: true, replace: true});
|
||||
preventRouting = true;
|
||||
}
|
||||
// handle special routes
|
||||
if (!preventRouting) {
|
||||
var specialRoutes = [
|
||||
{name: 'login', condition: () => {
|
||||
var result = app.version.get('auth_required') && !app.user.get('authenticated');
|
||||
if (result && currentUrl != 'login' && currentUrl != 'logout') this.returnUrl = currentUrl;
|
||||
return result;
|
||||
}},
|
||||
{name: 'welcome', condition: (previousUrl) => {
|
||||
return previousUrl != 'logout' && !app.fuelSettings.get('statistics.user_choice_saved.value');
|
||||
}}
|
||||
];
|
||||
_.each(specialRoutes, (route) => {
|
||||
if (route.condition(currentRouteName)) {
|
||||
if (currentRouteName != route.name) {
|
||||
preventRouting = true;
|
||||
this.navigate(route.name, {trigger: true, replace: true});
|
||||
}
|
||||
return false;
|
||||
} else if (currentRouteName == route.name) {
|
||||
preventRouting = true;
|
||||
this.navigate('', {trigger: true});
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
return !preventRouting;
|
||||
// pre-route hook
|
||||
before(currentRouteName) {
|
||||
var currentUrl = Backbone.history.getHash();
|
||||
var preventRouting = false;
|
||||
// remove trailing slash
|
||||
if (_.endsWith(currentUrl, '/')) {
|
||||
this.navigate(currentUrl.substr(0, currentUrl.length - 1), {trigger: true, replace: true});
|
||||
preventRouting = true;
|
||||
}
|
||||
// handle special routes
|
||||
if (!preventRouting) {
|
||||
var specialRoutes = [
|
||||
{name: 'login', condition: () => {
|
||||
var result = app.version.get('auth_required') && !app.user.get('authenticated');
|
||||
if (result && currentUrl != 'login' && currentUrl != 'logout') this.returnUrl = currentUrl;
|
||||
return result;
|
||||
}},
|
||||
{name: 'welcome', condition: (previousUrl) => {
|
||||
return previousUrl != 'logout' && !app.fuelSettings.get('statistics.user_choice_saved.value');
|
||||
}}
|
||||
];
|
||||
_.each(specialRoutes, (route) => {
|
||||
if (route.condition(currentRouteName)) {
|
||||
if (currentRouteName != route.name) {
|
||||
preventRouting = true;
|
||||
this.navigate(route.name, {trigger: true, replace: true});
|
||||
}
|
||||
return false;
|
||||
} else if (currentRouteName == route.name) {
|
||||
preventRouting = true;
|
||||
this.navigate('', {trigger: true});
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
return !preventRouting;
|
||||
}
|
||||
|
||||
// routes
|
||||
default() {
|
||||
this.navigate('clusters', {trigger: true, replace: true});
|
||||
}
|
||||
// routes
|
||||
default() {
|
||||
this.navigate('clusters', {trigger: true, replace: true});
|
||||
}
|
||||
|
||||
login() {
|
||||
app.loadPage(LoginPage);
|
||||
}
|
||||
login() {
|
||||
app.loadPage(LoginPage);
|
||||
}
|
||||
|
||||
logout() {
|
||||
app.logout();
|
||||
}
|
||||
logout() {
|
||||
app.logout();
|
||||
}
|
||||
|
||||
welcome() {
|
||||
app.loadPage(WelcomePage);
|
||||
}
|
||||
welcome() {
|
||||
app.loadPage(WelcomePage);
|
||||
}
|
||||
|
||||
showCluster(clusterId, tab) {
|
||||
var tabs = _.pluck(ClusterPage.getTabs(), 'url');
|
||||
if (!tab || !_.contains(tabs, tab)) {
|
||||
this.navigate('cluster/' + clusterId + '/' + tabs[0], {trigger: true, replace: true});
|
||||
} else {
|
||||
app.loadPage(ClusterPage, arguments).fail(() => this.default());
|
||||
}
|
||||
}
|
||||
showCluster(clusterId, tab) {
|
||||
var tabs = _.pluck(ClusterPage.getTabs(), 'url');
|
||||
if (!tab || !_.contains(tabs, tab)) {
|
||||
this.navigate('cluster/' + clusterId + '/' + tabs[0], {trigger: true, replace: true});
|
||||
} else {
|
||||
app.loadPage(ClusterPage, arguments).fail(() => this.default());
|
||||
}
|
||||
}
|
||||
|
||||
listClusters() {
|
||||
app.loadPage(ClustersPage);
|
||||
}
|
||||
listClusters() {
|
||||
app.loadPage(ClustersPage);
|
||||
}
|
||||
|
||||
showEquipmentPage() {
|
||||
app.loadPage(EquipmentPage);
|
||||
}
|
||||
showEquipmentPage() {
|
||||
app.loadPage(EquipmentPage);
|
||||
}
|
||||
|
||||
listReleases() {
|
||||
app.loadPage(ReleasesPage);
|
||||
}
|
||||
listReleases() {
|
||||
app.loadPage(ReleasesPage);
|
||||
}
|
||||
|
||||
listPlugins() {
|
||||
app.loadPage(PluginsPage);
|
||||
}
|
||||
listPlugins() {
|
||||
app.loadPage(PluginsPage);
|
||||
}
|
||||
|
||||
showNotifications() {
|
||||
app.loadPage(NotificationsPage);
|
||||
}
|
||||
showNotifications() {
|
||||
app.loadPage(NotificationsPage);
|
||||
}
|
||||
|
||||
showSupportPage() {
|
||||
app.loadPage(SupportPage);
|
||||
}
|
||||
showSupportPage() {
|
||||
app.loadPage(SupportPage);
|
||||
}
|
||||
|
||||
showCapacityPage() {
|
||||
app.loadPage(CapacityPage);
|
||||
showCapacityPage() {
|
||||
app.loadPage(CapacityPage);
|
||||
}
|
||||
}
|
||||
|
||||
class App {
|
||||
constructor() {
|
||||
this.initialized = false;
|
||||
|
||||
// this is needed for IE, which caches requests resulting in wrong results (e.g /ostf/testruns/last/1)
|
||||
$.ajaxSetup({cache: false});
|
||||
|
||||
this.router = new Router();
|
||||
this.keystoneClient = new KeystoneClient('/keystone', {
|
||||
cacheTokenFor: 10 * 60 * 1000,
|
||||
tenant: 'admin'
|
||||
});
|
||||
this.version = new models.FuelVersion();
|
||||
this.fuelSettings = new models.FuelSettings();
|
||||
this.user = new models.User();
|
||||
this.statistics = new models.NodesStatistics();
|
||||
this.notifications = new models.Notifications();
|
||||
this.releases = new models.Releases();
|
||||
this.nodeNetworkGroups = new models.NodeNetworkGroups();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.initialized = true;
|
||||
this.mountNode = $('#main-container');
|
||||
|
||||
document.title = i18n('common.title');
|
||||
|
||||
return this.version.fetch()
|
||||
.then(() => {
|
||||
this.user.set({authenticated: !this.version.get('auth_required')});
|
||||
this.patchBackboneSync();
|
||||
if (this.version.get('auth_required')) {
|
||||
_.extend(this.keystoneClient, this.user.pick('token'));
|
||||
return this.keystoneClient.authenticate()
|
||||
.done(() => this.user.set({authenticated: true}));
|
||||
}
|
||||
return $.Deferred().resolve();
|
||||
})
|
||||
.then(() => {
|
||||
return $.when(
|
||||
this.fuelSettings.fetch(),
|
||||
this.nodeNetworkGroups.fetch()
|
||||
);
|
||||
})
|
||||
.then(null, () => {
|
||||
if (this.version.get('auth_required') && !this.user.get('authenticated')) {
|
||||
return $.Deferred().resolve();
|
||||
} else {
|
||||
this.mountNode.empty();
|
||||
NailgunUnavailabilityDialog.show({}, {preventDuplicate: true});
|
||||
}
|
||||
})
|
||||
.then(() => Backbone.history.start());
|
||||
}
|
||||
|
||||
renderLayout() {
|
||||
var wrappedRootComponent = ReactDOM.render(
|
||||
React.createElement(
|
||||
RootComponent,
|
||||
_.pick(this, 'version', 'user', 'fuelSettings', 'statistics', 'notifications')
|
||||
),
|
||||
this.mountNode[0]
|
||||
);
|
||||
// RootComponent is wrapped with React-DnD, extracting link to it using ref
|
||||
this.rootComponent = wrappedRootComponent.refs.child;
|
||||
}
|
||||
|
||||
loadPage(Page, options = []) {
|
||||
return (Page.fetchData ? Page.fetchData(...options) : $.Deferred().resolve()).done((pageOptions) => {
|
||||
if (!this.rootComponent) this.renderLayout();
|
||||
this.setPage(Page, pageOptions);
|
||||
});
|
||||
}
|
||||
|
||||
setPage(Page, options) {
|
||||
this.page = this.rootComponent.setPage(Page, options);
|
||||
}
|
||||
|
||||
navigate(...args) {
|
||||
return this.router.navigate(...args);
|
||||
}
|
||||
|
||||
logout() {
|
||||
if (this.user.get('authenticated') && this.version.get('auth_required')) {
|
||||
this.user.set('authenticated', false);
|
||||
this.user.unset('username');
|
||||
this.user.unset('token');
|
||||
|
||||
this.keystoneClient.deauthenticate();
|
||||
}
|
||||
|
||||
class App {
|
||||
constructor() {
|
||||
this.initialized = false;
|
||||
_.defer(() => this.navigate('login', {trigger: true, replace: true}));
|
||||
}
|
||||
|
||||
// this is needed for IE, which caches requests resulting in wrong results (e.g /ostf/testruns/last/1)
|
||||
$.ajaxSetup({cache: false});
|
||||
|
||||
this.router = new Router();
|
||||
this.keystoneClient = new KeystoneClient('/keystone', {
|
||||
cacheTokenFor: 10 * 60 * 1000,
|
||||
tenant: 'admin'
|
||||
});
|
||||
this.version = new models.FuelVersion();
|
||||
this.fuelSettings = new models.FuelSettings();
|
||||
this.user = new models.User();
|
||||
this.statistics = new models.NodesStatistics();
|
||||
this.notifications = new models.Notifications();
|
||||
this.releases = new models.Releases();
|
||||
this.nodeNetworkGroups = new models.NodeNetworkGroups();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.initialized = true;
|
||||
this.mountNode = $('#main-container');
|
||||
|
||||
document.title = i18n('common.title');
|
||||
|
||||
return this.version.fetch()
|
||||
.then(() => {
|
||||
this.user.set({authenticated: !this.version.get('auth_required')});
|
||||
this.patchBackboneSync();
|
||||
if (this.version.get('auth_required')) {
|
||||
_.extend(this.keystoneClient, this.user.pick('token'));
|
||||
return this.keystoneClient.authenticate()
|
||||
.done(() => this.user.set({authenticated: true}));
|
||||
}
|
||||
return $.Deferred().resolve();
|
||||
})
|
||||
.then(() => {
|
||||
return $.when(
|
||||
this.fuelSettings.fetch(),
|
||||
this.nodeNetworkGroups.fetch()
|
||||
);
|
||||
})
|
||||
.then(null, () => {
|
||||
if (this.version.get('auth_required') && !this.user.get('authenticated')) {
|
||||
return $.Deferred().resolve();
|
||||
} else {
|
||||
this.mountNode.empty();
|
||||
NailgunUnavailabilityDialog.show({}, {preventDuplicate: true});
|
||||
}
|
||||
})
|
||||
.then(() => Backbone.history.start());
|
||||
}
|
||||
|
||||
renderLayout() {
|
||||
var wrappedRootComponent = ReactDOM.render(
|
||||
React.createElement(
|
||||
RootComponent,
|
||||
_.pick(this, 'version', 'user', 'fuelSettings', 'statistics', 'notifications')
|
||||
),
|
||||
this.mountNode[0]
|
||||
);
|
||||
// RootComponent is wrapped with React-DnD, extracting link to it using ref
|
||||
this.rootComponent = wrappedRootComponent.refs.child;
|
||||
}
|
||||
|
||||
loadPage(Page, options = []) {
|
||||
return (Page.fetchData ? Page.fetchData(...options) : $.Deferred().resolve()).done((pageOptions) => {
|
||||
if (!this.rootComponent) this.renderLayout();
|
||||
this.setPage(Page, pageOptions);
|
||||
});
|
||||
}
|
||||
|
||||
setPage(Page, options) {
|
||||
this.page = this.rootComponent.setPage(Page, options);
|
||||
}
|
||||
|
||||
navigate(...args) {
|
||||
return this.router.navigate(...args);
|
||||
}
|
||||
|
||||
logout() {
|
||||
if (this.user.get('authenticated') && this.version.get('auth_required')) {
|
||||
this.user.set('authenticated', false);
|
||||
this.user.unset('username');
|
||||
this.user.unset('token');
|
||||
|
||||
this.keystoneClient.deauthenticate();
|
||||
patchBackboneSync() {
|
||||
var originalSync = Backbone.sync;
|
||||
if (originalSync.patched) return;
|
||||
Backbone.sync = function(method, model, options = {}) {
|
||||
// our server doesn't support PATCH, so use PUT instead
|
||||
if (method == 'patch') {
|
||||
method = 'update';
|
||||
}
|
||||
// add auth token to header if auth is enabled
|
||||
if (app.version.get('auth_required') && !this.authExempt) {
|
||||
return app.keystoneClient.authenticate()
|
||||
.fail(() => app.logout())
|
||||
.then(() => {
|
||||
options.headers = options.headers || {};
|
||||
options.headers['X-Auth-Token'] = app.keystoneClient.token;
|
||||
return originalSync.call(this, method, model, options);
|
||||
})
|
||||
.fail((response) => {
|
||||
if (response && response.status == 401) {
|
||||
app.logout();
|
||||
}
|
||||
});
|
||||
}
|
||||
return originalSync.call(this, method, model, options);
|
||||
};
|
||||
Backbone.sync.patched = true;
|
||||
}
|
||||
}
|
||||
|
||||
_.defer(() => this.navigate('login', {trigger: true, replace: true}));
|
||||
}
|
||||
window.app = new App();
|
||||
|
||||
patchBackboneSync() {
|
||||
var originalSync = Backbone.sync;
|
||||
if (originalSync.patched) return;
|
||||
Backbone.sync = function(method, model, options = {}) {
|
||||
// our server doesn't support PATCH, so use PUT instead
|
||||
if (method == 'patch') {
|
||||
method = 'update';
|
||||
}
|
||||
// add auth token to header if auth is enabled
|
||||
if (app.version.get('auth_required') && !this.authExempt) {
|
||||
return app.keystoneClient.authenticate()
|
||||
.fail(() => app.logout())
|
||||
.then(() => {
|
||||
options.headers = options.headers || {};
|
||||
options.headers['X-Auth-Token'] = app.keystoneClient.token;
|
||||
return originalSync.call(this, method, model, options);
|
||||
})
|
||||
.fail((response) => {
|
||||
if (response && response.status == 401) {
|
||||
app.logout();
|
||||
}
|
||||
});
|
||||
}
|
||||
return originalSync.call(this, method, model, options);
|
||||
};
|
||||
Backbone.sync.patched = true;
|
||||
}
|
||||
}
|
||||
$(() => app.initialize());
|
||||
|
||||
window.app = new App();
|
||||
|
||||
$(() => app.initialize());
|
||||
|
||||
export default app;
|
||||
export default app;
|
||||
|
|
|
@ -23,135 +23,135 @@ import dispatcher from 'dispatcher';
|
|||
import {DiscardSettingsChangesDialog} from 'views/dialogs';
|
||||
import 'react.backbone';
|
||||
|
||||
export var backboneMixin = React.BackboneMixin;
|
||||
export var backboneMixin = React.BackboneMixin;
|
||||
|
||||
export function dispatcherMixin(events, callback) {
|
||||
return {
|
||||
componentDidMount() {
|
||||
dispatcher.on(events, _.isString(callback) ? this[callback] : callback, this);
|
||||
},
|
||||
componentWillUnmount() {
|
||||
dispatcher.off(null, null, this);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export var unsavedChangesMixin = {
|
||||
onBeforeunloadEvent() {
|
||||
if (this.hasChanges()) return _.result(this, 'getStayMessage') || i18n('dialog.dismiss_settings.default_message');
|
||||
},
|
||||
componentWillMount() {
|
||||
this.eventName = _.uniqueId('unsavedchanges');
|
||||
$(window).on('beforeunload.' + this.eventName, this.onBeforeunloadEvent);
|
||||
$('body').on('click.' + this.eventName, 'a[href^=#]:not(.no-leave-check)', this.onLeave);
|
||||
},
|
||||
componentWillUnmount() {
|
||||
$(window).off('beforeunload.' + this.eventName);
|
||||
$('body').off('click.' + this.eventName);
|
||||
},
|
||||
onLeave(e) {
|
||||
var href = $(e.currentTarget).attr('href');
|
||||
if (Backbone.history.getHash() != href.substr(1) && _.result(this, 'hasChanges')) {
|
||||
e.preventDefault();
|
||||
|
||||
DiscardSettingsChangesDialog
|
||||
.show({
|
||||
isDiscardingPossible: _.result(this, 'isDiscardingPossible'),
|
||||
isSavingPossible: _.result(this, 'isSavingPossible'),
|
||||
applyChanges: this.applyChanges,
|
||||
revertChanges: this.revertChanges
|
||||
}).done(() => {
|
||||
app.navigate(href, {trigger: true});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function pollingMixin(updateInterval, delayedStart) {
|
||||
updateInterval = updateInterval * 1000;
|
||||
return {
|
||||
scheduleDataFetch() {
|
||||
var shouldDataBeFetched = !_.isFunction(this.shouldDataBeFetched) || this.shouldDataBeFetched();
|
||||
if (this.isMounted() && !this.activeTimeout && shouldDataBeFetched) {
|
||||
this.activeTimeout = _.delay(this.startPolling, updateInterval);
|
||||
}
|
||||
},
|
||||
startPolling(force) {
|
||||
var shouldDataBeFetched = force || !_.isFunction(this.shouldDataBeFetched) || this.shouldDataBeFetched();
|
||||
if (shouldDataBeFetched) {
|
||||
this.stopPolling();
|
||||
return this.fetchData().always(this.scheduleDataFetch);
|
||||
}
|
||||
},
|
||||
stopPolling() {
|
||||
if (this.activeTimeout) clearTimeout(this.activeTimeout);
|
||||
delete this.activeTimeout;
|
||||
},
|
||||
componentDidMount() {
|
||||
if (delayedStart) {
|
||||
this.scheduleDataFetch();
|
||||
} else {
|
||||
this.startPolling();
|
||||
}
|
||||
},
|
||||
componentWillUnmount() {
|
||||
this.stopPolling();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export var outerClickMixin = {
|
||||
propTypes: {
|
||||
toggle: React.PropTypes.func.isRequired
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
clickEventName: 'click.' + _.uniqueId('outer-click')
|
||||
};
|
||||
},
|
||||
handleBodyClick(e) {
|
||||
if (!$(e.target).closest(ReactDOM.findDOMNode(this)).length) {
|
||||
_.defer(_.partial(this.props.toggle, false));
|
||||
}
|
||||
},
|
||||
componentDidMount() {
|
||||
$('html').on(this.state.clickEventName, this.handleBodyClick);
|
||||
Backbone.history.on('route', _.partial(this.props.toggle, false), this);
|
||||
},
|
||||
componentWillUnmount() {
|
||||
$('html').off(this.state.clickEventName);
|
||||
Backbone.history.off('route', null, this);
|
||||
}
|
||||
};
|
||||
|
||||
export function renamingMixin(refname) {
|
||||
return {
|
||||
getInitialState() {
|
||||
return {
|
||||
isRenaming: false,
|
||||
renamingMixinEventName: 'click.' + _.uniqueId('rename')
|
||||
};
|
||||
},
|
||||
componentWillUnmount() {
|
||||
$('html').off(this.state.renamingMixinEventName);
|
||||
},
|
||||
startRenaming(e) {
|
||||
e.preventDefault();
|
||||
$('html').on(this.state.renamingMixinEventName, (e) => {
|
||||
if (e && !$(e.target).closest(ReactDOM.findDOMNode(this.refs[refname])).length) {
|
||||
this.endRenaming();
|
||||
} else {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
this.setState({isRenaming: true});
|
||||
},
|
||||
endRenaming() {
|
||||
$('html').off(this.state.renamingMixinEventName);
|
||||
this.setState({
|
||||
isRenaming: false,
|
||||
actionInProgress: false
|
||||
});
|
||||
}
|
||||
};
|
||||
export function dispatcherMixin(events, callback) {
|
||||
return {
|
||||
componentDidMount() {
|
||||
dispatcher.on(events, _.isString(callback) ? this[callback] : callback, this);
|
||||
},
|
||||
componentWillUnmount() {
|
||||
dispatcher.off(null, null, this);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export var unsavedChangesMixin = {
|
||||
onBeforeunloadEvent() {
|
||||
if (this.hasChanges()) return _.result(this, 'getStayMessage') || i18n('dialog.dismiss_settings.default_message');
|
||||
},
|
||||
componentWillMount() {
|
||||
this.eventName = _.uniqueId('unsavedchanges');
|
||||
$(window).on('beforeunload.' + this.eventName, this.onBeforeunloadEvent);
|
||||
$('body').on('click.' + this.eventName, 'a[href^=#]:not(.no-leave-check)', this.onLeave);
|
||||
},
|
||||
componentWillUnmount() {
|
||||
$(window).off('beforeunload.' + this.eventName);
|
||||
$('body').off('click.' + this.eventName);
|
||||
},
|
||||
onLeave(e) {
|
||||
var href = $(e.currentTarget).attr('href');
|
||||
if (Backbone.history.getHash() != href.substr(1) && _.result(this, 'hasChanges')) {
|
||||
e.preventDefault();
|
||||
|
||||
DiscardSettingsChangesDialog
|
||||
.show({
|
||||
isDiscardingPossible: _.result(this, 'isDiscardingPossible'),
|
||||
isSavingPossible: _.result(this, 'isSavingPossible'),
|
||||
applyChanges: this.applyChanges,
|
||||
revertChanges: this.revertChanges
|
||||
}).done(() => {
|
||||
app.navigate(href, {trigger: true});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function pollingMixin(updateInterval, delayedStart) {
|
||||
updateInterval = updateInterval * 1000;
|
||||
return {
|
||||
scheduleDataFetch() {
|
||||
var shouldDataBeFetched = !_.isFunction(this.shouldDataBeFetched) || this.shouldDataBeFetched();
|
||||
if (this.isMounted() && !this.activeTimeout && shouldDataBeFetched) {
|
||||
this.activeTimeout = _.delay(this.startPolling, updateInterval);
|
||||
}
|
||||
},
|
||||
startPolling(force) {
|
||||
var shouldDataBeFetched = force || !_.isFunction(this.shouldDataBeFetched) || this.shouldDataBeFetched();
|
||||
if (shouldDataBeFetched) {
|
||||
this.stopPolling();
|
||||
return this.fetchData().always(this.scheduleDataFetch);
|
||||
}
|
||||
},
|
||||
stopPolling() {
|
||||
if (this.activeTimeout) clearTimeout(this.activeTimeout);
|
||||
delete this.activeTimeout;
|
||||
},
|
||||
componentDidMount() {
|
||||
if (delayedStart) {
|
||||
this.scheduleDataFetch();
|
||||
} else {
|
||||
this.startPolling();
|
||||
}
|
||||
},
|
||||
componentWillUnmount() {
|
||||
this.stopPolling();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export var outerClickMixin = {
|
||||
propTypes: {
|
||||
toggle: React.PropTypes.func.isRequired
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
clickEventName: 'click.' + _.uniqueId('outer-click')
|
||||
};
|
||||
},
|
||||
handleBodyClick(e) {
|
||||
if (!$(e.target).closest(ReactDOM.findDOMNode(this)).length) {
|
||||
_.defer(_.partial(this.props.toggle, false));
|
||||
}
|
||||
},
|
||||
componentDidMount() {
|
||||
$('html').on(this.state.clickEventName, this.handleBodyClick);
|
||||
Backbone.history.on('route', _.partial(this.props.toggle, false), this);
|
||||
},
|
||||
componentWillUnmount() {
|
||||
$('html').off(this.state.clickEventName);
|
||||
Backbone.history.off('route', null, this);
|
||||
}
|
||||
};
|
||||
|
||||
export function renamingMixin(refname) {
|
||||
return {
|
||||
getInitialState() {
|
||||
return {
|
||||
isRenaming: false,
|
||||
renamingMixinEventName: 'click.' + _.uniqueId('rename')
|
||||
};
|
||||
},
|
||||
componentWillUnmount() {
|
||||
$('html').off(this.state.renamingMixinEventName);
|
||||
},
|
||||
startRenaming(e) {
|
||||
e.preventDefault();
|
||||
$('html').on(this.state.renamingMixinEventName, (e) => {
|
||||
if (e && !$(e.target).closest(ReactDOM.findDOMNode(this.refs[refname])).length) {
|
||||
this.endRenaming();
|
||||
} else {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
this.setState({isRenaming: true});
|
||||
},
|
||||
endRenaming() {
|
||||
$('html').off(this.state.renamingMixinEventName);
|
||||
this.setState({
|
||||
isRenaming: false,
|
||||
actionInProgress: false
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
|
||||
var dispatcher = _.clone(Backbone.Events);
|
||||
_.bindAll(dispatcher);
|
||||
var dispatcher = _.clone(Backbone.Events);
|
||||
_.bindAll(dispatcher);
|
||||
|
||||
export default dispatcher;
|
||||
export default dispatcher;
|
||||
|
|
|
@ -17,38 +17,38 @@ import _ from 'underscore';
|
|||
import ExpressionParser from 'expression/parser';
|
||||
import * as expressionObjects from 'expression/objects';
|
||||
|
||||
var expressionCache = {};
|
||||
var expressionCache = {};
|
||||
|
||||
class Expression {
|
||||
constructor(expressionText, models = {}, {strict = true} = {}) {
|
||||
this.strict = strict;
|
||||
this.expressionText = expressionText;
|
||||
this.models = models;
|
||||
this.compiledExpression = this.getCompiledExpression();
|
||||
return this;
|
||||
}
|
||||
class Expression {
|
||||
constructor(expressionText, models = {}, {strict = true} = {}) {
|
||||
this.strict = strict;
|
||||
this.expressionText = expressionText;
|
||||
this.models = models;
|
||||
this.compiledExpression = this.getCompiledExpression();
|
||||
return this;
|
||||
}
|
||||
|
||||
evaluate(extraModels) {
|
||||
// FIXME(vkramskikh): currently Jison supports sharing state
|
||||
// only via ExpressionParser.yy. It is unsafe and could lead to
|
||||
// issues in case we start to use webworkers
|
||||
ExpressionParser.yy.expression = this;
|
||||
this.modelPaths = {};
|
||||
this.extraModels = extraModels;
|
||||
var value = this.compiledExpression.evaluate();
|
||||
delete this.extraModels;
|
||||
return value;
|
||||
}
|
||||
evaluate(extraModels) {
|
||||
// FIXME(vkramskikh): currently Jison supports sharing state
|
||||
// only via ExpressionParser.yy. It is unsafe and could lead to
|
||||
// issues in case we start to use webworkers
|
||||
ExpressionParser.yy.expression = this;
|
||||
this.modelPaths = {};
|
||||
this.extraModels = extraModels;
|
||||
var value = this.compiledExpression.evaluate();
|
||||
delete this.extraModels;
|
||||
return value;
|
||||
}
|
||||
|
||||
getCompiledExpression() {
|
||||
var cacheEntry = expressionCache[this.expressionText];
|
||||
if (!cacheEntry) {
|
||||
cacheEntry = expressionCache[this.expressionText] = ExpressionParser.parse(this.expressionText);
|
||||
}
|
||||
return cacheEntry;
|
||||
}
|
||||
getCompiledExpression() {
|
||||
var cacheEntry = expressionCache[this.expressionText];
|
||||
if (!cacheEntry) {
|
||||
cacheEntry = expressionCache[this.expressionText] = ExpressionParser.parse(this.expressionText);
|
||||
}
|
||||
return cacheEntry;
|
||||
}
|
||||
}
|
||||
|
||||
_.extend(ExpressionParser.yy, expressionObjects);
|
||||
_.extend(ExpressionParser.yy, expressionObjects);
|
||||
|
||||
export default Expression;
|
||||
export default Expression;
|
||||
|
|
|
@ -16,93 +16,91 @@
|
|||
import _ from 'underscore';
|
||||
import ExpressionParser from 'expression/parser';
|
||||
|
||||
class ModelPath {
|
||||
constructor(path) {
|
||||
var pathParts = path.split(':');
|
||||
if (_.isUndefined(pathParts[1])) {
|
||||
this.modelName = 'default';
|
||||
this.attribute = pathParts[0];
|
||||
} else {
|
||||
this.modelName = pathParts[0];
|
||||
this.attribute = pathParts[1];
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
setModel(models, extraModels = {}) {
|
||||
this.model = extraModels[this.modelName] || models[this.modelName];
|
||||
if (!this.model) {
|
||||
throw new Error('No model with name "' + this.modelName + '" defined');
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
get(options) {
|
||||
return this.model.get(this.attribute, options);
|
||||
}
|
||||
|
||||
set(value, options) {
|
||||
return this.model.set(this.attribute, value, options);
|
||||
}
|
||||
|
||||
change(callback, context) {
|
||||
return this.model.on('change:' + this.attribute, callback, context);
|
||||
}
|
||||
export class ModelPath {
|
||||
constructor(path) {
|
||||
var pathParts = path.split(':');
|
||||
if (_.isUndefined(pathParts[1])) {
|
||||
this.modelName = 'default';
|
||||
this.attribute = pathParts[0];
|
||||
} else {
|
||||
this.modelName = pathParts[0];
|
||||
this.attribute = pathParts[1];
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
class ScalarWrapper {
|
||||
constructor(value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
evaluate() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.value;
|
||||
}
|
||||
setModel(models, extraModels = {}) {
|
||||
this.model = extraModels[this.modelName] || models[this.modelName];
|
||||
if (!this.model) {
|
||||
throw new Error('No model with name "' + this.modelName + '" defined');
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
class SubexpressionWrapper {
|
||||
constructor(subexpression) {
|
||||
this.subexpression = subexpression;
|
||||
}
|
||||
get(options) {
|
||||
return this.model.get(this.attribute, options);
|
||||
}
|
||||
|
||||
evaluate() {
|
||||
return this.subexpression();
|
||||
}
|
||||
set(value, options) {
|
||||
return this.model.set(this.attribute, value, options);
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.subexpression();
|
||||
}
|
||||
change(callback, context) {
|
||||
return this.model.on('change:' + this.attribute, callback, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class ScalarWrapper {
|
||||
constructor(value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
evaluate() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
export class SubexpressionWrapper {
|
||||
constructor(subexpression) {
|
||||
this.subexpression = subexpression;
|
||||
}
|
||||
|
||||
evaluate() {
|
||||
return this.subexpression();
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.subexpression();
|
||||
}
|
||||
}
|
||||
|
||||
export class ModelPathWrapper {
|
||||
constructor(modelPathText) {
|
||||
this.modelPath = new ModelPath(modelPathText);
|
||||
this.modelPathText = modelPathText;
|
||||
}
|
||||
|
||||
evaluate() {
|
||||
var expression = ExpressionParser.yy.expression;
|
||||
this.modelPath.setModel(expression.models, expression.extraModels);
|
||||
var result = this.modelPath.get();
|
||||
if (_.isUndefined(result)) {
|
||||
if (expression.strict) {
|
||||
throw new TypeError('Value of ' + this.modelPathText + ' is undefined. Set options.strict to false to allow undefined values.');
|
||||
}
|
||||
result = null;
|
||||
}
|
||||
this.lastResult = result;
|
||||
expression.modelPaths[this.modelPathText] = this.modelPath;
|
||||
return this.modelPath;
|
||||
}
|
||||
|
||||
class ModelPathWrapper {
|
||||
constructor(modelPathText) {
|
||||
this.modelPath = new ModelPath(modelPathText);
|
||||
this.modelPathText = modelPathText;
|
||||
}
|
||||
|
||||
evaluate() {
|
||||
var expression = ExpressionParser.yy.expression;
|
||||
this.modelPath.setModel(expression.models, expression.extraModels);
|
||||
var result = this.modelPath.get();
|
||||
if (_.isUndefined(result)) {
|
||||
if (expression.strict) {
|
||||
throw new TypeError('Value of ' + this.modelPathText + ' is undefined. Set options.strict to false to allow undefined values.');
|
||||
}
|
||||
result = null;
|
||||
}
|
||||
this.lastResult = result;
|
||||
expression.modelPaths[this.modelPathText] = this.modelPath;
|
||||
return this.modelPath;
|
||||
}
|
||||
|
||||
getValue() {
|
||||
this.evaluate();
|
||||
return this.lastResult;
|
||||
}
|
||||
}
|
||||
|
||||
export {ScalarWrapper, SubexpressionWrapper, ModelPathWrapper, ModelPath};
|
||||
getValue() {
|
||||
this.evaluate();
|
||||
return this.lastResult;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,37 +17,37 @@ import _ from 'underscore';
|
|||
import i18next from 'i18next-client';
|
||||
import translations from './translations/core.json';
|
||||
|
||||
var defaultLocale = 'en-US';
|
||||
var defaultLocale = 'en-US';
|
||||
|
||||
var i18n = _.extend(_.bind(i18next.t, i18next), {
|
||||
getLocaleName(locale) {
|
||||
return i18n('language', {lng: locale});
|
||||
},
|
||||
getLanguageName(locale) {
|
||||
return i18n('language_name', {lng: locale});
|
||||
},
|
||||
getAvailableLocales() {
|
||||
return _.keys(translations).sort();
|
||||
},
|
||||
getCurrentLocale() {
|
||||
return i18next.lng();
|
||||
},
|
||||
setLocale(locale) {
|
||||
i18next.setLng(locale, {});
|
||||
},
|
||||
addTranslations(extraTranslations) {
|
||||
_.merge(i18next.options.resStore, extraTranslations);
|
||||
}
|
||||
});
|
||||
var i18n = _.extend(_.bind(i18next.t, i18next), {
|
||||
getLocaleName(locale) {
|
||||
return i18n('language', {lng: locale});
|
||||
},
|
||||
getLanguageName(locale) {
|
||||
return i18n('language_name', {lng: locale});
|
||||
},
|
||||
getAvailableLocales() {
|
||||
return _.keys(translations).sort();
|
||||
},
|
||||
getCurrentLocale() {
|
||||
return i18next.lng();
|
||||
},
|
||||
setLocale(locale) {
|
||||
i18next.setLng(locale, {});
|
||||
},
|
||||
addTranslations(extraTranslations) {
|
||||
_.merge(i18next.options.resStore, extraTranslations);
|
||||
}
|
||||
});
|
||||
|
||||
i18next.init({resStore: translations, fallbackLng: defaultLocale});
|
||||
i18next.init({resStore: translations, fallbackLng: defaultLocale});
|
||||
|
||||
// reset locale to default if current locale is not available
|
||||
if (!_.contains(i18n.getAvailableLocales(), i18n.getCurrentLocale())) {
|
||||
i18n.setLocale(defaultLocale);
|
||||
}
|
||||
// reset locale to default if current locale is not available
|
||||
if (!_.contains(i18n.getAvailableLocales(), i18n.getCurrentLocale())) {
|
||||
i18n.setLocale(defaultLocale);
|
||||
}
|
||||
|
||||
// export global i18n variable to use in templates
|
||||
window.i18n = i18n;
|
||||
// export global i18n variable to use in templates
|
||||
window.i18n = i18n;
|
||||
|
||||
export default i18n;
|
||||
export default i18n;
|
||||
|
|
|
@ -17,108 +17,108 @@ import $ from 'jquery';
|
|||
import _ from 'underscore';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
class KeystoneClient {
|
||||
constructor(url, options) {
|
||||
this.DEFAULT_PASSWORD = 'admin';
|
||||
_.extend(this, {
|
||||
url: url,
|
||||
cacheTokenFor: 10 * 60 * 1000
|
||||
}, options);
|
||||
}
|
||||
class KeystoneClient {
|
||||
constructor(url, options) {
|
||||
this.DEFAULT_PASSWORD = 'admin';
|
||||
_.extend(this, {
|
||||
url: url,
|
||||
cacheTokenFor: 10 * 60 * 1000
|
||||
}, options);
|
||||
}
|
||||
|
||||
authenticate(username, password, options = {}) {
|
||||
if (this.tokenUpdateRequest) return this.tokenUpdateRequest;
|
||||
authenticate(username, password, options = {}) {
|
||||
if (this.tokenUpdateRequest) return this.tokenUpdateRequest;
|
||||
|
||||
if (!options.force && this.tokenUpdateTime && (this.cacheTokenFor > (new Date() - this.tokenUpdateTime))) {
|
||||
return $.Deferred().resolve();
|
||||
}
|
||||
var data = {auth: {}};
|
||||
if (username && password) {
|
||||
data.auth.passwordCredentials = {
|
||||
username: username,
|
||||
password: password
|
||||
};
|
||||
} else if (this.token) {
|
||||
data.auth.token = {id: this.token};
|
||||
} else {
|
||||
return $.Deferred().reject();
|
||||
}
|
||||
if (this.tenant) {
|
||||
data.auth.tenantName = this.tenant;
|
||||
}
|
||||
this.tokenUpdateRequest = $.ajax(this.url + '/v2.0/tokens', {
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(data)
|
||||
}).then((result, state, deferred) => {
|
||||
try {
|
||||
this.userId = result.access.user.id;
|
||||
this.token = result.access.token.id;
|
||||
this.tokenUpdateTime = new Date();
|
||||
|
||||
Cookies.set('token', result.access.token.id);
|
||||
|
||||
return deferred;
|
||||
} catch (e) {
|
||||
return $.Deferred().reject();
|
||||
}
|
||||
})
|
||||
.fail(() => delete this.tokenUpdateTime)
|
||||
.always(() => delete this.tokenUpdateRequest);
|
||||
|
||||
return this.tokenUpdateRequest;
|
||||
}
|
||||
|
||||
changePassword(currentPassword, newPassword) {
|
||||
var data = {
|
||||
user: {
|
||||
password: newPassword,
|
||||
original_password: currentPassword
|
||||
}
|
||||
};
|
||||
return $.ajax(this.url + '/v2.0/OS-KSCRUD/users/' + this.userId, {
|
||||
type: 'PATCH',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(data),
|
||||
headers: {'X-Auth-Token': this.token}
|
||||
}).then((result, state, deferred) => {
|
||||
try {
|
||||
this.token = result.access.token.id;
|
||||
this.tokenUpdateTime = new Date();
|
||||
|
||||
Cookies.set('token', result.access.token.id);
|
||||
|
||||
return deferred;
|
||||
} catch (e) {
|
||||
return $.Deferred().reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deauthenticate() {
|
||||
var token = this.token;
|
||||
|
||||
if (this.tokenUpdateRequest) return this.tokenUpdateRequest;
|
||||
if (!token) return $.Deferred().reject();
|
||||
|
||||
delete this.userId;
|
||||
delete this.token;
|
||||
delete this.tokenUpdateTime;
|
||||
|
||||
Cookies.remove('token');
|
||||
|
||||
this.tokenRemoveRequest = $.ajax(this.url + '/v2.0/tokens/' + token, {
|
||||
type: 'DELETE',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
headers: {'X-Auth-Token': token}
|
||||
})
|
||||
.always(() => delete this.tokenRemoveRequest);
|
||||
|
||||
return this.tokenRemoveRequest;
|
||||
}
|
||||
if (!options.force && this.tokenUpdateTime && (this.cacheTokenFor > (new Date() - this.tokenUpdateTime))) {
|
||||
return $.Deferred().resolve();
|
||||
}
|
||||
var data = {auth: {}};
|
||||
if (username && password) {
|
||||
data.auth.passwordCredentials = {
|
||||
username: username,
|
||||
password: password
|
||||
};
|
||||
} else if (this.token) {
|
||||
data.auth.token = {id: this.token};
|
||||
} else {
|
||||
return $.Deferred().reject();
|
||||
}
|
||||
if (this.tenant) {
|
||||
data.auth.tenantName = this.tenant;
|
||||
}
|
||||
this.tokenUpdateRequest = $.ajax(this.url + '/v2.0/tokens', {
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(data)
|
||||
}).then((result, state, deferred) => {
|
||||
try {
|
||||
this.userId = result.access.user.id;
|
||||
this.token = result.access.token.id;
|
||||
this.tokenUpdateTime = new Date();
|
||||
|
||||
export default KeystoneClient;
|
||||
Cookies.set('token', result.access.token.id);
|
||||
|
||||
return deferred;
|
||||
} catch (e) {
|
||||
return $.Deferred().reject();
|
||||
}
|
||||
})
|
||||
.fail(() => delete this.tokenUpdateTime)
|
||||
.always(() => delete this.tokenUpdateRequest);
|
||||
|
||||
return this.tokenUpdateRequest;
|
||||
}
|
||||
|
||||
changePassword(currentPassword, newPassword) {
|
||||
var data = {
|
||||
user: {
|
||||
password: newPassword,
|
||||
original_password: currentPassword
|
||||
}
|
||||
};
|
||||
return $.ajax(this.url + '/v2.0/OS-KSCRUD/users/' + this.userId, {
|
||||
type: 'PATCH',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(data),
|
||||
headers: {'X-Auth-Token': this.token}
|
||||
}).then((result, state, deferred) => {
|
||||
try {
|
||||
this.token = result.access.token.id;
|
||||
this.tokenUpdateTime = new Date();
|
||||
|
||||
Cookies.set('token', result.access.token.id);
|
||||
|
||||
return deferred;
|
||||
} catch (e) {
|
||||
return $.Deferred().reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deauthenticate() {
|
||||
var token = this.token;
|
||||
|
||||
if (this.tokenUpdateRequest) return this.tokenUpdateRequest;
|
||||
if (!token) return $.Deferred().reject();
|
||||
|
||||
delete this.userId;
|
||||
delete this.token;
|
||||
delete this.tokenUpdateTime;
|
||||
|
||||
Cookies.remove('token');
|
||||
|
||||
this.tokenRemoveRequest = $.ajax(this.url + '/v2.0/tokens/' + token, {
|
||||
type: 'DELETE',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
headers: {'X-Auth-Token': token}
|
||||
})
|
||||
.always(() => delete this.tokenRemoveRequest);
|
||||
|
||||
return this.tokenRemoveRequest;
|
||||
}
|
||||
}
|
||||
|
||||
export default KeystoneClient;
|
||||
|
|
2870
static/models.js
2870
static/models.js
File diff suppressed because it is too large
Load Diff
|
@ -19,6 +19,6 @@ import translations from 'plugins/vmware/translations.json';
|
|||
import i18n from 'i18n';
|
||||
import './styles.less';
|
||||
|
||||
i18n.addTranslations(translations);
|
||||
i18n.addTranslations(translations);
|
||||
|
||||
export {VmWareTab, VmWareModels};
|
||||
export {VmWareTab, VmWareModels};
|
||||
|
|
|
@ -18,289 +18,289 @@ import i18n from 'i18n';
|
|||
import Backbone from 'backbone';
|
||||
import models from 'models';
|
||||
|
||||
var VmWareModels = {};
|
||||
var VmWareModels = {};
|
||||
|
||||
VmWareModels.isRegularField = function(field) {
|
||||
return _.contains(['text', 'password', 'checkbox', 'select'], field.type);
|
||||
};
|
||||
VmWareModels.isRegularField = function(field) {
|
||||
return _.contains(['text', 'password', 'checkbox', 'select'], field.type);
|
||||
};
|
||||
|
||||
// models for testing restrictions
|
||||
var restrictionModels = {};
|
||||
// models for testing restrictions
|
||||
var restrictionModels = {};
|
||||
|
||||
// Test regex using regex cache
|
||||
var regexCache = {};
|
||||
function testRegex(regexText, value) {
|
||||
if (!regexCache[regexText]) {
|
||||
regexCache[regexText] = new RegExp(regexText);
|
||||
// Test regex using regex cache
|
||||
var regexCache = {};
|
||||
function testRegex(regexText, value) {
|
||||
if (!regexCache[regexText]) {
|
||||
regexCache[regexText] = new RegExp(regexText);
|
||||
}
|
||||
return regexCache[regexText].test(value);
|
||||
}
|
||||
|
||||
var BaseModel = Backbone.Model.extend(models.superMixin).extend(models.cacheMixin).extend(models.restrictionMixin).extend({
|
||||
constructorName: 'BaseModel',
|
||||
cacheFor: 60 * 1000,
|
||||
toJSON() {
|
||||
return _.omit(this.attributes, 'metadata');
|
||||
},
|
||||
validate() {
|
||||
var result = {};
|
||||
_.each(this.attributes.metadata, function(field) {
|
||||
if (!VmWareModels.isRegularField(field) || field.type == 'checkbox') {
|
||||
return;
|
||||
}
|
||||
var isDisabled = this.checkRestrictions(restrictionModels, undefined, field);
|
||||
if (isDisabled.result) {
|
||||
return;
|
||||
}
|
||||
var value = this.get(field.name);
|
||||
if (field.regex) {
|
||||
if (!testRegex(field.regex.source, value)) {
|
||||
result[field.name] = field.regex.error;
|
||||
}
|
||||
return regexCache[regexText].test(value);
|
||||
}
|
||||
}, this);
|
||||
return _.isEmpty(result) ? null : result;
|
||||
},
|
||||
testRestrictions() {
|
||||
var results = {
|
||||
hide: {},
|
||||
disable: {}
|
||||
};
|
||||
var metadata = this.get('metadata');
|
||||
_.each(metadata, function(field) {
|
||||
var disableResult = this.checkRestrictions(restrictionModels, undefined, field);
|
||||
results.disable[field.name] = disableResult;
|
||||
|
||||
var hideResult = this.checkRestrictions(restrictionModels, 'hide', field);
|
||||
results.hide[field.name] = hideResult;
|
||||
}, this);
|
||||
return results;
|
||||
}
|
||||
});
|
||||
|
||||
var BaseCollection = Backbone.Collection.extend(models.superMixin).extend(models.cacheMixin).extend({
|
||||
constructorName: 'BaseCollection',
|
||||
model: BaseModel,
|
||||
cacheFor: 60 * 1000,
|
||||
isValid() {
|
||||
this.validationError = this.validate();
|
||||
return this.validationError;
|
||||
},
|
||||
validate() {
|
||||
var errors = _.compact(this.models.map((model) => {
|
||||
model.isValid();
|
||||
return model.validationError;
|
||||
}));
|
||||
return _.isEmpty(errors) ? null : errors;
|
||||
},
|
||||
testRestrictions() {
|
||||
_.invoke(this.models, 'testRestrictions', restrictionModels);
|
||||
}
|
||||
});
|
||||
|
||||
VmWareModels.NovaCompute = BaseModel.extend({
|
||||
constructorName: 'NovaCompute',
|
||||
checkEmptyTargetNode() {
|
||||
var targetNode = this.get('target_node');
|
||||
if (targetNode.current && targetNode.current.id == 'invalid') {
|
||||
this.validationError = this.validationError || {};
|
||||
this.validationError.target_node = i18n('vmware.invalid_target_node');
|
||||
}
|
||||
},
|
||||
checkDuplicateField(keys, fieldName) {
|
||||
/*jshint validthis:true */
|
||||
var fieldValue = this.get(fieldName);
|
||||
if (fieldValue.length > 0 && keys[fieldName] && keys[fieldName][fieldValue]) {
|
||||
this.validationError = this.validationError || {};
|
||||
this.validationError[fieldName] = i18n('vmware.duplicate_value');
|
||||
}
|
||||
keys[fieldName] = keys[fieldName] || {};
|
||||
keys[fieldName][fieldValue] = true;
|
||||
},
|
||||
checkDuplicates(keys) {
|
||||
this.checkDuplicateField(keys, 'vsphere_cluster');
|
||||
this.checkDuplicateField(keys, 'service_name');
|
||||
|
||||
var targetNode = this.get('target_node') || {};
|
||||
if (targetNode.current) {
|
||||
if (targetNode.current.id && targetNode.current.id != 'controllers' &&
|
||||
keys.target_node && keys.target_node[targetNode.current.id]) {
|
||||
this.validationError = this.validationError || {};
|
||||
this.validationError.target_node = i18n('vmware.duplicate_value');
|
||||
}
|
||||
keys.target_node = keys.target_node || {};
|
||||
keys.target_node[targetNode.current.id] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var NovaComputes = BaseCollection.extend({
|
||||
constructorName: 'NovaComputes',
|
||||
model: VmWareModels.NovaCompute,
|
||||
validate() {
|
||||
this._super('validate', arguments);
|
||||
|
||||
var keys = {vsphere_clusters: {}, service_names: {}};
|
||||
this.invoke('checkDuplicates', keys);
|
||||
this.invoke('checkEmptyTargetNode');
|
||||
|
||||
var errors = _.compact(_.pluck(this.models, 'validationError'));
|
||||
return _.isEmpty(errors) ? null : errors;
|
||||
}
|
||||
});
|
||||
|
||||
var AvailabilityZone = BaseModel.extend({
|
||||
constructorName: 'AvailabilityZone',
|
||||
constructor(data) {
|
||||
Backbone.Model.apply(this, arguments);
|
||||
if (data) {
|
||||
this.set(this.parse(data));
|
||||
}
|
||||
},
|
||||
parse(response) {
|
||||
var result = {};
|
||||
var metadata = response.metadata;
|
||||
result.metadata = metadata;
|
||||
|
||||
// regular fields
|
||||
_.each(metadata, (field) => {
|
||||
if (VmWareModels.isRegularField(field)) {
|
||||
result[field.name] = response[field.name];
|
||||
}
|
||||
}, this);
|
||||
|
||||
// nova_computes
|
||||
var novaMetadata = _.find(metadata, {name: 'nova_computes'});
|
||||
var novaValues = _.clone(response.nova_computes);
|
||||
novaValues = _.map(novaValues, (value) => {
|
||||
value.metadata = novaMetadata.fields;
|
||||
return new VmWareModels.NovaCompute(value);
|
||||
});
|
||||
result.nova_computes = new NovaComputes(novaValues);
|
||||
|
||||
return result;
|
||||
},
|
||||
toJSON() {
|
||||
var result = _.omit(this.attributes, 'metadata', 'nova_computes');
|
||||
result.nova_computes = this.get('nova_computes').toJSON();
|
||||
return result;
|
||||
},
|
||||
validate() {
|
||||
var errors = _.merge({}, BaseModel.prototype.validate.call(this));
|
||||
|
||||
var novaComputes = this.get('nova_computes');
|
||||
novaComputes.isValid();
|
||||
if (novaComputes.validationError) {
|
||||
errors.nova_computes = novaComputes.validationError;
|
||||
}
|
||||
|
||||
var BaseModel = Backbone.Model.extend(models.superMixin).extend(models.cacheMixin).extend(models.restrictionMixin).extend({
|
||||
constructorName: 'BaseModel',
|
||||
cacheFor: 60 * 1000,
|
||||
toJSON() {
|
||||
return _.omit(this.attributes, 'metadata');
|
||||
},
|
||||
validate() {
|
||||
var result = {};
|
||||
_.each(this.attributes.metadata, function(field) {
|
||||
if (!VmWareModels.isRegularField(field) || field.type == 'checkbox') {
|
||||
return;
|
||||
}
|
||||
var isDisabled = this.checkRestrictions(restrictionModels, undefined, field);
|
||||
if (isDisabled.result) {
|
||||
return;
|
||||
}
|
||||
var value = this.get(field.name);
|
||||
if (field.regex) {
|
||||
if (!testRegex(field.regex.source, value)) {
|
||||
result[field.name] = field.regex.error;
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
return _.isEmpty(result) ? null : result;
|
||||
},
|
||||
testRestrictions() {
|
||||
var results = {
|
||||
hide: {},
|
||||
disable: {}
|
||||
};
|
||||
var metadata = this.get('metadata');
|
||||
_.each(metadata, function(field) {
|
||||
var disableResult = this.checkRestrictions(restrictionModels, undefined, field);
|
||||
results.disable[field.name] = disableResult;
|
||||
return _.isEmpty(errors) ? null : errors;
|
||||
}
|
||||
});
|
||||
|
||||
var hideResult = this.checkRestrictions(restrictionModels, 'hide', field);
|
||||
results.hide[field.name] = hideResult;
|
||||
}, this);
|
||||
return results;
|
||||
var AvailabilityZones = BaseCollection.extend({
|
||||
constructorName: 'AvailabilityZones',
|
||||
model: AvailabilityZone
|
||||
});
|
||||
|
||||
VmWareModels.Network = BaseModel.extend({constructorName: 'Network'});
|
||||
VmWareModels.Glance = BaseModel.extend({constructorName: 'Glance'});
|
||||
|
||||
VmWareModels.VCenter = BaseModel.extend({
|
||||
constructorName: 'VCenter',
|
||||
url() {
|
||||
return '/api/v1/clusters/' + this.id + '/vmware_attributes' + (this.loadDefaults ? '/defaults' : '');
|
||||
},
|
||||
parse(response) {
|
||||
if (!response.editable || !response.editable.metadata || !response.editable.value) {
|
||||
return;
|
||||
}
|
||||
var metadata = response.editable.metadata || [],
|
||||
value = response.editable.value || {};
|
||||
|
||||
// Availability Zone(s)
|
||||
var azMetadata = _.find(metadata, {name: 'availability_zones'});
|
||||
var azValues = _.clone(value.availability_zones);
|
||||
azValues = _.map(azValues, (value) => {
|
||||
value.metadata = azMetadata.fields;
|
||||
return value;
|
||||
});
|
||||
|
||||
// Network
|
||||
var networkMetadata = _.find(metadata, {name: 'network'});
|
||||
var networkValue = _.extend(_.clone(value.network), {metadata: networkMetadata.fields});
|
||||
|
||||
// Glance
|
||||
var glanceMetadata = _.find(metadata, {name: 'glance'});
|
||||
var glanceValue = _.extend(_.clone(value.glance), {metadata: glanceMetadata.fields});
|
||||
|
||||
return {
|
||||
metadata: metadata,
|
||||
availability_zones: new AvailabilityZones(azValues),
|
||||
network: new VmWareModels.Network(networkValue),
|
||||
glance: new VmWareModels.Glance(glanceValue)
|
||||
};
|
||||
},
|
||||
isFilled() {
|
||||
var result = this.get('availability_zones') && this.get('network') && this.get('glance');
|
||||
return !!result;
|
||||
},
|
||||
toJSON() {
|
||||
if (!this.isFilled()) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
editable: {
|
||||
value: {
|
||||
availability_zones: this.get('availability_zones').toJSON(),
|
||||
network: this.get('network').toJSON(),
|
||||
glance: this.get('glance').toJSON()
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
validate() {
|
||||
if (!this.isFilled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var errors = {};
|
||||
_.each(this.get('metadata'), function(field) {
|
||||
var model = this.get(field.name);
|
||||
// do not validate disabled restrictions
|
||||
var isDisabled = this.checkRestrictions(restrictionModels, undefined, field);
|
||||
if (isDisabled.result) {
|
||||
return;
|
||||
}
|
||||
model.isValid();
|
||||
if (model.validationError) {
|
||||
errors[field.name] = model.validationError;
|
||||
}
|
||||
}, this);
|
||||
|
||||
// check unassigned nodes exist
|
||||
var assignedNodes = {};
|
||||
var availabilityZones = this.get('availability_zones') || [];
|
||||
availabilityZones.each(function(zone) {
|
||||
var novaComputes = zone.get('nova_computes') || [];
|
||||
novaComputes.each((compute) => {
|
||||
var targetNode = compute.get('target_node');
|
||||
assignedNodes[targetNode.current.id] = targetNode.current.label;
|
||||
}, this);
|
||||
}, this);
|
||||
var unassignedNodes = restrictionModels.cluster.get('nodes').filter((node) => {
|
||||
return _.contains(node.get('pending_roles'), 'compute-vmware') && !assignedNodes[node.get('hostname')];
|
||||
});
|
||||
if (unassignedNodes.length > 0) {
|
||||
errors.unassigned_nodes = unassignedNodes;
|
||||
}
|
||||
|
||||
var BaseCollection = Backbone.Collection.extend(models.superMixin).extend(models.cacheMixin).extend({
|
||||
constructorName: 'BaseCollection',
|
||||
model: BaseModel,
|
||||
cacheFor: 60 * 1000,
|
||||
isValid() {
|
||||
this.validationError = this.validate();
|
||||
return this.validationError;
|
||||
},
|
||||
validate() {
|
||||
var errors = _.compact(this.models.map((model) => {
|
||||
model.isValid();
|
||||
return model.validationError;
|
||||
}));
|
||||
return _.isEmpty(errors) ? null : errors;
|
||||
},
|
||||
testRestrictions() {
|
||||
_.invoke(this.models, 'testRestrictions', restrictionModels);
|
||||
}
|
||||
});
|
||||
return _.isEmpty(errors) ? null : errors;
|
||||
},
|
||||
setModels(models) {
|
||||
restrictionModels = models;
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
VmWareModels.NovaCompute = BaseModel.extend({
|
||||
constructorName: 'NovaCompute',
|
||||
checkEmptyTargetNode() {
|
||||
var targetNode = this.get('target_node');
|
||||
if (targetNode.current && targetNode.current.id == 'invalid') {
|
||||
this.validationError = this.validationError || {};
|
||||
this.validationError.target_node = i18n('vmware.invalid_target_node');
|
||||
}
|
||||
},
|
||||
checkDuplicateField(keys, fieldName) {
|
||||
/*jshint validthis:true */
|
||||
var fieldValue = this.get(fieldName);
|
||||
if (fieldValue.length > 0 && keys[fieldName] && keys[fieldName][fieldValue]) {
|
||||
this.validationError = this.validationError || {};
|
||||
this.validationError[fieldName] = i18n('vmware.duplicate_value');
|
||||
}
|
||||
keys[fieldName] = keys[fieldName] || {};
|
||||
keys[fieldName][fieldValue] = true;
|
||||
},
|
||||
checkDuplicates(keys) {
|
||||
this.checkDuplicateField(keys, 'vsphere_cluster');
|
||||
this.checkDuplicateField(keys, 'service_name');
|
||||
|
||||
var targetNode = this.get('target_node') || {};
|
||||
if (targetNode.current) {
|
||||
if (targetNode.current.id && targetNode.current.id != 'controllers' &&
|
||||
keys.target_node && keys.target_node[targetNode.current.id]) {
|
||||
this.validationError = this.validationError || {};
|
||||
this.validationError.target_node = i18n('vmware.duplicate_value');
|
||||
}
|
||||
keys.target_node = keys.target_node || {};
|
||||
keys.target_node[targetNode.current.id] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var NovaComputes = BaseCollection.extend({
|
||||
constructorName: 'NovaComputes',
|
||||
model: VmWareModels.NovaCompute,
|
||||
validate() {
|
||||
this._super('validate', arguments);
|
||||
|
||||
var keys = {vsphere_clusters: {}, service_names: {}};
|
||||
this.invoke('checkDuplicates', keys);
|
||||
this.invoke('checkEmptyTargetNode');
|
||||
|
||||
var errors = _.compact(_.pluck(this.models, 'validationError'));
|
||||
return _.isEmpty(errors) ? null : errors;
|
||||
}
|
||||
});
|
||||
|
||||
var AvailabilityZone = BaseModel.extend({
|
||||
constructorName: 'AvailabilityZone',
|
||||
constructor(data) {
|
||||
Backbone.Model.apply(this, arguments);
|
||||
if (data) {
|
||||
this.set(this.parse(data));
|
||||
}
|
||||
},
|
||||
parse(response) {
|
||||
var result = {};
|
||||
var metadata = response.metadata;
|
||||
result.metadata = metadata;
|
||||
|
||||
// regular fields
|
||||
_.each(metadata, (field) => {
|
||||
if (VmWareModels.isRegularField(field)) {
|
||||
result[field.name] = response[field.name];
|
||||
}
|
||||
}, this);
|
||||
|
||||
// nova_computes
|
||||
var novaMetadata = _.find(metadata, {name: 'nova_computes'});
|
||||
var novaValues = _.clone(response.nova_computes);
|
||||
novaValues = _.map(novaValues, (value) => {
|
||||
value.metadata = novaMetadata.fields;
|
||||
return new VmWareModels.NovaCompute(value);
|
||||
});
|
||||
result.nova_computes = new NovaComputes(novaValues);
|
||||
|
||||
return result;
|
||||
},
|
||||
toJSON() {
|
||||
var result = _.omit(this.attributes, 'metadata', 'nova_computes');
|
||||
result.nova_computes = this.get('nova_computes').toJSON();
|
||||
return result;
|
||||
},
|
||||
validate() {
|
||||
var errors = _.merge({}, BaseModel.prototype.validate.call(this));
|
||||
|
||||
var novaComputes = this.get('nova_computes');
|
||||
novaComputes.isValid();
|
||||
if (novaComputes.validationError) {
|
||||
errors.nova_computes = novaComputes.validationError;
|
||||
}
|
||||
|
||||
return _.isEmpty(errors) ? null : errors;
|
||||
}
|
||||
});
|
||||
|
||||
var AvailabilityZones = BaseCollection.extend({
|
||||
constructorName: 'AvailabilityZones',
|
||||
model: AvailabilityZone
|
||||
});
|
||||
|
||||
VmWareModels.Network = BaseModel.extend({constructorName: 'Network'});
|
||||
VmWareModels.Glance = BaseModel.extend({constructorName: 'Glance'});
|
||||
|
||||
VmWareModels.VCenter = BaseModel.extend({
|
||||
constructorName: 'VCenter',
|
||||
url() {
|
||||
return '/api/v1/clusters/' + this.id + '/vmware_attributes' + (this.loadDefaults ? '/defaults' : '');
|
||||
},
|
||||
parse(response) {
|
||||
if (!response.editable || !response.editable.metadata || !response.editable.value) {
|
||||
return;
|
||||
}
|
||||
var metadata = response.editable.metadata || [],
|
||||
value = response.editable.value || {};
|
||||
|
||||
// Availability Zone(s)
|
||||
var azMetadata = _.find(metadata, {name: 'availability_zones'});
|
||||
var azValues = _.clone(value.availability_zones);
|
||||
azValues = _.map(azValues, (value) => {
|
||||
value.metadata = azMetadata.fields;
|
||||
return value;
|
||||
});
|
||||
|
||||
// Network
|
||||
var networkMetadata = _.find(metadata, {name: 'network'});
|
||||
var networkValue = _.extend(_.clone(value.network), {metadata: networkMetadata.fields});
|
||||
|
||||
// Glance
|
||||
var glanceMetadata = _.find(metadata, {name: 'glance'});
|
||||
var glanceValue = _.extend(_.clone(value.glance), {metadata: glanceMetadata.fields});
|
||||
|
||||
return {
|
||||
metadata: metadata,
|
||||
availability_zones: new AvailabilityZones(azValues),
|
||||
network: new VmWareModels.Network(networkValue),
|
||||
glance: new VmWareModels.Glance(glanceValue)
|
||||
};
|
||||
},
|
||||
isFilled() {
|
||||
var result = this.get('availability_zones') && this.get('network') && this.get('glance');
|
||||
return !!result;
|
||||
},
|
||||
toJSON() {
|
||||
if (!this.isFilled()) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
editable: {
|
||||
value: {
|
||||
availability_zones: this.get('availability_zones').toJSON(),
|
||||
network: this.get('network').toJSON(),
|
||||
glance: this.get('glance').toJSON()
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
validate() {
|
||||
if (!this.isFilled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var errors = {};
|
||||
_.each(this.get('metadata'), function(field) {
|
||||
var model = this.get(field.name);
|
||||
// do not validate disabled restrictions
|
||||
var isDisabled = this.checkRestrictions(restrictionModels, undefined, field);
|
||||
if (isDisabled.result) {
|
||||
return;
|
||||
}
|
||||
model.isValid();
|
||||
if (model.validationError) {
|
||||
errors[field.name] = model.validationError;
|
||||
}
|
||||
}, this);
|
||||
|
||||
// check unassigned nodes exist
|
||||
var assignedNodes = {};
|
||||
var availabilityZones = this.get('availability_zones') || [];
|
||||
availabilityZones.each(function(zone) {
|
||||
var novaComputes = zone.get('nova_computes') || [];
|
||||
novaComputes.each((compute) => {
|
||||
var targetNode = compute.get('target_node');
|
||||
assignedNodes[targetNode.current.id] = targetNode.current.label;
|
||||
}, this);
|
||||
}, this);
|
||||
var unassignedNodes = restrictionModels.cluster.get('nodes').filter((node) => {
|
||||
return _.contains(node.get('pending_roles'), 'compute-vmware') && !assignedNodes[node.get('hostname')];
|
||||
});
|
||||
if (unassignedNodes.length > 0) {
|
||||
errors.unassigned_nodes = unassignedNodes;
|
||||
}
|
||||
|
||||
return _.isEmpty(errors) ? null : errors;
|
||||
},
|
||||
setModels(models) {
|
||||
restrictionModels = models;
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
export default VmWareModels;
|
||||
export default VmWareModels;
|
||||
|
|
|
@ -22,418 +22,418 @@ import {Input, Tooltip} from 'views/controls';
|
|||
import {unsavedChangesMixin} from 'component_mixins';
|
||||
import VmWareModels from 'plugins/vmware/vmware_models';
|
||||
|
||||
var Field = React.createClass({
|
||||
onChange(name, value) {
|
||||
var currentValue = this.props.model.get(name);
|
||||
if (currentValue.current) {
|
||||
currentValue.current.id = value;
|
||||
currentValue.current.label = value;
|
||||
} else {
|
||||
currentValue = value;
|
||||
var Field = React.createClass({
|
||||
onChange(name, value) {
|
||||
var currentValue = this.props.model.get(name);
|
||||
if (currentValue.current) {
|
||||
currentValue.current.id = value;
|
||||
currentValue.current.label = value;
|
||||
} else {
|
||||
currentValue = value;
|
||||
}
|
||||
this.props.model.set(name, currentValue);
|
||||
this.setState({model: this.props.model});
|
||||
_.defer(() => dispatcher.trigger('vcenter_model_update'));
|
||||
},
|
||||
render() {
|
||||
var metadata = this.props.metadata,
|
||||
value = this.props.model.get(metadata.name);
|
||||
return (
|
||||
<Input
|
||||
{... _.pick(metadata, 'name', 'type', 'label', 'description')}
|
||||
value={metadata.type == 'select' ? value.current.id : value}
|
||||
checked={value}
|
||||
toggleable={metadata.type == 'password'}
|
||||
onChange={this.onChange}
|
||||
disabled={this.props.disabled}
|
||||
error={(this.props.model.validationError || {})[metadata.name]}
|
||||
>
|
||||
{metadata.type == 'select' && value.options.map((value) => {
|
||||
return <option key={value.id} value={value.id}>{value.label}</option>;
|
||||
})}
|
||||
</Input>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var FieldGroup = React.createClass({
|
||||
render() {
|
||||
var restrictions = this.props.model.testRestrictions();
|
||||
var metadata = _.filter(this.props.model.get('metadata'), VmWareModels.isRegularField);
|
||||
var fields = metadata.map(function(meta) {
|
||||
return (
|
||||
<Field
|
||||
key={meta.name}
|
||||
model={this.props.model}
|
||||
metadata={meta}
|
||||
disabled={this.props.disabled}
|
||||
disableWarning={restrictions.disable[meta.name]}
|
||||
/>
|
||||
);
|
||||
}, this);
|
||||
return (
|
||||
<div>
|
||||
{fields}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var GenericSection = React.createClass({
|
||||
render() {
|
||||
if (!this.props.model) return null;
|
||||
return (
|
||||
<div className='col-xs-12 forms-box'>
|
||||
<h3>
|
||||
{this.props.title}
|
||||
{this.props.tooltipText &&
|
||||
<Tooltip text={this.props.tooltipText} placement='right'>
|
||||
<i className='glyphicon glyphicon-warning-sign tooltip-icon' />
|
||||
</Tooltip>
|
||||
}
|
||||
</h3>
|
||||
<FieldGroup model={this.props.model} disabled={this.props.disabled}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var NovaCompute = React.createClass({
|
||||
render() {
|
||||
if (!this.props.model) return null;
|
||||
|
||||
// add nodes of 'compute-vmware' type to targetNode select
|
||||
var targetNode = this.props.model.get('target_node') || {};
|
||||
var nodes = this.props.cluster.get('nodes').filter((node) => node.hasRole('compute-vmware'));
|
||||
|
||||
targetNode.options = [];
|
||||
if (targetNode.current.id == 'controllers' || !this.props.isLocked) {
|
||||
targetNode.options.push({id: 'controllers', label: 'controllers'});
|
||||
} else {
|
||||
targetNode.options.push({id: 'invalid', label: 'Select node'});
|
||||
}
|
||||
nodes.forEach((node) => {
|
||||
targetNode.options.push({
|
||||
id: node.get('hostname'),
|
||||
label: node.get('name') + ' (' + node.get('mac').substr(9) + ')'
|
||||
});
|
||||
});
|
||||
|
||||
this.props.model.set('target_node', targetNode);
|
||||
|
||||
return (
|
||||
<div className='nova-compute'>
|
||||
<h4>
|
||||
<div className='btn-group'>
|
||||
<button
|
||||
className='btn btn-link'
|
||||
disabled={this.props.disabled}
|
||||
onClick={() => {
|
||||
this.props.onAdd(this.props.model);
|
||||
}}
|
||||
>
|
||||
<i className='glyphicon glyphicon-plus-sign' />
|
||||
</button>
|
||||
{!this.props.isRemovable &&
|
||||
<button
|
||||
className='btn btn-link'
|
||||
disabled={this.props.disabled}
|
||||
onClick={() => {
|
||||
this.props.onRemove(this.props.model);
|
||||
}}
|
||||
>
|
||||
<i className='glyphicon glyphicon-minus-sign' />
|
||||
</button>
|
||||
}
|
||||
this.props.model.set(name, currentValue);
|
||||
this.setState({model: this.props.model});
|
||||
_.defer(() => dispatcher.trigger('vcenter_model_update'));
|
||||
},
|
||||
render() {
|
||||
var metadata = this.props.metadata,
|
||||
value = this.props.model.get(metadata.name);
|
||||
return (
|
||||
<Input
|
||||
{... _.pick(metadata, 'name', 'type', 'label', 'description')}
|
||||
value={metadata.type == 'select' ? value.current.id : value}
|
||||
checked={value}
|
||||
toggleable={metadata.type == 'password'}
|
||||
onChange={this.onChange}
|
||||
disabled={this.props.disabled}
|
||||
error={(this.props.model.validationError || {})[metadata.name]}
|
||||
>
|
||||
{metadata.type == 'select' && value.options.map((value) => {
|
||||
return <option key={value.id} value={value.id}>{value.label}</option>;
|
||||
})}
|
||||
</Input>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
{i18n('vmware.nova_compute')}
|
||||
</h4>
|
||||
<FieldGroup model={this.props.model} disabled={this.props.disabled}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var AvailabilityZone = React.createClass({
|
||||
addNovaCompute(current) {
|
||||
var collection = this.props.model.get('nova_computes'),
|
||||
index = collection.indexOf(current),
|
||||
newItem = current.clone();
|
||||
var targetNode = _.cloneDeep(newItem.get('target_node'));
|
||||
if (this.props.isLocked) {
|
||||
targetNode.current = {id: 'invalid'};
|
||||
}
|
||||
newItem.set('target_node', targetNode);
|
||||
collection.add(newItem, {at: index + 1});
|
||||
this.setState({model: this.props.model});
|
||||
_.defer(() => dispatcher.trigger('vcenter_model_update'));
|
||||
},
|
||||
removeNovaCompute(current) {
|
||||
var collection = this.props.model.get('nova_computes');
|
||||
collection.remove(current);
|
||||
this.setState({model: this.props.model});
|
||||
_.defer(() => dispatcher.trigger('vcenter_model_update'));
|
||||
},
|
||||
renderFields() {
|
||||
var model = this.props.model,
|
||||
meta = model.get('metadata');
|
||||
meta = _.filter(meta, VmWareModels.isRegularField);
|
||||
return (
|
||||
<FieldGroup model={model} disabled={this.props.isLocked || this.props.disabled}/>
|
||||
);
|
||||
},
|
||||
renderComputes(actions) {
|
||||
var novaComputes = this.props.model.get('nova_computes'),
|
||||
isSingleInstance = novaComputes.length == 1,
|
||||
disabled = actions.disable.nova_computes,
|
||||
cluster = this.props.cluster;
|
||||
|
||||
return (
|
||||
<div className='col-xs-offset-1'>
|
||||
<h3>
|
||||
{i18n('vmware.nova_computes')}
|
||||
</h3>
|
||||
{novaComputes.map(function(compute) {
|
||||
return (
|
||||
<NovaCompute
|
||||
key={compute.cid}
|
||||
model={compute}
|
||||
onAdd={this.addNovaCompute}
|
||||
onRemove={this.removeNovaCompute}
|
||||
isRemovable={isSingleInstance}
|
||||
disabled={disabled.result || this.props.disabled}
|
||||
isLocked={this.props.isLocked}
|
||||
cluster={cluster}
|
||||
/>
|
||||
);
|
||||
}, this)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render() {
|
||||
var restrictActions = this.props.model.testRestrictions();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.renderFields(restrictActions)}
|
||||
{this.renderComputes(restrictActions)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var AvailabilityZones = React.createClass({
|
||||
render() {
|
||||
if (!this.props.collection) return null;
|
||||
return (
|
||||
<div className='col-xs-12 forms-box'>
|
||||
<h3>
|
||||
{i18n('vmware.availability_zones')}
|
||||
{this.props.tooltipText &&
|
||||
<Tooltip text={this.props.tooltipText} placement='right'>
|
||||
<i className='glyphicon glyphicon-warning-sign tooltip-icon' />
|
||||
</Tooltip>
|
||||
}
|
||||
</h3>
|
||||
{this.props.collection.map(function(model) {
|
||||
return <AvailabilityZone key={model.cid} model={model} disabled={this.props.disabled} cluster={this.props.cluster} isLocked={this.props.isLocked}/>;
|
||||
}, this)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var UnassignedNodesWarning = React.createClass({
|
||||
render() {
|
||||
if (!this.props.errors || !this.props.errors.unassigned_nodes) return null;
|
||||
return (
|
||||
<div className='alert alert-danger'>
|
||||
<div>
|
||||
{i18n('vmware.unassigned_nodes')}
|
||||
</div>
|
||||
<ul className='unassigned-node-list'>
|
||||
{
|
||||
this.props.errors.unassigned_nodes.map((node) => {
|
||||
return (
|
||||
<li key={node.id}
|
||||
className='unassigned-node'>
|
||||
<span
|
||||
className='unassigned-node-name'>{node.get('name')}</span>
|
||||
|
||||
({node.get('mac')})
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var VmWareTab = React.createClass({
|
||||
mixins: [
|
||||
unsavedChangesMixin
|
||||
],
|
||||
statics: {
|
||||
isVisible(cluster) {
|
||||
return cluster.get('settings').get('common.use_vcenter').value;
|
||||
},
|
||||
fetchData(options) {
|
||||
if (!options.cluster.get('vcenter_defaults')) {
|
||||
var defaultModel = new VmWareModels.VCenter({id: options.cluster.id});
|
||||
defaultModel.loadDefaults = true;
|
||||
options.cluster.set({vcenter_defaults: defaultModel});
|
||||
}
|
||||
return $.when(
|
||||
options.cluster.get('vcenter').fetch({cache: true}),
|
||||
options.cluster.get('vcenter_defaults').fetch({cache: true})
|
||||
);
|
||||
}
|
||||
},
|
||||
onModelSync() {
|
||||
this.actions = this.model.testRestrictions();
|
||||
if (!this.model.loadDefaults) {
|
||||
this.json = JSON.stringify(this.model.toJSON());
|
||||
}
|
||||
this.model.loadDefaults = false;
|
||||
this.setState({model: this.model});
|
||||
},
|
||||
componentDidMount() {
|
||||
this.clusterId = this.props.cluster.id;
|
||||
this.model = this.props.cluster.get('vcenter');
|
||||
this.model.on('sync', this.onModelSync, this);
|
||||
this.defaultModel = this.props.cluster.get('vcenter_defaults');
|
||||
this.defaultsJson = JSON.stringify(this.defaultModel.toJSON());
|
||||
this.setState({model: this.model, defaultModel: this.defaultModel});
|
||||
|
||||
this.model.setModels({
|
||||
cluster: this.props.cluster,
|
||||
settings: this.props.cluster.get('settings'),
|
||||
networking_parameters: this.props.cluster.get('networkConfiguration').get('networking_parameters')
|
||||
});
|
||||
|
||||
var FieldGroup = React.createClass({
|
||||
render() {
|
||||
var restrictions = this.props.model.testRestrictions();
|
||||
var metadata = _.filter(this.props.model.get('metadata'), VmWareModels.isRegularField);
|
||||
var fields = metadata.map(function(meta) {
|
||||
return (
|
||||
<Field
|
||||
key={meta.name}
|
||||
model={this.props.model}
|
||||
metadata={meta}
|
||||
disabled={this.props.disabled}
|
||||
disableWarning={restrictions.disable[meta.name]}
|
||||
/>
|
||||
);
|
||||
}, this);
|
||||
return (
|
||||
<div>
|
||||
{fields}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
this.onModelSync();
|
||||
dispatcher.on('vcenter_model_update', () => {
|
||||
if (this.isMounted()) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
});
|
||||
|
||||
var GenericSection = React.createClass({
|
||||
render() {
|
||||
if (!this.props.model) return null;
|
||||
return (
|
||||
<div className='col-xs-12 forms-box'>
|
||||
<h3>
|
||||
{this.props.title}
|
||||
{this.props.tooltipText &&
|
||||
<Tooltip text={this.props.tooltipText} placement='right'>
|
||||
<i className='glyphicon glyphicon-warning-sign tooltip-icon' />
|
||||
</Tooltip>
|
||||
}
|
||||
</h3>
|
||||
<FieldGroup model={this.props.model} disabled={this.props.disabled}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
componentWillUnmount() {
|
||||
this.model.off('sync', null, this);
|
||||
dispatcher.off('vcenter_model_update');
|
||||
},
|
||||
getInitialState() {
|
||||
return {model: null};
|
||||
},
|
||||
readData() {
|
||||
return this.model.fetch();
|
||||
},
|
||||
onLoadDefaults() {
|
||||
this.model.loadDefaults = true;
|
||||
this.model.fetch().done(() => {
|
||||
this.model.loadDefaults = false;
|
||||
});
|
||||
|
||||
var NovaCompute = React.createClass({
|
||||
render() {
|
||||
if (!this.props.model) return null;
|
||||
|
||||
// add nodes of 'compute-vmware' type to targetNode select
|
||||
var targetNode = this.props.model.get('target_node') || {};
|
||||
var nodes = this.props.cluster.get('nodes').filter((node) => node.hasRole('compute-vmware'));
|
||||
|
||||
targetNode.options = [];
|
||||
if (targetNode.current.id == 'controllers' || !this.props.isLocked) {
|
||||
targetNode.options.push({id: 'controllers', label: 'controllers'});
|
||||
} else {
|
||||
targetNode.options.push({id: 'invalid', label: 'Select node'});
|
||||
}
|
||||
nodes.forEach((node) => {
|
||||
targetNode.options.push({
|
||||
id: node.get('hostname'),
|
||||
label: node.get('name') + ' (' + node.get('mac').substr(9) + ')'
|
||||
});
|
||||
});
|
||||
|
||||
this.props.model.set('target_node', targetNode);
|
||||
|
||||
return (
|
||||
<div className='nova-compute'>
|
||||
<h4>
|
||||
<div className='btn-group'>
|
||||
<button
|
||||
className='btn btn-link'
|
||||
disabled={this.props.disabled}
|
||||
onClick={() => {
|
||||
this.props.onAdd(this.props.model);
|
||||
}}
|
||||
>
|
||||
<i className='glyphicon glyphicon-plus-sign' />
|
||||
</button>
|
||||
{!this.props.isRemovable &&
|
||||
<button
|
||||
className='btn btn-link'
|
||||
disabled={this.props.disabled}
|
||||
onClick={() => {
|
||||
this.props.onRemove(this.props.model);
|
||||
}}
|
||||
>
|
||||
<i className='glyphicon glyphicon-minus-sign' />
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
{i18n('vmware.nova_compute')}
|
||||
</h4>
|
||||
<FieldGroup model={this.props.model} disabled={this.props.disabled}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
applyChanges() {
|
||||
return this.model.save();
|
||||
},
|
||||
revertChanges() {
|
||||
return this.readData();
|
||||
},
|
||||
hasChanges() {
|
||||
return this.detectChanges(this.json, JSON.stringify(this.model.toJSON()));
|
||||
},
|
||||
detectChanges(oldJson, currentJson) {
|
||||
var old, current;
|
||||
try {
|
||||
old = JSON.parse(oldJson);
|
||||
current = JSON.parse(currentJson);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
var oldData = JSON.stringify(old, (key, data) => {
|
||||
if (key == 'target_node') {
|
||||
delete data.options;
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
var AvailabilityZone = React.createClass({
|
||||
addNovaCompute(current) {
|
||||
var collection = this.props.model.get('nova_computes'),
|
||||
index = collection.indexOf(current),
|
||||
newItem = current.clone();
|
||||
var targetNode = _.cloneDeep(newItem.get('target_node'));
|
||||
if (this.props.isLocked) {
|
||||
targetNode.current = {id: 'invalid'};
|
||||
}
|
||||
newItem.set('target_node', targetNode);
|
||||
collection.add(newItem, {at: index + 1});
|
||||
this.setState({model: this.props.model});
|
||||
_.defer(() => dispatcher.trigger('vcenter_model_update'));
|
||||
},
|
||||
removeNovaCompute(current) {
|
||||
var collection = this.props.model.get('nova_computes');
|
||||
collection.remove(current);
|
||||
this.setState({model: this.props.model});
|
||||
_.defer(() => dispatcher.trigger('vcenter_model_update'));
|
||||
},
|
||||
renderFields() {
|
||||
var model = this.props.model,
|
||||
meta = model.get('metadata');
|
||||
meta = _.filter(meta, VmWareModels.isRegularField);
|
||||
return (
|
||||
<FieldGroup model={model} disabled={this.props.isLocked || this.props.disabled}/>
|
||||
);
|
||||
},
|
||||
renderComputes(actions) {
|
||||
var novaComputes = this.props.model.get('nova_computes'),
|
||||
isSingleInstance = novaComputes.length == 1,
|
||||
disabled = actions.disable.nova_computes,
|
||||
cluster = this.props.cluster;
|
||||
|
||||
return (
|
||||
<div className='col-xs-offset-1'>
|
||||
<h3>
|
||||
{i18n('vmware.nova_computes')}
|
||||
</h3>
|
||||
{novaComputes.map(function(compute) {
|
||||
return (
|
||||
<NovaCompute
|
||||
key={compute.cid}
|
||||
model={compute}
|
||||
onAdd={this.addNovaCompute}
|
||||
onRemove={this.removeNovaCompute}
|
||||
isRemovable={isSingleInstance}
|
||||
disabled={disabled.result || this.props.disabled}
|
||||
isLocked={this.props.isLocked}
|
||||
cluster={cluster}
|
||||
/>
|
||||
);
|
||||
}, this)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render() {
|
||||
var restrictActions = this.props.model.testRestrictions();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.renderFields(restrictActions)}
|
||||
{this.renderComputes(restrictActions)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
var currentData = JSON.stringify(current, (key, data) => {
|
||||
if (key == 'target_node') {
|
||||
delete data.options;
|
||||
}
|
||||
return data;
|
||||
});
|
||||
return oldData != currentData;
|
||||
},
|
||||
isSavingPossible() {
|
||||
return !this.state.model.validationError;
|
||||
},
|
||||
render() {
|
||||
if (!this.state.model || !this.actions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var AvailabilityZones = React.createClass({
|
||||
render() {
|
||||
if (!this.props.collection) return null;
|
||||
return (
|
||||
<div className='col-xs-12 forms-box'>
|
||||
<h3>
|
||||
{i18n('vmware.availability_zones')}
|
||||
{this.props.tooltipText &&
|
||||
<Tooltip text={this.props.tooltipText} placement='right'>
|
||||
<i className='glyphicon glyphicon-warning-sign tooltip-icon' />
|
||||
</Tooltip>
|
||||
}
|
||||
</h3>
|
||||
{this.props.collection.map(function(model) {
|
||||
return <AvailabilityZone key={model.cid} model={model} disabled={this.props.disabled} cluster={this.props.cluster} isLocked={this.props.isLocked}/>;
|
||||
}, this)}
|
||||
</div>
|
||||
);
|
||||
var model = this.state.model,
|
||||
currentJson = JSON.stringify(this.model.toJSON()),
|
||||
editable = this.props.cluster.isAvailableForSettingsChanges(),
|
||||
hide = this.actions.hide || {},
|
||||
disable = this.actions.disable || {};
|
||||
|
||||
model.isValid();
|
||||
var hasChanges = this.detectChanges(this.json, currentJson);
|
||||
var hasDefaultsChanges = this.detectChanges(this.defaultsJson, currentJson);
|
||||
var saveDisabled = !hasChanges || !this.isSavingPossible(),
|
||||
defaultsDisabled = !hasDefaultsChanges;
|
||||
|
||||
return (
|
||||
<div className='row'>
|
||||
<div className='title'>{i18n('vmware.title')}</div>
|
||||
<UnassignedNodesWarning errors={model.validationError}/>
|
||||
{!hide.availability_zones.result &&
|
||||
<AvailabilityZones
|
||||
collection={model.get('availability_zones')}
|
||||
disabled={disable.availability_zones.result}
|
||||
tooltipText={disable.availability_zones.message}
|
||||
isLocked={!editable}
|
||||
cluster={this.props.cluster}
|
||||
/>
|
||||
}
|
||||
});
|
||||
|
||||
var UnassignedNodesWarning = React.createClass({
|
||||
render() {
|
||||
if (!this.props.errors || !this.props.errors.unassigned_nodes) return null;
|
||||
return (
|
||||
<div className='alert alert-danger'>
|
||||
<div>
|
||||
{i18n('vmware.unassigned_nodes')}
|
||||
</div>
|
||||
<ul className='unassigned-node-list'>
|
||||
{
|
||||
this.props.errors.unassigned_nodes.map((node) => {
|
||||
return (
|
||||
<li key={node.id}
|
||||
className='unassigned-node'>
|
||||
<span
|
||||
className='unassigned-node-name'>{node.get('name')}</span>
|
||||
|
||||
({node.get('mac')})
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
{!hide.network.result &&
|
||||
<GenericSection
|
||||
model={model.get('network')}
|
||||
title={i18n('vmware.network')}
|
||||
disabled={!editable || disable.network.result}
|
||||
tooltipText={disable.network.message}
|
||||
/>
|
||||
}
|
||||
});
|
||||
|
||||
var VmWareTab = React.createClass({
|
||||
mixins: [
|
||||
unsavedChangesMixin
|
||||
],
|
||||
statics: {
|
||||
isVisible(cluster) {
|
||||
return cluster.get('settings').get('common.use_vcenter').value;
|
||||
},
|
||||
fetchData(options) {
|
||||
if (!options.cluster.get('vcenter_defaults')) {
|
||||
var defaultModel = new VmWareModels.VCenter({id: options.cluster.id});
|
||||
defaultModel.loadDefaults = true;
|
||||
options.cluster.set({vcenter_defaults: defaultModel});
|
||||
}
|
||||
return $.when(
|
||||
options.cluster.get('vcenter').fetch({cache: true}),
|
||||
options.cluster.get('vcenter_defaults').fetch({cache: true})
|
||||
);
|
||||
}
|
||||
},
|
||||
onModelSync() {
|
||||
this.actions = this.model.testRestrictions();
|
||||
if (!this.model.loadDefaults) {
|
||||
this.json = JSON.stringify(this.model.toJSON());
|
||||
}
|
||||
this.model.loadDefaults = false;
|
||||
this.setState({model: this.model});
|
||||
},
|
||||
componentDidMount() {
|
||||
this.clusterId = this.props.cluster.id;
|
||||
this.model = this.props.cluster.get('vcenter');
|
||||
this.model.on('sync', this.onModelSync, this);
|
||||
this.defaultModel = this.props.cluster.get('vcenter_defaults');
|
||||
this.defaultsJson = JSON.stringify(this.defaultModel.toJSON());
|
||||
this.setState({model: this.model, defaultModel: this.defaultModel});
|
||||
|
||||
this.model.setModels({
|
||||
cluster: this.props.cluster,
|
||||
settings: this.props.cluster.get('settings'),
|
||||
networking_parameters: this.props.cluster.get('networkConfiguration').get('networking_parameters')
|
||||
});
|
||||
|
||||
this.onModelSync();
|
||||
dispatcher.on('vcenter_model_update', () => {
|
||||
if (this.isMounted()) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
});
|
||||
},
|
||||
componentWillUnmount() {
|
||||
this.model.off('sync', null, this);
|
||||
dispatcher.off('vcenter_model_update');
|
||||
},
|
||||
getInitialState() {
|
||||
return {model: null};
|
||||
},
|
||||
readData() {
|
||||
return this.model.fetch();
|
||||
},
|
||||
onLoadDefaults() {
|
||||
this.model.loadDefaults = true;
|
||||
this.model.fetch().done(() => {
|
||||
this.model.loadDefaults = false;
|
||||
});
|
||||
},
|
||||
applyChanges() {
|
||||
return this.model.save();
|
||||
},
|
||||
revertChanges() {
|
||||
return this.readData();
|
||||
},
|
||||
hasChanges() {
|
||||
return this.detectChanges(this.json, JSON.stringify(this.model.toJSON()));
|
||||
},
|
||||
detectChanges(oldJson, currentJson) {
|
||||
var old, current;
|
||||
try {
|
||||
old = JSON.parse(oldJson);
|
||||
current = JSON.parse(currentJson);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
var oldData = JSON.stringify(old, (key, data) => {
|
||||
if (key == 'target_node') {
|
||||
delete data.options;
|
||||
}
|
||||
return data;
|
||||
});
|
||||
var currentData = JSON.stringify(current, (key, data) => {
|
||||
if (key == 'target_node') {
|
||||
delete data.options;
|
||||
}
|
||||
return data;
|
||||
});
|
||||
return oldData != currentData;
|
||||
},
|
||||
isSavingPossible() {
|
||||
return !this.state.model.validationError;
|
||||
},
|
||||
render() {
|
||||
if (!this.state.model || !this.actions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var model = this.state.model,
|
||||
currentJson = JSON.stringify(this.model.toJSON()),
|
||||
editable = this.props.cluster.isAvailableForSettingsChanges(),
|
||||
hide = this.actions.hide || {},
|
||||
disable = this.actions.disable || {};
|
||||
|
||||
model.isValid();
|
||||
var hasChanges = this.detectChanges(this.json, currentJson);
|
||||
var hasDefaultsChanges = this.detectChanges(this.defaultsJson, currentJson);
|
||||
var saveDisabled = !hasChanges || !this.isSavingPossible(),
|
||||
defaultsDisabled = !hasDefaultsChanges;
|
||||
|
||||
return (
|
||||
<div className='row'>
|
||||
<div className='title'>{i18n('vmware.title')}</div>
|
||||
<UnassignedNodesWarning errors={model.validationError}/>
|
||||
{!hide.availability_zones.result &&
|
||||
<AvailabilityZones
|
||||
collection={model.get('availability_zones')}
|
||||
disabled={disable.availability_zones.result}
|
||||
tooltipText={disable.availability_zones.message}
|
||||
isLocked={!editable}
|
||||
cluster={this.props.cluster}
|
||||
/>
|
||||
}
|
||||
{!hide.network.result &&
|
||||
<GenericSection
|
||||
model={model.get('network')}
|
||||
title={i18n('vmware.network')}
|
||||
disabled={!editable || disable.network.result}
|
||||
tooltipText={disable.network.message}
|
||||
/>
|
||||
}
|
||||
{!hide.glance.result &&
|
||||
<GenericSection
|
||||
model={model.get('glance')}
|
||||
title={i18n('vmware.glance')}
|
||||
disabled={!editable || disable.glance.result}
|
||||
tooltipText={disable.glance.message}
|
||||
/>
|
||||
}
|
||||
<div className='col-xs-12 page-buttons content-elements'>
|
||||
<div className='well clearfix'>
|
||||
<div className='btn-group pull-right'>
|
||||
<button className='btn btn-default btn-load-defaults' onClick={this.onLoadDefaults} disabled={!editable || defaultsDisabled}>
|
||||
{i18n('vmware.reset_to_defaults')}
|
||||
</button>
|
||||
<button className='btn btn-default btn-revert-changes' onClick={this.revertChanges} disabled={!hasChanges}>
|
||||
{i18n('vmware.cancel')}
|
||||
</button>
|
||||
<button className='btn btn-success btn-apply-changes' onClick={this.applyChanges} disabled={saveDisabled}>
|
||||
{i18n('vmware.apply')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
{!hide.glance.result &&
|
||||
<GenericSection
|
||||
model={model.get('glance')}
|
||||
title={i18n('vmware.glance')}
|
||||
disabled={!editable || disable.glance.result}
|
||||
tooltipText={disable.glance.message}
|
||||
/>
|
||||
}
|
||||
});
|
||||
<div className='col-xs-12 page-buttons content-elements'>
|
||||
<div className='well clearfix'>
|
||||
<div className='btn-group pull-right'>
|
||||
<button className='btn btn-default btn-load-defaults' onClick={this.onLoadDefaults} disabled={!editable || defaultsDisabled}>
|
||||
{i18n('vmware.reset_to_defaults')}
|
||||
</button>
|
||||
<button className='btn btn-default btn-revert-changes' onClick={this.revertChanges} disabled={!hasChanges}>
|
||||
{i18n('vmware.cancel')}
|
||||
</button>
|
||||
<button className='btn btn-success btn-apply-changes' onClick={this.applyChanges} disabled={saveDisabled}>
|
||||
{i18n('vmware.apply')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default VmWareTab;
|
||||
export default VmWareTab;
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
**/
|
||||
|
||||
define(['./intern'], function(config) {
|
||||
'use strict';
|
||||
config.environments = [{browserName: 'chrome'}];
|
||||
return config;
|
||||
'use strict';
|
||||
config.environments = [{browserName: 'chrome'}];
|
||||
return config;
|
||||
});
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
**/
|
||||
|
||||
define(['./intern'], function(config) {
|
||||
'use strict';
|
||||
config.environments = [{browserName: 'firefox'}];
|
||||
return config;
|
||||
'use strict';
|
||||
config.environments = [{browserName: 'firefox'}];
|
||||
return config;
|
||||
});
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
**/
|
||||
|
||||
define(['./intern'], function(config) {
|
||||
'use strict';
|
||||
config.environments = [{browserName: 'phantomjs'}];
|
||||
return config;
|
||||
'use strict';
|
||||
config.environments = [{browserName: 'phantomjs'}];
|
||||
return config;
|
||||
});
|
||||
|
|
|
@ -15,14 +15,14 @@
|
|||
**/
|
||||
|
||||
define(function() {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
return {
|
||||
proxyPort: 9057,
|
||||
proxyUrl: 'http://localhost:9057/',
|
||||
maxConcurrency: 1,
|
||||
grep: /^/,
|
||||
excludeInstrumentation: /^/,
|
||||
reporters: ['console', 'tests/functional/screenshot_on_fail']
|
||||
};
|
||||
return {
|
||||
proxyPort: 9057,
|
||||
proxyUrl: 'http://localhost:9057/',
|
||||
maxConcurrency: 1,
|
||||
grep: /^/,
|
||||
excludeInstrumentation: /^/,
|
||||
reporters: ['console', 'tests/functional/screenshot_on_fail']
|
||||
};
|
||||
});
|
||||
|
|
|
@ -15,494 +15,494 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern/dojo/node!lodash',
|
||||
'intern/chai!assert',
|
||||
'intern/dojo/node!fs',
|
||||
'intern/dojo/node!leadfoot/Command'
|
||||
'intern/dojo/node!lodash',
|
||||
'intern/chai!assert',
|
||||
'intern/dojo/node!fs',
|
||||
'intern/dojo/node!leadfoot/Command'
|
||||
], function(_, assert, fs, Command) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
_.defaults(Command.prototype, {
|
||||
clickLinkByText: function(text) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByLinkText(text)
|
||||
.click()
|
||||
.end();
|
||||
});
|
||||
},
|
||||
clickByCssSelector: function(cssSelector) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.click()
|
||||
.end();
|
||||
});
|
||||
},
|
||||
takeScreenshotAndSave: function(filename) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.takeScreenshot()
|
||||
.then(function(buffer) {
|
||||
var targetDir = process.env.ARTIFACTS || process.cwd();
|
||||
if (!filename) filename = new Date().toTimeString();
|
||||
filename = filename.replace(/[\s\*\?\\\/]/g, '_');
|
||||
filename = targetDir + '/' + filename + '.png';
|
||||
console.log('Saving screenshot to', filename); // eslint-disable-line no-console
|
||||
fs.writeFileSync(filename, buffer);
|
||||
});
|
||||
});
|
||||
},
|
||||
waitForCssSelector: function(cssSelector, timeout) {
|
||||
return new this.constructor(this, function() {
|
||||
var self = this, currentTimeout;
|
||||
return this.parent
|
||||
.getFindTimeout()
|
||||
.then(function(value) {
|
||||
currentTimeout = value;
|
||||
})
|
||||
.setFindTimeout(timeout)
|
||||
.findByCssSelector(cssSelector)
|
||||
.catch(function(error) {
|
||||
self.parent.setFindTimeout(currentTimeout);
|
||||
throw error;
|
||||
})
|
||||
.end()
|
||||
.then(function() {
|
||||
self.parent.setFindTimeout(currentTimeout);
|
||||
});
|
||||
});
|
||||
},
|
||||
waitForElementDeletion: function(cssSelector, timeout) {
|
||||
return new this.constructor(this, function() {
|
||||
var self = this, currentTimeout;
|
||||
return this.parent
|
||||
.getFindTimeout()
|
||||
.then(function(value) {
|
||||
currentTimeout = value;
|
||||
})
|
||||
.setFindTimeout(timeout)
|
||||
.waitForDeletedByCssSelector(cssSelector)
|
||||
.catch(function(error) {
|
||||
self.parent.setFindTimeout(currentTimeout);
|
||||
if (error.name != 'Timeout') throw error;
|
||||
})
|
||||
.then(function() {
|
||||
self.parent.setFindTimeout(currentTimeout);
|
||||
});
|
||||
});
|
||||
},
|
||||
setInputValue: function(cssSelector, value) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.clearValue()
|
||||
.type(value)
|
||||
.end();
|
||||
});
|
||||
},
|
||||
// Drag-n-drop helpers
|
||||
// Taken from not yet accepted pull request to leadfoot from
|
||||
// https://github.com/theintern/leadfoot/pull/16
|
||||
dragFrom: function(element, x, y) {
|
||||
if (typeof element === 'number') {
|
||||
y = x;
|
||||
x = element;
|
||||
element = null;
|
||||
_.defaults(Command.prototype, {
|
||||
clickLinkByText: function(text) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByLinkText(text)
|
||||
.click()
|
||||
.end();
|
||||
});
|
||||
},
|
||||
clickByCssSelector: function(cssSelector) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.click()
|
||||
.end();
|
||||
});
|
||||
},
|
||||
takeScreenshotAndSave: function(filename) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.takeScreenshot()
|
||||
.then(function(buffer) {
|
||||
var targetDir = process.env.ARTIFACTS || process.cwd();
|
||||
if (!filename) filename = new Date().toTimeString();
|
||||
filename = filename.replace(/[\s\*\?\\\/]/g, '_');
|
||||
filename = targetDir + '/' + filename + '.png';
|
||||
console.log('Saving screenshot to', filename); // eslint-disable-line no-console
|
||||
fs.writeFileSync(filename, buffer);
|
||||
});
|
||||
});
|
||||
},
|
||||
waitForCssSelector: function(cssSelector, timeout) {
|
||||
return new this.constructor(this, function() {
|
||||
var self = this, currentTimeout;
|
||||
return this.parent
|
||||
.getFindTimeout()
|
||||
.then(function(value) {
|
||||
currentTimeout = value;
|
||||
})
|
||||
.setFindTimeout(timeout)
|
||||
.findByCssSelector(cssSelector)
|
||||
.catch(function(error) {
|
||||
self.parent.setFindTimeout(currentTimeout);
|
||||
throw error;
|
||||
})
|
||||
.end()
|
||||
.then(function() {
|
||||
self.parent.setFindTimeout(currentTimeout);
|
||||
});
|
||||
});
|
||||
},
|
||||
waitForElementDeletion: function(cssSelector, timeout) {
|
||||
return new this.constructor(this, function() {
|
||||
var self = this, currentTimeout;
|
||||
return this.parent
|
||||
.getFindTimeout()
|
||||
.then(function(value) {
|
||||
currentTimeout = value;
|
||||
})
|
||||
.setFindTimeout(timeout)
|
||||
.waitForDeletedByCssSelector(cssSelector)
|
||||
.catch(function(error) {
|
||||
self.parent.setFindTimeout(currentTimeout);
|
||||
if (error.name != 'Timeout') throw error;
|
||||
})
|
||||
.then(function() {
|
||||
self.parent.setFindTimeout(currentTimeout);
|
||||
});
|
||||
});
|
||||
},
|
||||
setInputValue: function(cssSelector, value) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.clearValue()
|
||||
.type(value)
|
||||
.end();
|
||||
});
|
||||
},
|
||||
// Drag-n-drop helpers
|
||||
// Taken from not yet accepted pull request to leadfoot from
|
||||
// https://github.com/theintern/leadfoot/pull/16
|
||||
dragFrom: function(element, x, y) {
|
||||
if (typeof element === 'number') {
|
||||
y = x;
|
||||
x = element;
|
||||
element = null;
|
||||
}
|
||||
|
||||
return new this.constructor(this, function() {
|
||||
this._session._dragSource = {
|
||||
element: element || this.parent._context[0],
|
||||
x: x,
|
||||
y: y
|
||||
};
|
||||
});
|
||||
},
|
||||
dragTo: function(element, x, y) {
|
||||
if (typeof element === 'number') {
|
||||
y = x;
|
||||
x = element;
|
||||
element = null;
|
||||
}
|
||||
|
||||
return new this.constructor(this, function() {
|
||||
var dragTarget = {
|
||||
element: element || this.parent._context[0],
|
||||
x: x,
|
||||
y: y
|
||||
};
|
||||
var dragSource = this._session._dragSource;
|
||||
this._session._dragSource = null;
|
||||
|
||||
return this.parent.executeAsync(function(dragFrom, dragTo, done) {
|
||||
var dragAndDrop = (function() {
|
||||
var dispatchEvent;
|
||||
var createEvent;
|
||||
|
||||
// Setup methods to call the proper event creation and dispatch functions for the current platform.
|
||||
if (document.createEvent) {
|
||||
dispatchEvent = function(element, eventName, event) {
|
||||
element.dispatchEvent(event);
|
||||
return event;
|
||||
};
|
||||
|
||||
createEvent = function(eventName) {
|
||||
return document.createEvent(eventName);
|
||||
};
|
||||
} else if (document.createEventObject) {
|
||||
dispatchEvent = function(element, eventName, event) {
|
||||
element.fireEvent('on' + eventName, event);
|
||||
return event;
|
||||
};
|
||||
|
||||
createEvent = function(eventType) {
|
||||
return document.createEventObject(eventType);
|
||||
};
|
||||
}
|
||||
|
||||
return new this.constructor(this, function() {
|
||||
this._session._dragSource = {
|
||||
element: element || this.parent._context[0],
|
||||
x: x,
|
||||
y: y
|
||||
};
|
||||
});
|
||||
},
|
||||
dragTo: function(element, x, y) {
|
||||
if (typeof element === 'number') {
|
||||
y = x;
|
||||
x = element;
|
||||
element = null;
|
||||
function createCustomEvent(eventName, screenX, screenY, clientX, clientY) {
|
||||
var event = createEvent('CustomEvent');
|
||||
if (event.initCustomEvent) {
|
||||
event.initCustomEvent(eventName, true, true, null);
|
||||
}
|
||||
|
||||
event.view = window;
|
||||
event.detail = 0;
|
||||
event.screenX = screenX;
|
||||
event.screenY = screenY;
|
||||
event.clientX = clientX;
|
||||
event.clientY = clientY;
|
||||
event.ctrlKey = false;
|
||||
event.altKey = false;
|
||||
event.shiftKey = false;
|
||||
event.metaKey = false;
|
||||
event.button = 0;
|
||||
event.relatedTarget = null;
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
return new this.constructor(this, function() {
|
||||
var dragTarget = {
|
||||
element: element || this.parent._context[0],
|
||||
x: x,
|
||||
y: y
|
||||
function createDragEvent(eventName, options, dataTransfer) {
|
||||
var screenX = window.screenX + options.clientX;
|
||||
var screenY = window.screenY + options.clientY;
|
||||
var clientX = options.clientX;
|
||||
var clientY = options.clientY;
|
||||
var event;
|
||||
|
||||
if (!dataTransfer) {
|
||||
dataTransfer = {
|
||||
data: options.dragData || {},
|
||||
setData: function(eventName, val) {
|
||||
if (typeof val === 'string') {
|
||||
this.data[eventName] = val;
|
||||
}
|
||||
},
|
||||
getData: function(eventName) {
|
||||
return this.data[eventName];
|
||||
},
|
||||
clearData: function() {
|
||||
this.data = {};
|
||||
return true;
|
||||
},
|
||||
setDragImage: function() {}
|
||||
};
|
||||
var dragSource = this._session._dragSource;
|
||||
this._session._dragSource = null;
|
||||
}
|
||||
|
||||
return this.parent.executeAsync(function(dragFrom, dragTo, done) {
|
||||
var dragAndDrop = (function() {
|
||||
var dispatchEvent;
|
||||
var createEvent;
|
||||
try {
|
||||
event = createEvent('DragEvent');
|
||||
event.initDragEvent(eventName, true, true, window, 0, screenX, screenY, clientX,
|
||||
clientY, false, false, false, false, 0, null, dataTransfer);
|
||||
} catch (error) {
|
||||
event = createCustomEvent(eventName, screenX, screenY, clientX, clientY);
|
||||
event.dataTransfer = dataTransfer;
|
||||
}
|
||||
|
||||
// Setup methods to call the proper event creation and dispatch functions for the current platform.
|
||||
if (document.createEvent) {
|
||||
dispatchEvent = function(element, eventName, event) {
|
||||
element.dispatchEvent(event);
|
||||
return event;
|
||||
};
|
||||
return event;
|
||||
}
|
||||
|
||||
createEvent = function(eventName) {
|
||||
return document.createEvent(eventName);
|
||||
};
|
||||
} else if (document.createEventObject) {
|
||||
dispatchEvent = function(element, eventName, event) {
|
||||
element.fireEvent('on' + eventName, event);
|
||||
return event;
|
||||
};
|
||||
function createMouseEvent(eventName, options, dataTransfer) {
|
||||
var screenX = window.screenX + options.clientX;
|
||||
var screenY = window.screenY + options.clientY;
|
||||
var clientX = options.clientX;
|
||||
var clientY = options.clientY;
|
||||
var event;
|
||||
|
||||
createEvent = function(eventType) {
|
||||
return document.createEventObject(eventType);
|
||||
};
|
||||
}
|
||||
try {
|
||||
event = createEvent('MouseEvent');
|
||||
event.initMouseEvent(eventName, true, true, window, 0, screenX, screenY, clientX, clientY,
|
||||
false, false, false, false, 0, null);
|
||||
} catch (error) {
|
||||
event = createCustomEvent(eventName, screenX, screenY, clientX, clientY);
|
||||
}
|
||||
|
||||
function createCustomEvent(eventName, screenX, screenY, clientX, clientY) {
|
||||
var event = createEvent('CustomEvent');
|
||||
if (event.initCustomEvent) {
|
||||
event.initCustomEvent(eventName, true, true, null);
|
||||
}
|
||||
if (dataTransfer) {
|
||||
event.dataTransfer = dataTransfer;
|
||||
}
|
||||
|
||||
event.view = window;
|
||||
event.detail = 0;
|
||||
event.screenX = screenX;
|
||||
event.screenY = screenY;
|
||||
event.clientX = clientX;
|
||||
event.clientY = clientY;
|
||||
event.ctrlKey = false;
|
||||
event.altKey = false;
|
||||
event.shiftKey = false;
|
||||
event.metaKey = false;
|
||||
event.button = 0;
|
||||
event.relatedTarget = null;
|
||||
return event;
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
function simulateEvent(element, eventName, dragStartEvent, options) {
|
||||
var dataTransfer = dragStartEvent ? dragStartEvent.dataTransfer : null;
|
||||
var createEvent = eventName.indexOf('mouse') !== -1 ? createMouseEvent : createDragEvent;
|
||||
var event = createEvent(eventName, options, dataTransfer);
|
||||
return dispatchEvent(element, eventName, event);
|
||||
}
|
||||
|
||||
function createDragEvent(eventName, options, dataTransfer) {
|
||||
var screenX = window.screenX + options.clientX;
|
||||
var screenY = window.screenY + options.clientY;
|
||||
var clientX = options.clientX;
|
||||
var clientY = options.clientY;
|
||||
var event;
|
||||
function getClientOffset(elementInfo) {
|
||||
var bounds = elementInfo.element.getBoundingClientRect();
|
||||
var xOffset = bounds.left + (elementInfo.x || ((bounds.right - bounds.left) / 2));
|
||||
var yOffset = bounds.top + (elementInfo.y || ((bounds.bottom - bounds.top) / 2));
|
||||
return {clientX: xOffset, clientY: yOffset};
|
||||
}
|
||||
|
||||
if (!dataTransfer) {
|
||||
dataTransfer = {
|
||||
data: options.dragData || {},
|
||||
setData: function(eventName, val) {
|
||||
if (typeof val === 'string') {
|
||||
this.data[eventName] = val;
|
||||
}
|
||||
},
|
||||
getData: function(eventName) {
|
||||
return this.data[eventName];
|
||||
},
|
||||
clearData: function() {
|
||||
this.data = {};
|
||||
return true;
|
||||
},
|
||||
setDragImage: function() {}
|
||||
};
|
||||
}
|
||||
function doDragAndDrop(source, target, sourceOffset, targetOffset) {
|
||||
simulateEvent(source, 'mousedown', null, sourceOffset);
|
||||
var start = simulateEvent(source, 'dragstart', null, sourceOffset);
|
||||
simulateEvent(target, 'dragenter', start, targetOffset);
|
||||
simulateEvent(target, 'dragover', start, targetOffset);
|
||||
simulateEvent(target, 'drop', start, targetOffset);
|
||||
simulateEvent(source, 'dragend', start, targetOffset);
|
||||
}
|
||||
|
||||
try {
|
||||
event = createEvent('DragEvent');
|
||||
event.initDragEvent(eventName, true, true, window, 0, screenX, screenY, clientX,
|
||||
clientY, false, false, false, false, 0, null, dataTransfer);
|
||||
} catch (error) {
|
||||
event = createCustomEvent(eventName, screenX, screenY, clientX, clientY);
|
||||
event.dataTransfer = dataTransfer;
|
||||
}
|
||||
return function(dragFrom, dragTo) {
|
||||
var fromOffset = getClientOffset(dragFrom);
|
||||
var toOffset = getClientOffset(dragTo);
|
||||
doDragAndDrop(dragFrom.element, dragTo.element, fromOffset, toOffset);
|
||||
};
|
||||
})();
|
||||
|
||||
return event;
|
||||
}
|
||||
try {
|
||||
dragAndDrop(dragFrom, dragTo);
|
||||
done(null);
|
||||
} catch (error) {
|
||||
done(error.message);
|
||||
}
|
||||
}, [dragSource, dragTarget]).finally(function(result) {
|
||||
if (result) {
|
||||
var error = new Error(result);
|
||||
error.name = 'DragAndDropError';
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
// assertion helpers
|
||||
assertElementsExist: function(cssSelector, amount, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findAllByCssSelector(cssSelector)
|
||||
.then(function(elements) {
|
||||
if (!_.isNumber(amount)) {
|
||||
// no amount given - check if any amount of such elements exist
|
||||
message = amount;
|
||||
return assert.ok(elements.length, message);
|
||||
} else {
|
||||
return assert.equal(elements.length, amount, message);
|
||||
}
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementExists: function(cssSelector, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent.assertElementsExist(cssSelector, 1, message);
|
||||
});
|
||||
},
|
||||
assertElementNotExists: function(cssSelector, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent.assertElementsExist(cssSelector, 0, message);
|
||||
});
|
||||
},
|
||||
assertElementAppears: function(cssSelector, timeout, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.waitForCssSelector(cssSelector, timeout)
|
||||
.catch(_.constant(true))
|
||||
.assertElementExists(cssSelector, message);
|
||||
});
|
||||
},
|
||||
assertElementsAppear: function(cssSelector, timeout, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.waitForCssSelector(cssSelector, timeout)
|
||||
.catch(_.constant(true))
|
||||
.assertElementsExist(cssSelector, message);
|
||||
});
|
||||
},
|
||||
assertElementDisappears: function(cssSelector, timeout, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.waitForElementDeletion(cssSelector, timeout)
|
||||
.catch(_.constant(true))
|
||||
.assertElementNotExists(cssSelector, message);
|
||||
});
|
||||
},
|
||||
assertElementEnabled: function(cssSelector, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.isEnabled()
|
||||
.then(function(isEnabled) {
|
||||
return assert.isTrue(isEnabled, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementDisabled: function(cssSelector, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.isEnabled()
|
||||
.then(function(isEnabled) {
|
||||
return assert.isFalse(isEnabled, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementDisplayed: function(cssSelector, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.isDisplayed()
|
||||
.then(function(isDisplayed) {
|
||||
return assert.isTrue(isDisplayed, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementNotDisplayed: function(cssSelector, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.isDisplayed()
|
||||
.then(function(isDisplayed) {
|
||||
return assert.isFalse(isDisplayed, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementTextEquals: function(cssSelector, expectedText, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.getVisibleText()
|
||||
.then(function(actualText) {
|
||||
assert.equal(actualText, expectedText, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementContainsText: function(cssSelector, text, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.getVisibleText()
|
||||
.then(function(actualText) {
|
||||
assert.include(actualText, text, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementAttributeEquals: function(cssSelector, attribute, expectedText, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.getAttribute(attribute)
|
||||
.then(function(actualText) {
|
||||
assert.equal(actualText, expectedText, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementAttributeContains: function(cssSelector, attribute, text, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.getAttribute(attribute)
|
||||
.then(function(actualText) {
|
||||
assert.include(actualText, text, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementPropertyEquals: function(cssSelector, attribute, expectedText, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.getProperty(attribute)
|
||||
.then(function(actualText) {
|
||||
assert.equal(actualText, expectedText, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementPropertyNotEquals: function(cssSelector, attribute, textToCheck, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.getProperty(attribute)
|
||||
.then(function(actualText) {
|
||||
assert.notEqual(actualText, textToCheck, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementPropertyContains: function(cssSelector, attribute, text, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.getProperty(attribute)
|
||||
.then(function(actualText) {
|
||||
assert.include(actualText, text, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementSelected: function(cssSelector, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.isSelected()
|
||||
.then(function(isSelected) {
|
||||
assert.isTrue(isSelected, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementNotSelected: function(cssSelector, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.isSelected()
|
||||
.then(function(isSelected) {
|
||||
assert.isFalse(isSelected, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertIsIntegerContentPositive: function(cssSelector, attributeName) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.getVisibleText().then(function(text) {
|
||||
return assert.isTrue(parseInt(text, 10) > 0, attributeName + ' is greater than 0');
|
||||
})
|
||||
.end();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function createMouseEvent(eventName, options, dataTransfer) {
|
||||
var screenX = window.screenX + options.clientX;
|
||||
var screenY = window.screenY + options.clientY;
|
||||
var clientX = options.clientX;
|
||||
var clientY = options.clientY;
|
||||
var event;
|
||||
var serverHost = '127.0.0.1',
|
||||
serverPort = process.env.NAILGUN_PORT || 5544,
|
||||
serverUrl = 'http://' + serverHost + ':' + serverPort,
|
||||
username = 'admin',
|
||||
password = 'admin';
|
||||
|
||||
try {
|
||||
event = createEvent('MouseEvent');
|
||||
event.initMouseEvent(eventName, true, true, window, 0, screenX, screenY, clientX, clientY,
|
||||
false, false, false, false, 0, null);
|
||||
} catch (error) {
|
||||
event = createCustomEvent(eventName, screenX, screenY, clientX, clientY);
|
||||
}
|
||||
|
||||
if (dataTransfer) {
|
||||
event.dataTransfer = dataTransfer;
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
function simulateEvent(element, eventName, dragStartEvent, options) {
|
||||
var dataTransfer = dragStartEvent ? dragStartEvent.dataTransfer : null;
|
||||
var createEvent = eventName.indexOf('mouse') !== -1 ? createMouseEvent : createDragEvent;
|
||||
var event = createEvent(eventName, options, dataTransfer);
|
||||
return dispatchEvent(element, eventName, event);
|
||||
}
|
||||
|
||||
function getClientOffset(elementInfo) {
|
||||
var bounds = elementInfo.element.getBoundingClientRect();
|
||||
var xOffset = bounds.left + (elementInfo.x || ((bounds.right - bounds.left) / 2));
|
||||
var yOffset = bounds.top + (elementInfo.y || ((bounds.bottom - bounds.top) / 2));
|
||||
return {clientX: xOffset, clientY: yOffset};
|
||||
}
|
||||
|
||||
function doDragAndDrop(source, target, sourceOffset, targetOffset) {
|
||||
simulateEvent(source, 'mousedown', null, sourceOffset);
|
||||
var start = simulateEvent(source, 'dragstart', null, sourceOffset);
|
||||
simulateEvent(target, 'dragenter', start, targetOffset);
|
||||
simulateEvent(target, 'dragover', start, targetOffset);
|
||||
simulateEvent(target, 'drop', start, targetOffset);
|
||||
simulateEvent(source, 'dragend', start, targetOffset);
|
||||
}
|
||||
|
||||
return function(dragFrom, dragTo) {
|
||||
var fromOffset = getClientOffset(dragFrom);
|
||||
var toOffset = getClientOffset(dragTo);
|
||||
doDragAndDrop(dragFrom.element, dragTo.element, fromOffset, toOffset);
|
||||
};
|
||||
})();
|
||||
|
||||
try {
|
||||
dragAndDrop(dragFrom, dragTo);
|
||||
done(null);
|
||||
} catch (error) {
|
||||
done(error.message);
|
||||
}
|
||||
}, [dragSource, dragTarget]).finally(function(result) {
|
||||
if (result) {
|
||||
var error = new Error(result);
|
||||
error.name = 'DragAndDropError';
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
// assertion helpers
|
||||
assertElementsExist: function(cssSelector, amount, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findAllByCssSelector(cssSelector)
|
||||
.then(function(elements) {
|
||||
if (!_.isNumber(amount)) {
|
||||
// no amount given - check if any amount of such elements exist
|
||||
message = amount;
|
||||
return assert.ok(elements.length, message);
|
||||
} else {
|
||||
return assert.equal(elements.length, amount, message);
|
||||
}
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementExists: function(cssSelector, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent.assertElementsExist(cssSelector, 1, message);
|
||||
});
|
||||
},
|
||||
assertElementNotExists: function(cssSelector, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent.assertElementsExist(cssSelector, 0, message);
|
||||
});
|
||||
},
|
||||
assertElementAppears: function(cssSelector, timeout, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.waitForCssSelector(cssSelector, timeout)
|
||||
.catch(_.constant(true))
|
||||
.assertElementExists(cssSelector, message);
|
||||
});
|
||||
},
|
||||
assertElementsAppear: function(cssSelector, timeout, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.waitForCssSelector(cssSelector, timeout)
|
||||
.catch(_.constant(true))
|
||||
.assertElementsExist(cssSelector, message);
|
||||
});
|
||||
},
|
||||
assertElementDisappears: function(cssSelector, timeout, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.waitForElementDeletion(cssSelector, timeout)
|
||||
.catch(_.constant(true))
|
||||
.assertElementNotExists(cssSelector, message);
|
||||
});
|
||||
},
|
||||
assertElementEnabled: function(cssSelector, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.isEnabled()
|
||||
.then(function(isEnabled) {
|
||||
return assert.isTrue(isEnabled, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementDisabled: function(cssSelector, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.isEnabled()
|
||||
.then(function(isEnabled) {
|
||||
return assert.isFalse(isEnabled, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementDisplayed: function(cssSelector, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.isDisplayed()
|
||||
.then(function(isDisplayed) {
|
||||
return assert.isTrue(isDisplayed, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementNotDisplayed: function(cssSelector, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.isDisplayed()
|
||||
.then(function(isDisplayed) {
|
||||
return assert.isFalse(isDisplayed, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementTextEquals: function(cssSelector, expectedText, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.getVisibleText()
|
||||
.then(function(actualText) {
|
||||
assert.equal(actualText, expectedText, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementContainsText: function(cssSelector, text, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.getVisibleText()
|
||||
.then(function(actualText) {
|
||||
assert.include(actualText, text, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementAttributeEquals: function(cssSelector, attribute, expectedText, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.getAttribute(attribute)
|
||||
.then(function(actualText) {
|
||||
assert.equal(actualText, expectedText, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementAttributeContains: function(cssSelector, attribute, text, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.getAttribute(attribute)
|
||||
.then(function(actualText) {
|
||||
assert.include(actualText, text, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementPropertyEquals: function(cssSelector, attribute, expectedText, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.getProperty(attribute)
|
||||
.then(function(actualText) {
|
||||
assert.equal(actualText, expectedText, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementPropertyNotEquals: function(cssSelector, attribute, textToCheck, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.getProperty(attribute)
|
||||
.then(function(actualText) {
|
||||
assert.notEqual(actualText, textToCheck, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementPropertyContains: function(cssSelector, attribute, text, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.getProperty(attribute)
|
||||
.then(function(actualText) {
|
||||
assert.include(actualText, text, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementSelected: function(cssSelector, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.isSelected()
|
||||
.then(function(isSelected) {
|
||||
assert.isTrue(isSelected, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertElementNotSelected: function(cssSelector, message) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.isSelected()
|
||||
.then(function(isSelected) {
|
||||
assert.isFalse(isSelected, message);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
},
|
||||
assertIsIntegerContentPositive: function(cssSelector, attributeName) {
|
||||
return new this.constructor(this, function() {
|
||||
return this.parent
|
||||
.findByCssSelector(cssSelector)
|
||||
.getVisibleText().then(function(text) {
|
||||
return assert.isTrue(parseInt(text, 10) > 0, attributeName + ' is greater than 0');
|
||||
})
|
||||
.end();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var serverHost = '127.0.0.1',
|
||||
serverPort = process.env.NAILGUN_PORT || 5544,
|
||||
serverUrl = 'http://' + serverHost + ':' + serverPort,
|
||||
username = 'admin',
|
||||
password = 'admin';
|
||||
|
||||
return {
|
||||
username: username,
|
||||
password: password,
|
||||
serverUrl: serverUrl
|
||||
};
|
||||
return {
|
||||
username: username,
|
||||
password: password,
|
||||
serverUrl: serverUrl
|
||||
};
|
||||
});
|
||||
|
|
|
@ -15,137 +15,137 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern/dojo/node!lodash',
|
||||
'tests/functional/pages/modal',
|
||||
'intern/dojo/node!leadfoot/helpers/pollUntil',
|
||||
'tests/functional/helpers'
|
||||
'intern/dojo/node!lodash',
|
||||
'tests/functional/pages/modal',
|
||||
'intern/dojo/node!leadfoot/helpers/pollUntil',
|
||||
'tests/functional/helpers'
|
||||
], function(_, ModalWindow, pollUntil) {
|
||||
'use strict';
|
||||
function ClusterPage(remote) {
|
||||
this.remote = remote;
|
||||
this.modal = new ModalWindow(remote);
|
||||
}
|
||||
'use strict';
|
||||
function ClusterPage(remote) {
|
||||
this.remote = remote;
|
||||
this.modal = new ModalWindow(remote);
|
||||
}
|
||||
|
||||
ClusterPage.prototype = {
|
||||
constructor: ClusterPage,
|
||||
goToTab: function(tabName) {
|
||||
return this.remote
|
||||
.findByCssSelector('.cluster-page .tabs')
|
||||
.clickLinkByText(tabName)
|
||||
.end()
|
||||
.then(pollUntil(function(textToFind) {
|
||||
return window.$('.cluster-tab.active').text() == textToFind || null;
|
||||
}, [tabName], 3000));
|
||||
},
|
||||
removeCluster: function(clusterName) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.clickLinkByText('Dashboard')
|
||||
.clickByCssSelector('button.delete-environment-btn')
|
||||
ClusterPage.prototype = {
|
||||
constructor: ClusterPage,
|
||||
goToTab: function(tabName) {
|
||||
return this.remote
|
||||
.findByCssSelector('.cluster-page .tabs')
|
||||
.clickLinkByText(tabName)
|
||||
.end()
|
||||
.then(pollUntil(function(textToFind) {
|
||||
return window.$('.cluster-tab.active').text() == textToFind || null;
|
||||
}, [tabName], 3000));
|
||||
},
|
||||
removeCluster: function(clusterName) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.clickLinkByText('Dashboard')
|
||||
.clickByCssSelector('button.delete-environment-btn')
|
||||
.then(function() {
|
||||
return self.modal.waitToOpen();
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.clickFooterButton('Delete');
|
||||
})
|
||||
.findAllByCssSelector('div.confirm-deletion-form input[type=text]')
|
||||
.then(function(confirmInputs) {
|
||||
if (confirmInputs.length)
|
||||
return confirmInputs[0]
|
||||
.type(clusterName)
|
||||
.then(function() {
|
||||
return self.modal.waitToOpen();
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.clickFooterButton('Delete');
|
||||
})
|
||||
.findAllByCssSelector('div.confirm-deletion-form input[type=text]')
|
||||
.then(function(confirmInputs) {
|
||||
if (confirmInputs.length)
|
||||
return confirmInputs[0]
|
||||
.type(clusterName)
|
||||
.then(function() {
|
||||
return self.modal.clickFooterButton('Delete');
|
||||
});
|
||||
})
|
||||
.end()
|
||||
.then(function() {
|
||||
return self.modal.waitToClose();
|
||||
})
|
||||
.waitForCssSelector('.clusters-page', 2000)
|
||||
.waitForDeletedByCssSelector('.clusterbox', 20000);
|
||||
},
|
||||
searchForNode: function(nodeName) {
|
||||
return this.remote
|
||||
.clickByCssSelector('button.btn-search')
|
||||
.setInputValue('input[name=search]', nodeName);
|
||||
},
|
||||
checkNodeRoles: function(assignRoles) {
|
||||
return this.remote
|
||||
.findAllByCssSelector('div.role-panel label')
|
||||
.then(function(roles) {
|
||||
return roles.reduce(
|
||||
function(result, role) {
|
||||
return role
|
||||
.getVisibleText()
|
||||
.then(function(label) {
|
||||
var index = assignRoles.indexOf(label.substr(1));
|
||||
if (index >= 0) {
|
||||
role.click();
|
||||
assignRoles.splice(index, 1);
|
||||
return assignRoles.length == 0;
|
||||
}
|
||||
});
|
||||
},
|
||||
false
|
||||
);
|
||||
return self.modal.clickFooterButton('Delete');
|
||||
});
|
||||
},
|
||||
checkNodes: function(amount, status) {
|
||||
var self = this;
|
||||
status = status || 'discover';
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return _.range(amount).reduce(
|
||||
function(result, index) {
|
||||
return self.remote
|
||||
.findAllByCssSelector('.node' + '.' + status + ' > label')
|
||||
.then(function(nodes) {
|
||||
return nodes[index].click();
|
||||
})
|
||||
.catch(function(e) {
|
||||
throw new Error('Failed to add ' + amount + ' nodes to the cluster: ' + e);
|
||||
});
|
||||
},
|
||||
true);
|
||||
})
|
||||
.end()
|
||||
.then(function() {
|
||||
return self.modal.waitToClose();
|
||||
})
|
||||
.waitForCssSelector('.clusters-page', 2000)
|
||||
.waitForDeletedByCssSelector('.clusterbox', 20000);
|
||||
},
|
||||
searchForNode: function(nodeName) {
|
||||
return this.remote
|
||||
.clickByCssSelector('button.btn-search')
|
||||
.setInputValue('input[name=search]', nodeName);
|
||||
},
|
||||
checkNodeRoles: function(assignRoles) {
|
||||
return this.remote
|
||||
.findAllByCssSelector('div.role-panel label')
|
||||
.then(function(roles) {
|
||||
return roles.reduce(
|
||||
function(result, role) {
|
||||
return role
|
||||
.getVisibleText()
|
||||
.then(function(label) {
|
||||
var index = assignRoles.indexOf(label.substr(1));
|
||||
if (index >= 0) {
|
||||
role.click();
|
||||
assignRoles.splice(index, 1);
|
||||
return assignRoles.length == 0;
|
||||
}
|
||||
});
|
||||
},
|
||||
resetEnvironment: function(clusterName) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.clickByCssSelector('button.reset-environment-btn')
|
||||
},
|
||||
false
|
||||
);
|
||||
});
|
||||
},
|
||||
checkNodes: function(amount, status) {
|
||||
var self = this;
|
||||
status = status || 'discover';
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return _.range(amount).reduce(
|
||||
function(result, index) {
|
||||
return self.remote
|
||||
.findAllByCssSelector('.node' + '.' + status + ' > label')
|
||||
.then(function(nodes) {
|
||||
return nodes[index].click();
|
||||
})
|
||||
.catch(function(e) {
|
||||
throw new Error('Failed to add ' + amount + ' nodes to the cluster: ' + e);
|
||||
});
|
||||
},
|
||||
true);
|
||||
});
|
||||
},
|
||||
resetEnvironment: function(clusterName) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.clickByCssSelector('button.reset-environment-btn')
|
||||
.then(function() {
|
||||
return self.modal.waitToOpen();
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.checkTitle('Reset Environment');
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.clickFooterButton('Reset');
|
||||
})
|
||||
.findAllByCssSelector('div.confirm-reset-form input[type=text]')
|
||||
.then(function(confirmationInputs) {
|
||||
if (confirmationInputs.length)
|
||||
return confirmationInputs[0]
|
||||
.type(clusterName)
|
||||
.then(function() {
|
||||
return self.modal.waitToOpen();
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.checkTitle('Reset Environment');
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.clickFooterButton('Reset');
|
||||
})
|
||||
.findAllByCssSelector('div.confirm-reset-form input[type=text]')
|
||||
.then(function(confirmationInputs) {
|
||||
if (confirmationInputs.length)
|
||||
return confirmationInputs[0]
|
||||
.type(clusterName)
|
||||
.then(function() {
|
||||
return self.modal.clickFooterButton('Reset');
|
||||
});
|
||||
})
|
||||
.end()
|
||||
.then(function() {
|
||||
return self.modal.waitToClose();
|
||||
})
|
||||
.waitForElementDeletion('div.progress-bar', 20000);
|
||||
},
|
||||
isTabLocked: function(tabName) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return self.goToTab(tabName);
|
||||
})
|
||||
.waitForCssSelector('div.tab-content div.row.changes-locked', 2000)
|
||||
.then(_.constant(true), _.constant(false));
|
||||
}
|
||||
};
|
||||
return ClusterPage;
|
||||
return self.modal.clickFooterButton('Reset');
|
||||
});
|
||||
})
|
||||
.end()
|
||||
.then(function() {
|
||||
return self.modal.waitToClose();
|
||||
})
|
||||
.waitForElementDeletion('div.progress-bar', 20000);
|
||||
},
|
||||
isTabLocked: function(tabName) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return self.goToTab(tabName);
|
||||
})
|
||||
.waitForCssSelector('div.tab-content div.row.changes-locked', 2000)
|
||||
.then(_.constant(true), _.constant(false));
|
||||
}
|
||||
};
|
||||
return ClusterPage;
|
||||
});
|
||||
|
|
|
@ -15,79 +15,79 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern/dojo/node!lodash',
|
||||
'tests/functional/pages/modal',
|
||||
'tests/functional/helpers'
|
||||
'intern/dojo/node!lodash',
|
||||
'tests/functional/pages/modal',
|
||||
'tests/functional/helpers'
|
||||
], function(_, ModalWindow) {
|
||||
'use strict';
|
||||
function ClustersPage(remote) {
|
||||
this.remote = remote;
|
||||
this.modal = new ModalWindow(remote);
|
||||
}
|
||||
'use strict';
|
||||
function ClustersPage(remote) {
|
||||
this.remote = remote;
|
||||
this.modal = new ModalWindow(remote);
|
||||
}
|
||||
|
||||
ClustersPage.prototype = {
|
||||
constructor: ClustersPage,
|
||||
createCluster: function(clusterName, stepsMethods) {
|
||||
var self = this,
|
||||
stepMethod = function(stepName) {
|
||||
return _.bind(_.get(stepsMethods, stepName, _.noop), self);
|
||||
};
|
||||
return this.remote
|
||||
.clickByCssSelector('.create-cluster')
|
||||
.then(function() {
|
||||
return self.modal.waitToOpen();
|
||||
})
|
||||
// Name and release
|
||||
.setInputValue('[name=name]', clusterName)
|
||||
.then(stepMethod('Name and Release'))
|
||||
.pressKeys('\uE007')
|
||||
// Compute
|
||||
.then(stepMethod('Compute'))
|
||||
.pressKeys('\uE007')
|
||||
// Networking Setup
|
||||
.then(stepMethod('Networking Setup'))
|
||||
.pressKeys('\uE007')
|
||||
//Storage Backends
|
||||
.then(stepMethod('Storage Backends'))
|
||||
.pressKeys('\uE007')
|
||||
// Additional Services
|
||||
.then(stepMethod('Additional Services'))
|
||||
.pressKeys('\uE007')
|
||||
// Finish
|
||||
.pressKeys('\uE007')
|
||||
.then(function() {
|
||||
return self.modal.waitToClose();
|
||||
});
|
||||
},
|
||||
clusterSelector: '.clusterbox div.name',
|
||||
goToEnvironment: function(clusterName) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.findAllByCssSelector(self.clusterSelector)
|
||||
.then(function(divs) {
|
||||
return divs.reduce(
|
||||
function(matchFound, element) {
|
||||
return element.getVisibleText().then(
|
||||
function(name) {
|
||||
if (name === clusterName) {
|
||||
element.click();
|
||||
return true;
|
||||
}
|
||||
return matchFound;
|
||||
}
|
||||
);
|
||||
},
|
||||
false
|
||||
);
|
||||
})
|
||||
.then(function(result) {
|
||||
if (!result) {
|
||||
throw new Error('Cluster ' + clusterName + ' not found');
|
||||
}
|
||||
ClustersPage.prototype = {
|
||||
constructor: ClustersPage,
|
||||
createCluster: function(clusterName, stepsMethods) {
|
||||
var self = this,
|
||||
stepMethod = function(stepName) {
|
||||
return _.bind(_.get(stepsMethods, stepName, _.noop), self);
|
||||
};
|
||||
return this.remote
|
||||
.clickByCssSelector('.create-cluster')
|
||||
.then(function() {
|
||||
return self.modal.waitToOpen();
|
||||
})
|
||||
// Name and release
|
||||
.setInputValue('[name=name]', clusterName)
|
||||
.then(stepMethod('Name and Release'))
|
||||
.pressKeys('\uE007')
|
||||
// Compute
|
||||
.then(stepMethod('Compute'))
|
||||
.pressKeys('\uE007')
|
||||
// Networking Setup
|
||||
.then(stepMethod('Networking Setup'))
|
||||
.pressKeys('\uE007')
|
||||
//Storage Backends
|
||||
.then(stepMethod('Storage Backends'))
|
||||
.pressKeys('\uE007')
|
||||
// Additional Services
|
||||
.then(stepMethod('Additional Services'))
|
||||
.pressKeys('\uE007')
|
||||
// Finish
|
||||
.pressKeys('\uE007')
|
||||
.then(function() {
|
||||
return self.modal.waitToClose();
|
||||
});
|
||||
},
|
||||
clusterSelector: '.clusterbox div.name',
|
||||
goToEnvironment: function(clusterName) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.findAllByCssSelector(self.clusterSelector)
|
||||
.then(function(divs) {
|
||||
return divs.reduce(
|
||||
function(matchFound, element) {
|
||||
return element.getVisibleText().then(
|
||||
function(name) {
|
||||
if (name === clusterName) {
|
||||
element.click();
|
||||
return true;
|
||||
})
|
||||
.waitForCssSelector('.dashboard-tab', 1000);
|
||||
}
|
||||
};
|
||||
return ClustersPage;
|
||||
}
|
||||
return matchFound;
|
||||
}
|
||||
);
|
||||
},
|
||||
false
|
||||
);
|
||||
})
|
||||
.then(function(result) {
|
||||
if (!result) {
|
||||
throw new Error('Cluster ' + clusterName + ' not found');
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.waitForCssSelector('.dashboard-tab', 1000);
|
||||
}
|
||||
};
|
||||
return ClustersPage;
|
||||
});
|
||||
|
|
|
@ -15,116 +15,116 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern/dojo/node!lodash',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/helpers',
|
||||
'tests/functional/pages/login',
|
||||
'tests/functional/pages/welcome',
|
||||
'tests/functional/pages/cluster',
|
||||
'tests/functional/pages/clusters'
|
||||
'intern/dojo/node!lodash',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/helpers',
|
||||
'tests/functional/pages/login',
|
||||
'tests/functional/pages/welcome',
|
||||
'tests/functional/pages/cluster',
|
||||
'tests/functional/pages/clusters'
|
||||
],
|
||||
function(_, assert, Helpers, LoginPage, WelcomePage, ClusterPage, ClustersPage) {
|
||||
'use strict';
|
||||
function CommonMethods(remote) {
|
||||
this.remote = remote;
|
||||
this.loginPage = new LoginPage(remote);
|
||||
this.welcomePage = new WelcomePage(remote);
|
||||
this.clusterPage = new ClusterPage(remote);
|
||||
this.clustersPage = new ClustersPage(remote);
|
||||
}
|
||||
function(_, assert, Helpers, LoginPage, WelcomePage, ClusterPage, ClustersPage) {
|
||||
'use strict';
|
||||
function CommonMethods(remote) {
|
||||
this.remote = remote;
|
||||
this.loginPage = new LoginPage(remote);
|
||||
this.welcomePage = new WelcomePage(remote);
|
||||
this.clusterPage = new ClusterPage(remote);
|
||||
this.clustersPage = new ClustersPage(remote);
|
||||
}
|
||||
|
||||
CommonMethods.prototype = {
|
||||
constructor: CommonMethods,
|
||||
pickRandomName: function(prefix) {
|
||||
return _.uniqueId((prefix || 'Item') + ' #');
|
||||
},
|
||||
getOut: function() {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return self.welcomePage.skip();
|
||||
})
|
||||
.then(function() {
|
||||
return self.loginPage.logout();
|
||||
});
|
||||
},
|
||||
getIn: function() {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return self.loginPage.logout();
|
||||
})
|
||||
.then(function() {
|
||||
return self.loginPage.login();
|
||||
})
|
||||
.waitForElementDeletion('.login-btn', 2000)
|
||||
.then(function() {
|
||||
return self.welcomePage.skip();
|
||||
})
|
||||
.waitForCssSelector('.navbar-nav', 1000)
|
||||
.clickByCssSelector('.global-alert.alert-warning .close');
|
||||
},
|
||||
createCluster: function(clusterName, stepsMethods) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.clickLinkByText('Environments')
|
||||
.waitForCssSelector('.clusters-page', 2000)
|
||||
.then(function() {
|
||||
return self.clustersPage.createCluster(clusterName, stepsMethods);
|
||||
});
|
||||
},
|
||||
removeCluster: function(clusterName, suppressErrors) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.clickLinkByText('Environments')
|
||||
.waitForCssSelector('.clusters-page', 2000)
|
||||
.then(function() {
|
||||
return self.clustersPage.goToEnvironment(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return self.clusterPage.removeCluster(clusterName);
|
||||
})
|
||||
.catch(function(e) {
|
||||
if (!suppressErrors) throw new Error('Unable to delete cluster ' + clusterName + ': ' + e);
|
||||
});
|
||||
},
|
||||
doesClusterExist: function(clusterName) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.clickLinkByText('Environments')
|
||||
.waitForCssSelector('.clusters-page', 2000)
|
||||
.findAllByCssSelector(self.clustersPage.clusterSelector)
|
||||
.then(function(divs) {
|
||||
return divs.reduce(function(matchFound, element) {
|
||||
return element.getVisibleText().then(
|
||||
function(name) {
|
||||
return (name === clusterName) || matchFound;
|
||||
}
|
||||
);
|
||||
}, false);
|
||||
});
|
||||
},
|
||||
addNodesToCluster: function(nodesAmount, nodesRoles, nodeStatus, nodeNameFilter) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return self.clusterPage.goToTab('Nodes');
|
||||
})
|
||||
.waitForCssSelector('button.btn-add-nodes', 3000)
|
||||
.clickByCssSelector('button.btn-add-nodes')
|
||||
.waitForCssSelector('.node', 3000)
|
||||
.then(function() {
|
||||
if (nodeNameFilter) return self.clusterPage.searchForNode(nodeNameFilter);
|
||||
})
|
||||
.then(function() {
|
||||
return self.clusterPage.checkNodeRoles(nodesRoles);
|
||||
})
|
||||
.then(function() {
|
||||
return self.clusterPage.checkNodes(nodesAmount, nodeStatus);
|
||||
})
|
||||
.clickByCssSelector('.btn-apply')
|
||||
.waitForElementDeletion('.btn-apply', 3000);
|
||||
}
|
||||
};
|
||||
return CommonMethods;
|
||||
CommonMethods.prototype = {
|
||||
constructor: CommonMethods,
|
||||
pickRandomName: function(prefix) {
|
||||
return _.uniqueId((prefix || 'Item') + ' #');
|
||||
},
|
||||
getOut: function() {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return self.welcomePage.skip();
|
||||
})
|
||||
.then(function() {
|
||||
return self.loginPage.logout();
|
||||
});
|
||||
},
|
||||
getIn: function() {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return self.loginPage.logout();
|
||||
})
|
||||
.then(function() {
|
||||
return self.loginPage.login();
|
||||
})
|
||||
.waitForElementDeletion('.login-btn', 2000)
|
||||
.then(function() {
|
||||
return self.welcomePage.skip();
|
||||
})
|
||||
.waitForCssSelector('.navbar-nav', 1000)
|
||||
.clickByCssSelector('.global-alert.alert-warning .close');
|
||||
},
|
||||
createCluster: function(clusterName, stepsMethods) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.clickLinkByText('Environments')
|
||||
.waitForCssSelector('.clusters-page', 2000)
|
||||
.then(function() {
|
||||
return self.clustersPage.createCluster(clusterName, stepsMethods);
|
||||
});
|
||||
},
|
||||
removeCluster: function(clusterName, suppressErrors) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.clickLinkByText('Environments')
|
||||
.waitForCssSelector('.clusters-page', 2000)
|
||||
.then(function() {
|
||||
return self.clustersPage.goToEnvironment(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return self.clusterPage.removeCluster(clusterName);
|
||||
})
|
||||
.catch(function(e) {
|
||||
if (!suppressErrors) throw new Error('Unable to delete cluster ' + clusterName + ': ' + e);
|
||||
});
|
||||
},
|
||||
doesClusterExist: function(clusterName) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.clickLinkByText('Environments')
|
||||
.waitForCssSelector('.clusters-page', 2000)
|
||||
.findAllByCssSelector(self.clustersPage.clusterSelector)
|
||||
.then(function(divs) {
|
||||
return divs.reduce(function(matchFound, element) {
|
||||
return element.getVisibleText().then(
|
||||
function(name) {
|
||||
return (name === clusterName) || matchFound;
|
||||
}
|
||||
);
|
||||
}, false);
|
||||
});
|
||||
},
|
||||
addNodesToCluster: function(nodesAmount, nodesRoles, nodeStatus, nodeNameFilter) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return self.clusterPage.goToTab('Nodes');
|
||||
})
|
||||
.waitForCssSelector('button.btn-add-nodes', 3000)
|
||||
.clickByCssSelector('button.btn-add-nodes')
|
||||
.waitForCssSelector('.node', 3000)
|
||||
.then(function() {
|
||||
if (nodeNameFilter) return self.clusterPage.searchForNode(nodeNameFilter);
|
||||
})
|
||||
.then(function() {
|
||||
return self.clusterPage.checkNodeRoles(nodesRoles);
|
||||
})
|
||||
.then(function() {
|
||||
return self.clusterPage.checkNodes(nodesAmount, nodeStatus);
|
||||
})
|
||||
.clickByCssSelector('.btn-apply')
|
||||
.waitForElementDeletion('.btn-apply', 3000);
|
||||
}
|
||||
};
|
||||
return CommonMethods;
|
||||
});
|
||||
|
|
|
@ -15,83 +15,83 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'tests/functional/pages/modal',
|
||||
'tests/functional/helpers'
|
||||
'tests/functional/pages/modal',
|
||||
'tests/functional/helpers'
|
||||
], function(ModalWindow) {
|
||||
'use strict';
|
||||
function DashboardPage(remote) {
|
||||
this.remote = remote;
|
||||
this.modal = new ModalWindow(remote);
|
||||
this.deployButtonSelector = 'button.deploy-btn';
|
||||
}
|
||||
'use strict';
|
||||
function DashboardPage(remote) {
|
||||
this.remote = remote;
|
||||
this.modal = new ModalWindow(remote);
|
||||
this.deployButtonSelector = 'button.deploy-btn';
|
||||
}
|
||||
|
||||
DashboardPage.prototype = {
|
||||
constructor: DashboardPage,
|
||||
startDeployment: function() {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.clickByCssSelector(this.deployButtonSelector)
|
||||
.then(function() {
|
||||
return self.modal.waitToOpen();
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.checkTitle('Deploy Changes');
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.clickFooterButton('Deploy');
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.waitToClose();
|
||||
});
|
||||
},
|
||||
stopDeployment: function() {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.clickByCssSelector('button.stop-deployment-btn')
|
||||
.then(function() {
|
||||
return self.modal.waitToOpen();
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.checkTitle('Stop Deployment');
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.clickFooterButton('Stop');
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.waitToClose();
|
||||
});
|
||||
},
|
||||
startClusterRenaming: function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.cluster-info-value.name .glyphicon-pencil');
|
||||
},
|
||||
setClusterName: function(name) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return self.startClusterRenaming();
|
||||
})
|
||||
.findByCssSelector('.rename-block input[type=text]')
|
||||
.clearValue()
|
||||
.type(name)
|
||||
// Enter
|
||||
.type('\uE007')
|
||||
.end();
|
||||
},
|
||||
discardChanges: function() {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.clickByCssSelector('.btn-discard-changes')
|
||||
.then(function() {
|
||||
return self.modal.waitToOpen();
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.clickFooterButton('Discard');
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.waitToClose();
|
||||
});
|
||||
}
|
||||
};
|
||||
return DashboardPage;
|
||||
DashboardPage.prototype = {
|
||||
constructor: DashboardPage,
|
||||
startDeployment: function() {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.clickByCssSelector(this.deployButtonSelector)
|
||||
.then(function() {
|
||||
return self.modal.waitToOpen();
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.checkTitle('Deploy Changes');
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.clickFooterButton('Deploy');
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.waitToClose();
|
||||
});
|
||||
},
|
||||
stopDeployment: function() {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.clickByCssSelector('button.stop-deployment-btn')
|
||||
.then(function() {
|
||||
return self.modal.waitToOpen();
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.checkTitle('Stop Deployment');
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.clickFooterButton('Stop');
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.waitToClose();
|
||||
});
|
||||
},
|
||||
startClusterRenaming: function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.cluster-info-value.name .glyphicon-pencil');
|
||||
},
|
||||
setClusterName: function(name) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return self.startClusterRenaming();
|
||||
})
|
||||
.findByCssSelector('.rename-block input[type=text]')
|
||||
.clearValue()
|
||||
.type(name)
|
||||
// Enter
|
||||
.type('\uE007')
|
||||
.end();
|
||||
},
|
||||
discardChanges: function() {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.clickByCssSelector('.btn-discard-changes')
|
||||
.then(function() {
|
||||
return self.modal.waitToOpen();
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.clickFooterButton('Discard');
|
||||
})
|
||||
.then(function() {
|
||||
return self.modal.waitToClose();
|
||||
});
|
||||
}
|
||||
};
|
||||
return DashboardPage;
|
||||
});
|
||||
|
|
|
@ -15,139 +15,139 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern/dojo/node!lodash',
|
||||
'intern/chai!assert'
|
||||
'intern/dojo/node!lodash',
|
||||
'intern/chai!assert'
|
||||
], function(_, assert) {
|
||||
'use strict';
|
||||
function InterfacesPage(remote) {
|
||||
this.remote = remote;
|
||||
'use strict';
|
||||
function InterfacesPage(remote) {
|
||||
this.remote = remote;
|
||||
}
|
||||
|
||||
InterfacesPage.prototype = {
|
||||
constructor: InterfacesPage,
|
||||
findInterfaceElement: function(ifcName) {
|
||||
return this.remote
|
||||
.findAllByCssSelector('div.ifc-inner-container')
|
||||
.then(function(ifcElements) {
|
||||
return ifcElements.reduce(function(result, ifcElement) {
|
||||
return ifcElement
|
||||
.findByCssSelector('.ifc-name')
|
||||
.then(function(ifcDiv) {
|
||||
return ifcDiv
|
||||
.getVisibleText()
|
||||
.then(function(currentIfcName) {
|
||||
return _.trim(currentIfcName) == ifcName ? ifcElement : result;
|
||||
});
|
||||
});
|
||||
}, null);
|
||||
});
|
||||
},
|
||||
findInterfaceElementInBond: function(ifcName) {
|
||||
return this.remote
|
||||
.findAllByCssSelector('.ifc-info-block')
|
||||
.then(function(ifcsElements) {
|
||||
return ifcsElements.reduce(function(result, ifcElement) {
|
||||
return ifcElement
|
||||
.findByCssSelector('.ifc-name')
|
||||
.then(function(ifcNameElement) {
|
||||
return ifcNameElement
|
||||
.getVisibleText()
|
||||
.then(function(foundIfcName) {
|
||||
return ifcName == foundIfcName ? ifcElement : result;
|
||||
});
|
||||
});
|
||||
}, null);
|
||||
});
|
||||
},
|
||||
removeInterfaceFromBond: function(ifcName) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return self.findInterfaceElementInBond(ifcName);
|
||||
})
|
||||
.then(function(ifcElement) {
|
||||
return ifcElement
|
||||
.findByCssSelector('.ifc-info > .btn-link')
|
||||
.then(function(btnRemove) {
|
||||
return btnRemove.click();
|
||||
});
|
||||
});
|
||||
},
|
||||
assignNetworkToInterface: function(networkName, ifcName) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.findAllByCssSelector('div.network-block')
|
||||
.then(function(networkElements) {
|
||||
return networkElements.reduce(function(result, networkElement) {
|
||||
return networkElement
|
||||
.getVisibleText()
|
||||
.then(function(currentNetworkName) {
|
||||
return currentNetworkName == networkName ? networkElement : result;
|
||||
});
|
||||
}, null);
|
||||
})
|
||||
.then(function(networkElement) {
|
||||
return this.parent.dragFrom(networkElement);
|
||||
})
|
||||
.then(function() {
|
||||
return self.findInterfaceElement(ifcName);
|
||||
})
|
||||
.then(function(ifcElement) {
|
||||
return this.parent.dragTo(ifcElement);
|
||||
});
|
||||
},
|
||||
selectInterface: function(ifcName) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return self.findInterfaceElement(ifcName);
|
||||
})
|
||||
.then(function(ifcElement) {
|
||||
if (!ifcElement) throw new Error('Unable to select interface ' + ifcName);
|
||||
return ifcElement
|
||||
.findByCssSelector('input[type=checkbox]:not(:checked)')
|
||||
.then(function(ifcCheckbox) {
|
||||
return ifcCheckbox.click();
|
||||
});
|
||||
});
|
||||
},
|
||||
bondInterfaces: function(ifc1, ifc2) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return self.selectInterface(ifc1);
|
||||
})
|
||||
.then(function() {
|
||||
return self.selectInterface(ifc2);
|
||||
})
|
||||
.clickByCssSelector('.btn-bond');
|
||||
},
|
||||
checkBondInterfaces: function(bondName, ifcsNames) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return self.findInterfaceElement(bondName);
|
||||
})
|
||||
.then(function(bondElement) {
|
||||
ifcsNames.push(bondName);
|
||||
return bondElement
|
||||
.findAllByCssSelector('.ifc-name')
|
||||
.then(function(ifcNamesElements) {
|
||||
assert.equal(ifcNamesElements.length, ifcsNames.length, 'Unexpected number of interfaces in bond');
|
||||
|
||||
return ifcNamesElements.forEach(
|
||||
function(ifcNameElement) {
|
||||
return ifcNameElement
|
||||
.getVisibleText()
|
||||
.then(function(name) {
|
||||
name = _.trim(name);
|
||||
if (!_.contains(ifcsNames, name))
|
||||
throw new Error('Unexpected name in bond: ' + name);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
InterfacesPage.prototype = {
|
||||
constructor: InterfacesPage,
|
||||
findInterfaceElement: function(ifcName) {
|
||||
return this.remote
|
||||
.findAllByCssSelector('div.ifc-inner-container')
|
||||
.then(function(ifcElements) {
|
||||
return ifcElements.reduce(function(result, ifcElement) {
|
||||
return ifcElement
|
||||
.findByCssSelector('.ifc-name')
|
||||
.then(function(ifcDiv) {
|
||||
return ifcDiv
|
||||
.getVisibleText()
|
||||
.then(function(currentIfcName) {
|
||||
return _.trim(currentIfcName) == ifcName ? ifcElement : result;
|
||||
});
|
||||
});
|
||||
}, null);
|
||||
});
|
||||
},
|
||||
findInterfaceElementInBond: function(ifcName) {
|
||||
return this.remote
|
||||
.findAllByCssSelector('.ifc-info-block')
|
||||
.then(function(ifcsElements) {
|
||||
return ifcsElements.reduce(function(result, ifcElement) {
|
||||
return ifcElement
|
||||
.findByCssSelector('.ifc-name')
|
||||
.then(function(ifcNameElement) {
|
||||
return ifcNameElement
|
||||
.getVisibleText()
|
||||
.then(function(foundIfcName) {
|
||||
return ifcName == foundIfcName ? ifcElement : result;
|
||||
});
|
||||
});
|
||||
}, null);
|
||||
});
|
||||
},
|
||||
removeInterfaceFromBond: function(ifcName) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return self.findInterfaceElementInBond(ifcName);
|
||||
})
|
||||
.then(function(ifcElement) {
|
||||
return ifcElement
|
||||
.findByCssSelector('.ifc-info > .btn-link')
|
||||
.then(function(btnRemove) {
|
||||
return btnRemove.click();
|
||||
});
|
||||
});
|
||||
},
|
||||
assignNetworkToInterface: function(networkName, ifcName) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.findAllByCssSelector('div.network-block')
|
||||
.then(function(networkElements) {
|
||||
return networkElements.reduce(function(result, networkElement) {
|
||||
return networkElement
|
||||
.getVisibleText()
|
||||
.then(function(currentNetworkName) {
|
||||
return currentNetworkName == networkName ? networkElement : result;
|
||||
});
|
||||
}, null);
|
||||
})
|
||||
.then(function(networkElement) {
|
||||
return this.parent.dragFrom(networkElement);
|
||||
})
|
||||
.then(function() {
|
||||
return self.findInterfaceElement(ifcName);
|
||||
})
|
||||
.then(function(ifcElement) {
|
||||
return this.parent.dragTo(ifcElement);
|
||||
});
|
||||
},
|
||||
selectInterface: function(ifcName) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return self.findInterfaceElement(ifcName);
|
||||
})
|
||||
.then(function(ifcElement) {
|
||||
if (!ifcElement) throw new Error('Unable to select interface ' + ifcName);
|
||||
return ifcElement
|
||||
.findByCssSelector('input[type=checkbox]:not(:checked)')
|
||||
.then(function(ifcCheckbox) {
|
||||
return ifcCheckbox.click();
|
||||
});
|
||||
});
|
||||
},
|
||||
bondInterfaces: function(ifc1, ifc2) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return self.selectInterface(ifc1);
|
||||
})
|
||||
.then(function() {
|
||||
return self.selectInterface(ifc2);
|
||||
})
|
||||
.clickByCssSelector('.btn-bond');
|
||||
},
|
||||
checkBondInterfaces: function(bondName, ifcsNames) {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return self.findInterfaceElement(bondName);
|
||||
})
|
||||
.then(function(bondElement) {
|
||||
ifcsNames.push(bondName);
|
||||
return bondElement
|
||||
.findAllByCssSelector('.ifc-name')
|
||||
.then(function(ifcNamesElements) {
|
||||
assert.equal(ifcNamesElements.length, ifcsNames.length, 'Unexpected number of interfaces in bond');
|
||||
|
||||
return ifcNamesElements.forEach(
|
||||
function(ifcNameElement) {
|
||||
return ifcNameElement
|
||||
.getVisibleText()
|
||||
.then(function(name) {
|
||||
name = _.trim(name);
|
||||
if (!_.contains(ifcsNames, name))
|
||||
throw new Error('Unexpected name in bond: ' + name);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
return InterfacesPage;
|
||||
};
|
||||
return InterfacesPage;
|
||||
});
|
||||
|
|
|
@ -15,59 +15,59 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'tests/functional/helpers'
|
||||
'tests/functional/helpers'
|
||||
], function(Helpers) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
function LoginPage(remote) {
|
||||
this.remote = remote;
|
||||
function LoginPage(remote) {
|
||||
this.remote = remote;
|
||||
}
|
||||
|
||||
LoginPage.prototype = {
|
||||
constructor: LoginPage,
|
||||
login: function(username, password) {
|
||||
username = username || Helpers.username;
|
||||
password = password || Helpers.password;
|
||||
var self = this;
|
||||
|
||||
return this.remote
|
||||
.setFindTimeout(500)
|
||||
.setWindowSize(1280, 1024)
|
||||
.getCurrentUrl()
|
||||
.then(function(url) {
|
||||
if (url !== Helpers.serverUrl + '/#login') {
|
||||
return self.logout();
|
||||
}
|
||||
})
|
||||
.setInputValue('[name=username]', username)
|
||||
.setInputValue('[name=password]', password)
|
||||
.clickByCssSelector('.login-btn');
|
||||
},
|
||||
logout: function() {
|
||||
return this.remote
|
||||
.getCurrentUrl()
|
||||
.then(function(url) {
|
||||
if (url.indexOf(Helpers.serverUrl) !== 0) {
|
||||
return this.parent
|
||||
.get(Helpers.serverUrl + '/#logout')
|
||||
.findByClassName('login-btn')
|
||||
.then(function() {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
})
|
||||
.clickByCssSelector('li.user-icon')
|
||||
.clickByCssSelector('.user-popover button.btn-logout')
|
||||
.findByCssSelector('.login-btn')
|
||||
.then(
|
||||
function() {
|
||||
return true;
|
||||
},
|
||||
function() {
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
LoginPage.prototype = {
|
||||
constructor: LoginPage,
|
||||
login: function(username, password) {
|
||||
username = username || Helpers.username;
|
||||
password = password || Helpers.password;
|
||||
var self = this;
|
||||
|
||||
return this.remote
|
||||
.setFindTimeout(500)
|
||||
.setWindowSize(1280, 1024)
|
||||
.getCurrentUrl()
|
||||
.then(function(url) {
|
||||
if (url !== Helpers.serverUrl + '/#login') {
|
||||
return self.logout();
|
||||
}
|
||||
})
|
||||
.setInputValue('[name=username]', username)
|
||||
.setInputValue('[name=password]', password)
|
||||
.clickByCssSelector('.login-btn');
|
||||
},
|
||||
logout: function() {
|
||||
return this.remote
|
||||
.getCurrentUrl()
|
||||
.then(function(url) {
|
||||
if (url.indexOf(Helpers.serverUrl) !== 0) {
|
||||
return this.parent
|
||||
.get(Helpers.serverUrl + '/#logout')
|
||||
.findByClassName('login-btn')
|
||||
.then(function() {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
})
|
||||
.clickByCssSelector('li.user-icon')
|
||||
.clickByCssSelector('.user-popover button.btn-logout')
|
||||
.findByCssSelector('.login-btn')
|
||||
.then(
|
||||
function() {
|
||||
return true;
|
||||
},
|
||||
function() {
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
return LoginPage;
|
||||
};
|
||||
return LoginPage;
|
||||
});
|
||||
|
|
|
@ -15,61 +15,60 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern/dojo/node!leadfoot/helpers/pollUntil',
|
||||
'tests/functional/helpers'
|
||||
'intern/dojo/node!leadfoot/helpers/pollUntil',
|
||||
'tests/functional/helpers'
|
||||
], function(pollUntil) {
|
||||
'use strict';
|
||||
function ModalWindow(remote) {
|
||||
this.remote = remote;
|
||||
}
|
||||
'use strict';
|
||||
function ModalWindow(remote) {
|
||||
this.remote = remote;
|
||||
}
|
||||
|
||||
ModalWindow.prototype = {
|
||||
constructor: ModalWindow,
|
||||
modalSelector: '#modal-container > .modal',
|
||||
waitToOpen: function() {
|
||||
return this.remote
|
||||
.waitForCssSelector(this.modalSelector, 2000)
|
||||
.then(pollUntil(function(modalSelector) {
|
||||
return window.$(modalSelector).css('opacity') == 1 || null;
|
||||
}, [this.modalSelector], 3000));
|
||||
},
|
||||
checkTitle: function(expectedTitle) {
|
||||
return this.remote
|
||||
.assertElementContainsText(this.modalSelector + ' h4.modal-title', expectedTitle, 'Unexpected modal window title');
|
||||
},
|
||||
close: function() {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.clickByCssSelector(this.modalSelector + ' .modal-header button.close')
|
||||
.then(function() {
|
||||
return self.waitToClose();
|
||||
});
|
||||
},
|
||||
clickFooterButton: function(buttonText) {
|
||||
return this.remote
|
||||
.findAllByCssSelector(this.modalSelector + ' .modal-footer button')
|
||||
.then(function(buttons) {
|
||||
return buttons.reduce(function(result, button) {
|
||||
return button.getVisibleText()
|
||||
.then(function(buttonTitle) {
|
||||
if (buttonTitle == buttonText)
|
||||
return button.isEnabled()
|
||||
.then(function(isEnabled) {
|
||||
if (isEnabled) {
|
||||
return button.click();
|
||||
} else
|
||||
throw Error('Unable to click disabled button "' + buttonText + '"');
|
||||
});
|
||||
return result;
|
||||
});
|
||||
}, null);
|
||||
});
|
||||
},
|
||||
waitToClose: function() {
|
||||
return this.remote
|
||||
.waitForElementDeletion(this.modalSelector, 5000);
|
||||
}
|
||||
};
|
||||
return ModalWindow;
|
||||
ModalWindow.prototype = {
|
||||
constructor: ModalWindow,
|
||||
modalSelector: '#modal-container > .modal',
|
||||
waitToOpen: function() {
|
||||
return this.remote
|
||||
.waitForCssSelector(this.modalSelector, 2000)
|
||||
.then(pollUntil(function(modalSelector) {
|
||||
return window.$(modalSelector).css('opacity') == 1 || null;
|
||||
}, [this.modalSelector], 3000));
|
||||
},
|
||||
checkTitle: function(expectedTitle) {
|
||||
return this.remote
|
||||
.assertElementContainsText(this.modalSelector + ' h4.modal-title', expectedTitle, 'Unexpected modal window title');
|
||||
},
|
||||
close: function() {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.clickByCssSelector(this.modalSelector + ' .modal-header button.close')
|
||||
.then(function() {
|
||||
return self.waitToClose();
|
||||
});
|
||||
},
|
||||
clickFooterButton: function(buttonText) {
|
||||
return this.remote
|
||||
.findAllByCssSelector(this.modalSelector + ' .modal-footer button')
|
||||
.then(function(buttons) {
|
||||
return buttons.reduce(function(result, button) {
|
||||
return button.getVisibleText()
|
||||
.then(function(buttonTitle) {
|
||||
if (buttonTitle == buttonText)
|
||||
return button.isEnabled()
|
||||
.then(function(isEnabled) {
|
||||
if (isEnabled) {
|
||||
return button.click();
|
||||
} else
|
||||
throw Error('Unable to click disabled button "' + buttonText + '"');
|
||||
});
|
||||
return result;
|
||||
});
|
||||
}, null);
|
||||
});
|
||||
},
|
||||
waitToClose: function() {
|
||||
return this.remote
|
||||
.waitForElementDeletion(this.modalSelector, 5000);
|
||||
}
|
||||
);
|
||||
};
|
||||
return ModalWindow;
|
||||
});
|
||||
|
|
|
@ -15,21 +15,21 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'tests/functional/helpers'
|
||||
'tests/functional/helpers'
|
||||
], function() {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
function NetworksPage(remote) {
|
||||
this.applyButtonSelector = '.apply-btn';
|
||||
this.remote = remote;
|
||||
function NetworksPage(remote) {
|
||||
this.applyButtonSelector = '.apply-btn';
|
||||
this.remote = remote;
|
||||
}
|
||||
|
||||
NetworksPage.prototype = {
|
||||
constructor: NetworksPage,
|
||||
switchNetworkManager: function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('input[name=net_provider]:not(:checked)');
|
||||
}
|
||||
|
||||
NetworksPage.prototype = {
|
||||
constructor: NetworksPage,
|
||||
switchNetworkManager: function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('input[name=net_provider]:not(:checked)');
|
||||
}
|
||||
};
|
||||
return NetworksPage;
|
||||
};
|
||||
return NetworksPage;
|
||||
});
|
||||
|
|
|
@ -15,73 +15,73 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'tests/functional/pages/modal',
|
||||
'tests/functional/helpers'
|
||||
'tests/functional/pages/modal',
|
||||
'tests/functional/helpers'
|
||||
], function(ModalWindow) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
function NodeComponent(remote) {
|
||||
this.remote = remote;
|
||||
this.modal = new ModalWindow(this.remote);
|
||||
function NodeComponent(remote) {
|
||||
this.remote = remote;
|
||||
this.modal = new ModalWindow(this.remote);
|
||||
}
|
||||
|
||||
NodeComponent.prototype = {
|
||||
constructor: NodeComponent,
|
||||
openCompactNodeExtendedView: function() {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.findByCssSelector('div.compact-node .node-hardware p:not(.btn)')
|
||||
.then(function(element) {
|
||||
return self.remote.moveMouseTo(element);
|
||||
})
|
||||
.end()
|
||||
// the following timeout as we have 0.3s transition for the button
|
||||
.sleep(500)
|
||||
.clickByCssSelector('div.compact-node .node-hardware p.btn')
|
||||
.waitForCssSelector('.node-popover', 1000);
|
||||
},
|
||||
openNodePopup: function(fromExtendedView) {
|
||||
var self = this,
|
||||
cssSelector = fromExtendedView ? '.node-popover' : '.node';
|
||||
return this.remote
|
||||
.findByCssSelector(cssSelector)
|
||||
.clickByCssSelector('.node-settings')
|
||||
.end()
|
||||
.then(function() {
|
||||
return self.modal.waitToOpen();
|
||||
});
|
||||
},
|
||||
discardNode: function(fromExtendedView) {
|
||||
var self = this,
|
||||
cssSelector = fromExtendedView ? '.node-popover' : '.node';
|
||||
return this.remote
|
||||
.findByCssSelector(cssSelector)
|
||||
.clickByCssSelector('.btn-discard')
|
||||
.end()
|
||||
.then(function() {
|
||||
// deletion confirmation shows up
|
||||
return self.modal.waitToOpen();
|
||||
})
|
||||
// confirm deletion
|
||||
.clickByCssSelector('div.modal-content button.btn-delete')
|
||||
.then(function() {
|
||||
return self.modal.waitToClose();
|
||||
});
|
||||
},
|
||||
renameNode: function(newName, fromExtendedView) {
|
||||
var cssSelector = fromExtendedView ? '.node-popover' : '.node';
|
||||
return this.remote
|
||||
.findByCssSelector(cssSelector)
|
||||
.clickByCssSelector('.name p')
|
||||
.findByCssSelector('input.node-name-input')
|
||||
// node name gets editable upon clicking on it
|
||||
.clearValue()
|
||||
.type(newName)
|
||||
.pressKeys('\uE007')
|
||||
.end()
|
||||
.waitForCssSelector('.name p', 1000)
|
||||
.end();
|
||||
}
|
||||
|
||||
NodeComponent.prototype = {
|
||||
constructor: NodeComponent,
|
||||
openCompactNodeExtendedView: function() {
|
||||
var self = this;
|
||||
return this.remote
|
||||
.findByCssSelector('div.compact-node .node-hardware p:not(.btn)')
|
||||
.then(function(element) {
|
||||
return self.remote.moveMouseTo(element);
|
||||
})
|
||||
.end()
|
||||
// the following timeout as we have 0.3s transition for the button
|
||||
.sleep(500)
|
||||
.clickByCssSelector('div.compact-node .node-hardware p.btn')
|
||||
.waitForCssSelector('.node-popover', 1000);
|
||||
},
|
||||
openNodePopup: function(fromExtendedView) {
|
||||
var self = this,
|
||||
cssSelector = fromExtendedView ? '.node-popover' : '.node';
|
||||
return this.remote
|
||||
.findByCssSelector(cssSelector)
|
||||
.clickByCssSelector('.node-settings')
|
||||
.end()
|
||||
.then(function() {
|
||||
return self.modal.waitToOpen();
|
||||
});
|
||||
},
|
||||
discardNode: function(fromExtendedView) {
|
||||
var self = this,
|
||||
cssSelector = fromExtendedView ? '.node-popover' : '.node';
|
||||
return this.remote
|
||||
.findByCssSelector(cssSelector)
|
||||
.clickByCssSelector('.btn-discard')
|
||||
.end()
|
||||
.then(function() {
|
||||
// deletion confirmation shows up
|
||||
return self.modal.waitToOpen();
|
||||
})
|
||||
// confirm deletion
|
||||
.clickByCssSelector('div.modal-content button.btn-delete')
|
||||
.then(function() {
|
||||
return self.modal.waitToClose();
|
||||
});
|
||||
},
|
||||
renameNode: function(newName, fromExtendedView) {
|
||||
var cssSelector = fromExtendedView ? '.node-popover' : '.node';
|
||||
return this.remote
|
||||
.findByCssSelector(cssSelector)
|
||||
.clickByCssSelector('.name p')
|
||||
.findByCssSelector('input.node-name-input')
|
||||
// node name gets editable upon clicking on it
|
||||
.clearValue()
|
||||
.type(newName)
|
||||
.pressKeys('\uE007')
|
||||
.end()
|
||||
.waitForCssSelector('.name p', 1000)
|
||||
.end();
|
||||
}
|
||||
};
|
||||
return NodeComponent;
|
||||
};
|
||||
return NodeComponent;
|
||||
});
|
||||
|
|
|
@ -15,22 +15,22 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'tests/functional/helpers'
|
||||
'tests/functional/helpers'
|
||||
], function() {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
function SettingsPage(remote) {
|
||||
this.remote = remote;
|
||||
function SettingsPage(remote) {
|
||||
this.remote = remote;
|
||||
}
|
||||
|
||||
SettingsPage.prototype = {
|
||||
constructor: SettingsPage,
|
||||
waitForRequestCompleted: function() {
|
||||
return this.remote
|
||||
// Load Defaults button is locked during any request is in progress on the tab
|
||||
// so this is a hacky way to track request state
|
||||
.waitForElementDeletion('.btn-load-defaults:disabled', 2000);
|
||||
}
|
||||
|
||||
SettingsPage.prototype = {
|
||||
constructor: SettingsPage,
|
||||
waitForRequestCompleted: function() {
|
||||
return this.remote
|
||||
// Load Defaults button is locked during any request is in progress on the tab
|
||||
// so this is a hacky way to track request state
|
||||
.waitForElementDeletion('.btn-load-defaults:disabled', 2000);
|
||||
}
|
||||
};
|
||||
return SettingsPage;
|
||||
};
|
||||
return SettingsPage;
|
||||
});
|
||||
|
|
|
@ -15,34 +15,35 @@
|
|||
**/
|
||||
|
||||
define(['tests/functional/helpers'], function() {
|
||||
'use strict';
|
||||
function WelcomePage(remote) {
|
||||
this.remote = remote;
|
||||
}
|
||||
'use strict';
|
||||
function WelcomePage(remote) {
|
||||
this.remote = remote;
|
||||
}
|
||||
|
||||
WelcomePage.prototype = {
|
||||
constructor: WelcomePage,
|
||||
skip: function(strictCheck) {
|
||||
return this.remote
|
||||
.waitForCssSelector('.welcome-page', 3000)
|
||||
.then(function() {
|
||||
return this.parent
|
||||
.clickByCssSelector('.welcome-button-box button')
|
||||
.waitForDeletedByCssSelector('.welcome-button-box button', 2000)
|
||||
.then(
|
||||
function() {
|
||||
return true;
|
||||
},
|
||||
function() {
|
||||
return !strictCheck;
|
||||
}
|
||||
);
|
||||
},
|
||||
function() {
|
||||
return !strictCheck;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
return WelcomePage;
|
||||
WelcomePage.prototype = {
|
||||
constructor: WelcomePage,
|
||||
skip: function(strictCheck) {
|
||||
return this.remote
|
||||
.waitForCssSelector('.welcome-page', 3000)
|
||||
.then(
|
||||
function() {
|
||||
return this.parent
|
||||
.clickByCssSelector('.welcome-button-box button')
|
||||
.waitForDeletedByCssSelector('.welcome-button-box button', 2000)
|
||||
.then(
|
||||
function() {
|
||||
return true;
|
||||
},
|
||||
function() {
|
||||
return !strictCheck;
|
||||
}
|
||||
);
|
||||
},
|
||||
function() {
|
||||
return !strictCheck;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
return WelcomePage;
|
||||
});
|
||||
|
|
|
@ -15,20 +15,20 @@
|
|||
**/
|
||||
|
||||
define(function() {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
var remotes = {};
|
||||
var remotes = {};
|
||||
|
||||
function saveScreenshot(testOrSuite) {
|
||||
var remote = remotes[testOrSuite.sessionId];
|
||||
if (remote && remote.takeScreenshotAndSave) remote.takeScreenshotAndSave(testOrSuite.id);
|
||||
}
|
||||
function saveScreenshot(testOrSuite) {
|
||||
var remote = remotes[testOrSuite.sessionId];
|
||||
if (remote && remote.takeScreenshotAndSave) remote.takeScreenshotAndSave(testOrSuite.id);
|
||||
}
|
||||
|
||||
return {
|
||||
'/session/start': function(remote) {
|
||||
remotes[remote.sessionId] = remote;
|
||||
},
|
||||
'/suite/error': saveScreenshot,
|
||||
'/test/fail': saveScreenshot
|
||||
};
|
||||
return {
|
||||
'/session/start': function(remote) {
|
||||
remotes[remote.sessionId] = remote;
|
||||
},
|
||||
'/suite/error': saveScreenshot,
|
||||
'/test/fail': saveScreenshot
|
||||
};
|
||||
});
|
||||
|
|
|
@ -15,264 +15,264 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/cluster',
|
||||
'tests/functional/pages/clusters',
|
||||
'tests/functional/pages/dashboard'
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/cluster',
|
||||
'tests/functional/pages/clusters',
|
||||
'tests/functional/pages/dashboard'
|
||||
], function(registerSuite, assert, Common, ClusterPage, ClustersPage, DashboardPage) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
clusterPage,
|
||||
clustersPage,
|
||||
dashboardPage,
|
||||
clusterName;
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
clusterPage,
|
||||
clustersPage,
|
||||
dashboardPage,
|
||||
clusterName;
|
||||
|
||||
return {
|
||||
name: 'Dashboard tab',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
clustersPage = new ClustersPage(this.remote);
|
||||
dashboardPage = new DashboardPage(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
return {
|
||||
name: 'Dashboard tab',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
clustersPage = new ClustersPage(this.remote);
|
||||
dashboardPage = new DashboardPage(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
});
|
||||
},
|
||||
beforeEach: function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
});
|
||||
},
|
||||
'Renaming cluster works': function() {
|
||||
var initialName = clusterName,
|
||||
newName = clusterName + '!!!',
|
||||
renameInputSelector = '.rename-block input[type=text]',
|
||||
nameSelector = '.cluster-info-value.name .btn-link';
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return dashboardPage.startClusterRenaming();
|
||||
})
|
||||
.findByCssSelector(renameInputSelector)
|
||||
// Escape
|
||||
.type('\uE00C')
|
||||
.end()
|
||||
.assertElementNotExists(renameInputSelector, 'Rename control disappears')
|
||||
.assertElementTextEquals(nameSelector, initialName,
|
||||
'Switching rename control does not change cluster name')
|
||||
.then(function() {
|
||||
return dashboardPage.setClusterName(newName);
|
||||
})
|
||||
.assertElementTextEquals(nameSelector, newName, 'New name is applied')
|
||||
.then(function() {
|
||||
return dashboardPage.setClusterName(initialName);
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(newName);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.then(function() {
|
||||
return dashboardPage.setClusterName(initialName);
|
||||
})
|
||||
.assertElementAppears('.rename-block.has-error', 1000, 'Error style for duplicate name is applied')
|
||||
.assertElementTextEquals('.rename-block .text-danger', 'Environment with this name already exists',
|
||||
'Duplicate name error text appears'
|
||||
)
|
||||
.findByCssSelector(renameInputSelector)
|
||||
// Escape
|
||||
.type('\uE00C')
|
||||
.end()
|
||||
.clickLinkByText('Environments')
|
||||
.waitForCssSelector(clustersPage.clusterSelector, 2000)
|
||||
.then(function() {
|
||||
return clustersPage.goToEnvironment(initialName);
|
||||
});
|
||||
},
|
||||
'Provision button availability': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Virtual']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.assertElementTextEquals(dashboardPage.deployButtonSelector, 'Provision VMs',
|
||||
'After adding Virtual node deploy button has appropriate text')
|
||||
.then(function() {
|
||||
return dashboardPage.discardChanges();
|
||||
});
|
||||
},
|
||||
'Network validation error warning': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Networks');
|
||||
})
|
||||
.clickByCssSelector('.subtab-link-network_verification')
|
||||
.assertElementContainsText('.alert-warning', 'At least two online nodes are required',
|
||||
'Network verification warning appears if only one node added')
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.assertElementContainsText('.warnings-block',
|
||||
'Please verify your network settings before deployment', 'Network verification warning is shown')
|
||||
.then(function() {
|
||||
return dashboardPage.discardChanges();
|
||||
});
|
||||
},
|
||||
'No controller warning': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
// Adding single compute
|
||||
return common.addNodesToCluster(1, ['Compute']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.assertElementDisabled(dashboardPage.deployButtonSelector, 'No deployment should be possible without controller nodes added')
|
||||
.assertElementExists('div.instruction.invalid', 'Invalid configuration message is shown')
|
||||
.assertElementContainsText('.environment-alerts ul.text-danger li',
|
||||
'At least 1 Controller nodes are required (0 selected currently).',
|
||||
'No controllers added warning should be shown')
|
||||
.then(function() {
|
||||
return dashboardPage.discardChanges();
|
||||
});
|
||||
},
|
||||
'Capacity table tests': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller', 'Storage - Cinder']);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(2, ['Compute']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.assertIsIntegerContentPositive('.capacity-items .cpu .capacity-value', 'CPU')
|
||||
.assertIsIntegerContentPositive('.capacity-items .hdd .capacity-value', 'HDD')
|
||||
.assertIsIntegerContentPositive('.capacity-items .ram .capacity-value', 'RAM')
|
||||
.then(function() {
|
||||
return dashboardPage.discardChanges();
|
||||
});
|
||||
},
|
||||
'Test statistics update': function() {
|
||||
this.timeout = 120000;
|
||||
var controllerNodes = 3,
|
||||
storageCinderNodes = 1,
|
||||
computeNodes = 2,
|
||||
operatingSystemNodes = 1,
|
||||
virtualNodes = 1,
|
||||
valueSelector = '.statistics-block .cluster-info-value',
|
||||
total = controllerNodes + storageCinderNodes + computeNodes + operatingSystemNodes + virtualNodes;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(controllerNodes, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(storageCinderNodes, ['Storage - Cinder']);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(computeNodes, ['Compute']);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(operatingSystemNodes, ['Operating System'], 'error');
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(virtualNodes, ['Virtual'], 'offline');
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.assertElementTextEquals(valueSelector + '.total', total,
|
||||
'The number of Total nodes in statistics is updated according to added nodes')
|
||||
.assertElementTextEquals(valueSelector + '.controller', controllerNodes,
|
||||
'The number of controllerNodes nodes in statistics is updated according to added nodes')
|
||||
.assertElementTextEquals(valueSelector + '.compute', computeNodes,
|
||||
'The number of Compute nodes in statistics is updated according to added nodes')
|
||||
.assertElementTextEquals(valueSelector + '.base-os', operatingSystemNodes,
|
||||
'The number of Operating Systems nodes in statistics is updated according to added nodes')
|
||||
.assertElementTextEquals(valueSelector + '.virt', virtualNodes,
|
||||
'The number of Virtual nodes in statistics is updated according to added nodes')
|
||||
.assertElementTextEquals(valueSelector + '.offline', 1,
|
||||
'The number of Offline nodes in statistics is updated according to added nodes')
|
||||
.assertElementTextEquals(valueSelector + '.error', 1,
|
||||
'The number of Error nodes in statistics is updated according to added nodes')
|
||||
.assertElementTextEquals(valueSelector + '.pending_addition', total,
|
||||
'The number of Pending Addition nodes in statistics is updated according to added nodes')
|
||||
.then(function() {
|
||||
return dashboardPage.discardChanges();
|
||||
});
|
||||
},
|
||||
'Testing error nodes in cluster deploy': function() {
|
||||
this.timeout = 120000;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller'], 'error');
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.assertElementTextEquals('.statistics-block .cluster-info-value.error', 1,
|
||||
'Error node is reflected in Statistics block')
|
||||
.then(function() {
|
||||
return dashboardPage.startDeployment();
|
||||
})
|
||||
.assertElementDisappears('.dashboard-block .progress', 60000, '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',
|
||||
'Deployment failed in case of adding offline nodes')
|
||||
.then(function() {
|
||||
return clusterPage.resetEnvironment(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return dashboardPage.discardChanges();
|
||||
});
|
||||
},
|
||||
'VCenter warning appears': function() {
|
||||
var vCenterClusterName = clusterName + 'VCenter test';
|
||||
return this.remote
|
||||
.clickLinkByText('Environments')
|
||||
.assertElementsAppear('a.clusterbox', 2000, 'The list of clusters is shown when navigating to Environments link')
|
||||
.then(function() {
|
||||
return common.createCluster(
|
||||
vCenterClusterName,
|
||||
{
|
||||
Compute: function() {
|
||||
// Selecting VCenter
|
||||
return this.remote
|
||||
.clickByCssSelector('.custom-tumbler input[name=hypervisor\\:vmware]');
|
||||
},
|
||||
'Networking Setup': function() {
|
||||
// Selecting Nova Network
|
||||
return this.remote
|
||||
.clickByCssSelector('.custom-tumbler input[value=network\\:nova_network]');
|
||||
}
|
||||
}
|
||||
);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.assertElementContainsText('.warnings-block', 'VMware settings are invalid', 'VMware warning is shown');
|
||||
}
|
||||
};
|
||||
});
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
});
|
||||
},
|
||||
beforeEach: function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
});
|
||||
},
|
||||
'Renaming cluster works': function() {
|
||||
var initialName = clusterName,
|
||||
newName = clusterName + '!!!',
|
||||
renameInputSelector = '.rename-block input[type=text]',
|
||||
nameSelector = '.cluster-info-value.name .btn-link';
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return dashboardPage.startClusterRenaming();
|
||||
})
|
||||
.findByCssSelector(renameInputSelector)
|
||||
// Escape
|
||||
.type('\uE00C')
|
||||
.end()
|
||||
.assertElementNotExists(renameInputSelector, 'Rename control disappears')
|
||||
.assertElementTextEquals(nameSelector, initialName,
|
||||
'Switching rename control does not change cluster name')
|
||||
.then(function() {
|
||||
return dashboardPage.setClusterName(newName);
|
||||
})
|
||||
.assertElementTextEquals(nameSelector, newName, 'New name is applied')
|
||||
.then(function() {
|
||||
return dashboardPage.setClusterName(initialName);
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(newName);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.then(function() {
|
||||
return dashboardPage.setClusterName(initialName);
|
||||
})
|
||||
.assertElementAppears('.rename-block.has-error', 1000, 'Error style for duplicate name is applied')
|
||||
.assertElementTextEquals('.rename-block .text-danger', 'Environment with this name already exists',
|
||||
'Duplicate name error text appears'
|
||||
)
|
||||
.findByCssSelector(renameInputSelector)
|
||||
// Escape
|
||||
.type('\uE00C')
|
||||
.end()
|
||||
.clickLinkByText('Environments')
|
||||
.waitForCssSelector(clustersPage.clusterSelector, 2000)
|
||||
.then(function() {
|
||||
return clustersPage.goToEnvironment(initialName);
|
||||
});
|
||||
},
|
||||
'Provision button availability': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Virtual']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.assertElementTextEquals(dashboardPage.deployButtonSelector, 'Provision VMs',
|
||||
'After adding Virtual node deploy button has appropriate text')
|
||||
.then(function() {
|
||||
return dashboardPage.discardChanges();
|
||||
});
|
||||
},
|
||||
'Network validation error warning': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Networks');
|
||||
})
|
||||
.clickByCssSelector('.subtab-link-network_verification')
|
||||
.assertElementContainsText('.alert-warning', 'At least two online nodes are required',
|
||||
'Network verification warning appears if only one node added')
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.assertElementContainsText('.warnings-block',
|
||||
'Please verify your network settings before deployment', 'Network verification warning is shown')
|
||||
.then(function() {
|
||||
return dashboardPage.discardChanges();
|
||||
});
|
||||
},
|
||||
'No controller warning': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
// Adding single compute
|
||||
return common.addNodesToCluster(1, ['Compute']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.assertElementDisabled(dashboardPage.deployButtonSelector, 'No deployment should be possible without controller nodes added')
|
||||
.assertElementExists('div.instruction.invalid', 'Invalid configuration message is shown')
|
||||
.assertElementContainsText('.environment-alerts ul.text-danger li',
|
||||
'At least 1 Controller nodes are required (0 selected currently).',
|
||||
'No controllers added warning should be shown')
|
||||
.then(function() {
|
||||
return dashboardPage.discardChanges();
|
||||
});
|
||||
},
|
||||
'Capacity table tests': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller', 'Storage - Cinder']);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(2, ['Compute']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.assertIsIntegerContentPositive('.capacity-items .cpu .capacity-value', 'CPU')
|
||||
.assertIsIntegerContentPositive('.capacity-items .hdd .capacity-value', 'HDD')
|
||||
.assertIsIntegerContentPositive('.capacity-items .ram .capacity-value', 'RAM')
|
||||
.then(function() {
|
||||
return dashboardPage.discardChanges();
|
||||
});
|
||||
},
|
||||
'Test statistics update': function() {
|
||||
this.timeout = 120000;
|
||||
var controllerNodes = 3,
|
||||
storageCinderNodes = 1,
|
||||
computeNodes = 2,
|
||||
operatingSystemNodes = 1,
|
||||
virtualNodes = 1,
|
||||
valueSelector = '.statistics-block .cluster-info-value',
|
||||
total = controllerNodes + storageCinderNodes + computeNodes + operatingSystemNodes + virtualNodes;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(controllerNodes, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(storageCinderNodes, ['Storage - Cinder']);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(computeNodes, ['Compute']);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(operatingSystemNodes, ['Operating System'], 'error');
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(virtualNodes, ['Virtual'], 'offline');
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.assertElementTextEquals(valueSelector + '.total', total,
|
||||
'The number of Total nodes in statistics is updated according to added nodes')
|
||||
.assertElementTextEquals(valueSelector + '.controller', controllerNodes,
|
||||
'The number of controllerNodes nodes in statistics is updated according to added nodes')
|
||||
.assertElementTextEquals(valueSelector + '.compute', computeNodes,
|
||||
'The number of Compute nodes in statistics is updated according to added nodes')
|
||||
.assertElementTextEquals(valueSelector + '.base-os', operatingSystemNodes,
|
||||
'The number of Operating Systems nodes in statistics is updated according to added nodes')
|
||||
.assertElementTextEquals(valueSelector + '.virt', virtualNodes,
|
||||
'The number of Virtual nodes in statistics is updated according to added nodes')
|
||||
.assertElementTextEquals(valueSelector + '.offline', 1,
|
||||
'The number of Offline nodes in statistics is updated according to added nodes')
|
||||
.assertElementTextEquals(valueSelector + '.error', 1,
|
||||
'The number of Error nodes in statistics is updated according to added nodes')
|
||||
.assertElementTextEquals(valueSelector + '.pending_addition', total,
|
||||
'The number of Pending Addition nodes in statistics is updated according to added nodes')
|
||||
.then(function() {
|
||||
return dashboardPage.discardChanges();
|
||||
});
|
||||
},
|
||||
'Testing error nodes in cluster deploy': function() {
|
||||
this.timeout = 120000;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller'], 'error');
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.assertElementTextEquals('.statistics-block .cluster-info-value.error', 1,
|
||||
'Error node is reflected in Statistics block')
|
||||
.then(function() {
|
||||
return dashboardPage.startDeployment();
|
||||
})
|
||||
.assertElementDisappears('.dashboard-block .progress', 60000, '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',
|
||||
'Deployment failed in case of adding offline nodes')
|
||||
.then(function() {
|
||||
return clusterPage.resetEnvironment(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return dashboardPage.discardChanges();
|
||||
});
|
||||
},
|
||||
'VCenter warning appears': function() {
|
||||
var vCenterClusterName = clusterName + 'VCenter test';
|
||||
return this.remote
|
||||
.clickLinkByText('Environments')
|
||||
.assertElementsAppear('a.clusterbox', 2000, 'The list of clusters is shown when navigating to Environments link')
|
||||
.then(function() {
|
||||
return common.createCluster(
|
||||
vCenterClusterName,
|
||||
{
|
||||
Compute: function() {
|
||||
// Selecting VCenter
|
||||
return this.remote
|
||||
.clickByCssSelector('.custom-tumbler input[name=hypervisor\\:vmware]');
|
||||
},
|
||||
'Networking Setup': function() {
|
||||
// Selecting Nova Network
|
||||
return this.remote
|
||||
.clickByCssSelector('.custom-tumbler input[value=network\\:nova_network]');
|
||||
}
|
||||
}
|
||||
);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.assertElementContainsText('.warnings-block', 'VMware settings are invalid', 'VMware warning is shown');
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,154 +15,154 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern/dojo/node!lodash',
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/helpers',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/cluster',
|
||||
'tests/functional/pages/dashboard',
|
||||
'tests/functional/pages/modal'
|
||||
'intern/dojo/node!lodash',
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/helpers',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/cluster',
|
||||
'tests/functional/pages/dashboard',
|
||||
'tests/functional/pages/modal'
|
||||
], function(_, registerSuite, assert, helpers, Common, ClusterPage, DashboardPage, ModalWindow) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
clusterPage,
|
||||
dashboardPage,
|
||||
modal,
|
||||
clusterName;
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
clusterPage,
|
||||
dashboardPage,
|
||||
modal,
|
||||
clusterName;
|
||||
|
||||
return {
|
||||
name: 'Cluster deployment',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
dashboardPage = new DashboardPage(this.remote);
|
||||
modal = new ModalWindow(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
return {
|
||||
name: 'Cluster deployment',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
dashboardPage = new DashboardPage(this.remote);
|
||||
modal = new ModalWindow(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
});
|
||||
},
|
||||
beforeEach: function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
});
|
||||
},
|
||||
'No deployment button when there are no nodes added': function() {
|
||||
return this.remote
|
||||
.assertElementNotExists(dashboardPage.deployButtonSelector, 'No deployment should be possible without nodes added');
|
||||
},
|
||||
'Discard changes': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
// Adding three controllers
|
||||
return common.addNodesToCluster(1, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.clickByCssSelector('.btn-discard-changes')
|
||||
.then(function() {
|
||||
return modal.waitToOpen();
|
||||
})
|
||||
.assertElementContainsText('h4.modal-title', 'Discard Changes', 'Discard Changes confirmation modal expected')
|
||||
.then(function() {
|
||||
return modal.clickFooterButton('Discard');
|
||||
})
|
||||
.then(function() {
|
||||
return modal.waitToClose();
|
||||
})
|
||||
.assertElementAppears('.dashboard-block a.btn-add-nodes', 2000, 'All changes discarded, add nodes button gets visible in deploy readiness block');
|
||||
},
|
||||
'Start/stop deployment': function() {
|
||||
this.timeout = 100000;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(3, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.assertElementAppears('.dashboard-tab', 200, 'Dashboard tab opened')
|
||||
.then(function() {
|
||||
return dashboardPage.startDeployment();
|
||||
})
|
||||
.assertElementAppears('div.deploy-process div.progress', 2000, 'Deployment started')
|
||||
.assertElementAppears('button.stop-deployment-btn:not(:disabled)', 5000, 'Stop button appears')
|
||||
.then(function() {
|
||||
return dashboardPage.stopDeployment();
|
||||
})
|
||||
.assertElementDisappears('div.deploy-process div.progress', 20000, 'Deployment stopped')
|
||||
.assertElementAppears(dashboardPage.deployButtonSelector, 1000, 'Deployment button available')
|
||||
.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
|
||||
.then(function() {
|
||||
return clusterPage.resetEnvironment(clusterName);
|
||||
});
|
||||
},
|
||||
'Test tabs locking after deployment completed': function() {
|
||||
this.timeout = 100000;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
// Adding single controller (enough for deployment)
|
||||
return common.addNodesToCluster(1, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.isTabLocked('Networks');
|
||||
})
|
||||
.then(function(isLocked) {
|
||||
assert.isFalse(isLocked, 'Networks tab is not locked for undeployed cluster');
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.isTabLocked('Settings');
|
||||
})
|
||||
.then(function(isLocked) {
|
||||
assert.isFalse(isLocked, 'Settings tab is not locked for undeployed cluster');
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.then(function() {
|
||||
return dashboardPage.startDeployment();
|
||||
})
|
||||
.assertElementDisappears('.dashboard-block .progress', 60000, 'Progress bar disappears after deployment')
|
||||
.assertElementAppears('.links-block', 5000, 'Deployment completed')
|
||||
.assertElementExists('.go-to-healthcheck', 'Healthcheck link is visible after deploy')
|
||||
.findByLinkText('Horizon')
|
||||
.getAttribute('href')
|
||||
.then(function(value) {
|
||||
return assert.isTrue(_.startsWith(value, 'http'), 'Link to Horizon is formed');
|
||||
})
|
||||
.end()
|
||||
.then(function() {
|
||||
return clusterPage.isTabLocked('Networks');
|
||||
})
|
||||
.then(function(isLocked) {
|
||||
assert.isTrue(isLocked, 'Networks tab should turn locked after deployment');
|
||||
})
|
||||
.assertElementEnabled('.add-nodegroup-btn', 'Add Node network group button is enabled after cluster deploy')
|
||||
.then(function() {
|
||||
return clusterPage.isTabLocked('Settings');
|
||||
})
|
||||
.then(function(isLocked) {
|
||||
assert.isTrue(isLocked, 'Settings tab should turn locked after deployment');
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.resetEnvironment(clusterName);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
});
|
||||
},
|
||||
beforeEach: function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
});
|
||||
},
|
||||
'No deployment button when there are no nodes added': function() {
|
||||
return this.remote
|
||||
.assertElementNotExists(dashboardPage.deployButtonSelector, 'No deployment should be possible without nodes added');
|
||||
},
|
||||
'Discard changes': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
// Adding three controllers
|
||||
return common.addNodesToCluster(1, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.clickByCssSelector('.btn-discard-changes')
|
||||
.then(function() {
|
||||
return modal.waitToOpen();
|
||||
})
|
||||
.assertElementContainsText('h4.modal-title', 'Discard Changes', 'Discard Changes confirmation modal expected')
|
||||
.then(function() {
|
||||
return modal.clickFooterButton('Discard');
|
||||
})
|
||||
.then(function() {
|
||||
return modal.waitToClose();
|
||||
})
|
||||
.assertElementAppears('.dashboard-block a.btn-add-nodes', 2000, 'All changes discarded, add nodes button gets visible in deploy readiness block');
|
||||
},
|
||||
'Start/stop deployment': function() {
|
||||
this.timeout = 100000;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(3, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.assertElementAppears('.dashboard-tab', 200, 'Dashboard tab opened')
|
||||
.then(function() {
|
||||
return dashboardPage.startDeployment();
|
||||
})
|
||||
.assertElementAppears('div.deploy-process div.progress', 2000, 'Deployment started')
|
||||
.assertElementAppears('button.stop-deployment-btn:not(:disabled)', 5000, 'Stop button appears')
|
||||
.then(function() {
|
||||
return dashboardPage.stopDeployment();
|
||||
})
|
||||
.assertElementDisappears('div.deploy-process div.progress', 20000, 'Deployment stopped')
|
||||
.assertElementAppears(dashboardPage.deployButtonSelector, 1000, 'Deployment button available')
|
||||
.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
|
||||
.then(function() {
|
||||
return clusterPage.resetEnvironment(clusterName);
|
||||
});
|
||||
},
|
||||
'Test tabs locking after deployment completed': function() {
|
||||
this.timeout = 100000;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
// Adding single controller (enough for deployment)
|
||||
return common.addNodesToCluster(1, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.isTabLocked('Networks');
|
||||
})
|
||||
.then(function(isLocked) {
|
||||
assert.isFalse(isLocked, 'Networks tab is not locked for undeployed cluster');
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.isTabLocked('Settings');
|
||||
})
|
||||
.then(function(isLocked) {
|
||||
assert.isFalse(isLocked, 'Settings tab is not locked for undeployed cluster');
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.then(function() {
|
||||
return dashboardPage.startDeployment();
|
||||
})
|
||||
.assertElementDisappears('.dashboard-block .progress', 60000, 'Progress bar disappears after deployment')
|
||||
.assertElementAppears('.links-block', 5000, 'Deployment completed')
|
||||
.assertElementExists('.go-to-healthcheck', 'Healthcheck link is visible after deploy')
|
||||
.findByLinkText('Horizon')
|
||||
.getAttribute('href')
|
||||
.then(function(value) {
|
||||
return assert.isTrue(_.startsWith(value, 'http'), 'Link to Horizon is formed');
|
||||
})
|
||||
.end()
|
||||
.then(function() {
|
||||
return clusterPage.isTabLocked('Networks');
|
||||
})
|
||||
.then(function(isLocked) {
|
||||
assert.isTrue(isLocked, 'Networks tab should turn locked after deployment');
|
||||
})
|
||||
.assertElementEnabled('.add-nodegroup-btn', 'Add Node network group button is enabled after cluster deploy')
|
||||
.then(function() {
|
||||
return clusterPage.isTabLocked('Settings');
|
||||
})
|
||||
.then(function(isLocked) {
|
||||
assert.isTrue(isLocked, 'Settings tab should turn locked after deployment');
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.resetEnvironment(clusterName);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,70 +15,70 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern!object',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/cluster'
|
||||
'intern!object',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/cluster'
|
||||
], function(registerSuite, Common, ClusterPage) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
clusterPage,
|
||||
clusterName;
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
clusterPage,
|
||||
clusterName;
|
||||
|
||||
return {
|
||||
name: 'Logs Tab',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
return {
|
||||
name: 'Logs Tab',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Logs');
|
||||
});
|
||||
},
|
||||
'"Show" button availability and logs displaying': function() {
|
||||
var showLogsButtonSelector = '.sticker button';
|
||||
return this.remote
|
||||
.assertElementsExist('.sticker select[name=source] > option', '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
|
||||
.clickByCssSelector('.sticker select[name=source] option[value=api]')
|
||||
// Change the selected value for the "Level" dropdown to DEBUG
|
||||
.clickByCssSelector('.sticker select[name=level] option[value=DEBUG]')
|
||||
.assertElementEnabled(showLogsButtonSelector, '"Show" button is enabled after source change')
|
||||
.execute(function() {
|
||||
window.fakeServer = sinon.fakeServer.create();
|
||||
window.fakeServer.autoRespond = true;
|
||||
window.fakeServer.autoRespondAfter = 1000;
|
||||
window.fakeServer.respondWith(/\/api\/logs.*/, [
|
||||
200, {'Content-Type': 'application/json'},
|
||||
JSON.stringify({
|
||||
from: 1,
|
||||
entries: [['Date', 'INFO', 'Test Log Entry']]
|
||||
})
|
||||
]);
|
||||
})
|
||||
.clickByCssSelector(showLogsButtonSelector)
|
||||
.assertElementDisappears('.logs-tab div.progress', 5000, 'Wait till Progress bar disappears')
|
||||
.assertElementsAppear('.log-entries > tbody > tr', 5000, 'Log entries are shown')
|
||||
.execute(function() {
|
||||
window.fakeServer.restore();
|
||||
})
|
||||
// "Other servers" option is present in "Logs" dropdown
|
||||
.clickByCssSelector('.sticker select[name=type] > option[value=remote]')
|
||||
.assertElementExists('.sticker select[name=node] > option', '"Node" dropdown is present');
|
||||
}
|
||||
};
|
||||
});
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Logs');
|
||||
});
|
||||
},
|
||||
'"Show" button availability and logs displaying': function() {
|
||||
var showLogsButtonSelector = '.sticker button';
|
||||
return this.remote
|
||||
.assertElementsExist('.sticker select[name=source] > option', '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
|
||||
.clickByCssSelector('.sticker select[name=source] option[value=api]')
|
||||
// Change the selected value for the "Level" dropdown to DEBUG
|
||||
.clickByCssSelector('.sticker select[name=level] option[value=DEBUG]')
|
||||
.assertElementEnabled(showLogsButtonSelector, '"Show" button is enabled after source change')
|
||||
.execute(function() {
|
||||
window.fakeServer = sinon.fakeServer.create();
|
||||
window.fakeServer.autoRespond = true;
|
||||
window.fakeServer.autoRespondAfter = 1000;
|
||||
window.fakeServer.respondWith(/\/api\/logs.*/, [
|
||||
200, {'Content-Type': 'application/json'},
|
||||
JSON.stringify({
|
||||
from: 1,
|
||||
entries: [['Date', 'INFO', 'Test Log Entry']]
|
||||
})
|
||||
]);
|
||||
})
|
||||
.clickByCssSelector(showLogsButtonSelector)
|
||||
.assertElementDisappears('.logs-tab div.progress', 5000, 'Wait till Progress bar disappears')
|
||||
.assertElementsAppear('.log-entries > tbody > tr', 5000, 'Log entries are shown')
|
||||
.execute(function() {
|
||||
window.fakeServer.restore();
|
||||
})
|
||||
// "Other servers" option is present in "Logs" dropdown
|
||||
.clickByCssSelector('.sticker select[name=type] > option[value=remote]')
|
||||
.assertElementExists('.sticker select[name=node] > option', '"Node" dropdown is present');
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,432 +15,432 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern/dojo/node!lodash',
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/networks',
|
||||
'tests/functional/pages/cluster',
|
||||
'tests/functional/pages/modal',
|
||||
'tests/functional/pages/dashboard'
|
||||
'intern/dojo/node!lodash',
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/networks',
|
||||
'tests/functional/pages/cluster',
|
||||
'tests/functional/pages/modal',
|
||||
'tests/functional/pages/dashboard'
|
||||
], function(_, registerSuite, assert, Common, NetworksPage, ClusterPage, ModalWindow, DashboardPage) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
networksPage,
|
||||
clusterPage,
|
||||
clusterName,
|
||||
dashboardPage;
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
networksPage,
|
||||
clusterPage,
|
||||
clusterName,
|
||||
dashboardPage;
|
||||
|
||||
return {
|
||||
name: 'Networks page Nova Network tests',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
networksPage = new NetworksPage(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
dashboardPage = new DashboardPage(this.remote);
|
||||
return {
|
||||
name: 'Networks page Nova Network tests',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
networksPage = new NetworksPage(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
dashboardPage = new DashboardPage(this.remote);
|
||||
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(
|
||||
clusterName,
|
||||
{
|
||||
Compute: function() {
|
||||
// select VCenter to enable Nova networking
|
||||
return this.remote
|
||||
.clickByCssSelector('input[name=hypervisor\\:vmware]');
|
||||
},
|
||||
'Networking Setup': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('input[value=network\\:nova_network]');
|
||||
}
|
||||
}
|
||||
);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Networks');
|
||||
});
|
||||
},
|
||||
afterEach: function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.btn-revert-changes');
|
||||
},
|
||||
'Network Tab is rendered correctly': function() {
|
||||
return this.remote
|
||||
.assertElementExists('.nova-managers .radio-group', 'Nova Network manager radiogroup is present')
|
||||
.assertElementsExist('.checkbox-group input[name=net_provider]', 2, 'Network manager options are present')
|
||||
.assertElementSelected('input[value=FlatDHCPManager]', 'Flat DHCP manager is chosen')
|
||||
.assertElementsExist('.network-tab h3', 3, 'All networks are present');
|
||||
},
|
||||
'Testing cluster networks: Save button interactions': function() {
|
||||
var self = this,
|
||||
cidrInitialValue,
|
||||
cidrElementSelector = '.storage input[name=cidr]';
|
||||
return this.remote
|
||||
.findByCssSelector(cidrElementSelector)
|
||||
.then(function(element) {
|
||||
return element.getAttribute('value')
|
||||
.then(function(value) {
|
||||
cidrInitialValue = value;
|
||||
});
|
||||
})
|
||||
.end()
|
||||
.setInputValue(cidrElementSelector, '240.0.1.0/25')
|
||||
.assertElementAppears(networksPage.applyButtonSelector + ':not(:disabled)', 200,
|
||||
'Save changes button is enabled if there are changes')
|
||||
.then(function() {
|
||||
return self.remote.setInputValue(cidrElementSelector, cidrInitialValue);
|
||||
})
|
||||
.assertElementAppears(networksPage.applyButtonSelector + ':disabled', 200,
|
||||
'Save changes button is disabled again if there are no changes');
|
||||
},
|
||||
'Testing cluster networks: change network manager': function() {
|
||||
var amountSelector = 'input[name=fixed_networks_amount]',
|
||||
sizeSelector = 'select[name=fixed_network_size]';
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return networksPage.switchNetworkManager();
|
||||
})
|
||||
.clickByCssSelector('.subtab-link-nova_configuration')
|
||||
.assertElementExists(amountSelector, 'Amount field for a fixed network is present in VLAN mode')
|
||||
.assertElementExists(sizeSelector, 'Size field for a fixed network is present in VLAN mode')
|
||||
.assertElementEnabled(networksPage.applyButtonSelector, 'Save changes button is enabled after manager was changed')
|
||||
.then(function() {
|
||||
return networksPage.switchNetworkManager();
|
||||
})
|
||||
.assertElementNotExists(amountSelector, 'Amount field was hidden after revert to FlatDHCP')
|
||||
.assertElementNotExists(sizeSelector, 'Size field was hidden after revert to FlatDHCP')
|
||||
.assertElementDisabled(networksPage.applyButtonSelector, 'Save changes button is disabled again after revert to FlatDHCP');
|
||||
},
|
||||
'Testing cluster networks: network notation change': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-default')
|
||||
.assertElementAppears('.storage', 2000, 'Storage network is shown')
|
||||
.assertElementSelected('.storage .cidr input[type=checkbox]', '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]')
|
||||
.assertElementNotExists('.storage .ip_ranges input[type=text]:disabled', 'Network notation was changed to "ip_ranges"');
|
||||
},
|
||||
'Testing cluster networks: VLAN range fields': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return networksPage.switchNetworkManager();
|
||||
})
|
||||
.clickByCssSelector('.subtab-link-nova_configuration')
|
||||
.assertElementAppears('input[name=range-end_fixed_networks_vlan_start]', 2000, 'VLAN range is displayed');
|
||||
},
|
||||
'Testing cluster networks: save network changes': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return networksPage.switchNetworkManager();
|
||||
})
|
||||
.clickByCssSelector(networksPage.applyButtonSelector)
|
||||
.assertElementsAppear('input:not(:disabled)', 2000, 'Inputs are not disabled')
|
||||
.assertElementNotExists('.alert-error', 'Correct settings were saved successfully');
|
||||
},
|
||||
'Testing cluster networks: save settings with group: network': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-network_settings')
|
||||
.clickByCssSelector('input[name=auto_assign_floating_ip][type=checkbox]')
|
||||
.clickByCssSelector(networksPage.applyButtonSelector)
|
||||
.assertElementsAppear('input:not(:disabled)', 2000, 'Inputs are not disabled')
|
||||
.assertElementDisabled(networksPage.applyButtonSelector, 'Save changes button is disabled again after successfull settings saving');
|
||||
},
|
||||
'Testing cluster networks: verification': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-network_verification')
|
||||
.assertElementDisabled('.verify-networks-btn', 'Verification button is disabled in case of no nodes')
|
||||
.assertElementTextEquals('.alert-warning',
|
||||
'At least two online nodes are required to verify environment network configuration',
|
||||
'Not enough nodes warning is shown')
|
||||
.clickByCssSelector('.subtab-link-default')
|
||||
.then(function() {
|
||||
// Adding 2 controllers
|
||||
return common.addNodesToCluster(2, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Networks');
|
||||
})
|
||||
.setInputValue('.public input[name=gateway]', '172.16.0.2')
|
||||
.clickByCssSelector('.subtab-link-network_verification')
|
||||
.clickByCssSelector('.verify-networks-btn')
|
||||
.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')
|
||||
// Testing cluster networks: verification task deletion
|
||||
.then(function() {
|
||||
return networksPage.switchNetworkManager();
|
||||
})
|
||||
.clickByCssSelector('.subtab-link-network_verification')
|
||||
.assertElementNotExists('.page-control-box .alert', 'Verification task was removed after settings has been changed')
|
||||
.clickByCssSelector('.btn-revert-changes')
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.then(function() {
|
||||
return dashboardPage.discardChanges();
|
||||
}) .then(function() {
|
||||
return clusterPage.goToTab('Networks');
|
||||
});
|
||||
},
|
||||
'Check VlanID field validation': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-default')
|
||||
.assertElementAppears('.management', 2000, 'Management network appears')
|
||||
.clickByCssSelector('.management .vlan-tagging input[type=checkbox]')
|
||||
.clickByCssSelector('.management .vlan-tagging input[type=checkbox]')
|
||||
.assertElementExists('.management .has-error input[name=vlan_start]',
|
||||
'Field validation has worked properly in case of empty value');
|
||||
},
|
||||
'Testing cluster networks: data validation on manager change': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return networksPage.switchNetworkManager();
|
||||
})
|
||||
.clickByCssSelector('.subtab-link-nova_configuration')
|
||||
.assertElementAppears('input[name=fixed_networks_vlan_start][type=checkbox]', 2000, 'Vlan range appearsse')
|
||||
.clickByCssSelector('input[name=fixed_networks_vlan_start][type=checkbox]')
|
||||
.then(function() {
|
||||
return networksPage.switchNetworkManager();
|
||||
})
|
||||
.assertElementExists('.has-error input[name=range-start_fixed_networks_vlan_start][type=text]',
|
||||
'Field validation has worked')
|
||||
.assertElementDisabled(networksPage.applyButtonSelector,
|
||||
'Save changes button is disabled if there is validation error')
|
||||
.then(function() {
|
||||
return networksPage.switchNetworkManager();
|
||||
})
|
||||
.clickByCssSelector('input[name=fixed_networks_vlan_start][type=checkbox]')
|
||||
.assertElementNotExists('.has-error input[name=range-start_fixed_networks_vlan_start][type=text]',
|
||||
'Field validation works properly');
|
||||
},
|
||||
'Testing cluster networks: data validation on invalid settings': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-default')
|
||||
.setInputValue('input[name=range-end_ip_ranges]', '172.16.0.2')
|
||||
.clickByCssSelector(networksPage.applyButtonSelector)
|
||||
.assertElementAppears('.alert-danger.network-alert', 2000, 'Validation error appears');
|
||||
}
|
||||
};
|
||||
});
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(
|
||||
clusterName,
|
||||
{
|
||||
Compute: function() {
|
||||
// select VCenter to enable Nova networking
|
||||
return this.remote
|
||||
.clickByCssSelector('input[name=hypervisor\\:vmware]');
|
||||
},
|
||||
'Networking Setup': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('input[value=network\\:nova_network]');
|
||||
}
|
||||
}
|
||||
);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Networks');
|
||||
});
|
||||
},
|
||||
afterEach: function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.btn-revert-changes');
|
||||
},
|
||||
'Network Tab is rendered correctly': function() {
|
||||
return this.remote
|
||||
.assertElementExists('.nova-managers .radio-group', 'Nova Network manager radiogroup is present')
|
||||
.assertElementsExist('.checkbox-group input[name=net_provider]', 2, 'Network manager options are present')
|
||||
.assertElementSelected('input[value=FlatDHCPManager]', 'Flat DHCP manager is chosen')
|
||||
.assertElementsExist('.network-tab h3', 3, 'All networks are present');
|
||||
},
|
||||
'Testing cluster networks: Save button interactions': function() {
|
||||
var self = this,
|
||||
cidrInitialValue,
|
||||
cidrElementSelector = '.storage input[name=cidr]';
|
||||
return this.remote
|
||||
.findByCssSelector(cidrElementSelector)
|
||||
.then(function(element) {
|
||||
return element.getAttribute('value')
|
||||
.then(function(value) {
|
||||
cidrInitialValue = value;
|
||||
});
|
||||
})
|
||||
.end()
|
||||
.setInputValue(cidrElementSelector, '240.0.1.0/25')
|
||||
.assertElementAppears(networksPage.applyButtonSelector + ':not(:disabled)', 200,
|
||||
'Save changes button is enabled if there are changes')
|
||||
.then(function() {
|
||||
return self.remote.setInputValue(cidrElementSelector, cidrInitialValue);
|
||||
})
|
||||
.assertElementAppears(networksPage.applyButtonSelector + ':disabled', 200,
|
||||
'Save changes button is disabled again if there are no changes');
|
||||
},
|
||||
'Testing cluster networks: change network manager': function() {
|
||||
var amountSelector = 'input[name=fixed_networks_amount]',
|
||||
sizeSelector = 'select[name=fixed_network_size]';
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return networksPage.switchNetworkManager();
|
||||
})
|
||||
.clickByCssSelector('.subtab-link-nova_configuration')
|
||||
.assertElementExists(amountSelector, 'Amount field for a fixed network is present in VLAN mode')
|
||||
.assertElementExists(sizeSelector, 'Size field for a fixed network is present in VLAN mode')
|
||||
.assertElementEnabled(networksPage.applyButtonSelector, 'Save changes button is enabled after manager was changed')
|
||||
.then(function() {
|
||||
return networksPage.switchNetworkManager();
|
||||
})
|
||||
.assertElementNotExists(amountSelector, 'Amount field was hidden after revert to FlatDHCP')
|
||||
.assertElementNotExists(sizeSelector, 'Size field was hidden after revert to FlatDHCP')
|
||||
.assertElementDisabled(networksPage.applyButtonSelector, 'Save changes button is disabled again after revert to FlatDHCP');
|
||||
},
|
||||
'Testing cluster networks: network notation change': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-default')
|
||||
.assertElementAppears('.storage', 2000, 'Storage network is shown')
|
||||
.assertElementSelected('.storage .cidr input[type=checkbox]', '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]')
|
||||
.assertElementNotExists('.storage .ip_ranges input[type=text]:disabled', 'Network notation was changed to "ip_ranges"');
|
||||
},
|
||||
'Testing cluster networks: VLAN range fields': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return networksPage.switchNetworkManager();
|
||||
})
|
||||
.clickByCssSelector('.subtab-link-nova_configuration')
|
||||
.assertElementAppears('input[name=range-end_fixed_networks_vlan_start]', 2000, 'VLAN range is displayed');
|
||||
},
|
||||
'Testing cluster networks: save network changes': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return networksPage.switchNetworkManager();
|
||||
})
|
||||
.clickByCssSelector(networksPage.applyButtonSelector)
|
||||
.assertElementsAppear('input:not(:disabled)', 2000, 'Inputs are not disabled')
|
||||
.assertElementNotExists('.alert-error', 'Correct settings were saved successfully');
|
||||
},
|
||||
'Testing cluster networks: save settings with group: network': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-network_settings')
|
||||
.clickByCssSelector('input[name=auto_assign_floating_ip][type=checkbox]')
|
||||
.clickByCssSelector(networksPage.applyButtonSelector)
|
||||
.assertElementsAppear('input:not(:disabled)', 2000, 'Inputs are not disabled')
|
||||
.assertElementDisabled(networksPage.applyButtonSelector, 'Save changes button is disabled again after successfull settings saving');
|
||||
},
|
||||
'Testing cluster networks: verification': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-network_verification')
|
||||
.assertElementDisabled('.verify-networks-btn', 'Verification button is disabled in case of no nodes')
|
||||
.assertElementTextEquals('.alert-warning',
|
||||
'At least two online nodes are required to verify environment network configuration',
|
||||
'Not enough nodes warning is shown')
|
||||
.clickByCssSelector('.subtab-link-default')
|
||||
.then(function() {
|
||||
// Adding 2 controllers
|
||||
return common.addNodesToCluster(2, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Networks');
|
||||
})
|
||||
.setInputValue('.public input[name=gateway]', '172.16.0.2')
|
||||
.clickByCssSelector('.subtab-link-network_verification')
|
||||
.clickByCssSelector('.verify-networks-btn')
|
||||
.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')
|
||||
// Testing cluster networks: verification task deletion
|
||||
.then(function() {
|
||||
return networksPage.switchNetworkManager();
|
||||
})
|
||||
.clickByCssSelector('.subtab-link-network_verification')
|
||||
.assertElementNotExists('.page-control-box .alert', 'Verification task was removed after settings has been changed')
|
||||
.clickByCssSelector('.btn-revert-changes')
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.then(function() {
|
||||
return dashboardPage.discardChanges();
|
||||
}) .then(function() {
|
||||
return clusterPage.goToTab('Networks');
|
||||
});
|
||||
},
|
||||
'Check VlanID field validation': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-default')
|
||||
.assertElementAppears('.management', 2000, 'Management network appears')
|
||||
.clickByCssSelector('.management .vlan-tagging input[type=checkbox]')
|
||||
.clickByCssSelector('.management .vlan-tagging input[type=checkbox]')
|
||||
.assertElementExists('.management .has-error input[name=vlan_start]',
|
||||
'Field validation has worked properly in case of empty value');
|
||||
},
|
||||
'Testing cluster networks: data validation on manager change': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return networksPage.switchNetworkManager();
|
||||
})
|
||||
.clickByCssSelector('.subtab-link-nova_configuration')
|
||||
.assertElementAppears('input[name=fixed_networks_vlan_start][type=checkbox]', 2000, 'Vlan range appearsse')
|
||||
.clickByCssSelector('input[name=fixed_networks_vlan_start][type=checkbox]')
|
||||
.then(function() {
|
||||
return networksPage.switchNetworkManager();
|
||||
})
|
||||
.assertElementExists('.has-error input[name=range-start_fixed_networks_vlan_start][type=text]',
|
||||
'Field validation has worked')
|
||||
.assertElementDisabled(networksPage.applyButtonSelector,
|
||||
'Save changes button is disabled if there is validation error')
|
||||
.then(function() {
|
||||
return networksPage.switchNetworkManager();
|
||||
})
|
||||
.clickByCssSelector('input[name=fixed_networks_vlan_start][type=checkbox]')
|
||||
.assertElementNotExists('.has-error input[name=range-start_fixed_networks_vlan_start][type=text]',
|
||||
'Field validation works properly');
|
||||
},
|
||||
'Testing cluster networks: data validation on invalid settings': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-default')
|
||||
.setInputValue('input[name=range-end_ip_ranges]', '172.16.0.2')
|
||||
.clickByCssSelector(networksPage.applyButtonSelector)
|
||||
.assertElementAppears('.alert-danger.network-alert', 2000, 'Validation error appears');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
networksPage,
|
||||
clusterPage,
|
||||
clusterName;
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
networksPage,
|
||||
clusterPage,
|
||||
clusterName;
|
||||
|
||||
return {
|
||||
name: 'Networks page Neutron tests',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
networksPage = new NetworksPage(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
return {
|
||||
name: 'Networks page Neutron tests',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
networksPage = new NetworksPage(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(
|
||||
clusterName,
|
||||
{
|
||||
'Networking Setup': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('input[value=network\\:neutron\\:ml2\\:vlan]')
|
||||
.clickByCssSelector('input[value=network\\:neutron\\:ml2\\:tun]');
|
||||
}
|
||||
}
|
||||
);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Networks');
|
||||
});
|
||||
},
|
||||
afterEach: function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.btn-revert-changes');
|
||||
},
|
||||
'Add ranges manipulations': function() {
|
||||
var rangeSelector = '.public .ip_ranges ';
|
||||
return this.remote
|
||||
.clickByCssSelector(rangeSelector + '.ip-ranges-add')
|
||||
.assertElementsExist(rangeSelector + '.ip-ranges-delete', 2, 'Remove ranges controls appear')
|
||||
.clickByCssSelector(networksPage.applyButtonSelector)
|
||||
.assertElementsExist(rangeSelector + '.range-row',
|
||||
'Empty range row is removed after saving changes')
|
||||
.assertElementNotExists(rangeSelector + '.ip-ranges-delete',
|
||||
'Remove button is absent for only one range');
|
||||
},
|
||||
'DNS nameservers manipulations': function() {
|
||||
var dnsNameserversSelector = '.dns_nameservers ';
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-neutron_l3')
|
||||
.clickByCssSelector(dnsNameserversSelector + '.ip-ranges-add')
|
||||
.assertElementExists(dnsNameserversSelector + '.range-row .has-error',
|
||||
'New nameserver is added and contains validation error');
|
||||
},
|
||||
'Segmentation types differences': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-default')
|
||||
// Tunneling segmentation tests
|
||||
.assertElementExists('.private',
|
||||
'Private Network is visible for tunneling segmentation type')
|
||||
.assertElementTextEquals('.segmentation-type', '(Neutron with tunneling segmentation)',
|
||||
'Segmentation type is correct for tunneling segmentation')
|
||||
// Vlan segmentation tests
|
||||
.clickLinkByText('Environments')
|
||||
.then(function() {
|
||||
return common.createCluster('Test vlan segmentation');
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Networks');
|
||||
})
|
||||
.assertElementNotExists('.private', 'Private Network is not visible for vlan segmentation type')
|
||||
.assertElementTextEquals('.segmentation-type', '(Neutron with VLAN segmentation)',
|
||||
'Segmentation type is correct for VLAN segmentation');
|
||||
},
|
||||
'Junk input in ip fields': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-default')
|
||||
.setInputValue('.public input[name=cidr]', 'blablabla')
|
||||
.assertElementAppears('.public .has-error input[name=cidr]', 1000,
|
||||
'Error class is applied for invalid cidr')
|
||||
.assertElementAppears('.subtab-link-default .subtab-icon.glyphicon-danger-sign', 1000,
|
||||
'Warning icon for node network group appears')
|
||||
.assertElementAppears('.add-nodegroup-btn .glyphicon-danger-sign', 1000,
|
||||
'Warning icon for add node network group appears')
|
||||
.setInputValue('.public input[name=range-start_ip_ranges]', 'blablabla')
|
||||
.assertElementAppears('.public .has-error input[name=range-start_ip_ranges]', 1000,
|
||||
'Error class is applied for invalid range start');
|
||||
},
|
||||
'Other settings validation error': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-network_settings')
|
||||
.setInputValue('input[name=dns_list]', 'blablabla')
|
||||
.assertElementAppears('.subtab-link-network_settings .glyphicon-danger-sign', 1000,
|
||||
'Warning icon for "Other" section appears');
|
||||
}
|
||||
};
|
||||
});
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(
|
||||
clusterName,
|
||||
{
|
||||
'Networking Setup': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('input[value=network\\:neutron\\:ml2\\:vlan]')
|
||||
.clickByCssSelector('input[value=network\\:neutron\\:ml2\\:tun]');
|
||||
}
|
||||
}
|
||||
);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Networks');
|
||||
});
|
||||
},
|
||||
afterEach: function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.btn-revert-changes');
|
||||
},
|
||||
'Add ranges manipulations': function() {
|
||||
var rangeSelector = '.public .ip_ranges ';
|
||||
return this.remote
|
||||
.clickByCssSelector(rangeSelector + '.ip-ranges-add')
|
||||
.assertElementsExist(rangeSelector + '.ip-ranges-delete', 2, 'Remove ranges controls appear')
|
||||
.clickByCssSelector(networksPage.applyButtonSelector)
|
||||
.assertElementsExist(rangeSelector + '.range-row',
|
||||
'Empty range row is removed after saving changes')
|
||||
.assertElementNotExists(rangeSelector + '.ip-ranges-delete',
|
||||
'Remove button is absent for only one range');
|
||||
},
|
||||
'DNS nameservers manipulations': function() {
|
||||
var dnsNameserversSelector = '.dns_nameservers ';
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-neutron_l3')
|
||||
.clickByCssSelector(dnsNameserversSelector + '.ip-ranges-add')
|
||||
.assertElementExists(dnsNameserversSelector + '.range-row .has-error',
|
||||
'New nameserver is added and contains validation error');
|
||||
},
|
||||
'Segmentation types differences': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-default')
|
||||
// Tunneling segmentation tests
|
||||
.assertElementExists('.private',
|
||||
'Private Network is visible for tunneling segmentation type')
|
||||
.assertElementTextEquals('.segmentation-type', '(Neutron with tunneling segmentation)',
|
||||
'Segmentation type is correct for tunneling segmentation')
|
||||
// Vlan segmentation tests
|
||||
.clickLinkByText('Environments')
|
||||
.then(function() {
|
||||
return common.createCluster('Test vlan segmentation');
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Networks');
|
||||
})
|
||||
.assertElementNotExists('.private', 'Private Network is not visible for vlan segmentation type')
|
||||
.assertElementTextEquals('.segmentation-type', '(Neutron with VLAN segmentation)',
|
||||
'Segmentation type is correct for VLAN segmentation');
|
||||
},
|
||||
'Junk input in ip fields': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-default')
|
||||
.setInputValue('.public input[name=cidr]', 'blablabla')
|
||||
.assertElementAppears('.public .has-error input[name=cidr]', 1000,
|
||||
'Error class is applied for invalid cidr')
|
||||
.assertElementAppears('.subtab-link-default .subtab-icon.glyphicon-danger-sign', 1000,
|
||||
'Warning icon for node network group appears')
|
||||
.assertElementAppears('.add-nodegroup-btn .glyphicon-danger-sign', 1000,
|
||||
'Warning icon for add node network group appears')
|
||||
.setInputValue('.public input[name=range-start_ip_ranges]', 'blablabla')
|
||||
.assertElementAppears('.public .has-error input[name=range-start_ip_ranges]', 1000,
|
||||
'Error class is applied for invalid range start');
|
||||
},
|
||||
'Other settings validation error': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-network_settings')
|
||||
.setInputValue('input[name=dns_list]', 'blablabla')
|
||||
.assertElementAppears('.subtab-link-network_settings .glyphicon-danger-sign', 1000,
|
||||
'Warning icon for "Other" section appears');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
clusterPage,
|
||||
dashboardPage,
|
||||
clusterName,
|
||||
modal;
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
clusterPage,
|
||||
dashboardPage,
|
||||
clusterName,
|
||||
modal;
|
||||
|
||||
return {
|
||||
name: 'Node network group tests',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
dashboardPage = new DashboardPage(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
modal = new ModalWindow(this.remote);
|
||||
return {
|
||||
name: 'Node network group tests',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
dashboardPage = new DashboardPage(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
modal = new ModalWindow(this.remote);
|
||||
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Networks');
|
||||
});
|
||||
},
|
||||
'Node network group creation': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.add-nodegroup-btn')
|
||||
.then(function() {
|
||||
return modal.waitToOpen();
|
||||
})
|
||||
.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')
|
||||
.then(function() {
|
||||
return modal.clickFooterButton('Add Group');
|
||||
})
|
||||
.then(function() {
|
||||
return modal.waitToClose();
|
||||
})
|
||||
.assertElementAppears('.node-network-groups-list', 2000, 'Node network groups title appears')
|
||||
.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');
|
||||
},
|
||||
'Verification is disabled for multirack': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-network_verification')
|
||||
.assertElementExists('.alert-warning', 'Warning is shown')
|
||||
.assertElementDisabled('.verify-networks-btn', 'Verify networks button is disabled');
|
||||
},
|
||||
'Node network group renaming': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-Node_Network_Group_1')
|
||||
.clickByCssSelector('.glyphicon-pencil')
|
||||
.waitForCssSelector('.network-group-name input[type=text]', 2000)
|
||||
.findByCssSelector('.node-group-renaming input[type=text]')
|
||||
.type('Node_Network_Group_2')
|
||||
// Enter
|
||||
.type('\uE007')
|
||||
.end()
|
||||
.assertElementDisplayed('.subtab-link-Node_Network_Group_2', 'Node network group was successfully renamed');
|
||||
},
|
||||
'Node network group deletion': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-default')
|
||||
.assertElementNotExists('.glyphicon-remove', 'It is not possible to delete default node network group')
|
||||
.clickByCssSelector('.subtab-link-Node_Network_Group_2')
|
||||
.assertElementAppears('.glyphicon-remove', 1000, 'Remove icon is shown')
|
||||
.clickByCssSelector('.glyphicon-remove')
|
||||
.then(function() {
|
||||
return modal.waitToOpen();
|
||||
})
|
||||
.assertElementContainsText('h4.modal-title', 'Remove Node Network Group', 'Remove Node Network Group modal expected')
|
||||
.then(function() {
|
||||
return modal.clickFooterButton('Delete');
|
||||
})
|
||||
.then(function() {
|
||||
return modal.waitToClose();
|
||||
})
|
||||
.assertElementDisappears('.subtab-link-Node_Network_Group_2', 2000, 'Node network groups title disappears');
|
||||
},
|
||||
'Node network group renaming in deployed environment': function() {
|
||||
this.timeout = 100000;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.then(function() {
|
||||
return dashboardPage.startDeployment();
|
||||
})
|
||||
.waitForElementDeletion('.dashboard-block .progress', 60000)
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Networks');
|
||||
})
|
||||
.clickByCssSelector('.subtab-link-default')
|
||||
.assertElementNotExists('.glyphicon-pencil', 'Renaming of a node network group is fobidden in deployed environment')
|
||||
.clickByCssSelector('.network-group-name .name')
|
||||
.assertElementNotExists('.network-group-name input[type=text]', 'Renaming is not started on a node network group name click')
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.resetEnvironment(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return dashboardPage.discardChanges();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Networks');
|
||||
});
|
||||
},
|
||||
'Node network group creation': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.add-nodegroup-btn')
|
||||
.then(function() {
|
||||
return modal.waitToOpen();
|
||||
})
|
||||
.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')
|
||||
.then(function() {
|
||||
return modal.clickFooterButton('Add Group');
|
||||
})
|
||||
.then(function() {
|
||||
return modal.waitToClose();
|
||||
})
|
||||
.assertElementAppears('.node-network-groups-list', 2000, 'Node network groups title appears')
|
||||
.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');
|
||||
},
|
||||
'Verification is disabled for multirack': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-network_verification')
|
||||
.assertElementExists('.alert-warning', 'Warning is shown')
|
||||
.assertElementDisabled('.verify-networks-btn', 'Verify networks button is disabled');
|
||||
},
|
||||
'Node network group renaming': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-Node_Network_Group_1')
|
||||
.clickByCssSelector('.glyphicon-pencil')
|
||||
.waitForCssSelector('.network-group-name input[type=text]', 2000)
|
||||
.findByCssSelector('.node-group-renaming input[type=text]')
|
||||
.type('Node_Network_Group_2')
|
||||
// Enter
|
||||
.type('\uE007')
|
||||
.end()
|
||||
.assertElementDisplayed('.subtab-link-Node_Network_Group_2', 'Node network group was successfully renamed');
|
||||
},
|
||||
'Node network group deletion': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.subtab-link-default')
|
||||
.assertElementNotExists('.glyphicon-remove', 'It is not possible to delete default node network group')
|
||||
.clickByCssSelector('.subtab-link-Node_Network_Group_2')
|
||||
.assertElementAppears('.glyphicon-remove', 1000, 'Remove icon is shown')
|
||||
.clickByCssSelector('.glyphicon-remove')
|
||||
.then(function() {
|
||||
return modal.waitToOpen();
|
||||
})
|
||||
.assertElementContainsText('h4.modal-title', 'Remove Node Network Group', 'Remove Node Network Group modal expected')
|
||||
.then(function() {
|
||||
return modal.clickFooterButton('Delete');
|
||||
})
|
||||
.then(function() {
|
||||
return modal.waitToClose();
|
||||
})
|
||||
.assertElementDisappears('.subtab-link-Node_Network_Group_2', 2000, 'Node network groups title disappears');
|
||||
},
|
||||
'Node network group renaming in deployed environment': function() {
|
||||
this.timeout = 100000;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.then(function() {
|
||||
return dashboardPage.startDeployment();
|
||||
})
|
||||
.waitForElementDeletion('.dashboard-block .progress', 60000)
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Networks');
|
||||
})
|
||||
.clickByCssSelector('.subtab-link-default')
|
||||
.assertElementNotExists('.glyphicon-pencil', 'Renaming of a node network group is fobidden in deployed environment')
|
||||
.clickByCssSelector('.network-group-name .name')
|
||||
.assertElementNotExists('.network-group-name input[type=text]', 'Renaming is not started on a node network group name click')
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.resetEnvironment(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return dashboardPage.discardChanges();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,101 +15,101 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern/dojo/node!lodash',
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/helpers',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/cluster'
|
||||
'intern/dojo/node!lodash',
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/helpers',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/cluster'
|
||||
], function(_, registerSuite, assert, helpers, Common, ClusterPage) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
clusterPage,
|
||||
clusterName,
|
||||
nodesAmount = 3,
|
||||
applyButtonSelector = 'button.btn-apply';
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
clusterPage,
|
||||
clusterName,
|
||||
nodesAmount = 3,
|
||||
applyButtonSelector = 'button.btn-apply';
|
||||
|
||||
return {
|
||||
name: 'Cluster page',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
return {
|
||||
name: 'Cluster page',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Nodes');
|
||||
});
|
||||
},
|
||||
'Add Cluster Nodes': function() {
|
||||
return this.remote
|
||||
.assertElementExists('.node-list .alert-warning', 'Node list shows warning if there are no nodes in environment')
|
||||
.clickByCssSelector('.btn-add-nodes')
|
||||
.assertElementsAppear('.node', 2000, 'Unallocated nodes loaded')
|
||||
.assertElementDisabled(applyButtonSelector, 'Apply button is disabled until both roles and nodes chosen')
|
||||
.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() {
|
||||
return clusterPage.checkNodeRoles(['Controller', 'Storage - Cinder']);
|
||||
})
|
||||
.assertElementDisabled('.role-panel [type=checkbox][name=compute]', 'Compute role can not be added together with selected roles')
|
||||
.assertElementDisabled(applyButtonSelector, 'Apply button is disabled until both roles and nodes chosen')
|
||||
.then(function() {
|
||||
return clusterPage.checkNodes(nodesAmount);
|
||||
})
|
||||
.clickByCssSelector(applyButtonSelector)
|
||||
.waitForElementDeletion(applyButtonSelector, 2000)
|
||||
.assertElementAppears('.nodes-group', 2000, 'Cluster node list loaded')
|
||||
.assertElementsExist('.node-list .node', nodesAmount, nodesAmount + ' nodes were successfully added to the cluster')
|
||||
.assertElementExists('.nodes-group', 'One node group is present');
|
||||
},
|
||||
'Edit cluster node roles': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Storage - Cinder']);
|
||||
})
|
||||
.assertElementsExist('.nodes-group', 2, 'Two node groups are present')
|
||||
// select all nodes
|
||||
.clickByCssSelector('.select-all label')
|
||||
.clickByCssSelector('.btn-edit-roles')
|
||||
.assertElementDisappears('.btn-edit-roles', 2000, 'Cluster nodes screen unmounted')
|
||||
.assertElementNotExists('.node-box [type=checkbox]:not(:disabled)', '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() {
|
||||
// uncheck Cinder role
|
||||
return clusterPage.checkNodeRoles(['Storage - Cinder', 'Storage - Cinder']);
|
||||
})
|
||||
.clickByCssSelector(applyButtonSelector)
|
||||
.assertElementDisappears('.btn-apply', 2000, 'Role editing screen unmounted')
|
||||
.assertElementsExist('.node-list .node', nodesAmount, 'One node was removed from cluster after editing roles');
|
||||
},
|
||||
'Remove Cluster': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.doesClusterExist(clusterName);
|
||||
})
|
||||
.then(function(result) {
|
||||
assert.ok(result, 'Cluster exists');
|
||||
})
|
||||
.then(function() {
|
||||
return common.removeCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return common.doesClusterExist(clusterName);
|
||||
})
|
||||
.then(function(result) {
|
||||
assert.notOk(result, 'Cluster removed successfully');
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Nodes');
|
||||
});
|
||||
},
|
||||
'Add Cluster Nodes': function() {
|
||||
return this.remote
|
||||
.assertElementExists('.node-list .alert-warning', 'Node list shows warning if there are no nodes in environment')
|
||||
.clickByCssSelector('.btn-add-nodes')
|
||||
.assertElementsAppear('.node', 2000, 'Unallocated nodes loaded')
|
||||
.assertElementDisabled(applyButtonSelector, 'Apply button is disabled until both roles and nodes chosen')
|
||||
.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() {
|
||||
return clusterPage.checkNodeRoles(['Controller', 'Storage - Cinder']);
|
||||
})
|
||||
.assertElementDisabled('.role-panel [type=checkbox][name=compute]', 'Compute role can not be added together with selected roles')
|
||||
.assertElementDisabled(applyButtonSelector, 'Apply button is disabled until both roles and nodes chosen')
|
||||
.then(function() {
|
||||
return clusterPage.checkNodes(nodesAmount);
|
||||
})
|
||||
.clickByCssSelector(applyButtonSelector)
|
||||
.waitForElementDeletion(applyButtonSelector, 2000)
|
||||
.assertElementAppears('.nodes-group', 2000, 'Cluster node list loaded')
|
||||
.assertElementsExist('.node-list .node', nodesAmount, nodesAmount + ' nodes were successfully added to the cluster')
|
||||
.assertElementExists('.nodes-group', 'One node group is present');
|
||||
},
|
||||
'Edit cluster node roles': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Storage - Cinder']);
|
||||
})
|
||||
.assertElementsExist('.nodes-group', 2, 'Two node groups are present')
|
||||
// select all nodes
|
||||
.clickByCssSelector('.select-all label')
|
||||
.clickByCssSelector('.btn-edit-roles')
|
||||
.assertElementDisappears('.btn-edit-roles', 2000, 'Cluster nodes screen unmounted')
|
||||
.assertElementNotExists('.node-box [type=checkbox]:not(:disabled)', '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() {
|
||||
// uncheck Cinder role
|
||||
return clusterPage.checkNodeRoles(['Storage - Cinder', 'Storage - Cinder']);
|
||||
})
|
||||
.clickByCssSelector(applyButtonSelector)
|
||||
.assertElementDisappears('.btn-apply', 2000, 'Role editing screen unmounted')
|
||||
.assertElementsExist('.node-list .node', nodesAmount, 'One node was removed from cluster after editing roles');
|
||||
},
|
||||
'Remove Cluster': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.doesClusterExist(clusterName);
|
||||
})
|
||||
.then(function(result) {
|
||||
assert.ok(result, 'Cluster exists');
|
||||
})
|
||||
.then(function() {
|
||||
return common.removeCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return common.doesClusterExist(clusterName);
|
||||
})
|
||||
.then(function(result) {
|
||||
assert.notOk(result, 'Cluster removed successfully');
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,152 +15,152 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/helpers',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/cluster',
|
||||
'tests/functional/pages/settings',
|
||||
'tests/functional/pages/modal'
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/helpers',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/cluster',
|
||||
'tests/functional/pages/settings',
|
||||
'tests/functional/pages/modal'
|
||||
], function(registerSuite, assert, helpers, Common, ClusterPage, SettingsPage, ModalWindow) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
clusterPage,
|
||||
settingsPage,
|
||||
modal,
|
||||
clusterName;
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
clusterPage,
|
||||
settingsPage,
|
||||
modal,
|
||||
clusterName;
|
||||
|
||||
return {
|
||||
name: 'Settings tab',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
settingsPage = new SettingsPage(this.remote);
|
||||
modal = new ModalWindow(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
return {
|
||||
name: 'Settings tab',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
settingsPage = new SettingsPage(this.remote);
|
||||
modal = new ModalWindow(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Settings');
|
||||
})
|
||||
// go to Storage subtab to use checkboxes for tests
|
||||
.clickLinkByText('Storage');
|
||||
},
|
||||
'Settings tab is rendered correctly': function() {
|
||||
return this.remote
|
||||
.assertElementNotExists('.nav .subtab-link-network', 'Subtab for Network settings is not presented in navigation')
|
||||
.assertElementEnabled('.btn-load-defaults', 'Load defaults button is enabled')
|
||||
.assertElementDisabled('.btn-revert-changes', 'Cancel Changes button is disabled')
|
||||
.assertElementDisabled('.btn-apply-changes', 'Save Settings button is disabled');
|
||||
},
|
||||
'Check Save Settings button': function() {
|
||||
return this.remote
|
||||
// introduce change
|
||||
.clickByCssSelector('input[type=checkbox]')
|
||||
.assertElementAppears('.btn-apply-changes:not(:disabled)', 200, 'Save Settings button is enabled if there are changes')
|
||||
// reset the change
|
||||
.clickByCssSelector('input[type=checkbox]')
|
||||
.assertElementAppears('.btn-apply-changes:disabled', 200, 'Save Settings button is disabled if there are no changes');
|
||||
},
|
||||
'Check Cancel Changes button': function() {
|
||||
return this.remote
|
||||
// introduce change
|
||||
.clickByCssSelector('input[type=checkbox]')
|
||||
.waitForCssSelector('.btn-apply-changes:not(:disabled)', 200)
|
||||
// try to move out of Settings tab
|
||||
.clickLinkByText('Dashboard')
|
||||
.then(function() {
|
||||
// check Discard Chasnges dialog appears
|
||||
return modal.waitToOpen();
|
||||
})
|
||||
.then(function() {
|
||||
return modal.close();
|
||||
})
|
||||
// reset changes
|
||||
.clickByCssSelector('.btn-revert-changes')
|
||||
.assertElementDisabled('.btn-apply-changes', 'Save Settings button is disabled after changes were cancelled');
|
||||
},
|
||||
'Check changes saving': function() {
|
||||
return this.remote
|
||||
// introduce change
|
||||
.clickByCssSelector('input[type=checkbox]')
|
||||
.waitForCssSelector('.btn-apply-changes:not(:disabled)', 200)
|
||||
.clickByCssSelector('.btn-apply-changes')
|
||||
.then(function() {
|
||||
return settingsPage.waitForRequestCompleted();
|
||||
})
|
||||
.assertElementDisabled('.btn-revert-changes', 'Cancel Changes button is disabled after changes were saved successfully');
|
||||
},
|
||||
'Check loading of defaults': function() {
|
||||
return this.remote
|
||||
// load defaults
|
||||
.clickByCssSelector('.btn-load-defaults')
|
||||
.then(function() {
|
||||
return settingsPage.waitForRequestCompleted();
|
||||
})
|
||||
.assertElementEnabled('.btn-apply-changes', '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
|
||||
.clickByCssSelector('.btn-revert-changes');
|
||||
},
|
||||
'The choice of subgroup is preserved when user navigates through the cluster tabs': function() {
|
||||
return this.remote
|
||||
.clickLinkByText('Logging')
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.then(function() {
|
||||
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');
|
||||
},
|
||||
'The page reacts on invalid input': function() {
|
||||
return this.remote
|
||||
.clickLinkByText('General')
|
||||
// "nova" is forbidden username
|
||||
.setInputValue('[type=text][name=user]', 'nova')
|
||||
.assertElementAppears('.setting-section .form-group.has-error', 200, 'Invalid field marked as 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
|
||||
.clickByCssSelector('.btn-revert-changes')
|
||||
.assertElementNotExists('.setting-section .form-group.has-error', '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() {
|
||||
var repoAmount,
|
||||
self = this;
|
||||
return this.remote
|
||||
.clickLinkByText('General')
|
||||
// get amount of default repositories
|
||||
.findAllByCssSelector('.repos .form-inline')
|
||||
.then(function(elements) {
|
||||
repoAmount = elements.length;
|
||||
})
|
||||
.end()
|
||||
.assertElementNotExists('.repos .form-inline:nth-of-type(1) .btn-link', 'The first repo can not be deleted')
|
||||
// delete some repo
|
||||
.clickByCssSelector('.repos .form-inline .btn-link')
|
||||
.then(function() {
|
||||
return self.remote.assertElementsExist('.repos .form-inline', repoAmount - 1, 'Repo was deleted');
|
||||
})
|
||||
// add new repo
|
||||
.clickByCssSelector('.btn-add-repo')
|
||||
.then(function() {
|
||||
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')
|
||||
// revert the change
|
||||
.clickByCssSelector('.btn-revert-changes');
|
||||
}
|
||||
};
|
||||
});
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Settings');
|
||||
})
|
||||
// go to Storage subtab to use checkboxes for tests
|
||||
.clickLinkByText('Storage');
|
||||
},
|
||||
'Settings tab is rendered correctly': function() {
|
||||
return this.remote
|
||||
.assertElementNotExists('.nav .subtab-link-network', 'Subtab for Network settings is not presented in navigation')
|
||||
.assertElementEnabled('.btn-load-defaults', 'Load defaults button is enabled')
|
||||
.assertElementDisabled('.btn-revert-changes', 'Cancel Changes button is disabled')
|
||||
.assertElementDisabled('.btn-apply-changes', 'Save Settings button is disabled');
|
||||
},
|
||||
'Check Save Settings button': function() {
|
||||
return this.remote
|
||||
// introduce change
|
||||
.clickByCssSelector('input[type=checkbox]')
|
||||
.assertElementAppears('.btn-apply-changes:not(:disabled)', 200, 'Save Settings button is enabled if there are changes')
|
||||
// reset the change
|
||||
.clickByCssSelector('input[type=checkbox]')
|
||||
.assertElementAppears('.btn-apply-changes:disabled', 200, 'Save Settings button is disabled if there are no changes');
|
||||
},
|
||||
'Check Cancel Changes button': function() {
|
||||
return this.remote
|
||||
// introduce change
|
||||
.clickByCssSelector('input[type=checkbox]')
|
||||
.waitForCssSelector('.btn-apply-changes:not(:disabled)', 200)
|
||||
// try to move out of Settings tab
|
||||
.clickLinkByText('Dashboard')
|
||||
.then(function() {
|
||||
// check Discard Chasnges dialog appears
|
||||
return modal.waitToOpen();
|
||||
})
|
||||
.then(function() {
|
||||
return modal.close();
|
||||
})
|
||||
// reset changes
|
||||
.clickByCssSelector('.btn-revert-changes')
|
||||
.assertElementDisabled('.btn-apply-changes', 'Save Settings button is disabled after changes were cancelled');
|
||||
},
|
||||
'Check changes saving': function() {
|
||||
return this.remote
|
||||
// introduce change
|
||||
.clickByCssSelector('input[type=checkbox]')
|
||||
.waitForCssSelector('.btn-apply-changes:not(:disabled)', 200)
|
||||
.clickByCssSelector('.btn-apply-changes')
|
||||
.then(function() {
|
||||
return settingsPage.waitForRequestCompleted();
|
||||
})
|
||||
.assertElementDisabled('.btn-revert-changes', 'Cancel Changes button is disabled after changes were saved successfully');
|
||||
},
|
||||
'Check loading of defaults': function() {
|
||||
return this.remote
|
||||
// load defaults
|
||||
.clickByCssSelector('.btn-load-defaults')
|
||||
.then(function() {
|
||||
return settingsPage.waitForRequestCompleted();
|
||||
})
|
||||
.assertElementEnabled('.btn-apply-changes', '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
|
||||
.clickByCssSelector('.btn-revert-changes');
|
||||
},
|
||||
'The choice of subgroup is preserved when user navigates through the cluster tabs': function() {
|
||||
return this.remote
|
||||
.clickLinkByText('Logging')
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.then(function() {
|
||||
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');
|
||||
},
|
||||
'The page reacts on invalid input': function() {
|
||||
return this.remote
|
||||
.clickLinkByText('General')
|
||||
// "nova" is forbidden username
|
||||
.setInputValue('[type=text][name=user]', 'nova')
|
||||
.assertElementAppears('.setting-section .form-group.has-error', 200, 'Invalid field marked as 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
|
||||
.clickByCssSelector('.btn-revert-changes')
|
||||
.assertElementNotExists('.setting-section .form-group.has-error', '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() {
|
||||
var repoAmount,
|
||||
self = this;
|
||||
return this.remote
|
||||
.clickLinkByText('General')
|
||||
// get amount of default repositories
|
||||
.findAllByCssSelector('.repos .form-inline')
|
||||
.then(function(elements) {
|
||||
repoAmount = elements.length;
|
||||
})
|
||||
.end()
|
||||
.assertElementNotExists('.repos .form-inline:nth-of-type(1) .btn-link', 'The first repo can not be deleted')
|
||||
// delete some repo
|
||||
.clickByCssSelector('.repos .form-inline .btn-link')
|
||||
.then(function() {
|
||||
return self.remote.assertElementsExist('.repos .form-inline', repoAmount - 1, 'Repo was deleted');
|
||||
})
|
||||
// add new repo
|
||||
.clickByCssSelector('.btn-add-repo')
|
||||
.then(function() {
|
||||
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')
|
||||
// revert the change
|
||||
.clickByCssSelector('.btn-revert-changes');
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,80 +15,80 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/helpers',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/modal'
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/helpers',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/modal'
|
||||
], function(registerSuite, assert, helpers, Common, ModalWindow) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
clusterName;
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
clusterName;
|
||||
|
||||
return {
|
||||
name: 'Clusters page',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
return {
|
||||
name: 'Clusters page',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
|
||||
return this.remote
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
});
|
||||
},
|
||||
beforeEach: function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
});
|
||||
},
|
||||
afterEach: function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.removeCluster(clusterName);
|
||||
});
|
||||
},
|
||||
'Create Cluster': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.doesClusterExist(clusterName);
|
||||
})
|
||||
.then(function(result) {
|
||||
assert.ok(result, 'Newly created cluster found in the list');
|
||||
});
|
||||
},
|
||||
'Attempt to create cluster with duplicate name': function() {
|
||||
return this.remote
|
||||
.clickLinkByText('Environments')
|
||||
.waitForCssSelector('.clusters-page', 2000)
|
||||
.then(function() {
|
||||
return common.createCluster(
|
||||
clusterName,
|
||||
{
|
||||
'Name and Release': function() {
|
||||
var modal = new ModalWindow(this.remote);
|
||||
return this.remote
|
||||
.pressKeys('\uE007')
|
||||
.assertElementTextEquals(
|
||||
'.create-cluster-form span.help-block',
|
||||
'Environment with name "' + clusterName + '" already exists',
|
||||
'Error message should say that environment with that name already exists'
|
||||
)
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
return modal.close();
|
||||
});
|
||||
},
|
||||
beforeEach: function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
});
|
||||
},
|
||||
afterEach: function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.removeCluster(clusterName);
|
||||
});
|
||||
},
|
||||
'Create Cluster': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.doesClusterExist(clusterName);
|
||||
})
|
||||
.then(function(result) {
|
||||
assert.ok(result, 'Newly created cluster found in the list');
|
||||
});
|
||||
},
|
||||
'Attempt to create cluster with duplicate name': function() {
|
||||
return this.remote
|
||||
.clickLinkByText('Environments')
|
||||
.waitForCssSelector('.clusters-page', 2000)
|
||||
.then(function() {
|
||||
return common.createCluster(
|
||||
clusterName,
|
||||
{
|
||||
'Name and Release': function() {
|
||||
var modal = new ModalWindow(this.remote);
|
||||
return this.remote
|
||||
.pressKeys('\uE007')
|
||||
.assertElementTextEquals(
|
||||
'.create-cluster-form span.help-block',
|
||||
'Environment with name "' + clusterName + '" already exists',
|
||||
'Error message should say that environment with that name already exists'
|
||||
)
|
||||
.then(function() {
|
||||
return modal.close();
|
||||
});
|
||||
}}
|
||||
);
|
||||
});
|
||||
},
|
||||
'Testing cluster list page': function() {
|
||||
return this.remote
|
||||
.clickLinkByText('Environments')
|
||||
.assertElementAppears('.clusters-page .clusterbox', 2000, 'Cluster container exists')
|
||||
.assertElementExists('.create-cluster', 'Cluster creation control exists');
|
||||
}
|
||||
};
|
||||
});
|
||||
}}
|
||||
);
|
||||
});
|
||||
},
|
||||
'Testing cluster list page': function() {
|
||||
return this.remote
|
||||
.clickLinkByText('Environments')
|
||||
.assertElementAppears('.clusters-page .clusterbox', 2000, 'Cluster container exists')
|
||||
.assertElementExists('.create-cluster', 'Cluster creation control exists');
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,81 +15,81 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern!object',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/node',
|
||||
'tests/functional/pages/modal'
|
||||
'intern!object',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/node',
|
||||
'tests/functional/pages/modal'
|
||||
], function(registerSuite, Common, NodeComponent, ModalWindow) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
node,
|
||||
modal;
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
node,
|
||||
modal;
|
||||
|
||||
return {
|
||||
name: 'Equipment Page',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
node = new NodeComponent(this.remote);
|
||||
modal = new ModalWindow(this.remote);
|
||||
return {
|
||||
name: 'Equipment Page',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
node = new NodeComponent(this.remote);
|
||||
modal = new ModalWindow(this.remote);
|
||||
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster('Env#1');
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(2, ['Controller']);
|
||||
})
|
||||
// go back to Environments page
|
||||
.clickLinkByText('Environments')
|
||||
.waitForCssSelector('.clusters-page', 2000)
|
||||
.then(function() {
|
||||
return common.createCluster('Env#2');
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(2, ['Compute', 'Virtual']);
|
||||
})
|
||||
// go back to Environments page
|
||||
.clickLinkByText('Environments')
|
||||
.waitForCssSelector('.clusters-page', 2000)
|
||||
// go to Equipment page
|
||||
.clickLinkByText('Equipment')
|
||||
.waitForCssSelector('.equipment-page', 5000);
|
||||
},
|
||||
'Equipment page is rendered correctly': function() {
|
||||
return this.remote
|
||||
.assertElementsExist('.node', 8, 'All Fuel nodes are presented')
|
||||
.assertElementNotExists('.control-buttons-box .btn', 'No management buttons presented')
|
||||
.assertElementsExist('.nodes-group', 4, 'The page has default sorting by node status');
|
||||
},
|
||||
'Check action buttons': function() {
|
||||
return this.remote
|
||||
.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')
|
||||
.clickByCssSelector('.node.pending_addition > label')
|
||||
.assertElementNotExists('.control-buttons-box .btn', 'No management buttons for selected node')
|
||||
.assertElementExists('.node-list-management-buttons .btn-labels:not(:disabled)', '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')
|
||||
.then(function() {
|
||||
return modal.waitToOpen();
|
||||
})
|
||||
.assertElementNotExists('.btn-edit-disks', 'No disks configuration buttons in node pop-up')
|
||||
.assertElementNotExists('.btn-edit-networks', 'No interfaces configuration buttons in node pop-up')
|
||||
.then(function() {
|
||||
return modal.close();
|
||||
})
|
||||
.clickByCssSelector('label.compact')
|
||||
.then(function() {
|
||||
return node.openCompactNodeExtendedView();
|
||||
})
|
||||
.assertElementNotExists('.node-popover .node-buttons .btn:not(.btn-view-logs)', 'No action buttons in node extended view in compact mode');
|
||||
}
|
||||
};
|
||||
});
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster('Env#1');
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(2, ['Controller']);
|
||||
})
|
||||
// go back to Environments page
|
||||
.clickLinkByText('Environments')
|
||||
.waitForCssSelector('.clusters-page', 2000)
|
||||
.then(function() {
|
||||
return common.createCluster('Env#2');
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(2, ['Compute', 'Virtual']);
|
||||
})
|
||||
// go back to Environments page
|
||||
.clickLinkByText('Environments')
|
||||
.waitForCssSelector('.clusters-page', 2000)
|
||||
// go to Equipment page
|
||||
.clickLinkByText('Equipment')
|
||||
.waitForCssSelector('.equipment-page', 5000);
|
||||
},
|
||||
'Equipment page is rendered correctly': function() {
|
||||
return this.remote
|
||||
.assertElementsExist('.node', 8, 'All Fuel nodes are presented')
|
||||
.assertElementNotExists('.control-buttons-box .btn', 'No management buttons presented')
|
||||
.assertElementsExist('.nodes-group', 4, 'The page has default sorting by node status');
|
||||
},
|
||||
'Check action buttons': function() {
|
||||
return this.remote
|
||||
.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')
|
||||
.clickByCssSelector('.node.pending_addition > label')
|
||||
.assertElementNotExists('.control-buttons-box .btn', 'No management buttons for selected node')
|
||||
.assertElementExists('.node-list-management-buttons .btn-labels:not(:disabled)', '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')
|
||||
.then(function() {
|
||||
return modal.waitToOpen();
|
||||
})
|
||||
.assertElementNotExists('.btn-edit-disks', 'No disks configuration buttons in node pop-up')
|
||||
.assertElementNotExists('.btn-edit-networks', 'No interfaces configuration buttons in node pop-up')
|
||||
.then(function() {
|
||||
return modal.close();
|
||||
})
|
||||
.clickByCssSelector('label.compact')
|
||||
.then(function() {
|
||||
return node.openCompactNodeExtendedView();
|
||||
})
|
||||
.assertElementNotExists('.node-popover .node-buttons .btn:not(.btn-view-logs)', 'No action buttons in node extended view in compact mode');
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,42 +15,42 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/helpers',
|
||||
'tests/functional/pages/login',
|
||||
'tests/functional/pages/common'
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/helpers',
|
||||
'tests/functional/pages/login',
|
||||
'tests/functional/pages/common'
|
||||
], function(registerSuite, assert, helpers, LoginPage, Common) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
registerSuite(function() {
|
||||
var loginPage, common;
|
||||
return {
|
||||
name: 'Login page',
|
||||
setup: function() {
|
||||
loginPage = new LoginPage(this.remote);
|
||||
common = new Common(this.remote);
|
||||
},
|
||||
beforeEach: function() {
|
||||
this.remote
|
||||
.then(function() {
|
||||
return common.getOut();
|
||||
});
|
||||
},
|
||||
'Login with incorrect credentials': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return loginPage.login('login', '*****');
|
||||
})
|
||||
.assertElementExists('div.login-error', 'Error message is expected to get displayed');
|
||||
},
|
||||
'Login with proper credentials': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return loginPage.login();
|
||||
})
|
||||
.assertElementDisappears('.login-btn', 2000, 'Login button disappears after successful login');
|
||||
}
|
||||
};
|
||||
});
|
||||
registerSuite(function() {
|
||||
var loginPage, common;
|
||||
return {
|
||||
name: 'Login page',
|
||||
setup: function() {
|
||||
loginPage = new LoginPage(this.remote);
|
||||
common = new Common(this.remote);
|
||||
},
|
||||
beforeEach: function() {
|
||||
this.remote
|
||||
.then(function() {
|
||||
return common.getOut();
|
||||
});
|
||||
},
|
||||
'Login with incorrect credentials': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return loginPage.login('login', '*****');
|
||||
})
|
||||
.assertElementExists('div.login-error', 'Error message is expected to get displayed');
|
||||
},
|
||||
'Login with proper credentials': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return loginPage.login();
|
||||
})
|
||||
.assertElementDisappears('.login-btn', 2000, 'Login button disappears after successful login');
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,143 +15,143 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/pages/common'
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/pages/common'
|
||||
], function(registerSuite, assert, Common) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
clusterName,
|
||||
initialImageSize,
|
||||
sdaDisk,
|
||||
applyButtonSelector, cancelButtonSelector, loadDefaultsButtonSelector;
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
clusterName,
|
||||
initialImageSize,
|
||||
sdaDisk,
|
||||
applyButtonSelector, cancelButtonSelector, loadDefaultsButtonSelector;
|
||||
|
||||
return {
|
||||
name: 'Node Disk',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
sdaDisk = '.disk-box[data-disk=sda]';
|
||||
applyButtonSelector = '.btn-apply';
|
||||
cancelButtonSelector = '.btn-revert-changes';
|
||||
loadDefaultsButtonSelector = '.btn-defaults';
|
||||
return {
|
||||
name: 'Node Disk',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
sdaDisk = '.disk-box[data-disk=sda]';
|
||||
applyButtonSelector = '.btn-apply';
|
||||
cancelButtonSelector = '.btn-revert-changes';
|
||||
loadDefaultsButtonSelector = '.btn-defaults';
|
||||
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller']);
|
||||
})
|
||||
// check node
|
||||
.clickByCssSelector('.node.pending_addition input[type=checkbox]')
|
||||
// click Configure Disks button
|
||||
.clickByCssSelector('.btn-configure-disks')
|
||||
.assertElementsAppear('.edit-node-disks-screen', 2000, 'Node disk screen loaded')
|
||||
.findByCssSelector(sdaDisk + ' input[type=number][name=image]')
|
||||
// get the initial size of the Image Storage volume
|
||||
.then(function(input) {
|
||||
return input.getProperty('value')
|
||||
.then(function(value) {
|
||||
initialImageSize = value;
|
||||
});
|
||||
})
|
||||
.end();
|
||||
},
|
||||
'Testing nodes disks layout': function() {
|
||||
return this.remote
|
||||
.assertElementDisabled(cancelButtonSelector, 'Cancel button is disabled')
|
||||
.assertElementDisabled(applyButtonSelector, 'Apply button is disabled')
|
||||
.assertElementEnabled(loadDefaultsButtonSelector, 'Load Defaults button is enabled');
|
||||
},
|
||||
'Check SDA disk layout': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector(sdaDisk + ' .disk-visual [data-volume=os] .toggle')
|
||||
.findByCssSelector(sdaDisk + ' .disk-utility-box [data-volume=os] input')
|
||||
.then(function(input) {
|
||||
return input.getProperty('value')
|
||||
.then(function(value) {
|
||||
assert.ok(value, 'Base System is allocated on SDA disk');
|
||||
});
|
||||
})
|
||||
.end()
|
||||
.findByCssSelector(sdaDisk + ' .disk-utility-box [data-volume=image] input')
|
||||
.then(function(input) {
|
||||
return input.getProperty('value')
|
||||
.then(function(value) {
|
||||
assert.ok(value, 'Image Storage is allocated on SDA disk');
|
||||
});
|
||||
})
|
||||
.end()
|
||||
.assertElementExists(sdaDisk + ' .disk-visual [data-volume=image] .close-btn', 'Button Close for Image Storage volume 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() {
|
||||
return this.remote
|
||||
.setInputValue(sdaDisk + ' input[type=number][name=image]', '5')
|
||||
.assertElementEnabled(cancelButtonSelector, 'Cancel button is enabled')
|
||||
.assertElementEnabled(applyButtonSelector, 'Apply button is enabled')
|
||||
.clickByCssSelector(applyButtonSelector)
|
||||
.assertElementDisappears('.btn-load-defaults:disabled', 2000, 'Wait for changes applied')
|
||||
.clickByCssSelector(loadDefaultsButtonSelector)
|
||||
.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')
|
||||
.assertElementEnabled(cancelButtonSelector, 'Cancel button is enabled')
|
||||
.assertElementEnabled(applyButtonSelector, 'Apply button is enabled')
|
||||
.clickByCssSelector(applyButtonSelector);
|
||||
},
|
||||
'Testing volume group deletion and Cancel button': function() {
|
||||
return this.remote
|
||||
.findByCssSelector(sdaDisk + ' .disk-visual [data-volume=image]')
|
||||
// check that visualisation div for Image Storage present and has positive width
|
||||
.then(function(element) {
|
||||
return element.getSize()
|
||||
.then(function(sizes) {
|
||||
assert.isTrue(sizes.width > 0, 'Expected positive width for Image Storage visual');
|
||||
});
|
||||
})
|
||||
.end()
|
||||
.clickByCssSelector(sdaDisk + ' .disk-visual [data-volume=image] .close-btn')
|
||||
.assertElementEnabled(applyButtonSelector, 'Apply button is enabled')
|
||||
.findByCssSelector(sdaDisk + ' .disk-visual [data-volume=image]')
|
||||
// check Image Storage volume deleted
|
||||
.then(function(element) {
|
||||
return element.getSize()
|
||||
.then(function(sizes) {
|
||||
assert.equal(sizes.width, 0, 'Expected null width for Image Storage visual');
|
||||
});
|
||||
})
|
||||
.end()
|
||||
.assertElementPropertyEquals(sdaDisk + ' input[type=number][name=image]', 'value', 0, 'Image Storage volume was removed successfully')
|
||||
.findByCssSelector(sdaDisk + ' .disk-visual [data-volume=unallocated]')
|
||||
// check that there is unallocated space after Image Storage removal
|
||||
.then(function(element) {
|
||||
return element.getSize()
|
||||
.then(function(sizes) {
|
||||
assert.isTrue(sizes.width > 0, 'There is unallocated space after Image Storage removal');
|
||||
});
|
||||
})
|
||||
.end()
|
||||
.clickByCssSelector(cancelButtonSelector)
|
||||
.assertElementPropertyEquals(sdaDisk + ' input[type=number][name=image]', 'value', initialImageSize, 'Image Storage volume control contains correct value')
|
||||
.assertElementDisabled(applyButtonSelector, 'Apply button is disabled');
|
||||
},
|
||||
'Test volume size validation': function() {
|
||||
return this.remote
|
||||
// reduce Image Storage volume size to free space on the disk
|
||||
.setInputValue(sdaDisk + ' input[type=number][name=image]', '5')
|
||||
// set Base OS volume size lower than required
|
||||
.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.')
|
||||
.assertElementDisabled(applyButtonSelector, 'Apply button is disabled in case of validation error')
|
||||
.clickByCssSelector(cancelButtonSelector);
|
||||
}
|
||||
};
|
||||
});
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller']);
|
||||
})
|
||||
// check node
|
||||
.clickByCssSelector('.node.pending_addition input[type=checkbox]')
|
||||
// click Configure Disks button
|
||||
.clickByCssSelector('.btn-configure-disks')
|
||||
.assertElementsAppear('.edit-node-disks-screen', 2000, 'Node disk screen loaded')
|
||||
.findByCssSelector(sdaDisk + ' input[type=number][name=image]')
|
||||
// get the initial size of the Image Storage volume
|
||||
.then(function(input) {
|
||||
return input.getProperty('value')
|
||||
.then(function(value) {
|
||||
initialImageSize = value;
|
||||
});
|
||||
})
|
||||
.end();
|
||||
},
|
||||
'Testing nodes disks layout': function() {
|
||||
return this.remote
|
||||
.assertElementDisabled(cancelButtonSelector, 'Cancel button is disabled')
|
||||
.assertElementDisabled(applyButtonSelector, 'Apply button is disabled')
|
||||
.assertElementEnabled(loadDefaultsButtonSelector, 'Load Defaults button is enabled');
|
||||
},
|
||||
'Check SDA disk layout': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector(sdaDisk + ' .disk-visual [data-volume=os] .toggle')
|
||||
.findByCssSelector(sdaDisk + ' .disk-utility-box [data-volume=os] input')
|
||||
.then(function(input) {
|
||||
return input.getProperty('value')
|
||||
.then(function(value) {
|
||||
assert.ok(value, 'Base System is allocated on SDA disk');
|
||||
});
|
||||
})
|
||||
.end()
|
||||
.findByCssSelector(sdaDisk + ' .disk-utility-box [data-volume=image] input')
|
||||
.then(function(input) {
|
||||
return input.getProperty('value')
|
||||
.then(function(value) {
|
||||
assert.ok(value, 'Image Storage is allocated on SDA disk');
|
||||
});
|
||||
})
|
||||
.end()
|
||||
.assertElementExists(sdaDisk + ' .disk-visual [data-volume=image] .close-btn', 'Button Close for Image Storage volume 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() {
|
||||
return this.remote
|
||||
.setInputValue(sdaDisk + ' input[type=number][name=image]', '5')
|
||||
.assertElementEnabled(cancelButtonSelector, 'Cancel button is enabled')
|
||||
.assertElementEnabled(applyButtonSelector, 'Apply button is enabled')
|
||||
.clickByCssSelector(applyButtonSelector)
|
||||
.assertElementDisappears('.btn-load-defaults:disabled', 2000, 'Wait for changes applied')
|
||||
.clickByCssSelector(loadDefaultsButtonSelector)
|
||||
.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')
|
||||
.assertElementEnabled(cancelButtonSelector, 'Cancel button is enabled')
|
||||
.assertElementEnabled(applyButtonSelector, 'Apply button is enabled')
|
||||
.clickByCssSelector(applyButtonSelector);
|
||||
},
|
||||
'Testing volume group deletion and Cancel button': function() {
|
||||
return this.remote
|
||||
.findByCssSelector(sdaDisk + ' .disk-visual [data-volume=image]')
|
||||
// check that visualisation div for Image Storage present and has positive width
|
||||
.then(function(element) {
|
||||
return element.getSize()
|
||||
.then(function(sizes) {
|
||||
assert.isTrue(sizes.width > 0, 'Expected positive width for Image Storage visual');
|
||||
});
|
||||
})
|
||||
.end()
|
||||
.clickByCssSelector(sdaDisk + ' .disk-visual [data-volume=image] .close-btn')
|
||||
.assertElementEnabled(applyButtonSelector, 'Apply button is enabled')
|
||||
.findByCssSelector(sdaDisk + ' .disk-visual [data-volume=image]')
|
||||
// check Image Storage volume deleted
|
||||
.then(function(element) {
|
||||
return element.getSize()
|
||||
.then(function(sizes) {
|
||||
assert.equal(sizes.width, 0, 'Expected null width for Image Storage visual');
|
||||
});
|
||||
})
|
||||
.end()
|
||||
.assertElementPropertyEquals(sdaDisk + ' input[type=number][name=image]', 'value', 0, 'Image Storage volume was removed successfully')
|
||||
.findByCssSelector(sdaDisk + ' .disk-visual [data-volume=unallocated]')
|
||||
// check that there is unallocated space after Image Storage removal
|
||||
.then(function(element) {
|
||||
return element.getSize()
|
||||
.then(function(sizes) {
|
||||
assert.isTrue(sizes.width > 0, 'There is unallocated space after Image Storage removal');
|
||||
});
|
||||
})
|
||||
.end()
|
||||
.clickByCssSelector(cancelButtonSelector)
|
||||
.assertElementPropertyEquals(sdaDisk + ' input[type=number][name=image]', 'value', initialImageSize, 'Image Storage volume control contains correct value')
|
||||
.assertElementDisabled(applyButtonSelector, 'Apply button is disabled');
|
||||
},
|
||||
'Test volume size validation': function() {
|
||||
return this.remote
|
||||
// reduce Image Storage volume size to free space on the disk
|
||||
.setInputValue(sdaDisk + ' input[type=number][name=image]', '5')
|
||||
// set Base OS volume size lower than required
|
||||
.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.')
|
||||
.assertElementDisabled(applyButtonSelector, 'Apply button is disabled in case of validation error')
|
||||
.clickByCssSelector(cancelButtonSelector);
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,126 +15,126 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern!object',
|
||||
'tests/functional/helpers',
|
||||
'tests/functional/pages/interfaces',
|
||||
'tests/functional/pages/common'
|
||||
'intern!object',
|
||||
'tests/functional/helpers',
|
||||
'tests/functional/pages/interfaces',
|
||||
'tests/functional/pages/common'
|
||||
], function(registerSuite, helpers, InterfacesPage, Common) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
interfacesPage,
|
||||
clusterName;
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
interfacesPage,
|
||||
clusterName;
|
||||
|
||||
return {
|
||||
name: 'Node Interfaces',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
interfacesPage = new InterfacesPage(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
return {
|
||||
name: 'Node Interfaces',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
interfacesPage = new InterfacesPage(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, 'Controller', null, 'Supermicro X9SCD');
|
||||
})
|
||||
.clickByCssSelector('.node.pending_addition input[type=checkbox]:not(:checked)')
|
||||
.clickByCssSelector('button.btn-configure-interfaces')
|
||||
.assertElementAppears('div.ifc-list', 2000, 'Node interfaces loaded');
|
||||
},
|
||||
afterEach: function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.btn-defaults')
|
||||
.waitForCssSelector('.btn-defaults:enabled', 2000);
|
||||
},
|
||||
teardown: function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.removeCluster(clusterName, true);
|
||||
});
|
||||
},
|
||||
'Untagged networks error': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return interfacesPage.assignNetworkToInterface('Public', 'eth0');
|
||||
})
|
||||
.assertElementExists('div.ifc-error', 'Untagged networks can not be assigned to the same interface message should appear');
|
||||
},
|
||||
'Bond interfaces with different speeds': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return interfacesPage.selectInterface('eth2');
|
||||
})
|
||||
.then(function() {
|
||||
return interfacesPage.selectInterface('eth3');
|
||||
})
|
||||
.assertElementExists('div.alert.alert-warning', 'Interfaces with different speeds bonding not recommended message should appear')
|
||||
.assertElementEnabled('.btn-bond', 'Bonding button should still be enabled');
|
||||
},
|
||||
'Interfaces bonding': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return interfacesPage.bondInterfaces('eth1', 'eth2');
|
||||
})
|
||||
.then(function() {
|
||||
// Two interfaces bonding
|
||||
return interfacesPage.checkBondInterfaces('bond0', ['eth1', 'eth2']);
|
||||
})
|
||||
.then(function() {
|
||||
return interfacesPage.bondInterfaces('bond0', 'eth5');
|
||||
})
|
||||
.then(function() {
|
||||
// Adding interface to existing bond
|
||||
return interfacesPage.checkBondInterfaces('bond0', ['eth1', 'eth2', 'eth5']);
|
||||
})
|
||||
.then(function() {
|
||||
return interfacesPage.removeInterfaceFromBond('eth2');
|
||||
})
|
||||
.then(function() {
|
||||
// Removing interface from the bond
|
||||
return interfacesPage.checkBondInterfaces('bond0', ['eth1', 'eth5']);
|
||||
});
|
||||
},
|
||||
'Interfaces unbonding': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return interfacesPage.bondInterfaces('eth1', 'eth2');
|
||||
})
|
||||
.then(function() {
|
||||
// Two interfaces bonding
|
||||
return interfacesPage.selectInterface('bond0');
|
||||
})
|
||||
.clickByCssSelector('.btn-unbond')
|
||||
.then(function() {
|
||||
return interfacesPage.selectInterface('eth1');
|
||||
})
|
||||
.then(function() {
|
||||
return interfacesPage.selectInterface('eth2');
|
||||
});
|
||||
},
|
||||
'Check that two bonds cannot be bonded': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return interfacesPage.bondInterfaces('eth0', 'eth2');
|
||||
})
|
||||
.then(function() {
|
||||
return interfacesPage.bondInterfaces('eth1', 'eth5');
|
||||
})
|
||||
.then(function() {
|
||||
return interfacesPage.selectInterface('bond0');
|
||||
})
|
||||
.then(function() {
|
||||
return interfacesPage.selectInterface('bond1');
|
||||
})
|
||||
.assertElementDisabled('.btn-bond', 'Making sure bond button is disabled')
|
||||
.assertElementContainsText('.alert.alert-warning', ' network interface is already bonded with other network inteface.', 'Warning message should appear when intended to bond bonds');
|
||||
}
|
||||
};
|
||||
});
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, 'Controller', null, 'Supermicro X9SCD');
|
||||
})
|
||||
.clickByCssSelector('.node.pending_addition input[type=checkbox]:not(:checked)')
|
||||
.clickByCssSelector('button.btn-configure-interfaces')
|
||||
.assertElementAppears('div.ifc-list', 2000, 'Node interfaces loaded');
|
||||
},
|
||||
afterEach: function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.btn-defaults')
|
||||
.waitForCssSelector('.btn-defaults:enabled', 2000);
|
||||
},
|
||||
teardown: function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.removeCluster(clusterName, true);
|
||||
});
|
||||
},
|
||||
'Untagged networks error': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return interfacesPage.assignNetworkToInterface('Public', 'eth0');
|
||||
})
|
||||
.assertElementExists('div.ifc-error', 'Untagged networks can not be assigned to the same interface message should appear');
|
||||
},
|
||||
'Bond interfaces with different speeds': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return interfacesPage.selectInterface('eth2');
|
||||
})
|
||||
.then(function() {
|
||||
return interfacesPage.selectInterface('eth3');
|
||||
})
|
||||
.assertElementExists('div.alert.alert-warning', 'Interfaces with different speeds bonding not recommended message should appear')
|
||||
.assertElementEnabled('.btn-bond', 'Bonding button should still be enabled');
|
||||
},
|
||||
'Interfaces bonding': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return interfacesPage.bondInterfaces('eth1', 'eth2');
|
||||
})
|
||||
.then(function() {
|
||||
// Two interfaces bonding
|
||||
return interfacesPage.checkBondInterfaces('bond0', ['eth1', 'eth2']);
|
||||
})
|
||||
.then(function() {
|
||||
return interfacesPage.bondInterfaces('bond0', 'eth5');
|
||||
})
|
||||
.then(function() {
|
||||
// Adding interface to existing bond
|
||||
return interfacesPage.checkBondInterfaces('bond0', ['eth1', 'eth2', 'eth5']);
|
||||
})
|
||||
.then(function() {
|
||||
return interfacesPage.removeInterfaceFromBond('eth2');
|
||||
})
|
||||
.then(function() {
|
||||
// Removing interface from the bond
|
||||
return interfacesPage.checkBondInterfaces('bond0', ['eth1', 'eth5']);
|
||||
});
|
||||
},
|
||||
'Interfaces unbonding': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return interfacesPage.bondInterfaces('eth1', 'eth2');
|
||||
})
|
||||
.then(function() {
|
||||
// Two interfaces bonding
|
||||
return interfacesPage.selectInterface('bond0');
|
||||
})
|
||||
.clickByCssSelector('.btn-unbond')
|
||||
.then(function() {
|
||||
return interfacesPage.selectInterface('eth1');
|
||||
})
|
||||
.then(function() {
|
||||
return interfacesPage.selectInterface('eth2');
|
||||
});
|
||||
},
|
||||
'Check that two bonds cannot be bonded': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return interfacesPage.bondInterfaces('eth0', 'eth2');
|
||||
})
|
||||
.then(function() {
|
||||
return interfacesPage.bondInterfaces('eth1', 'eth5');
|
||||
})
|
||||
.then(function() {
|
||||
return interfacesPage.selectInterface('bond0');
|
||||
})
|
||||
.then(function() {
|
||||
return interfacesPage.selectInterface('bond1');
|
||||
})
|
||||
.assertElementDisabled('.btn-bond', 'Making sure bond button is disabled')
|
||||
.assertElementContainsText('.alert.alert-warning', ' network interface is already bonded with other network inteface.', 'Warning message should appear when intended to bond bonds');
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,171 +15,171 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/cluster',
|
||||
'tests/functional/pages/dashboard',
|
||||
'tests/functional/helpers'
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/cluster',
|
||||
'tests/functional/pages/dashboard',
|
||||
'tests/functional/helpers'
|
||||
], function(registerSuite, assert, Common, ClusterPage, DashboardPage) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
clusterPage,
|
||||
dashboardPage,
|
||||
clusterName,
|
||||
searchButtonSelector = '.node-management-panel .btn-search',
|
||||
sortingButtonSelector = '.node-management-panel .btn-sorters',
|
||||
filtersButtonSelector = '.node-management-panel .btn-filters';
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
clusterPage,
|
||||
dashboardPage,
|
||||
clusterName,
|
||||
searchButtonSelector = '.node-management-panel .btn-search',
|
||||
sortingButtonSelector = '.node-management-panel .btn-sorters',
|
||||
filtersButtonSelector = '.node-management-panel .btn-filters';
|
||||
|
||||
return {
|
||||
name: 'Node management panel on cluster nodes page: search, sorting, filtering',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
return {
|
||||
name: 'Node management panel on cluster nodes page: search, sorting, filtering',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Nodes');
|
||||
});
|
||||
},
|
||||
'Test management controls state in new environment': function() {
|
||||
return this.remote
|
||||
.assertElementDisabled(searchButtonSelector, 'Search button is locked if there are no nodes in environment')
|
||||
.assertElementDisabled(sortingButtonSelector, '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': {
|
||||
setup: function() {
|
||||
dashboardPage = new DashboardPage(this.remote);
|
||||
},
|
||||
beforeEach: function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(3, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Compute'], 'error');
|
||||
});
|
||||
},
|
||||
afterEach: function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.then(function() {
|
||||
return dashboardPage.discardChanges();
|
||||
});
|
||||
},
|
||||
'Test search control': function() {
|
||||
var searchInputSelector = '.node-management-panel [name=search]';
|
||||
return this.remote
|
||||
.clickByCssSelector(searchButtonSelector)
|
||||
.assertElementAppears(searchInputSelector, 200, 'Search input appears on the page')
|
||||
.setInputValue(searchInputSelector, 'Super')
|
||||
// need to wait debounced search input
|
||||
.sleep(200)
|
||||
.assertElementsExist('.node-list .node', 3, 'Search was successfull')
|
||||
.clickByCssSelector('.page-title')
|
||||
.assertElementNotExists(searchButtonSelector, 'Active search control remains open when clicking outside the input')
|
||||
.clickByCssSelector('.node-management-panel .btn-clear-search')
|
||||
.assertElementsExist('.node-list .node', 4, 'Search was reset')
|
||||
.assertElementNotExists(searchButtonSelector, 'Search input is still shown after search reset')
|
||||
.clickByCssSelector('.node-list')
|
||||
.assertElementExists(searchButtonSelector, 'Empty search control is closed when clicking outside the input');
|
||||
},
|
||||
'Test node list sorting': function() {
|
||||
var activeSortersPanelSelector = '.active-sorters',
|
||||
moreControlSelector = '.sorters .more-control',
|
||||
firstNodeName,
|
||||
self = this;
|
||||
return this.remote
|
||||
.assertElementExists(activeSortersPanelSelector, '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)
|
||||
.assertElementExists('.sorters .sorter-control', 'Cluster node list has one sorting by default')
|
||||
.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 .btn-reset-sorting', 'Default sorting can not be reset')
|
||||
.findByCssSelector('.node-list .node-name .name p')
|
||||
.getVisibleText().then(function(text) {
|
||||
firstNodeName = text;
|
||||
})
|
||||
.end()
|
||||
.clickByCssSelector('.sorters .sort-by-roles-asc button')
|
||||
.findByCssSelector('.node-list .node-name .name p')
|
||||
.getVisibleText().then(function(text) {
|
||||
assert.notEqual(text, firstNodeName, 'Order of sorting by roles was changed to desc');
|
||||
})
|
||||
.end()
|
||||
.clickByCssSelector('.sorters .sort-by-roles-desc button')
|
||||
.then(function() {
|
||||
return self.remote.assertElementTextEquals('.node-list .node-name .name p', firstNodeName, 'Order of sorting by roles was changed to asc (default)');
|
||||
})
|
||||
.clickByCssSelector(moreControlSelector + ' button')
|
||||
.assertElementsExist(moreControlSelector + ' .popover .checkbox-group', 12, 'Standard node sorters are presented')
|
||||
// add sorting by CPU (real)
|
||||
.clickByCssSelector(moreControlSelector + ' .popover [name=cores]')
|
||||
// add sorting by manufacturer
|
||||
.clickByCssSelector(moreControlSelector + ' .popover [name=manufacturer]')
|
||||
.assertElementsExist('.nodes-group', 4, 'New sorting was applied and nodes were grouped')
|
||||
// remove sorting by manufacturer
|
||||
.clickByCssSelector('.sorters .sort-by-manufacturer-asc .btn-remove-sorting')
|
||||
.assertElementsExist('.nodes-group', 3, 'Particular sorting removal works')
|
||||
.clickByCssSelector('.sorters .btn-reset-sorting')
|
||||
.assertElementsExist('.nodes-group', 2, 'Sorting was successfully reset to default')
|
||||
.clickByCssSelector(sortingButtonSelector)
|
||||
.clickByCssSelector(activeSortersPanelSelector)
|
||||
// check active sorters panel is clickable and opens sorters panel
|
||||
.findByCssSelector('.sorters')
|
||||
.end();
|
||||
},
|
||||
'Test node list filtering': function() {
|
||||
var activeFiltersPanelSelector = '.active-filters',
|
||||
moreControlSelector = '.filters .more-control';
|
||||
return this.remote
|
||||
.assertElementNotExists(activeFiltersPanelSelector, 'Environment has no active filters by default')
|
||||
.clickByCssSelector(filtersButtonSelector)
|
||||
.assertElementsExist('.filters .filter-control', 2, 'Filters panel has 2 default filters')
|
||||
.clickByCssSelector('.filter-by-roles')
|
||||
.assertElementNotExists('.filter-by-roles [type=checkbox]:checked', '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')
|
||||
.clickByCssSelector(moreControlSelector + ' button')
|
||||
.assertElementsExist(moreControlSelector + ' .popover .checkbox-group', 8, 'Standard node filters are presented')
|
||||
.clickByCssSelector(moreControlSelector + ' [name=cores]')
|
||||
.assertElementsExist('.filters .filter-control', 3, 'New Cores (real) filter was added')
|
||||
.assertElementExists('.filter-by-cores .popover-content', 'New filter is open')
|
||||
.clickByCssSelector('.filters .filter-by-cores .btn-remove-filter')
|
||||
.assertElementsExist('.filters .filter-control', 2, 'Particular filter removal works')
|
||||
.clickByCssSelector(moreControlSelector + ' button')
|
||||
.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')
|
||||
// set min value more than max value
|
||||
.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')
|
||||
.assertElementNotExists('.node-list .node', 'No nodes match invalid filter')
|
||||
.clickByCssSelector('.filters .btn-reset-filters')
|
||||
.assertElementsExist('.node-list .node', 4, 'Node filtration was successfully reset')
|
||||
.clickByCssSelector('.filters .filter-by-status button')
|
||||
.clickByCssSelector('.filters .filter-by-status [name=error]')
|
||||
.assertElementExists('.node-list .node', 'Node with error status successfully filtered')
|
||||
.clickByCssSelector('.filters .filter-by-status [name=pending_addition]')
|
||||
.assertElementsExist('.node-list .node', 4, 'All nodes shown')
|
||||
.clickByCssSelector(filtersButtonSelector)
|
||||
.assertElementExists(activeFiltersPanelSelector, 'Applied filter is reflected in active filters panel')
|
||||
.assertElementExists('.active-filters .btn-reset-filters', 'Reset filters button exists in active filters panel');
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Nodes');
|
||||
});
|
||||
},
|
||||
'Test management controls state in new environment': function() {
|
||||
return this.remote
|
||||
.assertElementDisabled(searchButtonSelector, 'Search button is locked if there are no nodes in environment')
|
||||
.assertElementDisabled(sortingButtonSelector, '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': {
|
||||
setup: function() {
|
||||
dashboardPage = new DashboardPage(this.remote);
|
||||
},
|
||||
beforeEach: function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(3, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Compute'], 'error');
|
||||
});
|
||||
},
|
||||
afterEach: function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.then(function() {
|
||||
return dashboardPage.discardChanges();
|
||||
});
|
||||
},
|
||||
'Test search control': function() {
|
||||
var searchInputSelector = '.node-management-panel [name=search]';
|
||||
return this.remote
|
||||
.clickByCssSelector(searchButtonSelector)
|
||||
.assertElementAppears(searchInputSelector, 200, 'Search input appears on the page')
|
||||
.setInputValue(searchInputSelector, 'Super')
|
||||
// need to wait debounced search input
|
||||
.sleep(200)
|
||||
.assertElementsExist('.node-list .node', 3, 'Search was successfull')
|
||||
.clickByCssSelector('.page-title')
|
||||
.assertElementNotExists(searchButtonSelector, 'Active search control remains open when clicking outside the input')
|
||||
.clickByCssSelector('.node-management-panel .btn-clear-search')
|
||||
.assertElementsExist('.node-list .node', 4, 'Search was reset')
|
||||
.assertElementNotExists(searchButtonSelector, 'Search input is still shown after search reset')
|
||||
.clickByCssSelector('.node-list')
|
||||
.assertElementExists(searchButtonSelector, 'Empty search control is closed when clicking outside the input');
|
||||
},
|
||||
'Test node list sorting': function() {
|
||||
var activeSortersPanelSelector = '.active-sorters',
|
||||
moreControlSelector = '.sorters .more-control',
|
||||
firstNodeName,
|
||||
self = this;
|
||||
return this.remote
|
||||
.assertElementExists(activeSortersPanelSelector, '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)
|
||||
.assertElementExists('.sorters .sorter-control', 'Cluster node list has one sorting by default')
|
||||
.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 .btn-reset-sorting', 'Default sorting can not be reset')
|
||||
.findByCssSelector('.node-list .node-name .name p')
|
||||
.getVisibleText().then(function(text) {
|
||||
firstNodeName = text;
|
||||
})
|
||||
.end()
|
||||
.clickByCssSelector('.sorters .sort-by-roles-asc button')
|
||||
.findByCssSelector('.node-list .node-name .name p')
|
||||
.getVisibleText().then(function(text) {
|
||||
assert.notEqual(text, firstNodeName, 'Order of sorting by roles was changed to desc');
|
||||
})
|
||||
.end()
|
||||
.clickByCssSelector('.sorters .sort-by-roles-desc button')
|
||||
.then(function() {
|
||||
return self.remote.assertElementTextEquals('.node-list .node-name .name p', firstNodeName, 'Order of sorting by roles was changed to asc (default)');
|
||||
})
|
||||
.clickByCssSelector(moreControlSelector + ' button')
|
||||
.assertElementsExist(moreControlSelector + ' .popover .checkbox-group', 12, 'Standard node sorters are presented')
|
||||
// add sorting by CPU (real)
|
||||
.clickByCssSelector(moreControlSelector + ' .popover [name=cores]')
|
||||
// add sorting by manufacturer
|
||||
.clickByCssSelector(moreControlSelector + ' .popover [name=manufacturer]')
|
||||
.assertElementsExist('.nodes-group', 4, 'New sorting was applied and nodes were grouped')
|
||||
// remove sorting by manufacturer
|
||||
.clickByCssSelector('.sorters .sort-by-manufacturer-asc .btn-remove-sorting')
|
||||
.assertElementsExist('.nodes-group', 3, 'Particular sorting removal works')
|
||||
.clickByCssSelector('.sorters .btn-reset-sorting')
|
||||
.assertElementsExist('.nodes-group', 2, 'Sorting was successfully reset to default')
|
||||
.clickByCssSelector(sortingButtonSelector)
|
||||
.clickByCssSelector(activeSortersPanelSelector)
|
||||
// check active sorters panel is clickable and opens sorters panel
|
||||
.findByCssSelector('.sorters')
|
||||
.end();
|
||||
},
|
||||
'Test node list filtering': function() {
|
||||
var activeFiltersPanelSelector = '.active-filters',
|
||||
moreControlSelector = '.filters .more-control';
|
||||
return this.remote
|
||||
.assertElementNotExists(activeFiltersPanelSelector, 'Environment has no active filters by default')
|
||||
.clickByCssSelector(filtersButtonSelector)
|
||||
.assertElementsExist('.filters .filter-control', 2, 'Filters panel has 2 default filters')
|
||||
.clickByCssSelector('.filter-by-roles')
|
||||
.assertElementNotExists('.filter-by-roles [type=checkbox]:checked', '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')
|
||||
.clickByCssSelector(moreControlSelector + ' button')
|
||||
.assertElementsExist(moreControlSelector + ' .popover .checkbox-group', 8, 'Standard node filters are presented')
|
||||
.clickByCssSelector(moreControlSelector + ' [name=cores]')
|
||||
.assertElementsExist('.filters .filter-control', 3, 'New Cores (real) filter was added')
|
||||
.assertElementExists('.filter-by-cores .popover-content', 'New filter is open')
|
||||
.clickByCssSelector('.filters .filter-by-cores .btn-remove-filter')
|
||||
.assertElementsExist('.filters .filter-control', 2, 'Particular filter removal works')
|
||||
.clickByCssSelector(moreControlSelector + ' button')
|
||||
.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')
|
||||
// set min value more than max value
|
||||
.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')
|
||||
.assertElementNotExists('.node-list .node', 'No nodes match invalid filter')
|
||||
.clickByCssSelector('.filters .btn-reset-filters')
|
||||
.assertElementsExist('.node-list .node', 4, 'Node filtration was successfully reset')
|
||||
.clickByCssSelector('.filters .filter-by-status button')
|
||||
.clickByCssSelector('.filters .filter-by-status [name=error]')
|
||||
.assertElementExists('.node-list .node', 'Node with error status successfully filtered')
|
||||
.clickByCssSelector('.filters .filter-by-status [name=pending_addition]')
|
||||
.assertElementsExist('.node-list .node', 4, 'All nodes shown')
|
||||
.clickByCssSelector(filtersButtonSelector)
|
||||
.assertElementExists(activeFiltersPanelSelector, 'Applied filter is reflected in active filters panel')
|
||||
.assertElementExists('.active-filters .btn-reset-filters', 'Reset filters button exists in active filters panel');
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,154 +15,154 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/pages/node',
|
||||
'tests/functional/pages/modal',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/cluster',
|
||||
'tests/functional/helpers'
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/pages/node',
|
||||
'tests/functional/pages/modal',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/cluster',
|
||||
'tests/functional/helpers'
|
||||
], function(registerSuite, assert, NodeComponent, ModalWindow, Common, ClusterPage) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
node,
|
||||
modal,
|
||||
clusterPage,
|
||||
clusterName,
|
||||
nodeNewName = 'Node new name';
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
node,
|
||||
modal,
|
||||
clusterPage,
|
||||
clusterName,
|
||||
nodeNewName = 'Node new name';
|
||||
|
||||
return {
|
||||
name: 'Node view tests',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
node = new NodeComponent(this.remote);
|
||||
modal = new ModalWindow(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
return {
|
||||
name: 'Node view tests',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
node = new NodeComponent(this.remote);
|
||||
modal = new ModalWindow(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
clusterName = common.pickRandomName('Test Cluster');
|
||||
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
});
|
||||
},
|
||||
'Standard node panel': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller']);
|
||||
})
|
||||
.assertElementExists('label.standard.active', 'Standard mode chosen by default')
|
||||
.assertElementExists('.node .role-list', 'Role list is shown on node standard panel')
|
||||
.clickByCssSelector('.node input[type=checkbox]')
|
||||
.assertElementExists('.node.selected', 'Node gets selected upon clicking')
|
||||
.assertElementExists('button.btn-delete-nodes:not(:disabled)', 'Delete Nodes and ...')
|
||||
.assertElementExists('button.btn-edit-roles:not(:disabled)', '... Edit Roles buttons appear upon node selection')
|
||||
.then(function() {
|
||||
return node.renameNode(nodeNewName);
|
||||
})
|
||||
.assertElementTextEquals('.node .name p', nodeNewName, 'Node name has been updated')
|
||||
.clickByCssSelector('.node .btn-view-logs')
|
||||
.assertElementAppears('.logs-tab', 2000, 'Check redirect to Logs tab')
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Nodes');
|
||||
})
|
||||
.assertElementAppears('.node-list', 2000, 'Cluster node list loaded')
|
||||
.then(function() {
|
||||
return node.discardNode();
|
||||
})
|
||||
.assertElementNotExists('.node', 'Node has been removed');
|
||||
},
|
||||
'Node pop-up': function() {
|
||||
var newHostname = 'node-123';
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return node.openNodePopup();
|
||||
})
|
||||
.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-networks', 'Interfaces can be configured for cluster node')
|
||||
.clickByCssSelector('.change-hostname .btn-link')
|
||||
// change the hostname
|
||||
.findByCssSelector('.change-hostname [type=text]')
|
||||
.clearValue()
|
||||
.type(newHostname)
|
||||
.pressKeys('\uE007')
|
||||
.end()
|
||||
.assertElementDisappears('.change-hostname [type=text]', 2000, 'Hostname input disappears after submit')
|
||||
.assertElementTextEquals('span.node-hostname', newHostname, 'Node hostname has been updated')
|
||||
.then(function() {
|
||||
return modal.close();
|
||||
});
|
||||
},
|
||||
'Compact node panel': function() {
|
||||
return this.remote
|
||||
// switch to compact view mode
|
||||
.clickByCssSelector('label.compact')
|
||||
.findByCssSelector('.compact-node div.node-checkbox')
|
||||
.click()
|
||||
.assertElementExists('i.glyphicon-ok', 'Self node is selectable')
|
||||
.end()
|
||||
.clickByCssSelector('.compact-node .node-name p')
|
||||
.assertElementNotExists('.compact-node .node-name-input', '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() {
|
||||
var newName = 'Node new new name';
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return node.openCompactNodeExtendedView();
|
||||
})
|
||||
.clickByCssSelector('.node-name [type=checkbox]')
|
||||
.assertElementExists('.compact-node .node-checkbox i.glyphicon-ok', 'Node compact panel is checked')
|
||||
.then(function() {
|
||||
return node.openNodePopup(true);
|
||||
})
|
||||
.assertElementNotExists('.node-popover', 'Node popover is closed when node pop-up opened')
|
||||
.then(function() {
|
||||
// close node pop-up
|
||||
return modal.close();
|
||||
})
|
||||
.then(function() {
|
||||
return node.openCompactNodeExtendedView();
|
||||
})
|
||||
.findByCssSelector('.node-popover')
|
||||
.assertElementExists('.role-list', 'Role list is shown in cluster node extended view')
|
||||
.assertElementExists('.node-buttons', 'Cluster node action buttons are presented in extended view')
|
||||
.end()
|
||||
.then(function() {
|
||||
return node.renameNode(newName, true);
|
||||
})
|
||||
.assertElementTextEquals('.node-popover .name p', newName, 'Node name has been updated from extended view')
|
||||
.then(function() {
|
||||
return node.discardNode(true);
|
||||
})
|
||||
.assertElementNotExists('.node', 'Node has been removed');
|
||||
},
|
||||
'Additional tests for unallocated node': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('button.btn-add-nodes')
|
||||
.assertElementAppears('.node-list', 2000, 'Unallocated node list loaded')
|
||||
.then(function() {
|
||||
return node.openCompactNodeExtendedView();
|
||||
})
|
||||
.assertElementNotExists('.node-popover .role-list', '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() {
|
||||
return node.openNodePopup(true);
|
||||
})
|
||||
.assertElementNotExists('.modal .btn-edit-disks', 'Disks can not be configured for unallocated node')
|
||||
.assertElementNotExists('.modal .btn-edit-networks', 'Interfaces can not be configured for unallocated node')
|
||||
.then(function() {
|
||||
return modal.close();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
});
|
||||
},
|
||||
'Standard node panel': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller']);
|
||||
})
|
||||
.assertElementExists('label.standard.active', 'Standard mode chosen by default')
|
||||
.assertElementExists('.node .role-list', 'Role list is shown on node standard panel')
|
||||
.clickByCssSelector('.node input[type=checkbox]')
|
||||
.assertElementExists('.node.selected', 'Node gets selected upon clicking')
|
||||
.assertElementExists('button.btn-delete-nodes:not(:disabled)', 'Delete Nodes and ...')
|
||||
.assertElementExists('button.btn-edit-roles:not(:disabled)', '... Edit Roles buttons appear upon node selection')
|
||||
.then(function() {
|
||||
return node.renameNode(nodeNewName);
|
||||
})
|
||||
.assertElementTextEquals('.node .name p', nodeNewName, 'Node name has been updated')
|
||||
.clickByCssSelector('.node .btn-view-logs')
|
||||
.assertElementAppears('.logs-tab', 2000, 'Check redirect to Logs tab')
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Nodes');
|
||||
})
|
||||
.assertElementAppears('.node-list', 2000, 'Cluster node list loaded')
|
||||
.then(function() {
|
||||
return node.discardNode();
|
||||
})
|
||||
.assertElementNotExists('.node', 'Node has been removed');
|
||||
},
|
||||
'Node pop-up': function() {
|
||||
var newHostname = 'node-123';
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return node.openNodePopup();
|
||||
})
|
||||
.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-networks', 'Interfaces can be configured for cluster node')
|
||||
.clickByCssSelector('.change-hostname .btn-link')
|
||||
// change the hostname
|
||||
.findByCssSelector('.change-hostname [type=text]')
|
||||
.clearValue()
|
||||
.type(newHostname)
|
||||
.pressKeys('\uE007')
|
||||
.end()
|
||||
.assertElementDisappears('.change-hostname [type=text]', 2000, 'Hostname input disappears after submit')
|
||||
.assertElementTextEquals('span.node-hostname', newHostname, 'Node hostname has been updated')
|
||||
.then(function() {
|
||||
return modal.close();
|
||||
});
|
||||
},
|
||||
'Compact node panel': function() {
|
||||
return this.remote
|
||||
// switch to compact view mode
|
||||
.clickByCssSelector('label.compact')
|
||||
.findByCssSelector('.compact-node div.node-checkbox')
|
||||
.click()
|
||||
.assertElementExists('i.glyphicon-ok', 'Self node is selectable')
|
||||
.end()
|
||||
.clickByCssSelector('.compact-node .node-name p')
|
||||
.assertElementNotExists('.compact-node .node-name-input', '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() {
|
||||
var newName = 'Node new new name';
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return node.openCompactNodeExtendedView();
|
||||
})
|
||||
.clickByCssSelector('.node-name [type=checkbox]')
|
||||
.assertElementExists('.compact-node .node-checkbox i.glyphicon-ok', 'Node compact panel is checked')
|
||||
.then(function() {
|
||||
return node.openNodePopup(true);
|
||||
})
|
||||
.assertElementNotExists('.node-popover', 'Node popover is closed when node pop-up opened')
|
||||
.then(function() {
|
||||
// close node pop-up
|
||||
return modal.close();
|
||||
})
|
||||
.then(function() {
|
||||
return node.openCompactNodeExtendedView();
|
||||
})
|
||||
.findByCssSelector('.node-popover')
|
||||
.assertElementExists('.role-list', 'Role list is shown in cluster node extended view')
|
||||
.assertElementExists('.node-buttons', 'Cluster node action buttons are presented in extended view')
|
||||
.end()
|
||||
.then(function() {
|
||||
return node.renameNode(newName, true);
|
||||
})
|
||||
.assertElementTextEquals('.node-popover .name p', newName, 'Node name has been updated from extended view')
|
||||
.then(function() {
|
||||
return node.discardNode(true);
|
||||
})
|
||||
.assertElementNotExists('.node', 'Node has been removed');
|
||||
},
|
||||
'Additional tests for unallocated node': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('button.btn-add-nodes')
|
||||
.assertElementAppears('.node-list', 2000, 'Unallocated node list loaded')
|
||||
.then(function() {
|
||||
return node.openCompactNodeExtendedView();
|
||||
})
|
||||
.assertElementNotExists('.node-popover .role-list', '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() {
|
||||
return node.openNodePopup(true);
|
||||
})
|
||||
.assertElementNotExists('.modal .btn-edit-disks', 'Disks can not be configured for unallocated node')
|
||||
.assertElementNotExists('.modal .btn-edit-networks', 'Interfaces can not be configured for unallocated node')
|
||||
.then(function() {
|
||||
return modal.close();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,64 +15,64 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern!object',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/modal'
|
||||
'intern!object',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/modal'
|
||||
], function(registerSuite, Common, ModalWindow) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
modal;
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
modal;
|
||||
|
||||
return {
|
||||
name: 'Notifications',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
modal = new ModalWindow(this.remote);
|
||||
return {
|
||||
name: 'Notifications',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
modal = new ModalWindow(this.remote);
|
||||
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
});
|
||||
},
|
||||
'Notification Page': function() {
|
||||
return this.remote
|
||||
.assertElementDisplayed('.notifications-icon .badge', 'Badge notification indicator is shown in navigation')
|
||||
// Go to Notification page
|
||||
.clickByCssSelector('.notifications-icon')
|
||||
.clickLinkByText('View all')
|
||||
.assertElementAppears('.notifications-page', 2000, 'Notification page is rendered')
|
||||
.assertElementsExist('.notifications-page .notification', 'There are one or more notifications on the page')
|
||||
.assertElementNotDisplayed('.notifications-icon .badge', 'Badge notification indicator is hidden');
|
||||
},
|
||||
'Notification badge behaviour': function() {
|
||||
var clusterName = common.pickRandomName('Test Cluster');
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Storage - Cinder']);
|
||||
})
|
||||
// Just in case - reset and hide badge notification counter by clicking on it
|
||||
.clickByCssSelector('.notifications-icon')
|
||||
.then(function() {
|
||||
return common.removeCluster(clusterName);
|
||||
})
|
||||
.assertElementAppears('.notifications-icon .badge.visible', 3000, 'New notification appear after the cluster removal')
|
||||
.clickByCssSelector('.notifications-icon')
|
||||
.assertElementAppears('.notifications-popover .notification.clickable', 20000, 'Discovered node notification uploaded')
|
||||
// Check if Node Information dialog is shown
|
||||
.clickByCssSelector('.notifications-popover .notification.clickable p')
|
||||
.then(function() {
|
||||
return modal.waitToOpen();
|
||||
})
|
||||
.then(function() {
|
||||
// Dialog with node information is open
|
||||
return modal.checkTitle('Node Information');
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
});
|
||||
},
|
||||
'Notification Page': function() {
|
||||
return this.remote
|
||||
.assertElementDisplayed('.notifications-icon .badge', 'Badge notification indicator is shown in navigation')
|
||||
// Go to Notification page
|
||||
.clickByCssSelector('.notifications-icon')
|
||||
.clickLinkByText('View all')
|
||||
.assertElementAppears('.notifications-page', 2000, 'Notification page is rendered')
|
||||
.assertElementsExist('.notifications-page .notification', 'There are one or more notifications on the page')
|
||||
.assertElementNotDisplayed('.notifications-icon .badge', 'Badge notification indicator is hidden');
|
||||
},
|
||||
'Notification badge behaviour': function() {
|
||||
var clusterName = common.pickRandomName('Test Cluster');
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Storage - Cinder']);
|
||||
})
|
||||
// Just in case - reset and hide badge notification counter by clicking on it
|
||||
.clickByCssSelector('.notifications-icon')
|
||||
.then(function() {
|
||||
return common.removeCluster(clusterName);
|
||||
})
|
||||
.assertElementAppears('.notifications-icon .badge.visible', 3000, 'New notification appear after the cluster removal')
|
||||
.clickByCssSelector('.notifications-icon')
|
||||
.assertElementAppears('.notifications-popover .notification.clickable', 20000, 'Discovered node notification uploaded')
|
||||
// Check if Node Information dialog is shown
|
||||
.clickByCssSelector('.notifications-popover .notification.clickable p')
|
||||
.then(function() {
|
||||
return modal.waitToOpen();
|
||||
})
|
||||
.then(function() {
|
||||
// Dialog with node information is open
|
||||
return modal.checkTitle('Node Information');
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,148 +15,148 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/cluster',
|
||||
'tests/functional/pages/settings',
|
||||
'tests/functional/pages/dashboard'
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/cluster',
|
||||
'tests/functional/pages/settings',
|
||||
'tests/functional/pages/dashboard'
|
||||
], function(registerSuite, assert, Common, ClusterPage, SettingsPage, DashboardPage) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
clusterPage,
|
||||
settingsPage,
|
||||
dashboardPage,
|
||||
clusterName = 'Plugin UI tests',
|
||||
zabbixSectionSelector = '.setting-section-zabbix_monitoring ';
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
clusterPage,
|
||||
settingsPage,
|
||||
dashboardPage,
|
||||
clusterName = 'Plugin UI tests',
|
||||
zabbixSectionSelector = '.setting-section-zabbix_monitoring ';
|
||||
|
||||
return {
|
||||
name: 'Plugin UI tests',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
settingsPage = new SettingsPage(this.remote);
|
||||
dashboardPage = new DashboardPage(this.remote);
|
||||
return {
|
||||
name: 'Plugin UI tests',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
clusterPage = new ClusterPage(this.remote);
|
||||
settingsPage = new SettingsPage(this.remote);
|
||||
dashboardPage = new DashboardPage(this.remote);
|
||||
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
});
|
||||
},
|
||||
beforeEach: function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Settings');
|
||||
})
|
||||
.clickByCssSelector('.subtab-link-other');
|
||||
},
|
||||
afterEach: function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.removeCluster(clusterName);
|
||||
});
|
||||
},
|
||||
'Check plugin in not deployed environment': function() {
|
||||
var self = this,
|
||||
zabbixInitialVersion,
|
||||
zabbixTextInputValue;
|
||||
return this.remote
|
||||
.assertElementEnabled(zabbixSectionSelector + 'h3 input[type=checkbox]', 'Plugin is changeable')
|
||||
.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
|
||||
.clickByCssSelector(zabbixSectionSelector + 'h3 input[type=checkbox]')
|
||||
// save changes
|
||||
.clickByCssSelector('.btn-apply-changes')
|
||||
.then(function() {
|
||||
return settingsPage.waitForRequestCompleted();
|
||||
})
|
||||
.findByCssSelector(zabbixSectionSelector + '.plugin-versions input[type=radio]:checked')
|
||||
.getProperty('value')
|
||||
.then(function(value) {
|
||||
zabbixInitialVersion = value;
|
||||
})
|
||||
.end()
|
||||
.findByCssSelector(zabbixSectionSelector + '[name=zabbix_text_1]')
|
||||
.getProperty('value')
|
||||
.then(function(value) {
|
||||
zabbixTextInputValue = value;
|
||||
})
|
||||
.end()
|
||||
// change plugin version
|
||||
.clickByCssSelector(zabbixSectionSelector + '.plugin-versions input[type=radio]:not(:checked)')
|
||||
.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
|
||||
.setInputValue(zabbixSectionSelector + '[name=zabbix_text_with_regex]', 'aa-aa')
|
||||
.waitForElementDeletion('.subtab-link-other .glyphicon-danger-sign', 1000)
|
||||
.assertElementEnabled('.btn-apply-changes', 'The plugin change can be applied')
|
||||
// reset plugin version change
|
||||
.clickByCssSelector('.btn-revert-changes')
|
||||
.then(function() {
|
||||
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() {
|
||||
this.timeout = 100000;
|
||||
var self = this,
|
||||
zabbixInitialVersion;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.then(function() {
|
||||
return dashboardPage.startDeployment();
|
||||
})
|
||||
.waitForElementDeletion('.dashboard-block .progress', 60000)
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Settings');
|
||||
})
|
||||
.findByCssSelector(zabbixSectionSelector + '.plugin-versions input[type=radio]:checked')
|
||||
.getProperty('value')
|
||||
.then(function(value) {
|
||||
zabbixInitialVersion = value;
|
||||
})
|
||||
.end()
|
||||
// activate plugin
|
||||
.clickByCssSelector(zabbixSectionSelector + 'h3 input[type=checkbox]')
|
||||
.assertElementExists(zabbixSectionSelector + '.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
|
||||
.setInputValue(zabbixSectionSelector + '[name=zabbix_text_with_regex]', 'aa-aa')
|
||||
.waitForElementDeletion('.subtab-link-other .glyphicon-danger-sign', 1000)
|
||||
.assertElementEnabled('.btn-apply-changes', 'The plugin change can be applied')
|
||||
// deactivate plugin
|
||||
.clickByCssSelector(zabbixSectionSelector + 'h3 input[type=checkbox]')
|
||||
.then(function() {
|
||||
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');
|
||||
},
|
||||
'Check plugin restrictions': function() {
|
||||
var loggingSectionSelector = '.setting-section-logging ';
|
||||
return this.remote
|
||||
// activate Logging plugin
|
||||
.clickByCssSelector(loggingSectionSelector + 'h3 input[type=checkbox]')
|
||||
// activate Zabbix plugin
|
||||
.clickByCssSelector(zabbixSectionSelector + 'h3 input[type=checkbox]')
|
||||
.assertElementEnabled(loggingSectionSelector + '[name=logging_text]', 'No conflict with default Zabix plugin version')
|
||||
// change Zabbix plugin version
|
||||
.clickByCssSelector(zabbixSectionSelector + '.plugin-versions input[type=radio]:not(:checked)')
|
||||
.assertElementNotSelected(zabbixSectionSelector + '[name=zabbix_checkbox]', 'Zabbix checkbox is not activated')
|
||||
.clickByCssSelector(zabbixSectionSelector + '[name=zabbix_checkbox]')
|
||||
.assertElementDisabled(loggingSectionSelector + '[name=logging_text]', 'Conflict with Zabbix checkbox')
|
||||
// reset changes
|
||||
.clickByCssSelector('.btn-revert-changes');
|
||||
}
|
||||
};
|
||||
});
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
});
|
||||
},
|
||||
beforeEach: function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.createCluster(clusterName);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Settings');
|
||||
})
|
||||
.clickByCssSelector('.subtab-link-other');
|
||||
},
|
||||
afterEach: function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.removeCluster(clusterName);
|
||||
});
|
||||
},
|
||||
'Check plugin in not deployed environment': function() {
|
||||
var self = this,
|
||||
zabbixInitialVersion,
|
||||
zabbixTextInputValue;
|
||||
return this.remote
|
||||
.assertElementEnabled(zabbixSectionSelector + 'h3 input[type=checkbox]', 'Plugin is changeable')
|
||||
.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
|
||||
.clickByCssSelector(zabbixSectionSelector + 'h3 input[type=checkbox]')
|
||||
// save changes
|
||||
.clickByCssSelector('.btn-apply-changes')
|
||||
.then(function() {
|
||||
return settingsPage.waitForRequestCompleted();
|
||||
})
|
||||
.findByCssSelector(zabbixSectionSelector + '.plugin-versions input[type=radio]:checked')
|
||||
.getProperty('value')
|
||||
.then(function(value) {
|
||||
zabbixInitialVersion = value;
|
||||
})
|
||||
.end()
|
||||
.findByCssSelector(zabbixSectionSelector + '[name=zabbix_text_1]')
|
||||
.getProperty('value')
|
||||
.then(function(value) {
|
||||
zabbixTextInputValue = value;
|
||||
})
|
||||
.end()
|
||||
// change plugin version
|
||||
.clickByCssSelector(zabbixSectionSelector + '.plugin-versions input[type=radio]:not(:checked)')
|
||||
.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
|
||||
.setInputValue(zabbixSectionSelector + '[name=zabbix_text_with_regex]', 'aa-aa')
|
||||
.waitForElementDeletion('.subtab-link-other .glyphicon-danger-sign', 1000)
|
||||
.assertElementEnabled('.btn-apply-changes', 'The plugin change can be applied')
|
||||
// reset plugin version change
|
||||
.clickByCssSelector('.btn-revert-changes')
|
||||
.then(function() {
|
||||
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() {
|
||||
this.timeout = 100000;
|
||||
var self = this,
|
||||
zabbixInitialVersion;
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.addNodesToCluster(1, ['Controller']);
|
||||
})
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Dashboard');
|
||||
})
|
||||
.then(function() {
|
||||
return dashboardPage.startDeployment();
|
||||
})
|
||||
.waitForElementDeletion('.dashboard-block .progress', 60000)
|
||||
.then(function() {
|
||||
return clusterPage.goToTab('Settings');
|
||||
})
|
||||
.findByCssSelector(zabbixSectionSelector + '.plugin-versions input[type=radio]:checked')
|
||||
.getProperty('value')
|
||||
.then(function(value) {
|
||||
zabbixInitialVersion = value;
|
||||
})
|
||||
.end()
|
||||
// activate plugin
|
||||
.clickByCssSelector(zabbixSectionSelector + 'h3 input[type=checkbox]')
|
||||
.assertElementExists(zabbixSectionSelector + '.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
|
||||
.setInputValue(zabbixSectionSelector + '[name=zabbix_text_with_regex]', 'aa-aa')
|
||||
.waitForElementDeletion('.subtab-link-other .glyphicon-danger-sign', 1000)
|
||||
.assertElementEnabled('.btn-apply-changes', 'The plugin change can be applied')
|
||||
// deactivate plugin
|
||||
.clickByCssSelector(zabbixSectionSelector + 'h3 input[type=checkbox]')
|
||||
.then(function() {
|
||||
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');
|
||||
},
|
||||
'Check plugin restrictions': function() {
|
||||
var loggingSectionSelector = '.setting-section-logging ';
|
||||
return this.remote
|
||||
// activate Logging plugin
|
||||
.clickByCssSelector(loggingSectionSelector + 'h3 input[type=checkbox]')
|
||||
// activate Zabbix plugin
|
||||
.clickByCssSelector(zabbixSectionSelector + 'h3 input[type=checkbox]')
|
||||
.assertElementEnabled(loggingSectionSelector + '[name=logging_text]', 'No conflict with default Zabix plugin version')
|
||||
// change Zabbix plugin version
|
||||
.clickByCssSelector(zabbixSectionSelector + '.plugin-versions input[type=radio]:not(:checked)')
|
||||
.assertElementNotSelected(zabbixSectionSelector + '[name=zabbix_checkbox]', 'Zabbix checkbox is not activated')
|
||||
.clickByCssSelector(zabbixSectionSelector + '[name=zabbix_checkbox]')
|
||||
.assertElementDisabled(loggingSectionSelector + '[name=logging_text]', 'Conflict with Zabbix checkbox')
|
||||
// reset changes
|
||||
.clickByCssSelector('.btn-revert-changes');
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,113 +15,113 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern!object',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/modal'
|
||||
'intern!object',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/modal'
|
||||
], function(registerSuite, Common, ModalWindow) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
modal,
|
||||
saveStatisticsSettingsButton, sendStatisticsCheckbox;
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
modal,
|
||||
saveStatisticsSettingsButton, sendStatisticsCheckbox;
|
||||
|
||||
return {
|
||||
name: 'Support Page',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
modal = new ModalWindow(this.remote);
|
||||
saveStatisticsSettingsButton = '.tracking .btn';
|
||||
sendStatisticsCheckbox = '.tracking input[name=send_anonymous_statistic]';
|
||||
return {
|
||||
name: 'Support Page',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
modal = new ModalWindow(this.remote);
|
||||
saveStatisticsSettingsButton = '.tracking .btn';
|
||||
sendStatisticsCheckbox = '.tracking input[name=send_anonymous_statistic]';
|
||||
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
// Go to Support page
|
||||
.clickLinkByText('Support');
|
||||
},
|
||||
'Support page is rendered correctly': function() {
|
||||
return this.remote
|
||||
.assertElementExists('.documentation-link', 'Fuel Documentation block is present')
|
||||
.assertElementExists('.snapshot', 'Diagnostic Snapshot block is present')
|
||||
.assertElementExists('.capacity-audit', 'Capacity Audit block is present')
|
||||
.assertElementExists('.tracking', 'Statistics block is present')
|
||||
.assertElementSelected(sendStatisticsCheckbox, 'Save Staticstics checkbox is checked')
|
||||
.assertElementDisabled(saveStatisticsSettingsButton, '"Save changes" button is disabled until statistics checkbox uncheck');
|
||||
},
|
||||
'Diagnostic snapshot link generation': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.snapshot .btn')
|
||||
.assertElementAppears('.snapshot .ready', 5000, 'Diagnostic snapshot link is shown');
|
||||
},
|
||||
'Usage statistics option saving': function() {
|
||||
return this.remote
|
||||
// Uncheck "Send usage statistics" checkbox
|
||||
.clickByCssSelector(sendStatisticsCheckbox)
|
||||
.assertElementEnabled(saveStatisticsSettingsButton, '"Save changes" button is enabled after changing "Send usage statistics" checkbox value')
|
||||
.clickByCssSelector(saveStatisticsSettingsButton)
|
||||
.assertElementDisabled(saveStatisticsSettingsButton, '"Save changes" button is disabled after saving changes');
|
||||
},
|
||||
'Discard changes': function() {
|
||||
return this.remote
|
||||
// Check the "Send usage statistics" checkbox
|
||||
.clickByCssSelector(sendStatisticsCheckbox)
|
||||
.assertElementEnabled(saveStatisticsSettingsButton, '"Save changes" button is enabled')
|
||||
// Go to another page with not saved changes
|
||||
.clickLinkByText('Environments')
|
||||
.then(function() {
|
||||
return modal.waitToOpen();
|
||||
})
|
||||
.then(function() {
|
||||
// Check if Discard Changes dialog is open
|
||||
return modal.checkTitle('Confirm');
|
||||
})
|
||||
.then(function() {
|
||||
// Save the changes
|
||||
return modal.clickFooterButton('Save and Proceed');
|
||||
})
|
||||
.then(function() {
|
||||
return modal.waitToClose();
|
||||
})
|
||||
.assertElementAppears('.clusters-page', 1000, 'Redirecting to Environments')
|
||||
// Go back to Support Page and ...
|
||||
.clickLinkByText('Support')
|
||||
.assertElementSelected(sendStatisticsCheckbox, 'Changes saved successfully and save staticstics checkbox is checked')
|
||||
// Uncheck the "Send usage statistics" checkbox value
|
||||
.clickByCssSelector(sendStatisticsCheckbox)
|
||||
// Go to another page with not saved changes
|
||||
.clickLinkByText('Environments')
|
||||
.then(function() {
|
||||
return modal.waitToOpen();
|
||||
})
|
||||
.then(function() {
|
||||
// Now Discard the changes
|
||||
return modal.clickFooterButton('Discard Changes');
|
||||
})
|
||||
.then(function() {
|
||||
return modal.waitToClose();
|
||||
})
|
||||
.assertElementAppears('.clusters-page', 1000, 'Redirecting to Environments')
|
||||
// Go back to Support Page and ...
|
||||
.clickLinkByText('Support')
|
||||
.assertElementSelected(sendStatisticsCheckbox, 'Changes was not saved and save staticstics checkbox is checked')
|
||||
// Uncheck the "Send usage statistics" checkbox value
|
||||
.clickByCssSelector(sendStatisticsCheckbox)
|
||||
// Go to another page with not saved changes
|
||||
.clickLinkByText('Environments')
|
||||
.then(function() {
|
||||
return modal.waitToOpen();
|
||||
})
|
||||
.then(function() {
|
||||
// Click Cancel Button
|
||||
return modal.clickFooterButton('Cancel');
|
||||
})
|
||||
.then(function() {
|
||||
return modal.waitToClose();
|
||||
})
|
||||
.assertElementNotSelected(sendStatisticsCheckbox, 'We are still on the Support page, and checkbox is unchecked');
|
||||
}
|
||||
};
|
||||
});
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
})
|
||||
// Go to Support page
|
||||
.clickLinkByText('Support');
|
||||
},
|
||||
'Support page is rendered correctly': function() {
|
||||
return this.remote
|
||||
.assertElementExists('.documentation-link', 'Fuel Documentation block is present')
|
||||
.assertElementExists('.snapshot', 'Diagnostic Snapshot block is present')
|
||||
.assertElementExists('.capacity-audit', 'Capacity Audit block is present')
|
||||
.assertElementExists('.tracking', 'Statistics block is present')
|
||||
.assertElementSelected(sendStatisticsCheckbox, 'Save Staticstics checkbox is checked')
|
||||
.assertElementDisabled(saveStatisticsSettingsButton, '"Save changes" button is disabled until statistics checkbox uncheck');
|
||||
},
|
||||
'Diagnostic snapshot link generation': function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.snapshot .btn')
|
||||
.assertElementAppears('.snapshot .ready', 5000, 'Diagnostic snapshot link is shown');
|
||||
},
|
||||
'Usage statistics option saving': function() {
|
||||
return this.remote
|
||||
// Uncheck "Send usage statistics" checkbox
|
||||
.clickByCssSelector(sendStatisticsCheckbox)
|
||||
.assertElementEnabled(saveStatisticsSettingsButton, '"Save changes" button is enabled after changing "Send usage statistics" checkbox value')
|
||||
.clickByCssSelector(saveStatisticsSettingsButton)
|
||||
.assertElementDisabled(saveStatisticsSettingsButton, '"Save changes" button is disabled after saving changes');
|
||||
},
|
||||
'Discard changes': function() {
|
||||
return this.remote
|
||||
// Check the "Send usage statistics" checkbox
|
||||
.clickByCssSelector(sendStatisticsCheckbox)
|
||||
.assertElementEnabled(saveStatisticsSettingsButton, '"Save changes" button is enabled')
|
||||
// Go to another page with not saved changes
|
||||
.clickLinkByText('Environments')
|
||||
.then(function() {
|
||||
return modal.waitToOpen();
|
||||
})
|
||||
.then(function() {
|
||||
// Check if Discard Changes dialog is open
|
||||
return modal.checkTitle('Confirm');
|
||||
})
|
||||
.then(function() {
|
||||
// Save the changes
|
||||
return modal.clickFooterButton('Save and Proceed');
|
||||
})
|
||||
.then(function() {
|
||||
return modal.waitToClose();
|
||||
})
|
||||
.assertElementAppears('.clusters-page', 1000, 'Redirecting to Environments')
|
||||
// Go back to Support Page and ...
|
||||
.clickLinkByText('Support')
|
||||
.assertElementSelected(sendStatisticsCheckbox, 'Changes saved successfully and save staticstics checkbox is checked')
|
||||
// Uncheck the "Send usage statistics" checkbox value
|
||||
.clickByCssSelector(sendStatisticsCheckbox)
|
||||
// Go to another page with not saved changes
|
||||
.clickLinkByText('Environments')
|
||||
.then(function() {
|
||||
return modal.waitToOpen();
|
||||
})
|
||||
.then(function() {
|
||||
// Now Discard the changes
|
||||
return modal.clickFooterButton('Discard Changes');
|
||||
})
|
||||
.then(function() {
|
||||
return modal.waitToClose();
|
||||
})
|
||||
.assertElementAppears('.clusters-page', 1000, 'Redirecting to Environments')
|
||||
// Go back to Support Page and ...
|
||||
.clickLinkByText('Support')
|
||||
.assertElementSelected(sendStatisticsCheckbox, 'Changes was not saved and save staticstics checkbox is checked')
|
||||
// Uncheck the "Send usage statistics" checkbox value
|
||||
.clickByCssSelector(sendStatisticsCheckbox)
|
||||
// Go to another page with not saved changes
|
||||
.clickLinkByText('Environments')
|
||||
.then(function() {
|
||||
return modal.waitToOpen();
|
||||
})
|
||||
.then(function() {
|
||||
// Click Cancel Button
|
||||
return modal.clickFooterButton('Cancel');
|
||||
})
|
||||
.then(function() {
|
||||
return modal.waitToClose();
|
||||
})
|
||||
.assertElementNotSelected(sendStatisticsCheckbox, 'We are still on the Support page, and checkbox is unchecked');
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,34 +15,34 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/helpers',
|
||||
'tests/functional/pages/login',
|
||||
'tests/functional/pages/welcome'
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/helpers',
|
||||
'tests/functional/pages/login',
|
||||
'tests/functional/pages/welcome'
|
||||
], function(registerSuite, assert, helpers, LoginPage, WelcomePage) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
registerSuite(function() {
|
||||
var loginPage,
|
||||
welcomePage;
|
||||
registerSuite(function() {
|
||||
var loginPage,
|
||||
welcomePage;
|
||||
|
||||
return {
|
||||
name: 'Welcome page',
|
||||
setup: function() {
|
||||
loginPage = new LoginPage(this.remote);
|
||||
welcomePage = new WelcomePage(this.remote);
|
||||
},
|
||||
'Skip welcome page': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return loginPage.login();
|
||||
})
|
||||
.then(function() {
|
||||
return welcomePage.skip(true);
|
||||
})
|
||||
.assertElementNotExists('.welcome-button-box button', 'Welcome screen skipped');
|
||||
}
|
||||
};
|
||||
});
|
||||
return {
|
||||
name: 'Welcome page',
|
||||
setup: function() {
|
||||
loginPage = new LoginPage(this.remote);
|
||||
welcomePage = new WelcomePage(this.remote);
|
||||
},
|
||||
'Skip welcome page': function() {
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return loginPage.login();
|
||||
})
|
||||
.then(function() {
|
||||
return welcomePage.skip(true);
|
||||
})
|
||||
.assertElementNotExists('.welcome-button-box button', 'Welcome screen skipped');
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,60 +15,60 @@
|
|||
**/
|
||||
|
||||
define([
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/helpers',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/modal'
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'tests/functional/helpers',
|
||||
'tests/functional/pages/common',
|
||||
'tests/functional/pages/modal'
|
||||
], function(registerSuite, assert, helpers, Common, Modal) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
modal;
|
||||
registerSuite(function() {
|
||||
var common,
|
||||
modal;
|
||||
|
||||
return {
|
||||
name: 'Wizard Page',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
modal = new Modal(this.remote);
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
});
|
||||
},
|
||||
beforeEach: function() {
|
||||
var clusterName = common.pickRandomName('Temp');
|
||||
return this.remote
|
||||
.clickByCssSelector('.create-cluster')
|
||||
.then(function() {
|
||||
return modal.waitToOpen();
|
||||
})
|
||||
.setInputValue('[name=name]', clusterName);
|
||||
},
|
||||
afterEach: function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.close');
|
||||
},
|
||||
'Test steps manipulations': function() {
|
||||
return this.remote
|
||||
.assertElementExists('.wizard-step.active', 'There is only one active and available step at the beginning')
|
||||
// Compute
|
||||
.pressKeys('\uE007')
|
||||
// Network
|
||||
.pressKeys('\uE007')
|
||||
// Storage
|
||||
.pressKeys('\uE007')
|
||||
// Additional Services
|
||||
.pressKeys('\uE007')
|
||||
// Finish
|
||||
.pressKeys('\uE007')
|
||||
.assertElementsExist('.wizard-step.available', 5, 'All steps are available at the end')
|
||||
.clickLinkByText('Compute')
|
||||
.clickByCssSelector('input[name=hypervisor\\:vmware]')
|
||||
.assertElementExists('.wizard-step.available', 1,
|
||||
'Only one step is available after changing hypervisor');
|
||||
}
|
||||
};
|
||||
});
|
||||
return {
|
||||
name: 'Wizard Page',
|
||||
setup: function() {
|
||||
common = new Common(this.remote);
|
||||
modal = new Modal(this.remote);
|
||||
return this.remote
|
||||
.then(function() {
|
||||
return common.getIn();
|
||||
});
|
||||
},
|
||||
beforeEach: function() {
|
||||
var clusterName = common.pickRandomName('Temp');
|
||||
return this.remote
|
||||
.clickByCssSelector('.create-cluster')
|
||||
.then(function() {
|
||||
return modal.waitToOpen();
|
||||
})
|
||||
.setInputValue('[name=name]', clusterName);
|
||||
},
|
||||
afterEach: function() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.close');
|
||||
},
|
||||
'Test steps manipulations': function() {
|
||||
return this.remote
|
||||
.assertElementExists('.wizard-step.active', 'There is only one active and available step at the beginning')
|
||||
// Compute
|
||||
.pressKeys('\uE007')
|
||||
// Network
|
||||
.pressKeys('\uE007')
|
||||
// Storage
|
||||
.pressKeys('\uE007')
|
||||
// Additional Services
|
||||
.pressKeys('\uE007')
|
||||
// Finish
|
||||
.pressKeys('\uE007')
|
||||
.assertElementsExist('.wizard-step.available', 5, 'All steps are available at the end')
|
||||
.clickLinkByText('Compute')
|
||||
.clickByCssSelector('input[name=hypervisor\\:vmware]')
|
||||
.assertElementExists('.wizard-step.available', 1,
|
||||
'Only one step is available after changing hypervisor');
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,78 +18,78 @@ import models from 'models';
|
|||
import Expression from 'expression';
|
||||
import {ModelPath} from 'expression/objects';
|
||||
|
||||
suite('Expression', () => {
|
||||
test('Expression parser test', () => {
|
||||
var hypervisor = 'kvm';
|
||||
var testModels = {
|
||||
cluster: new models.Cluster({mode: 'ha_compact'}),
|
||||
settings: new models.Settings({common: {libvirt_type: {value: hypervisor}}}),
|
||||
release: new models.Release({roles: ['controller', 'compute']})
|
||||
};
|
||||
suite('Expression', () => {
|
||||
test('Expression parser test', () => {
|
||||
var hypervisor = 'kvm';
|
||||
var testModels = {
|
||||
cluster: new models.Cluster({mode: 'ha_compact'}),
|
||||
settings: new models.Settings({common: {libvirt_type: {value: hypervisor}}}),
|
||||
release: new models.Release({roles: ['controller', 'compute']})
|
||||
};
|
||||
|
||||
// if you change/add test cases, please also modify
|
||||
// nailgun/test/unit/test_expression_parser.py
|
||||
var testCases = [
|
||||
// test scalars
|
||||
['true', true],
|
||||
['false', false],
|
||||
['123', 123],
|
||||
['"123"', '123'],
|
||||
["'123'", '123'],
|
||||
// test null
|
||||
['null', null],
|
||||
['null == false', false],
|
||||
['null == true', false],
|
||||
['null == null', true],
|
||||
// test boolean operators
|
||||
['true or false', true],
|
||||
['true and false', false],
|
||||
['not true', false],
|
||||
// test precedence
|
||||
['true or true and false or false', true],
|
||||
['true == true and false == false', true],
|
||||
// test comparison
|
||||
['123 == 123', true],
|
||||
['123 == 321', false],
|
||||
['123 != 321', true],
|
||||
['123 != "123"', true],
|
||||
// test grouping
|
||||
['(true or true) and not (false or false)', true],
|
||||
// test errors
|
||||
['(true', Error],
|
||||
['false and', Error],
|
||||
['== 123', Error],
|
||||
['#^@$*()#@!', Error],
|
||||
// test modelpaths
|
||||
['cluster:mode', 'ha_compact'],
|
||||
['cluster:mode == "ha_compact"', true],
|
||||
['cluster:mode != "multinode"', true],
|
||||
['"controller" in release:roles', true],
|
||||
['"unknown-role" in release:roles', false],
|
||||
['settings:common.libvirt_type.value', hypervisor],
|
||||
['settings:common.libvirt_type.value == "' + hypervisor + '"', true],
|
||||
['cluster:mode == "ha_compact" and not (settings:common.libvirt_type.value != "' + hypervisor + '")', true],
|
||||
// test nonexistent keys
|
||||
['cluster:nonexistentkey', Error],
|
||||
['cluster:nonexistentkey == null', true, false],
|
||||
// test evaluation flow
|
||||
['cluster:mode != "ha_compact" and cluster:nonexistentkey == null', false],
|
||||
['cluster:mode == "ha_compact" and cluster:nonexistentkey == null', Error],
|
||||
['cluster:mode == "ha_compact" and cluster:nonexistentkey == null', true, false]
|
||||
];
|
||||
// if you change/add test cases, please also modify
|
||||
// nailgun/test/unit/test_expression_parser.py
|
||||
var testCases = [
|
||||
// test scalars
|
||||
['true', true],
|
||||
['false', false],
|
||||
['123', 123],
|
||||
['"123"', '123'],
|
||||
["'123'", '123'],
|
||||
// test null
|
||||
['null', null],
|
||||
['null == false', false],
|
||||
['null == true', false],
|
||||
['null == null', true],
|
||||
// test boolean operators
|
||||
['true or false', true],
|
||||
['true and false', false],
|
||||
['not true', false],
|
||||
// test precedence
|
||||
['true or true and false or false', true],
|
||||
['true == true and false == false', true],
|
||||
// test comparison
|
||||
['123 == 123', true],
|
||||
['123 == 321', false],
|
||||
['123 != 321', true],
|
||||
['123 != "123"', true],
|
||||
// test grouping
|
||||
['(true or true) and not (false or false)', true],
|
||||
// test errors
|
||||
['(true', Error],
|
||||
['false and', Error],
|
||||
['== 123', Error],
|
||||
['#^@$*()#@!', Error],
|
||||
// test modelpaths
|
||||
['cluster:mode', 'ha_compact'],
|
||||
['cluster:mode == "ha_compact"', true],
|
||||
['cluster:mode != "multinode"', true],
|
||||
['"controller" in release:roles', true],
|
||||
['"unknown-role" in release:roles', false],
|
||||
['settings:common.libvirt_type.value', hypervisor],
|
||||
['settings:common.libvirt_type.value == "' + hypervisor + '"', true],
|
||||
['cluster:mode == "ha_compact" and not (settings:common.libvirt_type.value != "' + hypervisor + '")', true],
|
||||
// test nonexistent keys
|
||||
['cluster:nonexistentkey', Error],
|
||||
['cluster:nonexistentkey == null', true, false],
|
||||
// test evaluation flow
|
||||
['cluster:mode != "ha_compact" and cluster:nonexistentkey == null', false],
|
||||
['cluster:mode == "ha_compact" and cluster:nonexistentkey == null', Error],
|
||||
['cluster:mode == "ha_compact" and cluster:nonexistentkey == null', true, false]
|
||||
];
|
||||
|
||||
function evaluate(expression, options) {
|
||||
var result = new Expression(expression, testModels, options).evaluate();
|
||||
return result instanceof ModelPath ? result.get() : result;
|
||||
}
|
||||
function evaluate(expression, options) {
|
||||
var result = new Expression(expression, testModels, options).evaluate();
|
||||
return result instanceof ModelPath ? result.get() : result;
|
||||
}
|
||||
|
||||
_.each(testCases, ([expression, result, strict]) => {
|
||||
var options = {strict};
|
||||
if (result === Error) {
|
||||
assert.throws(_.partial(evaluate, expression, options), Error, '', expression + ' throws an error');
|
||||
} else {
|
||||
assert.strictEqual(evaluate(expression, options), result, expression + ' evaluates correctly');
|
||||
}
|
||||
});
|
||||
});
|
||||
_.each(testCases, ([expression, result, strict]) => {
|
||||
var options = {strict};
|
||||
if (result === Error) {
|
||||
assert.throws(_.partial(evaluate, expression, options), Error, '', expression + ' throws an error');
|
||||
} else {
|
||||
assert.strictEqual(evaluate(expression, options), result, expression + ' evaluates correctly');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,81 +15,81 @@
|
|||
**/
|
||||
import {Input} from 'views/controls';
|
||||
|
||||
var input;
|
||||
suite('File Control', () => {
|
||||
setup(() => {
|
||||
input = new Input({
|
||||
type: 'file',
|
||||
name: 'some_file',
|
||||
label: 'Please select some file',
|
||||
description: 'File should be selected from the local disk',
|
||||
disabled: false,
|
||||
onChange: sinon.spy(),
|
||||
defaultValue: {
|
||||
name: 'certificate.crt',
|
||||
content: 'CERTIFICATE'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('Initialization', () => {
|
||||
var initialState = input.getInitialState();
|
||||
|
||||
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.content, 'CERTIFICATE', 'Content should be equal to the default');
|
||||
});
|
||||
|
||||
test('File selection', () => {
|
||||
var clickSpy = sinon.spy();
|
||||
|
||||
sinon.stub(input, 'getInputDOMNode').returns({
|
||||
click: clickSpy
|
||||
});
|
||||
|
||||
input.pickFile();
|
||||
assert.ok(clickSpy.calledOnce, 'When icon clicked input control should be clicked too to open select file dialog');
|
||||
});
|
||||
|
||||
test('File fetching', () => {
|
||||
var readMethod = sinon.mock(),
|
||||
readerObject = {
|
||||
readAsBinaryString: readMethod,
|
||||
result: 'File contents'
|
||||
},
|
||||
saveMethod = sinon.spy(input, 'saveFile');
|
||||
|
||||
window.FileReader = () => readerObject;
|
||||
|
||||
sinon.stub(input, 'getInputDOMNode').returns({
|
||||
value: '/dummy/path/to/somefile.ext',
|
||||
files: ['file1']
|
||||
});
|
||||
|
||||
input.readFile();
|
||||
|
||||
assert.ok(readMethod.calledOnce, 'File reading as binary expected to be executed once');
|
||||
sinon.assert.calledWith(readMethod, 'file1');
|
||||
|
||||
readerObject.onload();
|
||||
assert.ok(saveMethod.calledOnce, 'saveFile handler called once');
|
||||
sinon.assert.calledWith(saveMethod, 'somefile.ext', 'File contents');
|
||||
});
|
||||
|
||||
test('File saving', () => {
|
||||
var setState = sinon.spy(input, 'setState'),
|
||||
dummyName = 'dummy.ext',
|
||||
dummyContent = 'Lorem ipsum dolores';
|
||||
input.saveFile(dummyName, dummyContent);
|
||||
|
||||
assert.deepEqual(setState.args[0][0], {
|
||||
fileName: dummyName,
|
||||
content: dummyContent
|
||||
}, 'Save file must update control state with data supplied');
|
||||
|
||||
assert.deepEqual(input.props.onChange.args[0][1], {
|
||||
name: dummyName,
|
||||
content: dummyContent
|
||||
}, 'Control sends updated data upon changes');
|
||||
});
|
||||
var input;
|
||||
suite('File Control', () => {
|
||||
setup(() => {
|
||||
input = new Input({
|
||||
type: 'file',
|
||||
name: 'some_file',
|
||||
label: 'Please select some file',
|
||||
description: 'File should be selected from the local disk',
|
||||
disabled: false,
|
||||
onChange: sinon.spy(),
|
||||
defaultValue: {
|
||||
name: 'certificate.crt',
|
||||
content: 'CERTIFICATE'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('Initialization', () => {
|
||||
var initialState = input.getInitialState();
|
||||
|
||||
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.content, 'CERTIFICATE', 'Content should be equal to the default');
|
||||
});
|
||||
|
||||
test('File selection', () => {
|
||||
var clickSpy = sinon.spy();
|
||||
|
||||
sinon.stub(input, 'getInputDOMNode').returns({
|
||||
click: clickSpy
|
||||
});
|
||||
|
||||
input.pickFile();
|
||||
assert.ok(clickSpy.calledOnce, 'When icon clicked input control should be clicked too to open select file dialog');
|
||||
});
|
||||
|
||||
test('File fetching', () => {
|
||||
var readMethod = sinon.mock(),
|
||||
readerObject = {
|
||||
readAsBinaryString: readMethod,
|
||||
result: 'File contents'
|
||||
},
|
||||
saveMethod = sinon.spy(input, 'saveFile');
|
||||
|
||||
window.FileReader = () => readerObject;
|
||||
|
||||
sinon.stub(input, 'getInputDOMNode').returns({
|
||||
value: '/dummy/path/to/somefile.ext',
|
||||
files: ['file1']
|
||||
});
|
||||
|
||||
input.readFile();
|
||||
|
||||
assert.ok(readMethod.calledOnce, 'File reading as binary expected to be executed once');
|
||||
sinon.assert.calledWith(readMethod, 'file1');
|
||||
|
||||
readerObject.onload();
|
||||
assert.ok(saveMethod.calledOnce, 'saveFile handler called once');
|
||||
sinon.assert.calledWith(saveMethod, 'somefile.ext', 'File contents');
|
||||
});
|
||||
|
||||
test('File saving', () => {
|
||||
var setState = sinon.spy(input, 'setState'),
|
||||
dummyName = 'dummy.ext',
|
||||
dummyContent = 'Lorem ipsum dolores';
|
||||
input.saveFile(dummyName, dummyContent);
|
||||
|
||||
assert.deepEqual(setState.args[0][0], {
|
||||
fileName: dummyName,
|
||||
content: dummyContent
|
||||
}, 'Save file must update control state with data supplied');
|
||||
|
||||
assert.deepEqual(input.props.onChange.args[0][1], {
|
||||
name: dummyName,
|
||||
content: dummyContent
|
||||
}, 'Control sends updated data upon changes');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,89 +16,89 @@
|
|||
import _ from 'underscore';
|
||||
import models from 'models';
|
||||
|
||||
suite('Test models', () => {
|
||||
suite('Test Task model', () => {
|
||||
test('Test extendStatuses method', () => {
|
||||
var task = new models.Task(),
|
||||
filters, result;
|
||||
suite('Test models', () => {
|
||||
suite('Test Task model', () => {
|
||||
test('Test extendStatuses method', () => {
|
||||
var task = new models.Task(),
|
||||
filters, result;
|
||||
|
||||
filters = {status: []};
|
||||
result = ['running', 'pending', 'ready', 'error'];
|
||||
assert.deepEqual(task.extendStatuses(filters), result, 'All task statuses are acceptable if "status" filter not specified');
|
||||
filters = {status: []};
|
||||
result = ['running', 'pending', 'ready', 'error'];
|
||||
assert.deepEqual(task.extendStatuses(filters), result, 'All task statuses are acceptable if "status" filter not specified');
|
||||
|
||||
filters = {status: 'ready'};
|
||||
result = ['ready'];
|
||||
assert.deepEqual(task.extendStatuses(filters), result, '"status" filter can have string as a value');
|
||||
filters = {status: 'ready'};
|
||||
result = ['ready'];
|
||||
assert.deepEqual(task.extendStatuses(filters), result, '"status" filter can have string as a value');
|
||||
|
||||
filters = {status: ['ready', 'running']};
|
||||
result = ['ready', 'running'];
|
||||
assert.deepEqual(task.extendStatuses(filters), result, '"status" filter can have list of strings as a value');
|
||||
filters = {status: ['ready', 'running']};
|
||||
result = ['ready', 'running'];
|
||||
assert.deepEqual(task.extendStatuses(filters), result, '"status" filter can have list of strings as a value');
|
||||
|
||||
filters = {status: ['ready'], active: true};
|
||||
result = [];
|
||||
assert.deepEqual(task.extendStatuses(filters), result, '"status" and "active" filters are not intersected');
|
||||
filters = {status: ['ready'], active: true};
|
||||
result = [];
|
||||
assert.deepEqual(task.extendStatuses(filters), result, '"status" and "active" filters are not intersected');
|
||||
|
||||
filters = {status: ['running'], active: true};
|
||||
result = ['running'];
|
||||
assert.deepEqual(task.extendStatuses(filters), result, '"status" and "active" filters have intersection');
|
||||
filters = {status: ['running'], active: true};
|
||||
result = ['running'];
|
||||
assert.deepEqual(task.extendStatuses(filters), result, '"status" and "active" filters have intersection');
|
||||
|
||||
filters = {status: ['running'], active: false};
|
||||
result = [];
|
||||
assert.deepEqual(task.extendStatuses(filters), result, '"status" and "active" filters are not intersected');
|
||||
filters = {status: ['running'], active: false};
|
||||
result = [];
|
||||
assert.deepEqual(task.extendStatuses(filters), result, '"status" and "active" filters are not intersected');
|
||||
|
||||
filters = {status: ['ready', 'running'], active: false};
|
||||
result = ['ready'];
|
||||
assert.deepEqual(task.extendStatuses(filters), result, '"status" and "active" filters have intersection');
|
||||
filters = {status: ['ready', 'running'], active: false};
|
||||
result = ['ready'];
|
||||
assert.deepEqual(task.extendStatuses(filters), result, '"status" and "active" filters have intersection');
|
||||
|
||||
filters = {active: true};
|
||||
result = ['running', 'pending'];
|
||||
assert.deepEqual(task.extendStatuses(filters), result, 'True value of "active" filter parsed correctly');
|
||||
filters = {active: true};
|
||||
result = ['running', 'pending'];
|
||||
assert.deepEqual(task.extendStatuses(filters), result, 'True value of "active" filter parsed correctly');
|
||||
|
||||
filters = {active: false};
|
||||
result = ['ready', 'error'];
|
||||
assert.deepEqual(task.extendStatuses(filters), result, 'False value of \'active\' filter parsed correctly');
|
||||
});
|
||||
|
||||
test('Test extendGroups method', () => {
|
||||
var task = new models.Task(),
|
||||
allTaskNames = _.flatten(_.values(task.groups)),
|
||||
filters, result;
|
||||
|
||||
filters = {name: []};
|
||||
result = allTaskNames;
|
||||
assert.deepEqual(task.extendGroups(filters), result, 'All task names are acceptable if "name" filter not specified');
|
||||
|
||||
filters = {group: []};
|
||||
result = allTaskNames;
|
||||
assert.deepEqual(task.extendGroups(filters), result, 'All task names are acceptable if "group" filter not specified');
|
||||
|
||||
filters = {name: 'deploy'};
|
||||
result = ['deploy'];
|
||||
assert.deepEqual(task.extendGroups(filters), result, '"name" filter can have string as a value');
|
||||
|
||||
filters = {name: 'dump'};
|
||||
result = ['dump'];
|
||||
assert.deepEqual(task.extendGroups(filters), result, 'Tasks, that are not related to any task group, handled properly');
|
||||
|
||||
filters = {name: ['deploy', 'check_networks']};
|
||||
result = ['deploy', 'check_networks'];
|
||||
assert.deepEqual(task.extendGroups(filters), result, '"name" filter can have list of strings as a value');
|
||||
|
||||
filters = {group: 'deployment'};
|
||||
result = task.groups.deployment;
|
||||
assert.deepEqual(task.extendGroups(filters), result, '"group" filter can have string as a value');
|
||||
|
||||
filters = {group: ['deployment', 'network']};
|
||||
result = allTaskNames;
|
||||
assert.deepEqual(task.extendGroups(filters), result, '"group" filter can have list of strings as a value');
|
||||
|
||||
filters = {name: 'deploy', group: 'deployment'};
|
||||
result = ['deploy'];
|
||||
assert.deepEqual(task.extendGroups(filters), result, '"name" and "group" filters have intersection');
|
||||
|
||||
filters = {name: 'deploy', group: 'network'};
|
||||
result = [];
|
||||
assert.deepEqual(task.extendGroups(filters), result, '"name" and "group" filters are not intersected');
|
||||
});
|
||||
});
|
||||
filters = {active: false};
|
||||
result = ['ready', 'error'];
|
||||
assert.deepEqual(task.extendStatuses(filters), result, 'False value of \'active\' filter parsed correctly');
|
||||
});
|
||||
|
||||
test('Test extendGroups method', () => {
|
||||
var task = new models.Task(),
|
||||
allTaskNames = _.flatten(_.values(task.groups)),
|
||||
filters, result;
|
||||
|
||||
filters = {name: []};
|
||||
result = allTaskNames;
|
||||
assert.deepEqual(task.extendGroups(filters), result, 'All task names are acceptable if "name" filter not specified');
|
||||
|
||||
filters = {group: []};
|
||||
result = allTaskNames;
|
||||
assert.deepEqual(task.extendGroups(filters), result, 'All task names are acceptable if "group" filter not specified');
|
||||
|
||||
filters = {name: 'deploy'};
|
||||
result = ['deploy'];
|
||||
assert.deepEqual(task.extendGroups(filters), result, '"name" filter can have string as a value');
|
||||
|
||||
filters = {name: 'dump'};
|
||||
result = ['dump'];
|
||||
assert.deepEqual(task.extendGroups(filters), result, 'Tasks, that are not related to any task group, handled properly');
|
||||
|
||||
filters = {name: ['deploy', 'check_networks']};
|
||||
result = ['deploy', 'check_networks'];
|
||||
assert.deepEqual(task.extendGroups(filters), result, '"name" filter can have list of strings as a value');
|
||||
|
||||
filters = {group: 'deployment'};
|
||||
result = task.groups.deployment;
|
||||
assert.deepEqual(task.extendGroups(filters), result, '"group" filter can have string as a value');
|
||||
|
||||
filters = {group: ['deployment', 'network']};
|
||||
result = allTaskNames;
|
||||
assert.deepEqual(task.extendGroups(filters), result, '"group" filter can have list of strings as a value');
|
||||
|
||||
filters = {name: 'deploy', group: 'deployment'};
|
||||
result = ['deploy'];
|
||||
assert.deepEqual(task.extendGroups(filters), result, '"name" and "group" filters have intersection');
|
||||
|
||||
filters = {name: 'deploy', group: 'network'};
|
||||
result = [];
|
||||
assert.deepEqual(task.extendGroups(filters), result, '"name" and "group" filters are not intersected');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,76 +15,76 @@
|
|||
**/
|
||||
import OffloadingModes from 'views/cluster_page_tabs/nodes_tab_screens/offloading_modes_control';
|
||||
|
||||
var offloadingModesConrol,
|
||||
TestMode22,
|
||||
TestMode31,
|
||||
fakeOffloadingModes,
|
||||
fakeInterface = {
|
||||
offloading_modes: fakeOffloadingModes,
|
||||
get(key) {
|
||||
assert.equal(key, 'offloading_modes', '"offloading_modes" interface property should be used to get data');
|
||||
return fakeOffloadingModes;
|
||||
},
|
||||
set(key, value) {
|
||||
assert.equal(key, 'offloading_modes', '"offloading_modes" interface property should be used to set data');
|
||||
fakeOffloadingModes = value;
|
||||
}
|
||||
};
|
||||
var offloadingModesConrol,
|
||||
TestMode22,
|
||||
TestMode31,
|
||||
fakeOffloadingModes,
|
||||
fakeInterface = {
|
||||
offloading_modes: fakeOffloadingModes,
|
||||
get(key) {
|
||||
assert.equal(key, 'offloading_modes', '"offloading_modes" interface property should be used to get data');
|
||||
return fakeOffloadingModes;
|
||||
},
|
||||
set(key, value) {
|
||||
assert.equal(key, 'offloading_modes', '"offloading_modes" interface property should be used to set data');
|
||||
fakeOffloadingModes = value;
|
||||
}
|
||||
};
|
||||
|
||||
suite('Offloadning Modes control', () => {
|
||||
setup(() => {
|
||||
TestMode22 = {name: 'TestName22', state: false, sub: []};
|
||||
TestMode31 = {name: 'TestName31', state: null, sub: []};
|
||||
fakeOffloadingModes = [
|
||||
{
|
||||
name: 'TestName1',
|
||||
state: true,
|
||||
sub: [
|
||||
{name: 'TestName11', state: true, sub: [
|
||||
TestMode31
|
||||
]},
|
||||
{name: 'TestName12', state: false, sub: []},
|
||||
{name: 'TestName13', state: null, sub: []}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'TestName2',
|
||||
state: false,
|
||||
sub: [
|
||||
{name: 'TestName21', state: false, sub: []},
|
||||
TestMode22,
|
||||
{name: 'TestName23', state: false, sub: []}
|
||||
]
|
||||
}
|
||||
];
|
||||
offloadingModesConrol = new OffloadingModes({
|
||||
interface: fakeInterface
|
||||
});
|
||||
});
|
||||
|
||||
test('Finding mode by name', () => {
|
||||
var mode = offloadingModesConrol.findMode(TestMode22.name, fakeOffloadingModes);
|
||||
assert.deepEqual(mode, TestMode22, 'Mode can be found by name');
|
||||
});
|
||||
test('Set mode state logic', () => {
|
||||
offloadingModesConrol.setModeState(TestMode31, true);
|
||||
assert.strictEqual(TestMode31.state, true, 'Mode state is changing');
|
||||
});
|
||||
test('Set submodes states logic', () => {
|
||||
var mode = offloadingModesConrol.findMode('TestName1', fakeOffloadingModes);
|
||||
offloadingModesConrol.setModeState(mode, false);
|
||||
assert.strictEqual(TestMode31.state, false, 'Parent state changing leads to all child modes states changing');
|
||||
});
|
||||
test('Disabled reversed logic', () => {
|
||||
var mode = offloadingModesConrol.findMode('TestName2', fakeOffloadingModes);
|
||||
offloadingModesConrol.setModeState(TestMode22, true);
|
||||
offloadingModesConrol.checkModes(null, fakeOffloadingModes);
|
||||
assert.strictEqual(mode.state, null, 'Parent state changing leads to all child modes states changing');
|
||||
});
|
||||
test('All Modes option logic', () => {
|
||||
var enableAllModes = offloadingModesConrol.onModeStateChange('All Modes', true);
|
||||
enableAllModes();
|
||||
var mode = offloadingModesConrol.findMode('TestName2', fakeOffloadingModes);
|
||||
assert.strictEqual(mode.state, true, 'All Modes option state changing leads to all parent modes states changing');
|
||||
});
|
||||
suite('Offloadning Modes control', () => {
|
||||
setup(() => {
|
||||
TestMode22 = {name: 'TestName22', state: false, sub: []};
|
||||
TestMode31 = {name: 'TestName31', state: null, sub: []};
|
||||
fakeOffloadingModes = [
|
||||
{
|
||||
name: 'TestName1',
|
||||
state: true,
|
||||
sub: [
|
||||
{name: 'TestName11', state: true, sub: [
|
||||
TestMode31
|
||||
]},
|
||||
{name: 'TestName12', state: false, sub: []},
|
||||
{name: 'TestName13', state: null, sub: []}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'TestName2',
|
||||
state: false,
|
||||
sub: [
|
||||
{name: 'TestName21', state: false, sub: []},
|
||||
TestMode22,
|
||||
{name: 'TestName23', state: false, sub: []}
|
||||
]
|
||||
}
|
||||
];
|
||||
offloadingModesConrol = new OffloadingModes({
|
||||
interface: fakeInterface
|
||||
});
|
||||
});
|
||||
|
||||
test('Finding mode by name', () => {
|
||||
var mode = offloadingModesConrol.findMode(TestMode22.name, fakeOffloadingModes);
|
||||
assert.deepEqual(mode, TestMode22, 'Mode can be found by name');
|
||||
});
|
||||
test('Set mode state logic', () => {
|
||||
offloadingModesConrol.setModeState(TestMode31, true);
|
||||
assert.strictEqual(TestMode31.state, true, 'Mode state is changing');
|
||||
});
|
||||
test('Set submodes states logic', () => {
|
||||
var mode = offloadingModesConrol.findMode('TestName1', fakeOffloadingModes);
|
||||
offloadingModesConrol.setModeState(mode, false);
|
||||
assert.strictEqual(TestMode31.state, false, 'Parent state changing leads to all child modes states changing');
|
||||
});
|
||||
test('Disabled reversed logic', () => {
|
||||
var mode = offloadingModesConrol.findMode('TestName2', fakeOffloadingModes);
|
||||
offloadingModesConrol.setModeState(TestMode22, true);
|
||||
offloadingModesConrol.checkModes(null, fakeOffloadingModes);
|
||||
assert.strictEqual(mode.state, null, 'Parent state changing leads to all child modes states changing');
|
||||
});
|
||||
test('All Modes option logic', () => {
|
||||
var enableAllModes = offloadingModesConrol.onModeStateChange('All Modes', true);
|
||||
enableAllModes();
|
||||
var mode = offloadingModesConrol.findMode('TestName2', fakeOffloadingModes);
|
||||
assert.strictEqual(mode.state, true, 'All Modes option state changing leads to all parent modes states changing');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,145 +17,145 @@ import utils from 'utils';
|
|||
import i18n from 'i18n';
|
||||
import Backbone from 'backbone';
|
||||
|
||||
suite('Test utils', () => {
|
||||
test('Test getResponseText', () => {
|
||||
var response;
|
||||
var getResponseText = utils.getResponseText;
|
||||
var serverErrorMessage = i18n('dialog.error_dialog.server_error');
|
||||
var serverUnavailableMessage = i18n('dialog.error_dialog.server_unavailable');
|
||||
suite('Test utils', () => {
|
||||
test('Test getResponseText', () => {
|
||||
var response;
|
||||
var getResponseText = utils.getResponseText;
|
||||
var serverErrorMessage = i18n('dialog.error_dialog.server_error');
|
||||
var serverUnavailableMessage = i18n('dialog.error_dialog.server_unavailable');
|
||||
|
||||
response = {status: 500, responseText: 'Server error occured'};
|
||||
assert.equal(getResponseText(response), serverErrorMessage, 'HTTP 500 is treated as a server error');
|
||||
response = {status: 500, responseText: 'Server error occured'};
|
||||
assert.equal(getResponseText(response), serverErrorMessage, 'HTTP 500 is treated as a server error');
|
||||
|
||||
response = {status: 502, responseText: 'Bad gateway'};
|
||||
assert.equal(getResponseText(response), serverUnavailableMessage, 'HTTP 502 is treated as server unavailability');
|
||||
response = {status: 502, responseText: 'Bad gateway'};
|
||||
assert.equal(getResponseText(response), serverUnavailableMessage, 'HTTP 502 is treated as server unavailability');
|
||||
|
||||
response = {status: 0, responseText: 'error'};
|
||||
assert.equal(getResponseText(response), serverUnavailableMessage, 'XHR object with no status is treated as server unavailability');
|
||||
response = {status: 0, responseText: 'error'};
|
||||
assert.equal(getResponseText(response), serverUnavailableMessage, 'XHR object with no status is treated as server unavailability');
|
||||
|
||||
response = {status: 400, responseText: 'Bad request'};
|
||||
assert.equal(getResponseText(response), serverErrorMessage, 'HTTP 400 with plain text response is treated as a server error');
|
||||
response = {status: 400, responseText: 'Bad request'};
|
||||
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'})};
|
||||
assert.equal(getResponseText(response), '123', 'HTTP 400 with JSON response is treated correctly');
|
||||
});
|
||||
response = {status: 400, responseText: JSON.stringify({message: '123'})};
|
||||
assert.equal(getResponseText(response), '123', 'HTTP 400 with JSON response is treated correctly');
|
||||
});
|
||||
|
||||
test('Test comparison', () => {
|
||||
var compare = utils.compare;
|
||||
var model1 = new Backbone.Model({
|
||||
string: 'bond2',
|
||||
number: 1,
|
||||
boolean: true,
|
||||
booleanFlagWithNull: null
|
||||
});
|
||||
var model2 = new Backbone.Model({
|
||||
string: 'bond10',
|
||||
number: 10,
|
||||
boolean: false,
|
||||
booleanFlagWithNull: false
|
||||
});
|
||||
|
||||
assert.equal(compare(model1, model2, {attr: 'string'}), -1, 'String comparison a<b');
|
||||
|
||||
assert.equal(compare(model2, model1, {attr: 'string'}), 1, 'String comparison a>b');
|
||||
|
||||
assert.equal(compare(model1, model1, {attr: 'string'}), 0, 'String comparison a=b');
|
||||
|
||||
assert.ok(compare(model1, model2, {attr: 'number'}) < 0, 'Number comparison a<b');
|
||||
|
||||
assert.ok(compare(model2, 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(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(model2, model2, {attr: 'boolean'}), 0, 'Boolean comparison false and false');
|
||||
|
||||
assert.equal(compare(model1, model2, {attr: 'booleanFlagWithNull'}), 0, 'Comparison null and false');
|
||||
});
|
||||
|
||||
test('Test highlightTestStep', () => {
|
||||
var text;
|
||||
var highlight = utils.highlightTestStep;
|
||||
|
||||
text = '1. Step 1\n2. Step 2\n3. Step 3';
|
||||
assert.equal(
|
||||
highlight(text, 1),
|
||||
'<b>1. Step 1</b>\n2. Step 2\n3. Step 3',
|
||||
'Highlighting first step in simple text works'
|
||||
);
|
||||
assert.equal(
|
||||
highlight(text, 2),
|
||||
'1. Step 1\n<b>2. Step 2</b>\n3. Step 3',
|
||||
'Highlighting middle step in simple text works'
|
||||
);
|
||||
assert.equal(
|
||||
highlight(text, 3),
|
||||
'1. Step 1\n2. Step 2\n<b>3. Step 3</b>',
|
||||
'Highlighting last step in simple text works'
|
||||
);
|
||||
|
||||
text = '1. Step 1\n1-1\n1-2\n2. Step 2\n2-1\n2-2\n3. Step 3\n3-1\n3-2';
|
||||
assert.equal(
|
||||
highlight(text, 1),
|
||||
'<b>1. Step 1\n1-1\n1-2</b>\n2. Step 2\n2-1\n2-2\n3. Step 3\n3-1\n3-2',
|
||||
'Highlighting first step in multiline text works'
|
||||
);
|
||||
assert.equal(
|
||||
highlight(text, 2),
|
||||
'1. Step 1\n1-1\n1-2\n<b>2. Step 2\n2-1\n2-2</b>\n3. Step 3\n3-1\n3-2',
|
||||
'Highlighting middle step in multiline text works'
|
||||
);
|
||||
assert.equal(
|
||||
highlight(text, 3),
|
||||
'1. Step 1\n1-1\n1-2\n2. Step 2\n2-1\n2-2\n<b>3. Step 3\n3-1\n3-2</b>',
|
||||
'Highlighting last step in multiline text works'
|
||||
);
|
||||
|
||||
text = ' \n \n 1. Step 1 \n \n';
|
||||
assert.equal(
|
||||
highlight(text, 1),
|
||||
' \n \n <b>1. Step 1 \n \n</b>',
|
||||
'Highlighting steps in padded text works'
|
||||
);
|
||||
|
||||
text = '1. Step 1\n3. Step 3';
|
||||
assert.equal(
|
||||
highlight(text, 2),
|
||||
text,
|
||||
'Attempting to highlight non-existent step keeps text as it is'
|
||||
);
|
||||
});
|
||||
|
||||
test('Test getDefaultGatewayForCidr', () => {
|
||||
var getGateway = utils.getDefaultGatewayForCidr;
|
||||
|
||||
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('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');
|
||||
});
|
||||
|
||||
test('Test 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('192.168.0.0/10', true), [['192.128.0.2', '192.191.255.254']], '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', () => {
|
||||
var validate = utils.validateIpCorrespondsToCIDR;
|
||||
|
||||
assert.ok(validate('172.16.0.0/20', '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.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');
|
||||
});
|
||||
test('Test comparison', () => {
|
||||
var compare = utils.compare;
|
||||
var model1 = new Backbone.Model({
|
||||
string: 'bond2',
|
||||
number: 1,
|
||||
boolean: true,
|
||||
booleanFlagWithNull: null
|
||||
});
|
||||
var model2 = new Backbone.Model({
|
||||
string: 'bond10',
|
||||
number: 10,
|
||||
boolean: false,
|
||||
booleanFlagWithNull: false
|
||||
});
|
||||
|
||||
assert.equal(compare(model1, model2, {attr: 'string'}), -1, 'String comparison a<b');
|
||||
|
||||
assert.equal(compare(model2, model1, {attr: 'string'}), 1, 'String comparison a>b');
|
||||
|
||||
assert.equal(compare(model1, model1, {attr: 'string'}), 0, 'String comparison a=b');
|
||||
|
||||
assert.ok(compare(model1, model2, {attr: 'number'}) < 0, 'Number comparison a<b');
|
||||
|
||||
assert.ok(compare(model2, 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(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(model2, model2, {attr: 'boolean'}), 0, 'Boolean comparison false and false');
|
||||
|
||||
assert.equal(compare(model1, model2, {attr: 'booleanFlagWithNull'}), 0, 'Comparison null and false');
|
||||
});
|
||||
|
||||
test('Test highlightTestStep', () => {
|
||||
var text;
|
||||
var highlight = utils.highlightTestStep;
|
||||
|
||||
text = '1. Step 1\n2. Step 2\n3. Step 3';
|
||||
assert.equal(
|
||||
highlight(text, 1),
|
||||
'<b>1. Step 1</b>\n2. Step 2\n3. Step 3',
|
||||
'Highlighting first step in simple text works'
|
||||
);
|
||||
assert.equal(
|
||||
highlight(text, 2),
|
||||
'1. Step 1\n<b>2. Step 2</b>\n3. Step 3',
|
||||
'Highlighting middle step in simple text works'
|
||||
);
|
||||
assert.equal(
|
||||
highlight(text, 3),
|
||||
'1. Step 1\n2. Step 2\n<b>3. Step 3</b>',
|
||||
'Highlighting last step in simple text works'
|
||||
);
|
||||
|
||||
text = '1. Step 1\n1-1\n1-2\n2. Step 2\n2-1\n2-2\n3. Step 3\n3-1\n3-2';
|
||||
assert.equal(
|
||||
highlight(text, 1),
|
||||
'<b>1. Step 1\n1-1\n1-2</b>\n2. Step 2\n2-1\n2-2\n3. Step 3\n3-1\n3-2',
|
||||
'Highlighting first step in multiline text works'
|
||||
);
|
||||
assert.equal(
|
||||
highlight(text, 2),
|
||||
'1. Step 1\n1-1\n1-2\n<b>2. Step 2\n2-1\n2-2</b>\n3. Step 3\n3-1\n3-2',
|
||||
'Highlighting middle step in multiline text works'
|
||||
);
|
||||
assert.equal(
|
||||
highlight(text, 3),
|
||||
'1. Step 1\n1-1\n1-2\n2. Step 2\n2-1\n2-2\n<b>3. Step 3\n3-1\n3-2</b>',
|
||||
'Highlighting last step in multiline text works'
|
||||
);
|
||||
|
||||
text = ' \n \n 1. Step 1 \n \n';
|
||||
assert.equal(
|
||||
highlight(text, 1),
|
||||
' \n \n <b>1. Step 1 \n \n</b>',
|
||||
'Highlighting steps in padded text works'
|
||||
);
|
||||
|
||||
text = '1. Step 1\n3. Step 3';
|
||||
assert.equal(
|
||||
highlight(text, 2),
|
||||
text,
|
||||
'Attempting to highlight non-existent step keeps text as it is'
|
||||
);
|
||||
});
|
||||
|
||||
test('Test getDefaultGatewayForCidr', () => {
|
||||
var getGateway = utils.getDefaultGatewayForCidr;
|
||||
|
||||
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('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');
|
||||
});
|
||||
|
||||
test('Test 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('192.168.0.0/10', true), [['192.128.0.2', '192.191.255.254']], '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', () => {
|
||||
var validate = utils.validateIpCorrespondsToCIDR;
|
||||
|
||||
assert.ok(validate('172.16.0.0/20', '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.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');
|
||||
});
|
||||
});
|
||||
|
|
610
static/utils.js
610
static/utils.js
|
@ -25,310 +25,310 @@ import IP from 'ip';
|
|||
import {ErrorDialog} from 'views/dialogs';
|
||||
import models from 'models';
|
||||
|
||||
var utils = {
|
||||
regexes: {
|
||||
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])$/,
|
||||
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])$/
|
||||
},
|
||||
serializeTabOptions(options) {
|
||||
return _.map(options, (value, key) => key + ':' + value).join(';');
|
||||
},
|
||||
deserializeTabOptions(serializedOptions) {
|
||||
return _.object(_.map((serializedOptions || '').split(';'), (option) => option.split(':')));
|
||||
},
|
||||
getNodeListFromTabOptions(options) {
|
||||
var nodeIds = utils.deserializeTabOptions(options.screenOptions[0]).nodes,
|
||||
ids = nodeIds ? nodeIds.split(',').map((id) => parseInt(id, 10)) : [],
|
||||
nodes = new models.Nodes(options.cluster.get('nodes').getByIds(ids));
|
||||
if (nodes.length == ids.length) return nodes;
|
||||
},
|
||||
renderMultilineText(text) {
|
||||
if (!text) return null;
|
||||
return <div>{text.split('\n').map((str, index) => <p key={index}>{str}</p>)}</div>;
|
||||
},
|
||||
linebreaks(text) {
|
||||
return text.replace(/\n/g, '<br/>');
|
||||
},
|
||||
composeLink(url) {
|
||||
return '<a target="_blank" href="' + url + '">' + url + '</a>';
|
||||
},
|
||||
urlify(text) {
|
||||
return utils.linebreaks(text).replace(new RegExp(utils.regexes.url.source, 'g'), utils.composeLink);
|
||||
},
|
||||
composeList(value) {
|
||||
return _.isUndefined(value) ? [] : _.isArray(value) ? value : [value];
|
||||
},
|
||||
// FIXME(vkramskikh): moved here from healthcheck_tab to make testable
|
||||
highlightTestStep(text, step) {
|
||||
return text.replace(new RegExp('(^|\\s*)(' + step + '\\.[\\s\\S]*?)(\\s*\\d+\\.|$)'), '$1<b>$2</b>$3');
|
||||
},
|
||||
classNames: classNames,
|
||||
parseModelPath(path, models) {
|
||||
var modelPath = new ModelPath(path);
|
||||
modelPath.setModel(models);
|
||||
return modelPath;
|
||||
},
|
||||
evaluateExpression(expression, models, options) {
|
||||
var compiledExpression = new Expression(expression, models, options),
|
||||
value = compiledExpression.evaluate();
|
||||
return {
|
||||
value: value,
|
||||
modelPaths: compiledExpression.modelPaths
|
||||
};
|
||||
},
|
||||
expandRestriction(restriction) {
|
||||
var result = {
|
||||
action: 'disable',
|
||||
message: null
|
||||
};
|
||||
if (_.isString(restriction)) {
|
||||
result.condition = restriction;
|
||||
} else if (_.isPlainObject(restriction)) {
|
||||
if (_.has(restriction, 'condition')) {
|
||||
_.extend(result, restriction);
|
||||
} else {
|
||||
result.condition = _.keys(restriction)[0];
|
||||
result.message = _.values(restriction)[0];
|
||||
}
|
||||
} else {
|
||||
throw new Error('Invalid restriction format');
|
||||
}
|
||||
return result;
|
||||
},
|
||||
showErrorDialog(options) {
|
||||
options.message = options.response ? utils.getResponseText(options.response) :
|
||||
options.message || i18n('dialog.error_dialog.server_error');
|
||||
ErrorDialog.show(options);
|
||||
},
|
||||
showBandwidth(bandwidth) {
|
||||
bandwidth = parseInt(bandwidth, 10);
|
||||
if (!_.isNumber(bandwidth) || _.isNaN(bandwidth)) return i18n('common.not_available');
|
||||
return (bandwidth / 1000).toFixed(1) + ' Gbps';
|
||||
},
|
||||
showFrequency(frequency) {
|
||||
frequency = parseInt(frequency, 10);
|
||||
if (!_.isNumber(frequency) || _.isNaN(frequency)) return i18n('common.not_available');
|
||||
var base = 1000;
|
||||
var treshold = 1000;
|
||||
return (frequency >= treshold ? (frequency / base).toFixed(2) + ' GHz' : frequency + ' MHz');
|
||||
},
|
||||
showSize(bytes, treshold) {
|
||||
bytes = parseInt(bytes, 10);
|
||||
if (!_.isNumber(bytes) || _.isNaN(bytes)) return i18n('common.not_available');
|
||||
var base = 1024;
|
||||
treshold = treshold || 256;
|
||||
var units = ['byte', 'kb', 'mb', 'gb', 'tb'];
|
||||
var i, result, unit = 'tb';
|
||||
for (i = 0; i < units.length; i += 1) {
|
||||
result = bytes / Math.pow(base, i);
|
||||
if (result < treshold) {
|
||||
unit = units[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (result ? result.toFixed(1) : result) + ' ' + i18n('common.size.' + unit, {count: result});
|
||||
},
|
||||
showMemorySize(bytes) {
|
||||
return utils.showSize(bytes, 1024);
|
||||
},
|
||||
showDiskSize(value, power) {
|
||||
power = power || 0;
|
||||
return utils.showSize(value * Math.pow(1024, power));
|
||||
},
|
||||
calculateNetworkSize(cidr) {
|
||||
return Math.pow(2, 32 - parseInt(_.last(cidr.split('/')), 10));
|
||||
},
|
||||
formatNumber(n) {
|
||||
return String(n).replace(/\d/g, (c, i, a) => i > 0 && c !== '.' && (a.length - i) % 3 === 0 ? ',' + c : c);
|
||||
},
|
||||
floor(n, decimals) {
|
||||
return Math.floor(n * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
||||
},
|
||||
isNaturalNumber(n) {
|
||||
return _.isNumber(n) && n > 0 && n % 1 === 0;
|
||||
},
|
||||
validateVlan(vlan, forbiddenVlans, field, disallowNullValue) {
|
||||
var error = {};
|
||||
if ((_.isNull(vlan) && disallowNullValue) || (!_.isNull(vlan) && (!utils.isNaturalNumber(vlan) || vlan < 1 || vlan > 4094))) {
|
||||
error[field] = i18n('cluster_page.network_tab.validation.invalid_vlan');
|
||||
return error;
|
||||
}
|
||||
if (_.contains(_.compact(forbiddenVlans), vlan)) {
|
||||
error[field] = i18n('cluster_page.network_tab.validation.forbidden_vlan');
|
||||
}
|
||||
return error[field] ? error : {};
|
||||
},
|
||||
validateCidr(cidr, field) {
|
||||
field = field || 'cidr';
|
||||
var error = {}, match;
|
||||
if (_.isString(cidr)) {
|
||||
match = cidr.match(utils.regexes.cidr);
|
||||
if (match) {
|
||||
var prefix = parseInt(match[1], 10);
|
||||
if (prefix < 2) {
|
||||
error[field] = i18n('cluster_page.network_tab.validation.large_network');
|
||||
} else if (prefix > 30) {
|
||||
error[field] = i18n('cluster_page.network_tab.validation.small_network');
|
||||
}
|
||||
} else {
|
||||
error[field] = i18n('cluster_page.network_tab.validation.invalid_cidr');
|
||||
}
|
||||
} else {
|
||||
error[field] = i18n('cluster_page.network_tab.validation.invalid_cidr');
|
||||
}
|
||||
return error[field] ? error : {};
|
||||
},
|
||||
validateIP(ip) {
|
||||
return _.isString(ip) && !!ip.match(utils.regexes.ip);
|
||||
},
|
||||
validateIPRanges(ranges, cidr, existingRanges = [], warnings = {}) {
|
||||
var ipRangesErrors = [],
|
||||
ns = 'cluster_page.network_tab.validation.';
|
||||
_.defaults(warnings, {
|
||||
INVALID_IP: i18n(ns + 'invalid_ip'),
|
||||
DOES_NOT_MATCH_CIDR: i18n(ns + 'ip_does_not_match_cidr'),
|
||||
INVALID_IP_RANGE: i18n(ns + 'invalid_ip_range'),
|
||||
EMPTY_IP_RANGE: i18n(ns + 'empty_ip_range'),
|
||||
IP_RANGES_INTERSECTION: i18n(ns + 'ip_ranges_intersection')
|
||||
});
|
||||
|
||||
if (_.any(ranges, (range) => _.compact(range).length)) {
|
||||
_.each(ranges, (range, index) => {
|
||||
if (_.any(range)) {
|
||||
var error = {};
|
||||
|
||||
if (!utils.validateIP(range[0])) {
|
||||
error.start = warnings.INVALID_IP;
|
||||
} else if (cidr && !utils.validateIpCorrespondsToCIDR(cidr, range[0])) {
|
||||
error.start = warnings.DOES_NOT_MATCH_CIDR;
|
||||
}
|
||||
|
||||
if (!utils.validateIP(range[1])) {
|
||||
error.end = warnings.INVALID_IP;
|
||||
} else if (cidr && !utils.validateIpCorrespondsToCIDR(cidr, range[1])) {
|
||||
error.end = warnings.DOES_NOT_MATCH_CIDR;
|
||||
}
|
||||
|
||||
if (_.isEmpty(error)) {
|
||||
if (IP.toLong(range[0]) > IP.toLong(range[1])) {
|
||||
error.start = error.end = warnings.INVALID_IP_RANGE;
|
||||
} else if (_.isUndefined(cidr)) {
|
||||
error.start = error.end = warnings.IP_RANGE_IS_NOT_IN_PUBLIC_CIDR;
|
||||
} else if (existingRanges.length) {
|
||||
var intersection = utils.checkIPRangesIntersection(range, existingRanges);
|
||||
if (intersection) {
|
||||
error.start = error.end = warnings.IP_RANGES_INTERSECTION + intersection.join(' - ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isEmpty(error)) {
|
||||
ipRangesErrors.push(_.extend(error, {index: index}));
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ipRangesErrors.push({
|
||||
index: 0,
|
||||
start: warnings.EMPTY_IP_RANGE,
|
||||
end: warnings.EMPTY_IP_RANGE
|
||||
});
|
||||
}
|
||||
return ipRangesErrors;
|
||||
},
|
||||
checkIPRangesIntersection([startIP, endIP], existingRanges) {
|
||||
var startIPInt = IP.toLong(startIP),
|
||||
endIPInt = IP.toLong(endIP);
|
||||
return _.find(existingRanges, ([ip1, ip2]) => IP.toLong(ip2) >= startIPInt && IP.toLong(ip1) <= endIPInt);
|
||||
},
|
||||
validateIpCorrespondsToCIDR(cidr, ip) {
|
||||
if (!cidr) return true;
|
||||
var networkData = IP.cidrSubnet(cidr),
|
||||
ipInt = IP.toLong(ip);
|
||||
return ipInt >= IP.toLong(networkData.firstAddress) && ipInt <= IP.toLong(networkData.lastAddress);
|
||||
},
|
||||
validateVlanRange(vlanStart, vlanEnd, vlan) {
|
||||
return vlan >= vlanStart && vlan <= vlanEnd;
|
||||
},
|
||||
getDefaultGatewayForCidr(cidr) {
|
||||
if (!_.isEmpty(utils.validateCidr(cidr))) return '';
|
||||
return IP.cidrSubnet(cidr).firstAddress;
|
||||
},
|
||||
getDefaultIPRangeForCidr(cidr, excludeGateway) {
|
||||
if (!_.isEmpty(utils.validateCidr(cidr))) return [['', '']];
|
||||
var networkData = IP.cidrSubnet(cidr);
|
||||
if (excludeGateway) {
|
||||
var startIPInt = IP.toLong(networkData.firstAddress);
|
||||
startIPInt++;
|
||||
return [[IP.fromLong(startIPInt), networkData.lastAddress]];
|
||||
}
|
||||
return [[networkData.firstAddress, networkData.lastAddress]];
|
||||
},
|
||||
sortEntryProperties(entry, sortOrder) {
|
||||
sortOrder = sortOrder || ['name'];
|
||||
var properties = _.keys(entry);
|
||||
return _.sortBy(properties, (property) => {
|
||||
var index = _.indexOf(sortOrder, property);
|
||||
return index == -1 ? properties.length : index;
|
||||
});
|
||||
},
|
||||
getResponseText(response, defaultText) {
|
||||
var serverErrorMessage = defaultText || i18n('dialog.error_dialog.server_error');
|
||||
var serverUnavailableMessage = i18n('dialog.error_dialog.server_unavailable');
|
||||
if (response && (!response.status || response.status >= 400)) {
|
||||
if (!response.status || response.status == 502) return serverUnavailableMessage;
|
||||
if (response.status == 500) return serverErrorMessage;
|
||||
// parsing new backend response format in responseText
|
||||
response = response.responseText || response;
|
||||
try {
|
||||
response = JSON.parse(response);
|
||||
return response.message || serverErrorMessage;
|
||||
} catch (exception) {
|
||||
return serverErrorMessage;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
},
|
||||
natsort(str1, str2, options = {}) {
|
||||
var {insensitive, desc} = options;
|
||||
naturalSort.insensitive = insensitive;
|
||||
return naturalSort(str1, str2) * (desc ? -1 : 1);
|
||||
},
|
||||
multiSort(model1, model2, attributes) {
|
||||
var result = utils.compare(model1, model2, attributes[0]);
|
||||
if (result === 0 && attributes.length > 1) {
|
||||
attributes.splice(0, 1);
|
||||
result = utils.multiSort(model1, model2, attributes);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
compare(model1, model2, options) {
|
||||
var getValue = function(model) {
|
||||
var attr = options.attr;
|
||||
return _.isFunction(model[attr]) ? model[attr]() : model.get(attr);
|
||||
};
|
||||
var value1 = getValue(model1),
|
||||
value2 = getValue(model2);
|
||||
if (_.isString(value1) && _.isString(value2)) {
|
||||
return utils.natsort(value1, value2, options);
|
||||
}
|
||||
var result;
|
||||
if (_.isNumber(value1) && _.isNumber(value2)) {
|
||||
result = value1 - value2;
|
||||
} else {
|
||||
result = value1 === value2 || !value1 && !value2 ? 0 : !value1 ? 1 : -1;
|
||||
}
|
||||
return options.desc ? -result : result;
|
||||
},
|
||||
composeDocumentationLink(link) {
|
||||
var isMirantisIso = _.contains(app.version.get('feature_groups'), 'mirantis'),
|
||||
release = app.version.get('release'),
|
||||
linkStart = isMirantisIso ? 'https://docs.mirantis.com/openstack/fuel/fuel-' :
|
||||
'https://docs.fuel-infra.org/openstack/fuel/fuel-';
|
||||
return linkStart + release + '/' + link;
|
||||
}
|
||||
var utils = {
|
||||
regexes: {
|
||||
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])$/,
|
||||
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])$/
|
||||
},
|
||||
serializeTabOptions(options) {
|
||||
return _.map(options, (value, key) => key + ':' + value).join(';');
|
||||
},
|
||||
deserializeTabOptions(serializedOptions) {
|
||||
return _.object(_.map((serializedOptions || '').split(';'), (option) => option.split(':')));
|
||||
},
|
||||
getNodeListFromTabOptions(options) {
|
||||
var nodeIds = utils.deserializeTabOptions(options.screenOptions[0]).nodes,
|
||||
ids = nodeIds ? nodeIds.split(',').map((id) => parseInt(id, 10)) : [],
|
||||
nodes = new models.Nodes(options.cluster.get('nodes').getByIds(ids));
|
||||
if (nodes.length == ids.length) return nodes;
|
||||
},
|
||||
renderMultilineText(text) {
|
||||
if (!text) return null;
|
||||
return <div>{text.split('\n').map((str, index) => <p key={index}>{str}</p>)}</div>;
|
||||
},
|
||||
linebreaks(text) {
|
||||
return text.replace(/\n/g, '<br/>');
|
||||
},
|
||||
composeLink(url) {
|
||||
return '<a target="_blank" href="' + url + '">' + url + '</a>';
|
||||
},
|
||||
urlify(text) {
|
||||
return utils.linebreaks(text).replace(new RegExp(utils.regexes.url.source, 'g'), utils.composeLink);
|
||||
},
|
||||
composeList(value) {
|
||||
return _.isUndefined(value) ? [] : _.isArray(value) ? value : [value];
|
||||
},
|
||||
// FIXME(vkramskikh): moved here from healthcheck_tab to make testable
|
||||
highlightTestStep(text, step) {
|
||||
return text.replace(new RegExp('(^|\\s*)(' + step + '\\.[\\s\\S]*?)(\\s*\\d+\\.|$)'), '$1<b>$2</b>$3');
|
||||
},
|
||||
classNames: classNames,
|
||||
parseModelPath(path, models) {
|
||||
var modelPath = new ModelPath(path);
|
||||
modelPath.setModel(models);
|
||||
return modelPath;
|
||||
},
|
||||
evaluateExpression(expression, models, options) {
|
||||
var compiledExpression = new Expression(expression, models, options),
|
||||
value = compiledExpression.evaluate();
|
||||
return {
|
||||
value: value,
|
||||
modelPaths: compiledExpression.modelPaths
|
||||
};
|
||||
},
|
||||
expandRestriction(restriction) {
|
||||
var result = {
|
||||
action: 'disable',
|
||||
message: null
|
||||
};
|
||||
if (_.isString(restriction)) {
|
||||
result.condition = restriction;
|
||||
} else if (_.isPlainObject(restriction)) {
|
||||
if (_.has(restriction, 'condition')) {
|
||||
_.extend(result, restriction);
|
||||
} else {
|
||||
result.condition = _.keys(restriction)[0];
|
||||
result.message = _.values(restriction)[0];
|
||||
}
|
||||
} else {
|
||||
throw new Error('Invalid restriction format');
|
||||
}
|
||||
return result;
|
||||
},
|
||||
showErrorDialog(options) {
|
||||
options.message = options.response ? utils.getResponseText(options.response) :
|
||||
options.message || i18n('dialog.error_dialog.server_error');
|
||||
ErrorDialog.show(options);
|
||||
},
|
||||
showBandwidth(bandwidth) {
|
||||
bandwidth = parseInt(bandwidth, 10);
|
||||
if (!_.isNumber(bandwidth) || _.isNaN(bandwidth)) return i18n('common.not_available');
|
||||
return (bandwidth / 1000).toFixed(1) + ' Gbps';
|
||||
},
|
||||
showFrequency(frequency) {
|
||||
frequency = parseInt(frequency, 10);
|
||||
if (!_.isNumber(frequency) || _.isNaN(frequency)) return i18n('common.not_available');
|
||||
var base = 1000;
|
||||
var treshold = 1000;
|
||||
return (frequency >= treshold ? (frequency / base).toFixed(2) + ' GHz' : frequency + ' MHz');
|
||||
},
|
||||
showSize(bytes, treshold) {
|
||||
bytes = parseInt(bytes, 10);
|
||||
if (!_.isNumber(bytes) || _.isNaN(bytes)) return i18n('common.not_available');
|
||||
var base = 1024;
|
||||
treshold = treshold || 256;
|
||||
var units = ['byte', 'kb', 'mb', 'gb', 'tb'];
|
||||
var i, result, unit = 'tb';
|
||||
for (i = 0; i < units.length; i += 1) {
|
||||
result = bytes / Math.pow(base, i);
|
||||
if (result < treshold) {
|
||||
unit = units[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (result ? result.toFixed(1) : result) + ' ' + i18n('common.size.' + unit, {count: result});
|
||||
},
|
||||
showMemorySize(bytes) {
|
||||
return utils.showSize(bytes, 1024);
|
||||
},
|
||||
showDiskSize(value, power) {
|
||||
power = power || 0;
|
||||
return utils.showSize(value * Math.pow(1024, power));
|
||||
},
|
||||
calculateNetworkSize(cidr) {
|
||||
return Math.pow(2, 32 - parseInt(_.last(cidr.split('/')), 10));
|
||||
},
|
||||
formatNumber(n) {
|
||||
return String(n).replace(/\d/g, (c, i, a) => i > 0 && c !== '.' && (a.length - i) % 3 === 0 ? ',' + c : c);
|
||||
},
|
||||
floor(n, decimals) {
|
||||
return Math.floor(n * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
||||
},
|
||||
isNaturalNumber(n) {
|
||||
return _.isNumber(n) && n > 0 && n % 1 === 0;
|
||||
},
|
||||
validateVlan(vlan, forbiddenVlans, field, disallowNullValue) {
|
||||
var error = {};
|
||||
if ((_.isNull(vlan) && disallowNullValue) || (!_.isNull(vlan) && (!utils.isNaturalNumber(vlan) || vlan < 1 || vlan > 4094))) {
|
||||
error[field] = i18n('cluster_page.network_tab.validation.invalid_vlan');
|
||||
return error;
|
||||
}
|
||||
if (_.contains(_.compact(forbiddenVlans), vlan)) {
|
||||
error[field] = i18n('cluster_page.network_tab.validation.forbidden_vlan');
|
||||
}
|
||||
return error[field] ? error : {};
|
||||
},
|
||||
validateCidr(cidr, field) {
|
||||
field = field || 'cidr';
|
||||
var error = {}, match;
|
||||
if (_.isString(cidr)) {
|
||||
match = cidr.match(utils.regexes.cidr);
|
||||
if (match) {
|
||||
var prefix = parseInt(match[1], 10);
|
||||
if (prefix < 2) {
|
||||
error[field] = i18n('cluster_page.network_tab.validation.large_network');
|
||||
} else if (prefix > 30) {
|
||||
error[field] = i18n('cluster_page.network_tab.validation.small_network');
|
||||
}
|
||||
} else {
|
||||
error[field] = i18n('cluster_page.network_tab.validation.invalid_cidr');
|
||||
}
|
||||
} else {
|
||||
error[field] = i18n('cluster_page.network_tab.validation.invalid_cidr');
|
||||
}
|
||||
return error[field] ? error : {};
|
||||
},
|
||||
validateIP(ip) {
|
||||
return _.isString(ip) && !!ip.match(utils.regexes.ip);
|
||||
},
|
||||
validateIPRanges(ranges, cidr, existingRanges = [], warnings = {}) {
|
||||
var ipRangesErrors = [],
|
||||
ns = 'cluster_page.network_tab.validation.';
|
||||
_.defaults(warnings, {
|
||||
INVALID_IP: i18n(ns + 'invalid_ip'),
|
||||
DOES_NOT_MATCH_CIDR: i18n(ns + 'ip_does_not_match_cidr'),
|
||||
INVALID_IP_RANGE: i18n(ns + 'invalid_ip_range'),
|
||||
EMPTY_IP_RANGE: i18n(ns + 'empty_ip_range'),
|
||||
IP_RANGES_INTERSECTION: i18n(ns + 'ip_ranges_intersection')
|
||||
});
|
||||
|
||||
export default utils;
|
||||
if (_.any(ranges, (range) => _.compact(range).length)) {
|
||||
_.each(ranges, (range, index) => {
|
||||
if (_.any(range)) {
|
||||
var error = {};
|
||||
|
||||
if (!utils.validateIP(range[0])) {
|
||||
error.start = warnings.INVALID_IP;
|
||||
} else if (cidr && !utils.validateIpCorrespondsToCIDR(cidr, range[0])) {
|
||||
error.start = warnings.DOES_NOT_MATCH_CIDR;
|
||||
}
|
||||
|
||||
if (!utils.validateIP(range[1])) {
|
||||
error.end = warnings.INVALID_IP;
|
||||
} else if (cidr && !utils.validateIpCorrespondsToCIDR(cidr, range[1])) {
|
||||
error.end = warnings.DOES_NOT_MATCH_CIDR;
|
||||
}
|
||||
|
||||
if (_.isEmpty(error)) {
|
||||
if (IP.toLong(range[0]) > IP.toLong(range[1])) {
|
||||
error.start = error.end = warnings.INVALID_IP_RANGE;
|
||||
} else if (_.isUndefined(cidr)) {
|
||||
error.start = error.end = warnings.IP_RANGE_IS_NOT_IN_PUBLIC_CIDR;
|
||||
} else if (existingRanges.length) {
|
||||
var intersection = utils.checkIPRangesIntersection(range, existingRanges);
|
||||
if (intersection) {
|
||||
error.start = error.end = warnings.IP_RANGES_INTERSECTION + intersection.join(' - ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isEmpty(error)) {
|
||||
ipRangesErrors.push(_.extend(error, {index: index}));
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ipRangesErrors.push({
|
||||
index: 0,
|
||||
start: warnings.EMPTY_IP_RANGE,
|
||||
end: warnings.EMPTY_IP_RANGE
|
||||
});
|
||||
}
|
||||
return ipRangesErrors;
|
||||
},
|
||||
checkIPRangesIntersection([startIP, endIP], existingRanges) {
|
||||
var startIPInt = IP.toLong(startIP),
|
||||
endIPInt = IP.toLong(endIP);
|
||||
return _.find(existingRanges, ([ip1, ip2]) => IP.toLong(ip2) >= startIPInt && IP.toLong(ip1) <= endIPInt);
|
||||
},
|
||||
validateIpCorrespondsToCIDR(cidr, ip) {
|
||||
if (!cidr) return true;
|
||||
var networkData = IP.cidrSubnet(cidr),
|
||||
ipInt = IP.toLong(ip);
|
||||
return ipInt >= IP.toLong(networkData.firstAddress) && ipInt <= IP.toLong(networkData.lastAddress);
|
||||
},
|
||||
validateVlanRange(vlanStart, vlanEnd, vlan) {
|
||||
return vlan >= vlanStart && vlan <= vlanEnd;
|
||||
},
|
||||
getDefaultGatewayForCidr(cidr) {
|
||||
if (!_.isEmpty(utils.validateCidr(cidr))) return '';
|
||||
return IP.cidrSubnet(cidr).firstAddress;
|
||||
},
|
||||
getDefaultIPRangeForCidr(cidr, excludeGateway) {
|
||||
if (!_.isEmpty(utils.validateCidr(cidr))) return [['', '']];
|
||||
var networkData = IP.cidrSubnet(cidr);
|
||||
if (excludeGateway) {
|
||||
var startIPInt = IP.toLong(networkData.firstAddress);
|
||||
startIPInt++;
|
||||
return [[IP.fromLong(startIPInt), networkData.lastAddress]];
|
||||
}
|
||||
return [[networkData.firstAddress, networkData.lastAddress]];
|
||||
},
|
||||
sortEntryProperties(entry, sortOrder) {
|
||||
sortOrder = sortOrder || ['name'];
|
||||
var properties = _.keys(entry);
|
||||
return _.sortBy(properties, (property) => {
|
||||
var index = _.indexOf(sortOrder, property);
|
||||
return index == -1 ? properties.length : index;
|
||||
});
|
||||
},
|
||||
getResponseText(response, defaultText) {
|
||||
var serverErrorMessage = defaultText || i18n('dialog.error_dialog.server_error');
|
||||
var serverUnavailableMessage = i18n('dialog.error_dialog.server_unavailable');
|
||||
if (response && (!response.status || response.status >= 400)) {
|
||||
if (!response.status || response.status == 502) return serverUnavailableMessage;
|
||||
if (response.status == 500) return serverErrorMessage;
|
||||
// parsing new backend response format in responseText
|
||||
response = response.responseText || response;
|
||||
try {
|
||||
response = JSON.parse(response);
|
||||
return response.message || serverErrorMessage;
|
||||
} catch (exception) {
|
||||
return serverErrorMessage;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
},
|
||||
natsort(str1, str2, options = {}) {
|
||||
var {insensitive, desc} = options;
|
||||
naturalSort.insensitive = insensitive;
|
||||
return naturalSort(str1, str2) * (desc ? -1 : 1);
|
||||
},
|
||||
multiSort(model1, model2, attributes) {
|
||||
var result = utils.compare(model1, model2, attributes[0]);
|
||||
if (result === 0 && attributes.length > 1) {
|
||||
attributes.splice(0, 1);
|
||||
result = utils.multiSort(model1, model2, attributes);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
compare(model1, model2, options) {
|
||||
var getValue = function(model) {
|
||||
var attr = options.attr;
|
||||
return _.isFunction(model[attr]) ? model[attr]() : model.get(attr);
|
||||
};
|
||||
var value1 = getValue(model1),
|
||||
value2 = getValue(model2);
|
||||
if (_.isString(value1) && _.isString(value2)) {
|
||||
return utils.natsort(value1, value2, options);
|
||||
}
|
||||
var result;
|
||||
if (_.isNumber(value1) && _.isNumber(value2)) {
|
||||
result = value1 - value2;
|
||||
} else {
|
||||
result = value1 === value2 || !value1 && !value2 ? 0 : !value1 ? 1 : -1;
|
||||
}
|
||||
return options.desc ? -result : result;
|
||||
},
|
||||
composeDocumentationLink(link) {
|
||||
var isMirantisIso = _.contains(app.version.get('feature_groups'), 'mirantis'),
|
||||
release = app.version.get('release'),
|
||||
linkStart = isMirantisIso ? 'https://docs.mirantis.com/openstack/fuel/fuel-' :
|
||||
'https://docs.fuel-infra.org/openstack/fuel/fuel-';
|
||||
return linkStart + release + '/' + link;
|
||||
}
|
||||
};
|
||||
|
||||
export default utils;
|
||||
|
|
|
@ -21,87 +21,87 @@ import models from 'models';
|
|||
import {backboneMixin, pollingMixin} from 'component_mixins';
|
||||
import {ProgressBar, Table} from 'views/controls';
|
||||
|
||||
var CapacityPage = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin('capacityLog'),
|
||||
pollingMixin(2)
|
||||
],
|
||||
statics: {
|
||||
title: i18n('capacity_page.title'),
|
||||
navbarActiveElement: 'support',
|
||||
breadcrumbsPath: [['home', '#'], ['support', '#support'], 'capacity'],
|
||||
fetchData() {
|
||||
var task = new models.Task();
|
||||
return task.save({}, {url: '/api/capacity/', method: 'PUT'})
|
||||
.then(() => ({capacityLog: new models.CapacityLog()}));
|
||||
}
|
||||
},
|
||||
shouldDataBeFetched() {
|
||||
return this.props.capacityLog.isNew();
|
||||
},
|
||||
fetchData() {
|
||||
return this.props.capacityLog.fetch();
|
||||
},
|
||||
render() {
|
||||
var capacityLog = this.props.capacityLog;
|
||||
return (
|
||||
<div className='capacity-page'>
|
||||
<div className='page-title'>
|
||||
<h1 className='title'>{i18n('capacity_page.title')}</h1>
|
||||
</div>
|
||||
<div className='content-box'>
|
||||
{!capacityLog.isNew() ?
|
||||
<LicenseUsage capacityLog={capacityLog} />
|
||||
:
|
||||
<ProgressBar />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
var CapacityPage = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin('capacityLog'),
|
||||
pollingMixin(2)
|
||||
],
|
||||
statics: {
|
||||
title: i18n('capacity_page.title'),
|
||||
navbarActiveElement: 'support',
|
||||
breadcrumbsPath: [['home', '#'], ['support', '#support'], 'capacity'],
|
||||
fetchData() {
|
||||
var task = new models.Task();
|
||||
return task.save({}, {url: '/api/capacity/', method: 'PUT'})
|
||||
.then(() => ({capacityLog: new models.CapacityLog()}));
|
||||
}
|
||||
},
|
||||
shouldDataBeFetched() {
|
||||
return this.props.capacityLog.isNew();
|
||||
},
|
||||
fetchData() {
|
||||
return this.props.capacityLog.fetch();
|
||||
},
|
||||
render() {
|
||||
var capacityLog = this.props.capacityLog;
|
||||
return (
|
||||
<div className='capacity-page'>
|
||||
<div className='page-title'>
|
||||
<h1 className='title'>{i18n('capacity_page.title')}</h1>
|
||||
</div>
|
||||
<div className='content-box'>
|
||||
{!capacityLog.isNew() ?
|
||||
<LicenseUsage capacityLog={capacityLog} />
|
||||
:
|
||||
<ProgressBar />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var LicenseUsage = React.createClass({
|
||||
render() {
|
||||
var capacityReport = this.props.capacityLog.get('report'),
|
||||
tableClassName = 'capacity-audit-table',
|
||||
headClassName = 'name';
|
||||
return (
|
||||
<div>
|
||||
<h3>{i18n('capacity_page.license_usage')}</h3>
|
||||
<Table
|
||||
head={[{label: i18n('capacity_page.fuel_version'), className: headClassName},
|
||||
{label: i18n('capacity_page.fuel_uuid')}]}
|
||||
body={[[capacityReport.fuel_data.release, capacityReport.fuel_data.uuid]]}
|
||||
tableClassName={tableClassName}
|
||||
/>
|
||||
<Table
|
||||
head={[{label: i18n('capacity_page.env_name'), className: headClassName},
|
||||
{label: i18n('capacity_page.node_count')}]}
|
||||
body={_.map(capacityReport.environment_stats, _.values)}
|
||||
tableClassName={tableClassName}
|
||||
/>
|
||||
<Table
|
||||
head={[{label: i18n('capacity_page.total_number_alloc_nodes'), className: headClassName},
|
||||
{label: i18n('capacity_page.total_number_unalloc_nodes')}]}
|
||||
body={[[capacityReport.allocation_stats.allocated,
|
||||
capacityReport.allocation_stats.unallocated]]}
|
||||
tableClassName={tableClassName}
|
||||
/>
|
||||
<Table
|
||||
head={[{label: i18n('capacity_page.node_role'), className: headClassName},
|
||||
{label: i18n('capacity_page.nodes_with_config')}]}
|
||||
body={_.zip(_.keys(capacityReport.roles_stat),
|
||||
_.values(capacityReport.roles_stat))}
|
||||
tableClassName={tableClassName}
|
||||
/>
|
||||
<a href='/api/capacity/csv' target='_blank' className='btn btn-info'>
|
||||
<i className='glyphicon glyphicon-download-alt' />{' '}
|
||||
{i18n('capacity_page.download_report')}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
var LicenseUsage = React.createClass({
|
||||
render() {
|
||||
var capacityReport = this.props.capacityLog.get('report'),
|
||||
tableClassName = 'capacity-audit-table',
|
||||
headClassName = 'name';
|
||||
return (
|
||||
<div>
|
||||
<h3>{i18n('capacity_page.license_usage')}</h3>
|
||||
<Table
|
||||
head={[{label: i18n('capacity_page.fuel_version'), className: headClassName},
|
||||
{label: i18n('capacity_page.fuel_uuid')}]}
|
||||
body={[[capacityReport.fuel_data.release, capacityReport.fuel_data.uuid]]}
|
||||
tableClassName={tableClassName}
|
||||
/>
|
||||
<Table
|
||||
head={[{label: i18n('capacity_page.env_name'), className: headClassName},
|
||||
{label: i18n('capacity_page.node_count')}]}
|
||||
body={_.map(capacityReport.environment_stats, _.values)}
|
||||
tableClassName={tableClassName}
|
||||
/>
|
||||
<Table
|
||||
head={[{label: i18n('capacity_page.total_number_alloc_nodes'), className: headClassName},
|
||||
{label: i18n('capacity_page.total_number_unalloc_nodes')}]}
|
||||
body={[[capacityReport.allocation_stats.allocated,
|
||||
capacityReport.allocation_stats.unallocated]]}
|
||||
tableClassName={tableClassName}
|
||||
/>
|
||||
<Table
|
||||
head={[{label: i18n('capacity_page.node_role'), className: headClassName},
|
||||
{label: i18n('capacity_page.nodes_with_config')}]}
|
||||
body={_.zip(_.keys(capacityReport.roles_stat),
|
||||
_.values(capacityReport.roles_stat))}
|
||||
tableClassName={tableClassName}
|
||||
/>
|
||||
<a href='/api/capacity/csv' target='_blank' className='btn btn-info'>
|
||||
<i className='glyphicon glyphicon-download-alt' />{' '}
|
||||
{i18n('capacity_page.download_report')}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default CapacityPage;
|
||||
export default CapacityPage;
|
||||
|
|
|
@ -29,296 +29,296 @@ import LogsTab from 'views/cluster_page_tabs/logs_tab';
|
|||
import HealthCheckTab from 'views/cluster_page_tabs/healthcheck_tab';
|
||||
import {VmWareTab, VmWareModels} from 'plugins/vmware/vmware';
|
||||
|
||||
var ClusterPage = React.createClass({
|
||||
mixins: [
|
||||
pollingMixin(5),
|
||||
backboneMixin('cluster', 'change:name change:is_customized change:release'),
|
||||
backboneMixin({
|
||||
modelOrCollection: (props) => props.cluster.get('nodes')
|
||||
}),
|
||||
backboneMixin({
|
||||
modelOrCollection: (props) => props.cluster.get('tasks'),
|
||||
renderOn: 'update change'
|
||||
}),
|
||||
dispatcherMixin('networkConfigurationUpdated', 'removeFinishedNetworkTasks'),
|
||||
dispatcherMixin('deploymentTasksUpdated', 'removeFinishedDeploymentTasks'),
|
||||
dispatcherMixin('deploymentTaskStarted', function() {
|
||||
this.refreshCluster().always(this.startPolling);
|
||||
}),
|
||||
dispatcherMixin('networkVerificationTaskStarted', function() {
|
||||
this.startPolling();
|
||||
}),
|
||||
dispatcherMixin('deploymentTaskFinished', function() {
|
||||
this.refreshCluster().always(() => dispatcher.trigger('updateNotifications'));
|
||||
})
|
||||
],
|
||||
statics: {
|
||||
navbarActiveElement: 'clusters',
|
||||
breadcrumbsPath(pageOptions) {
|
||||
var cluster = pageOptions.cluster,
|
||||
tabOptions = pageOptions.tabOptions[0],
|
||||
addScreenBreadcrumb = tabOptions && tabOptions.match(/^(?!list$)\w+$/),
|
||||
breadcrumbs = [
|
||||
['home', '#'],
|
||||
['environments', '#clusters'],
|
||||
[cluster.get('name'), '#cluster/' + cluster.get('id'), {skipTranslation: true}],
|
||||
[i18n('cluster_page.tabs.' + pageOptions.activeTab), '#cluster/' + cluster.get('id') + '/' + pageOptions.activeTab, {active: !addScreenBreadcrumb}]
|
||||
];
|
||||
if (addScreenBreadcrumb) {
|
||||
breadcrumbs.push([i18n('cluster_page.nodes_tab.breadcrumbs.' + tabOptions), null, {active: true}]);
|
||||
}
|
||||
return breadcrumbs;
|
||||
},
|
||||
title(pageOptions) {
|
||||
return pageOptions.cluster.get('name');
|
||||
},
|
||||
getTabs() {
|
||||
return [
|
||||
{url: 'dashboard', tab: DashboardTab},
|
||||
{url: 'nodes', tab: NodesTab},
|
||||
{url: 'network', tab: NetworkTab},
|
||||
{url: 'settings', tab: SettingsTab},
|
||||
{url: 'vmware', tab: VmWareTab},
|
||||
{url: 'logs', tab: LogsTab},
|
||||
{url: 'healthcheck', tab: HealthCheckTab}
|
||||
];
|
||||
},
|
||||
fetchData(id, activeTab, ...tabOptions) {
|
||||
var cluster, promise, currentClusterId;
|
||||
var nodeNetworkGroups = app.nodeNetworkGroups;
|
||||
var tab = _.find(this.getTabs(), {url: activeTab}).tab;
|
||||
try {
|
||||
currentClusterId = app.page.props.cluster.id;
|
||||
} catch (ignore) {}
|
||||
var ClusterPage = React.createClass({
|
||||
mixins: [
|
||||
pollingMixin(5),
|
||||
backboneMixin('cluster', 'change:name change:is_customized change:release'),
|
||||
backboneMixin({
|
||||
modelOrCollection: (props) => props.cluster.get('nodes')
|
||||
}),
|
||||
backboneMixin({
|
||||
modelOrCollection: (props) => props.cluster.get('tasks'),
|
||||
renderOn: 'update change'
|
||||
}),
|
||||
dispatcherMixin('networkConfigurationUpdated', 'removeFinishedNetworkTasks'),
|
||||
dispatcherMixin('deploymentTasksUpdated', 'removeFinishedDeploymentTasks'),
|
||||
dispatcherMixin('deploymentTaskStarted', function() {
|
||||
this.refreshCluster().always(this.startPolling);
|
||||
}),
|
||||
dispatcherMixin('networkVerificationTaskStarted', function() {
|
||||
this.startPolling();
|
||||
}),
|
||||
dispatcherMixin('deploymentTaskFinished', function() {
|
||||
this.refreshCluster().always(() => dispatcher.trigger('updateNotifications'));
|
||||
})
|
||||
],
|
||||
statics: {
|
||||
navbarActiveElement: 'clusters',
|
||||
breadcrumbsPath(pageOptions) {
|
||||
var cluster = pageOptions.cluster,
|
||||
tabOptions = pageOptions.tabOptions[0],
|
||||
addScreenBreadcrumb = tabOptions && tabOptions.match(/^(?!list$)\w+$/),
|
||||
breadcrumbs = [
|
||||
['home', '#'],
|
||||
['environments', '#clusters'],
|
||||
[cluster.get('name'), '#cluster/' + cluster.get('id'), {skipTranslation: true}],
|
||||
[i18n('cluster_page.tabs.' + pageOptions.activeTab), '#cluster/' + cluster.get('id') + '/' + pageOptions.activeTab, {active: !addScreenBreadcrumb}]
|
||||
];
|
||||
if (addScreenBreadcrumb) {
|
||||
breadcrumbs.push([i18n('cluster_page.nodes_tab.breadcrumbs.' + tabOptions), null, {active: true}]);
|
||||
}
|
||||
return breadcrumbs;
|
||||
},
|
||||
title(pageOptions) {
|
||||
return pageOptions.cluster.get('name');
|
||||
},
|
||||
getTabs() {
|
||||
return [
|
||||
{url: 'dashboard', tab: DashboardTab},
|
||||
{url: 'nodes', tab: NodesTab},
|
||||
{url: 'network', tab: NetworkTab},
|
||||
{url: 'settings', tab: SettingsTab},
|
||||
{url: 'vmware', tab: VmWareTab},
|
||||
{url: 'logs', tab: LogsTab},
|
||||
{url: 'healthcheck', tab: HealthCheckTab}
|
||||
];
|
||||
},
|
||||
fetchData(id, activeTab, ...tabOptions) {
|
||||
var cluster, promise, currentClusterId;
|
||||
var nodeNetworkGroups = app.nodeNetworkGroups;
|
||||
var tab = _.find(this.getTabs(), {url: activeTab}).tab;
|
||||
try {
|
||||
currentClusterId = app.page.props.cluster.id;
|
||||
} catch (ignore) {}
|
||||
|
||||
if (currentClusterId == id) {
|
||||
// just another tab has been chosen, do not load cluster again
|
||||
cluster = app.page.props.cluster;
|
||||
promise = tab.fetchData ? tab.fetchData({cluster: cluster, tabOptions: tabOptions}) : $.Deferred().resolve();
|
||||
} else {
|
||||
cluster = new models.Cluster({id: id});
|
||||
if (currentClusterId == id) {
|
||||
// just another tab has been chosen, do not load cluster again
|
||||
cluster = app.page.props.cluster;
|
||||
promise = tab.fetchData ? tab.fetchData({cluster: cluster, tabOptions: tabOptions}) : $.Deferred().resolve();
|
||||
} else {
|
||||
cluster = new models.Cluster({id: id});
|
||||
|
||||
var settings = new models.Settings();
|
||||
settings.url = _.result(cluster, 'url') + '/attributes';
|
||||
cluster.set({settings: settings});
|
||||
var settings = new models.Settings();
|
||||
settings.url = _.result(cluster, 'url') + '/attributes';
|
||||
cluster.set({settings: settings});
|
||||
|
||||
var roles = new models.Roles();
|
||||
roles.url = _.result(cluster, 'url') + '/roles';
|
||||
cluster.set({roles: roles});
|
||||
var roles = new models.Roles();
|
||||
roles.url = _.result(cluster, 'url') + '/roles';
|
||||
cluster.set({roles: roles});
|
||||
|
||||
var pluginLinks = new models.PluginLinks();
|
||||
pluginLinks.url = _.result(cluster, 'url') + '/plugin_links';
|
||||
cluster.set({pluginLinks: pluginLinks});
|
||||
var pluginLinks = new models.PluginLinks();
|
||||
pluginLinks.url = _.result(cluster, 'url') + '/plugin_links';
|
||||
cluster.set({pluginLinks: pluginLinks});
|
||||
|
||||
cluster.get('nodes').fetch = function(options) {
|
||||
return this.constructor.__super__.fetch.call(this, _.extend({data: {cluster_id: id}}, options));
|
||||
};
|
||||
promise = $.when(
|
||||
cluster.fetch(),
|
||||
cluster.get('settings').fetch(),
|
||||
cluster.get('roles').fetch(),
|
||||
cluster.get('pluginLinks').fetch({cache: true}),
|
||||
cluster.fetchRelated('nodes'),
|
||||
cluster.fetchRelated('tasks'),
|
||||
nodeNetworkGroups.fetch({cache: true})
|
||||
)
|
||||
.then(() => {
|
||||
var networkConfiguration = new models.NetworkConfiguration();
|
||||
networkConfiguration.url = _.result(cluster, 'url') + '/network_configuration/' + cluster.get('net_provider');
|
||||
cluster.set({
|
||||
networkConfiguration: networkConfiguration,
|
||||
release: new models.Release({id: cluster.get('release_id')})
|
||||
});
|
||||
return $.when(cluster.get('networkConfiguration').fetch(), cluster.get('release').fetch());
|
||||
})
|
||||
.then(() => {
|
||||
var useVcenter = cluster.get('settings').get('common.use_vcenter.value');
|
||||
if (!useVcenter) {
|
||||
return true;
|
||||
}
|
||||
var vcenter = new VmWareModels.VCenter({id: id});
|
||||
cluster.set({vcenter: vcenter});
|
||||
return vcenter.fetch();
|
||||
})
|
||||
.then(() => {
|
||||
return tab.fetchData ? tab.fetchData({cluster: cluster, tabOptions: tabOptions}) : $.Deferred().resolve();
|
||||
});
|
||||
}
|
||||
return promise.then((data) => {
|
||||
return {
|
||||
cluster: cluster,
|
||||
nodeNetworkGroups: nodeNetworkGroups,
|
||||
activeTab: activeTab,
|
||||
tabOptions: tabOptions,
|
||||
tabData: data
|
||||
};
|
||||
});
|
||||
cluster.get('nodes').fetch = function(options) {
|
||||
return this.constructor.__super__.fetch.call(this, _.extend({data: {cluster_id: id}}, options));
|
||||
};
|
||||
promise = $.when(
|
||||
cluster.fetch(),
|
||||
cluster.get('settings').fetch(),
|
||||
cluster.get('roles').fetch(),
|
||||
cluster.get('pluginLinks').fetch({cache: true}),
|
||||
cluster.fetchRelated('nodes'),
|
||||
cluster.fetchRelated('tasks'),
|
||||
nodeNetworkGroups.fetch({cache: true})
|
||||
)
|
||||
.then(() => {
|
||||
var networkConfiguration = new models.NetworkConfiguration();
|
||||
networkConfiguration.url = _.result(cluster, 'url') + '/network_configuration/' + cluster.get('net_provider');
|
||||
cluster.set({
|
||||
networkConfiguration: networkConfiguration,
|
||||
release: new models.Release({id: cluster.get('release_id')})
|
||||
});
|
||||
return $.when(cluster.get('networkConfiguration').fetch(), cluster.get('release').fetch());
|
||||
})
|
||||
.then(() => {
|
||||
var useVcenter = cluster.get('settings').get('common.use_vcenter.value');
|
||||
if (!useVcenter) {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
getDefaultProps() {
|
||||
return {
|
||||
defaultLogLevel: 'INFO'
|
||||
};
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
activeSettingsSectionName: this.pickDefaultSettingGroup(),
|
||||
activeNetworkSectionName: this.props.nodeNetworkGroups.find({is_default: true}).get('name'),
|
||||
selectedNodeIds: {},
|
||||
selectedLogs: {type: 'local', node: null, source: 'app', level: this.props.defaultLogLevel}
|
||||
};
|
||||
},
|
||||
removeFinishedNetworkTasks(callback) {
|
||||
var request = this.removeFinishedTasks(this.props.cluster.tasks({group: 'network'}));
|
||||
if (callback) request.always(callback);
|
||||
return request;
|
||||
},
|
||||
removeFinishedDeploymentTasks() {
|
||||
return this.removeFinishedTasks(this.props.cluster.tasks({group: 'deployment'}));
|
||||
},
|
||||
removeFinishedTasks(tasks) {
|
||||
var requests = [];
|
||||
_.each(tasks, function(task) {
|
||||
if (task.match({active: false})) {
|
||||
this.props.cluster.get('tasks').remove(task);
|
||||
requests.push(task.destroy({silent: true}));
|
||||
}
|
||||
}, this);
|
||||
return $.when(...requests);
|
||||
},
|
||||
shouldDataBeFetched() {
|
||||
return this.props.cluster.task({group: ['deployment', 'network'], active: true});
|
||||
},
|
||||
fetchData() {
|
||||
var task = this.props.cluster.task({group: 'deployment', active: true});
|
||||
if (task) {
|
||||
return task.fetch()
|
||||
.done(() => {
|
||||
if (task.match({active: false})) dispatcher.trigger('deploymentTaskFinished');
|
||||
})
|
||||
.then(() =>
|
||||
this.props.cluster.fetchRelated('nodes')
|
||||
);
|
||||
} else {
|
||||
task = this.props.cluster.task({name: 'verify_networks', active: true});
|
||||
return task ? task.fetch() : $.Deferred().resolve();
|
||||
}
|
||||
},
|
||||
refreshCluster() {
|
||||
return $.when(
|
||||
this.props.cluster.fetch(),
|
||||
this.props.cluster.fetchRelated('nodes'),
|
||||
this.props.cluster.fetchRelated('tasks'),
|
||||
this.props.cluster.get('pluginLinks').fetch()
|
||||
);
|
||||
},
|
||||
componentWillMount() {
|
||||
this.props.cluster.on('change:release_id', function() {
|
||||
var release = new models.Release({id: this.props.cluster.get('release_id')});
|
||||
release.fetch().done(() => {
|
||||
this.props.cluster.set({release: release});
|
||||
});
|
||||
}, this);
|
||||
this.updateLogSettings();
|
||||
},
|
||||
componentWillReceiveProps(newProps) {
|
||||
this.updateLogSettings(newProps);
|
||||
},
|
||||
updateLogSettings(props) {
|
||||
props = props || this.props;
|
||||
// FIXME: the following logs-related logic should be moved to Logs tab code
|
||||
// to keep parent component tightly coupled to its children
|
||||
if (props.activeTab == 'logs') {
|
||||
var selectedLogs;
|
||||
if (props.tabOptions[0]) {
|
||||
selectedLogs = utils.deserializeTabOptions(_.compact(props.tabOptions).join('/'));
|
||||
selectedLogs.level = selectedLogs.level ? selectedLogs.level.toUpperCase() : props.defaultLogLevel;
|
||||
this.setState({selectedLogs: selectedLogs});
|
||||
}
|
||||
}
|
||||
},
|
||||
changeLogSelection(selectedLogs) {
|
||||
this.setState({selectedLogs: selectedLogs});
|
||||
},
|
||||
getAvailableTabs(cluster) {
|
||||
return _.filter(this.constructor.getTabs(),
|
||||
(tabData) => !tabData.tab.isVisible || tabData.tab.isVisible(cluster));
|
||||
},
|
||||
pickDefaultSettingGroup() {
|
||||
return _.first(this.props.cluster.get('settings').getGroupList());
|
||||
},
|
||||
setActiveSettingsGroupName(value) {
|
||||
if (_.isUndefined(value)) value = this.pickDefaultSettingGroup();
|
||||
this.setState({activeSettingsSectionName: value});
|
||||
},
|
||||
var vcenter = new VmWareModels.VCenter({id: id});
|
||||
cluster.set({vcenter: vcenter});
|
||||
return vcenter.fetch();
|
||||
})
|
||||
.then(() => {
|
||||
return tab.fetchData ? tab.fetchData({cluster: cluster, tabOptions: tabOptions}) : $.Deferred().resolve();
|
||||
});
|
||||
}
|
||||
return promise.then((data) => {
|
||||
return {
|
||||
cluster: cluster,
|
||||
nodeNetworkGroups: nodeNetworkGroups,
|
||||
activeTab: activeTab,
|
||||
tabOptions: tabOptions,
|
||||
tabData: data
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
getDefaultProps() {
|
||||
return {
|
||||
defaultLogLevel: 'INFO'
|
||||
};
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
activeSettingsSectionName: this.pickDefaultSettingGroup(),
|
||||
activeNetworkSectionName: this.props.nodeNetworkGroups.find({is_default: true}).get('name'),
|
||||
selectedNodeIds: {},
|
||||
selectedLogs: {type: 'local', node: null, source: 'app', level: this.props.defaultLogLevel}
|
||||
};
|
||||
},
|
||||
removeFinishedNetworkTasks(callback) {
|
||||
var request = this.removeFinishedTasks(this.props.cluster.tasks({group: 'network'}));
|
||||
if (callback) request.always(callback);
|
||||
return request;
|
||||
},
|
||||
removeFinishedDeploymentTasks() {
|
||||
return this.removeFinishedTasks(this.props.cluster.tasks({group: 'deployment'}));
|
||||
},
|
||||
removeFinishedTasks(tasks) {
|
||||
var requests = [];
|
||||
_.each(tasks, function(task) {
|
||||
if (task.match({active: false})) {
|
||||
this.props.cluster.get('tasks').remove(task);
|
||||
requests.push(task.destroy({silent: true}));
|
||||
}
|
||||
}, this);
|
||||
return $.when(...requests);
|
||||
},
|
||||
shouldDataBeFetched() {
|
||||
return this.props.cluster.task({group: ['deployment', 'network'], active: true});
|
||||
},
|
||||
fetchData() {
|
||||
var task = this.props.cluster.task({group: 'deployment', active: true});
|
||||
if (task) {
|
||||
return task.fetch()
|
||||
.done(() => {
|
||||
if (task.match({active: false})) dispatcher.trigger('deploymentTaskFinished');
|
||||
})
|
||||
.then(() =>
|
||||
this.props.cluster.fetchRelated('nodes')
|
||||
);
|
||||
} else {
|
||||
task = this.props.cluster.task({name: 'verify_networks', active: true});
|
||||
return task ? task.fetch() : $.Deferred().resolve();
|
||||
}
|
||||
},
|
||||
refreshCluster() {
|
||||
return $.when(
|
||||
this.props.cluster.fetch(),
|
||||
this.props.cluster.fetchRelated('nodes'),
|
||||
this.props.cluster.fetchRelated('tasks'),
|
||||
this.props.cluster.get('pluginLinks').fetch()
|
||||
);
|
||||
},
|
||||
componentWillMount() {
|
||||
this.props.cluster.on('change:release_id', function() {
|
||||
var release = new models.Release({id: this.props.cluster.get('release_id')});
|
||||
release.fetch().done(() => {
|
||||
this.props.cluster.set({release: release});
|
||||
});
|
||||
}, this);
|
||||
this.updateLogSettings();
|
||||
},
|
||||
componentWillReceiveProps(newProps) {
|
||||
this.updateLogSettings(newProps);
|
||||
},
|
||||
updateLogSettings(props) {
|
||||
props = props || this.props;
|
||||
// FIXME: the following logs-related logic should be moved to Logs tab code
|
||||
// to keep parent component tightly coupled to its children
|
||||
if (props.activeTab == 'logs') {
|
||||
var selectedLogs;
|
||||
if (props.tabOptions[0]) {
|
||||
selectedLogs = utils.deserializeTabOptions(_.compact(props.tabOptions).join('/'));
|
||||
selectedLogs.level = selectedLogs.level ? selectedLogs.level.toUpperCase() : props.defaultLogLevel;
|
||||
this.setState({selectedLogs: selectedLogs});
|
||||
}
|
||||
}
|
||||
},
|
||||
changeLogSelection(selectedLogs) {
|
||||
this.setState({selectedLogs: selectedLogs});
|
||||
},
|
||||
getAvailableTabs(cluster) {
|
||||
return _.filter(this.constructor.getTabs(),
|
||||
(tabData) => !tabData.tab.isVisible || tabData.tab.isVisible(cluster));
|
||||
},
|
||||
pickDefaultSettingGroup() {
|
||||
return _.first(this.props.cluster.get('settings').getGroupList());
|
||||
},
|
||||
setActiveSettingsGroupName(value) {
|
||||
if (_.isUndefined(value)) value = this.pickDefaultSettingGroup();
|
||||
this.setState({activeSettingsSectionName: value});
|
||||
},
|
||||
|
||||
setActiveNetworkSectionName(name) {
|
||||
this.setState({activeNetworkSectionName: name});
|
||||
},
|
||||
selectNodes(ids, checked) {
|
||||
if (ids && ids.length) {
|
||||
var nodeSelection = this.state.selectedNodeIds;
|
||||
_.each(ids, (id) => {
|
||||
if (checked) {
|
||||
nodeSelection[id] = true;
|
||||
} else {
|
||||
delete nodeSelection[id];
|
||||
}
|
||||
});
|
||||
this.setState({selectedNodeIds: nodeSelection});
|
||||
} else {
|
||||
this.setState({selectedNodeIds: {}});
|
||||
}
|
||||
},
|
||||
render() {
|
||||
var cluster = this.props.cluster,
|
||||
availableTabs = this.getAvailableTabs(cluster),
|
||||
tabUrls = _.pluck(availableTabs, 'url'),
|
||||
tab = _.find(availableTabs, {url: this.props.activeTab});
|
||||
if (!tab) return null;
|
||||
var Tab = tab.tab;
|
||||
|
||||
return (
|
||||
<div className='cluster-page' key={cluster.id}>
|
||||
<div className='page-title'>
|
||||
<h1 className='title'>
|
||||
{cluster.get('name')}
|
||||
<div className='title-node-count'>({i18n('common.node', {count: cluster.get('nodes').length})})</div>
|
||||
</h1>
|
||||
</div>
|
||||
<div className='tabs-box'>
|
||||
<div className='tabs'>
|
||||
{tabUrls.map(function(url) {
|
||||
return (
|
||||
<a
|
||||
key={url}
|
||||
className={url + ' ' + utils.classNames({'cluster-tab': true, active: this.props.activeTab == url})}
|
||||
href={'#cluster/' + cluster.id + '/' + url}
|
||||
>
|
||||
<div className='icon'></div>
|
||||
<div className='label'>{i18n('cluster_page.tabs.' + url)}</div>
|
||||
</a>
|
||||
);
|
||||
}, this)}
|
||||
</div>
|
||||
</div>
|
||||
<div key={tab.url + cluster.id} className={'content-box tab-content ' + tab.url + '-tab'}>
|
||||
<Tab
|
||||
ref='tab'
|
||||
cluster={cluster}
|
||||
nodeNetworkGroups={this.props.nodeNetworkGroups}
|
||||
tabOptions={this.props.tabOptions}
|
||||
setActiveSettingsGroupName={this.setActiveSettingsGroupName}
|
||||
setActiveNetworkSectionName={this.setActiveNetworkSectionName}
|
||||
selectNodes={this.selectNodes}
|
||||
changeLogSelection={this.changeLogSelection}
|
||||
{...this.state}
|
||||
{...this.props.tabData}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
setActiveNetworkSectionName(name) {
|
||||
this.setState({activeNetworkSectionName: name});
|
||||
},
|
||||
selectNodes(ids, checked) {
|
||||
if (ids && ids.length) {
|
||||
var nodeSelection = this.state.selectedNodeIds;
|
||||
_.each(ids, (id) => {
|
||||
if (checked) {
|
||||
nodeSelection[id] = true;
|
||||
} else {
|
||||
delete nodeSelection[id];
|
||||
}
|
||||
});
|
||||
});
|
||||
this.setState({selectedNodeIds: nodeSelection});
|
||||
} else {
|
||||
this.setState({selectedNodeIds: {}});
|
||||
}
|
||||
},
|
||||
render() {
|
||||
var cluster = this.props.cluster,
|
||||
availableTabs = this.getAvailableTabs(cluster),
|
||||
tabUrls = _.pluck(availableTabs, 'url'),
|
||||
tab = _.find(availableTabs, {url: this.props.activeTab});
|
||||
if (!tab) return null;
|
||||
var Tab = tab.tab;
|
||||
|
||||
export default ClusterPage;
|
||||
return (
|
||||
<div className='cluster-page' key={cluster.id}>
|
||||
<div className='page-title'>
|
||||
<h1 className='title'>
|
||||
{cluster.get('name')}
|
||||
<div className='title-node-count'>({i18n('common.node', {count: cluster.get('nodes').length})})</div>
|
||||
</h1>
|
||||
</div>
|
||||
<div className='tabs-box'>
|
||||
<div className='tabs'>
|
||||
{tabUrls.map(function(url) {
|
||||
return (
|
||||
<a
|
||||
key={url}
|
||||
className={url + ' ' + utils.classNames({'cluster-tab': true, active: this.props.activeTab == url})}
|
||||
href={'#cluster/' + cluster.id + '/' + url}
|
||||
>
|
||||
<div className='icon'></div>
|
||||
<div className='label'>{i18n('cluster_page.tabs.' + url)}</div>
|
||||
</a>
|
||||
);
|
||||
}, this)}
|
||||
</div>
|
||||
</div>
|
||||
<div key={tab.url + cluster.id} className={'content-box tab-content ' + tab.url + '-tab'}>
|
||||
<Tab
|
||||
ref='tab'
|
||||
cluster={cluster}
|
||||
nodeNetworkGroups={this.props.nodeNetworkGroups}
|
||||
tabOptions={this.props.tabOptions}
|
||||
setActiveSettingsGroupName={this.setActiveSettingsGroupName}
|
||||
setActiveNetworkSectionName={this.setActiveNetworkSectionName}
|
||||
selectNodes={this.selectNodes}
|
||||
changeLogSelection={this.changeLogSelection}
|
||||
{...this.state}
|
||||
{...this.props.tabData}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default ClusterPage;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -23,423 +23,423 @@ import models from 'models';
|
|||
import {Input} from 'views/controls';
|
||||
import {backboneMixin, pollingMixin} from 'component_mixins';
|
||||
|
||||
var HealthCheckTab = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin({
|
||||
modelOrCollection: (props) => props.cluster.get('tasks'),
|
||||
renderOn: 'update change:status'
|
||||
}),
|
||||
backboneMixin('cluster', 'change:status')
|
||||
],
|
||||
statics: {
|
||||
fetchData(options) {
|
||||
if (!options.cluster.get('ostf')) {
|
||||
var ostf = {},
|
||||
clusterId = options.cluster.id;
|
||||
ostf.testsets = new models.TestSets();
|
||||
ostf.testsets.url = _.result(ostf.testsets, 'url') + '/' + clusterId;
|
||||
ostf.tests = new models.Tests();
|
||||
ostf.tests.url = _.result(ostf.tests, 'url') + '/' + clusterId;
|
||||
ostf.testruns = new models.TestRuns();
|
||||
ostf.testruns.url = _.result(ostf.testruns, 'url') + '/last/' + clusterId;
|
||||
return $.when(ostf.testsets.fetch(), ostf.tests.fetch(), ostf.testruns.fetch()).then(() => {
|
||||
options.cluster.set({ostf: ostf});
|
||||
return {};
|
||||
}, () => $.Deferred().resolve()
|
||||
);
|
||||
}
|
||||
return $.Deferred().resolve();
|
||||
}
|
||||
},
|
||||
render() {
|
||||
var ostf = this.props.cluster.get('ostf');
|
||||
return (
|
||||
<div className='row'>
|
||||
<div className='title'>
|
||||
{i18n('cluster_page.healthcheck_tab.title')}
|
||||
</div>
|
||||
<div className='col-xs-12 content-elements'>
|
||||
{ostf ?
|
||||
<HealthcheckTabContent
|
||||
ref='content'
|
||||
testsets={ostf.testsets}
|
||||
tests={ostf.tests}
|
||||
testruns={ostf.testruns}
|
||||
cluster={this.props.cluster}
|
||||
/>
|
||||
:
|
||||
<div className='alert alert-danger'>
|
||||
{i18n('cluster_page.healthcheck_tab.not_available_alert')}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
var HealthCheckTab = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin({
|
||||
modelOrCollection: (props) => props.cluster.get('tasks'),
|
||||
renderOn: 'update change:status'
|
||||
}),
|
||||
backboneMixin('cluster', 'change:status')
|
||||
],
|
||||
statics: {
|
||||
fetchData(options) {
|
||||
if (!options.cluster.get('ostf')) {
|
||||
var ostf = {},
|
||||
clusterId = options.cluster.id;
|
||||
ostf.testsets = new models.TestSets();
|
||||
ostf.testsets.url = _.result(ostf.testsets, 'url') + '/' + clusterId;
|
||||
ostf.tests = new models.Tests();
|
||||
ostf.tests.url = _.result(ostf.tests, 'url') + '/' + clusterId;
|
||||
ostf.testruns = new models.TestRuns();
|
||||
ostf.testruns.url = _.result(ostf.testruns, 'url') + '/last/' + clusterId;
|
||||
return $.when(ostf.testsets.fetch(), ostf.tests.fetch(), ostf.testruns.fetch()).then(() => {
|
||||
options.cluster.set({ostf: ostf});
|
||||
return {};
|
||||
}, () => $.Deferred().resolve()
|
||||
);
|
||||
}
|
||||
return $.Deferred().resolve();
|
||||
}
|
||||
},
|
||||
render() {
|
||||
var ostf = this.props.cluster.get('ostf');
|
||||
return (
|
||||
<div className='row'>
|
||||
<div className='title'>
|
||||
{i18n('cluster_page.healthcheck_tab.title')}
|
||||
</div>
|
||||
<div className='col-xs-12 content-elements'>
|
||||
{ostf ?
|
||||
<HealthcheckTabContent
|
||||
ref='content'
|
||||
testsets={ostf.testsets}
|
||||
tests={ostf.tests}
|
||||
testruns={ostf.testruns}
|
||||
cluster={this.props.cluster}
|
||||
/>
|
||||
:
|
||||
<div className='alert alert-danger'>
|
||||
{i18n('cluster_page.healthcheck_tab.not_available_alert')}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var HealthcheckTabContent = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin('tests', 'update change'),
|
||||
backboneMixin('testsets', 'update change:checked'),
|
||||
backboneMixin('testruns', 'update change'),
|
||||
pollingMixin(3)
|
||||
],
|
||||
shouldDataBeFetched() {
|
||||
return this.props.testruns.any({status: 'running'});
|
||||
},
|
||||
fetchData() {
|
||||
return this.props.testruns.fetch();
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
actionInProgress: false,
|
||||
credentialsVisible: null,
|
||||
credentials: _.transform(this.props.cluster.get('settings').get('access'), (result, value, key) => {
|
||||
result[key] = value.value;
|
||||
})
|
||||
var HealthcheckTabContent = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin('tests', 'update change'),
|
||||
backboneMixin('testsets', 'update change:checked'),
|
||||
backboneMixin('testruns', 'update change'),
|
||||
pollingMixin(3)
|
||||
],
|
||||
shouldDataBeFetched() {
|
||||
return this.props.testruns.any({status: 'running'});
|
||||
},
|
||||
fetchData() {
|
||||
return this.props.testruns.fetch();
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
actionInProgress: false,
|
||||
credentialsVisible: null,
|
||||
credentials: _.transform(this.props.cluster.get('settings').get('access'), (result, value, key) => {
|
||||
result[key] = value.value;
|
||||
})
|
||||
};
|
||||
},
|
||||
isLocked() {
|
||||
var cluster = this.props.cluster;
|
||||
return cluster.get('status') != 'operational' || !!cluster.task({group: 'deployment', active: true});
|
||||
},
|
||||
getNumberOfCheckedTests() {
|
||||
return this.props.tests.where({checked: true}).length;
|
||||
},
|
||||
toggleCredentials() {
|
||||
this.setState({credentialsVisible: !this.state.credentialsVisible});
|
||||
},
|
||||
handleSelectAllClick(name, value) {
|
||||
this.props.tests.invoke('set', {checked: value});
|
||||
},
|
||||
handleInputChange(name, value) {
|
||||
var credentials = this.state.credentials;
|
||||
credentials[name] = value;
|
||||
this.setState({credentials: credentials});
|
||||
},
|
||||
runTests() {
|
||||
var testruns = new models.TestRuns(),
|
||||
oldTestruns = new models.TestRuns(),
|
||||
testsetIds = this.props.testsets.pluck('id');
|
||||
this.setState({actionInProgress: true});
|
||||
_.each(testsetIds, function(testsetId) {
|
||||
var testsToRun = _.pluck(this.props.tests.where({
|
||||
testset: testsetId,
|
||||
checked: true
|
||||
}), 'id');
|
||||
if (testsToRun.length) {
|
||||
var testrunConfig = {tests: testsToRun},
|
||||
addCredentials = (obj) => {
|
||||
obj.ostf_os_access_creds = {
|
||||
ostf_os_username: this.state.credentials.user,
|
||||
ostf_os_tenant_name: this.state.credentials.tenant,
|
||||
ostf_os_password: this.state.credentials.password
|
||||
};
|
||||
},
|
||||
isLocked() {
|
||||
var cluster = this.props.cluster;
|
||||
return cluster.get('status') != 'operational' || !!cluster.task({group: 'deployment', active: true});
|
||||
},
|
||||
getNumberOfCheckedTests() {
|
||||
return this.props.tests.where({checked: true}).length;
|
||||
},
|
||||
toggleCredentials() {
|
||||
this.setState({credentialsVisible: !this.state.credentialsVisible});
|
||||
},
|
||||
handleSelectAllClick(name, value) {
|
||||
this.props.tests.invoke('set', {checked: value});
|
||||
},
|
||||
handleInputChange(name, value) {
|
||||
var credentials = this.state.credentials;
|
||||
credentials[name] = value;
|
||||
this.setState({credentials: credentials});
|
||||
},
|
||||
runTests() {
|
||||
var testruns = new models.TestRuns(),
|
||||
oldTestruns = new models.TestRuns(),
|
||||
testsetIds = this.props.testsets.pluck('id');
|
||||
this.setState({actionInProgress: true});
|
||||
_.each(testsetIds, function(testsetId) {
|
||||
var testsToRun = _.pluck(this.props.tests.where({
|
||||
testset: testsetId,
|
||||
checked: true
|
||||
}), 'id');
|
||||
if (testsToRun.length) {
|
||||
var testrunConfig = {tests: testsToRun},
|
||||
addCredentials = (obj) => {
|
||||
obj.ostf_os_access_creds = {
|
||||
ostf_os_username: this.state.credentials.user,
|
||||
ostf_os_tenant_name: this.state.credentials.tenant,
|
||||
ostf_os_password: this.state.credentials.password
|
||||
};
|
||||
return obj;
|
||||
};
|
||||
return obj;
|
||||
};
|
||||
|
||||
if (this.props.testruns.where({testset: testsetId}).length) {
|
||||
_.each(this.props.testruns.where({testset: testsetId}), (testrun) => {
|
||||
_.extend(testrunConfig, addCredentials({
|
||||
id: testrun.id,
|
||||
status: 'restarted'
|
||||
}));
|
||||
oldTestruns.add(new models.TestRun(testrunConfig));
|
||||
}, this);
|
||||
} else {
|
||||
_.extend(testrunConfig, {
|
||||
testset: testsetId,
|
||||
metadata: addCredentials({
|
||||
config: {},
|
||||
cluster_id: this.props.cluster.id
|
||||
})
|
||||
});
|
||||
testruns.add(new models.TestRun(testrunConfig));
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
if (this.props.testruns.where({testset: testsetId}).length) {
|
||||
_.each(this.props.testruns.where({testset: testsetId}), (testrun) => {
|
||||
_.extend(testrunConfig, addCredentials({
|
||||
id: testrun.id,
|
||||
status: 'restarted'
|
||||
}));
|
||||
oldTestruns.add(new models.TestRun(testrunConfig));
|
||||
}, this);
|
||||
} else {
|
||||
_.extend(testrunConfig, {
|
||||
testset: testsetId,
|
||||
metadata: addCredentials({
|
||||
config: {},
|
||||
cluster_id: this.props.cluster.id
|
||||
})
|
||||
});
|
||||
testruns.add(new models.TestRun(testrunConfig));
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
|
||||
var requests = [];
|
||||
if (testruns.length) {
|
||||
requests.push(Backbone.sync('create', testruns));
|
||||
var requests = [];
|
||||
if (testruns.length) {
|
||||
requests.push(Backbone.sync('create', testruns));
|
||||
}
|
||||
if (oldTestruns.length) {
|
||||
requests.push(Backbone.sync('update', oldTestruns));
|
||||
}
|
||||
$.when(...requests)
|
||||
.done(() => {
|
||||
this.startPolling(true);
|
||||
})
|
||||
.fail((response) => {
|
||||
utils.showErrorDialog({response: response});
|
||||
})
|
||||
.always(() => {
|
||||
this.setState({actionInProgress: false});
|
||||
});
|
||||
},
|
||||
getActiveTestRuns() {
|
||||
return this.props.testruns.where({status: 'running'});
|
||||
},
|
||||
stopTests() {
|
||||
var testruns = new models.TestRuns(this.getActiveTestRuns());
|
||||
if (testruns.length) {
|
||||
this.setState({actionInProgress: true});
|
||||
testruns.invoke('set', {status: 'stopped'});
|
||||
testruns.toJSON = function() {
|
||||
return this.map((testrun) =>
|
||||
_.pick(testrun.attributes, 'id', 'status')
|
||||
);
|
||||
};
|
||||
Backbone.sync('update', testruns).done(() => {
|
||||
this.setState({actionInProgress: false});
|
||||
this.startPolling(true);
|
||||
});
|
||||
}
|
||||
},
|
||||
render() {
|
||||
var disabledState = this.isLocked(),
|
||||
hasRunningTests = !!this.props.testruns.where({status: 'running'}).length;
|
||||
return (
|
||||
<div>
|
||||
{!disabledState &&
|
||||
<div className='healthcheck-controls row well well-sm'>
|
||||
<div className='pull-left'>
|
||||
<Input
|
||||
type='checkbox'
|
||||
name='selectAll'
|
||||
onChange={this.handleSelectAllClick}
|
||||
checked={this.getNumberOfCheckedTests() == this.props.tests.length}
|
||||
disabled={hasRunningTests}
|
||||
label={i18n('common.select_all')}
|
||||
wrapperClassName='select-all'
|
||||
/>
|
||||
</div>
|
||||
{hasRunningTests ?
|
||||
(<button className='btn btn-danger stop-tests-btn pull-right'
|
||||
disabled={this.state.actionInProgress}
|
||||
onClick={this.stopTests}
|
||||
>
|
||||
{i18n('cluster_page.healthcheck_tab.stop_tests_button')}
|
||||
</button>)
|
||||
:
|
||||
(<button className='btn btn-success run-tests-btn pull-right'
|
||||
disabled={!this.getNumberOfCheckedTests() || this.state.actionInProgress}
|
||||
onClick={this.runTests}
|
||||
>
|
||||
{i18n('cluster_page.healthcheck_tab.run_tests_button')}
|
||||
</button>)
|
||||
}
|
||||
if (oldTestruns.length) {
|
||||
requests.push(Backbone.sync('update', oldTestruns));
|
||||
}
|
||||
$.when(...requests)
|
||||
.done(() => {
|
||||
this.startPolling(true);
|
||||
})
|
||||
.fail((response) => {
|
||||
utils.showErrorDialog({response: response});
|
||||
})
|
||||
.always(() => {
|
||||
this.setState({actionInProgress: false});
|
||||
});
|
||||
},
|
||||
getActiveTestRuns() {
|
||||
return this.props.testruns.where({status: 'running'});
|
||||
},
|
||||
stopTests() {
|
||||
var testruns = new models.TestRuns(this.getActiveTestRuns());
|
||||
if (testruns.length) {
|
||||
this.setState({actionInProgress: true});
|
||||
testruns.invoke('set', {status: 'stopped'});
|
||||
testruns.toJSON = function() {
|
||||
return this.map((testrun) =>
|
||||
_.pick(testrun.attributes, 'id', 'status')
|
||||
);
|
||||
};
|
||||
Backbone.sync('update', testruns).done(() => {
|
||||
this.setState({actionInProgress: false});
|
||||
this.startPolling(true);
|
||||
});
|
||||
}
|
||||
},
|
||||
render() {
|
||||
var disabledState = this.isLocked(),
|
||||
hasRunningTests = !!this.props.testruns.where({status: 'running'}).length;
|
||||
return (
|
||||
<button
|
||||
className='btn btn-default toggle-credentials pull-right'
|
||||
data-toggle='collapse'
|
||||
data-target='.credentials'
|
||||
onClick={this.toggleCredentials}
|
||||
>
|
||||
{i18n('cluster_page.healthcheck_tab.provide_credentials')}
|
||||
</button>
|
||||
|
||||
<HealthcheckCredentials
|
||||
credentials={this.state.credentials}
|
||||
onInputChange={this.handleInputChange}
|
||||
disabled={hasRunningTests}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
{(this.props.cluster.get('status') == 'new') &&
|
||||
<div className='alert alert-warning'>{i18n('cluster_page.healthcheck_tab.deploy_alert')}</div>
|
||||
}
|
||||
<div key='testsets'>
|
||||
{this.props.testsets.map((testset) => {
|
||||
return <TestSet
|
||||
key={testset.id}
|
||||
testset={testset}
|
||||
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}))}
|
||||
disabled={disabledState || hasRunningTests}
|
||||
/>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var HealthcheckCredentials = React.createClass({
|
||||
render() {
|
||||
var inputFields = ['user', 'password', 'tenant'];
|
||||
return (
|
||||
<div className='credentials collapse col-xs-12'>
|
||||
<div className='forms-box'>
|
||||
<div className='alert alert-warning'>
|
||||
{i18n('cluster_page.healthcheck_tab.credentials_description')}
|
||||
</div>
|
||||
{_.map(inputFields, function(name) {
|
||||
return (<Input
|
||||
key={name}
|
||||
type={(name == 'password') ? 'password' : 'text'}
|
||||
name={name}
|
||||
label={i18n('cluster_page.healthcheck_tab.' + name + '_label')}
|
||||
value={this.props.credentials[name]}
|
||||
onChange={this.props.onInputChange}
|
||||
toggleable={name == 'password'}
|
||||
description={i18n('cluster_page.healthcheck_tab.' + name + '_description')}
|
||||
disabled={this.props.disabled}
|
||||
inputClassName='col-xs-3'
|
||||
/>);
|
||||
}, this)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var TestSet = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin('tests'),
|
||||
backboneMixin('testset')
|
||||
],
|
||||
handleTestSetCheck(name, value) {
|
||||
this.props.testset.set('checked', value);
|
||||
this.props.tests.invoke('set', {checked: value});
|
||||
},
|
||||
componentWillUnmount() {
|
||||
this.props.tests.invoke('off', 'change:checked', this.updateTestsetCheckbox, this);
|
||||
},
|
||||
componentWillMount() {
|
||||
this.props.tests.invoke('on', 'change:checked', this.updateTestsetCheckbox, this);
|
||||
},
|
||||
updateTestsetCheckbox() {
|
||||
this.props.testset.set('checked', this.props.tests.where({checked: true}).length == this.props.tests.length);
|
||||
},
|
||||
render() {
|
||||
var classes = {
|
||||
'table healthcheck-table': true,
|
||||
disabled: this.props.disabled
|
||||
};
|
||||
return (
|
||||
<table className={utils.classNames(classes)}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<Input
|
||||
type='checkbox'
|
||||
id={'testset-checkbox-' + this.props.testset.id}
|
||||
name={this.props.testset.get('name')}
|
||||
disabled={this.props.disabled}
|
||||
onChange={this.handleTestSetCheck}
|
||||
checked={this.props.testset.get('checked')}
|
||||
/>
|
||||
</th>
|
||||
<th className='col-xs-7 healthcheck-name'>
|
||||
<label htmlFor={'testset-checkbox-' + this.props.testset.id}>
|
||||
{this.props.testset.get('name')}
|
||||
</label>
|
||||
</th>
|
||||
<th className='healthcheck-col-duration col-xs-2'>
|
||||
{i18n('cluster_page.healthcheck_tab.expected_duration')}
|
||||
</th>
|
||||
<th className='healthcheck-col-duration col-xs-2'>
|
||||
{i18n('cluster_page.healthcheck_tab.actual_duration')}
|
||||
</th>
|
||||
<th className='healthcheck-col-status col-xs-1'>
|
||||
{i18n('cluster_page.healthcheck_tab.status')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.props.tests.map(function(test) {
|
||||
var result = this.props.testrun &&
|
||||
_.find(this.props.testrun.get('tests'), {id: test.id});
|
||||
var status = result && result.status || 'unknown';
|
||||
return <Test
|
||||
key={test.id}
|
||||
test={test}
|
||||
result={result}
|
||||
status={status}
|
||||
disabled={this.props.disabled}
|
||||
/>;
|
||||
}, this)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var Test = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin('test')
|
||||
],
|
||||
handleTestCheck(name, value) {
|
||||
this.props.test.set('checked', value);
|
||||
},
|
||||
render() {
|
||||
var test = this.props.test,
|
||||
result = this.props.result,
|
||||
description = _.escape(_.trim(test.get('description'))),
|
||||
status = this.props.status,
|
||||
currentStatusClassName = 'text-center healthcheck-status healthcheck-status-' + status,
|
||||
iconClasses = {
|
||||
success: 'glyphicon glyphicon-ok text-success',
|
||||
failure: 'glyphicon glyphicon-remove text-danger',
|
||||
error: 'glyphicon glyphicon-remove text-danger',
|
||||
running: 'glyphicon glyphicon-refresh animate-spin',
|
||||
wait_running: 'glyphicon glyphicon-time'
|
||||
};
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<Input
|
||||
type='checkbox'
|
||||
id={'test-checkbox-' + test.id}
|
||||
name={test.get('name')}
|
||||
disabled={this.props.disabled}
|
||||
onChange={this.handleTestCheck}
|
||||
checked={test.get('checked')}
|
||||
/>
|
||||
</td>
|
||||
<td className='healthcheck-name'>
|
||||
<label htmlFor={'test-checkbox-' + test.id}>{test.get('name')}</label>
|
||||
{_.contains(['failure', 'error', 'skipped'], status) &&
|
||||
<div className='text-danger'>
|
||||
{(result && result.message) &&
|
||||
<div>
|
||||
{!disabledState &&
|
||||
<div className='healthcheck-controls row well well-sm'>
|
||||
<div className='pull-left'>
|
||||
<Input
|
||||
type='checkbox'
|
||||
name='selectAll'
|
||||
onChange={this.handleSelectAllClick}
|
||||
checked={this.getNumberOfCheckedTests() == this.props.tests.length}
|
||||
disabled={hasRunningTests}
|
||||
label={i18n('common.select_all')}
|
||||
wrapperClassName='select-all'
|
||||
/>
|
||||
</div>
|
||||
{hasRunningTests ?
|
||||
(<button className='btn btn-danger stop-tests-btn pull-right'
|
||||
disabled={this.state.actionInProgress}
|
||||
onClick={this.stopTests}
|
||||
>
|
||||
{i18n('cluster_page.healthcheck_tab.stop_tests_button')}
|
||||
</button>)
|
||||
:
|
||||
(<button className='btn btn-success run-tests-btn pull-right'
|
||||
disabled={!this.getNumberOfCheckedTests() || this.state.actionInProgress}
|
||||
onClick={this.runTests}
|
||||
>
|
||||
{i18n('cluster_page.healthcheck_tab.run_tests_button')}
|
||||
</button>)
|
||||
}
|
||||
<button
|
||||
className='btn btn-default toggle-credentials pull-right'
|
||||
data-toggle='collapse'
|
||||
data-target='.credentials'
|
||||
onClick={this.toggleCredentials}
|
||||
>
|
||||
{i18n('cluster_page.healthcheck_tab.provide_credentials')}
|
||||
</button>
|
||||
|
||||
<HealthcheckCredentials
|
||||
credentials={this.state.credentials}
|
||||
onInputChange={this.handleInputChange}
|
||||
disabled={hasRunningTests}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
{(this.props.cluster.get('status') == 'new') &&
|
||||
<div className='alert alert-warning'>{i18n('cluster_page.healthcheck_tab.deploy_alert')}</div>
|
||||
}
|
||||
<div key='testsets'>
|
||||
{this.props.testsets.map((testset) => {
|
||||
return <TestSet
|
||||
key={testset.id}
|
||||
testset={testset}
|
||||
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}))}
|
||||
disabled={disabledState || hasRunningTests}
|
||||
/>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<b>{result.message}</b>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
<div className='well' dangerouslySetInnerHTML={{__html:
|
||||
utils.urlify(
|
||||
(result && _.isNumber(result.step)) ?
|
||||
utils.highlightTestStep(description, result.step)
|
||||
:
|
||||
description
|
||||
)
|
||||
}}>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
<td className='healthcheck-col-duration'>
|
||||
<div className='healthcheck-duration'>{test.get('duration') || ''}</div>
|
||||
</td>
|
||||
<td className='healthcheck-col-duration'>
|
||||
{(status != 'running' && result && _.isNumber(result.taken)) ?
|
||||
<div className='healthcheck-duration'>{result.taken.toFixed(1)}</div>
|
||||
:
|
||||
<div className='healthcheck-status healthcheck-status-unknown'>—</div>
|
||||
}
|
||||
</td>
|
||||
<td className='healthcheck-col-status'>
|
||||
<div className={currentStatusClassName}>
|
||||
{iconClasses[status] ? <i className={iconClasses[status]} /> : String.fromCharCode(0x2014)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var HealthcheckCredentials = React.createClass({
|
||||
render() {
|
||||
var inputFields = ['user', 'password', 'tenant'];
|
||||
return (
|
||||
<div className='credentials collapse col-xs-12'>
|
||||
<div className='forms-box'>
|
||||
<div className='alert alert-warning'>
|
||||
{i18n('cluster_page.healthcheck_tab.credentials_description')}
|
||||
</div>
|
||||
{_.map(inputFields, function(name) {
|
||||
return (<Input
|
||||
key={name}
|
||||
type={(name == 'password') ? 'password' : 'text'}
|
||||
name={name}
|
||||
label={i18n('cluster_page.healthcheck_tab.' + name + '_label')}
|
||||
value={this.props.credentials[name]}
|
||||
onChange={this.props.onInputChange}
|
||||
toggleable={name == 'password'}
|
||||
description={i18n('cluster_page.healthcheck_tab.' + name + '_description')}
|
||||
disabled={this.props.disabled}
|
||||
inputClassName='col-xs-3'
|
||||
/>);
|
||||
}, this)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var TestSet = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin('tests'),
|
||||
backboneMixin('testset')
|
||||
],
|
||||
handleTestSetCheck(name, value) {
|
||||
this.props.testset.set('checked', value);
|
||||
this.props.tests.invoke('set', {checked: value});
|
||||
},
|
||||
componentWillUnmount() {
|
||||
this.props.tests.invoke('off', 'change:checked', this.updateTestsetCheckbox, this);
|
||||
},
|
||||
componentWillMount() {
|
||||
this.props.tests.invoke('on', 'change:checked', this.updateTestsetCheckbox, this);
|
||||
},
|
||||
updateTestsetCheckbox() {
|
||||
this.props.testset.set('checked', this.props.tests.where({checked: true}).length == this.props.tests.length);
|
||||
},
|
||||
render() {
|
||||
var classes = {
|
||||
'table healthcheck-table': true,
|
||||
disabled: this.props.disabled
|
||||
};
|
||||
return (
|
||||
<table className={utils.classNames(classes)}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<Input
|
||||
type='checkbox'
|
||||
id={'testset-checkbox-' + this.props.testset.id}
|
||||
name={this.props.testset.get('name')}
|
||||
disabled={this.props.disabled}
|
||||
onChange={this.handleTestSetCheck}
|
||||
checked={this.props.testset.get('checked')}
|
||||
/>
|
||||
</th>
|
||||
<th className='col-xs-7 healthcheck-name'>
|
||||
<label htmlFor={'testset-checkbox-' + this.props.testset.id}>
|
||||
{this.props.testset.get('name')}
|
||||
</label>
|
||||
</th>
|
||||
<th className='healthcheck-col-duration col-xs-2'>
|
||||
{i18n('cluster_page.healthcheck_tab.expected_duration')}
|
||||
</th>
|
||||
<th className='healthcheck-col-duration col-xs-2'>
|
||||
{i18n('cluster_page.healthcheck_tab.actual_duration')}
|
||||
</th>
|
||||
<th className='healthcheck-col-status col-xs-1'>
|
||||
{i18n('cluster_page.healthcheck_tab.status')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.props.tests.map(function(test) {
|
||||
var result = this.props.testrun &&
|
||||
_.find(this.props.testrun.get('tests'), {id: test.id});
|
||||
var status = result && result.status || 'unknown';
|
||||
return <Test
|
||||
key={test.id}
|
||||
test={test}
|
||||
result={result}
|
||||
status={status}
|
||||
disabled={this.props.disabled}
|
||||
/>;
|
||||
}, this)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var Test = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin('test')
|
||||
],
|
||||
handleTestCheck(name, value) {
|
||||
this.props.test.set('checked', value);
|
||||
},
|
||||
render() {
|
||||
var test = this.props.test,
|
||||
result = this.props.result,
|
||||
description = _.escape(_.trim(test.get('description'))),
|
||||
status = this.props.status,
|
||||
currentStatusClassName = 'text-center healthcheck-status healthcheck-status-' + status,
|
||||
iconClasses = {
|
||||
success: 'glyphicon glyphicon-ok text-success',
|
||||
failure: 'glyphicon glyphicon-remove text-danger',
|
||||
error: 'glyphicon glyphicon-remove text-danger',
|
||||
running: 'glyphicon glyphicon-refresh animate-spin',
|
||||
wait_running: 'glyphicon glyphicon-time'
|
||||
};
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<Input
|
||||
type='checkbox'
|
||||
id={'test-checkbox-' + test.id}
|
||||
name={test.get('name')}
|
||||
disabled={this.props.disabled}
|
||||
onChange={this.handleTestCheck}
|
||||
checked={test.get('checked')}
|
||||
/>
|
||||
</td>
|
||||
<td className='healthcheck-name'>
|
||||
<label htmlFor={'test-checkbox-' + test.id}>{test.get('name')}</label>
|
||||
{_.contains(['failure', 'error', 'skipped'], status) &&
|
||||
<div className='text-danger'>
|
||||
{(result && result.message) &&
|
||||
<div>
|
||||
<b>{result.message}</b>
|
||||
</div>
|
||||
}
|
||||
<div className='well' dangerouslySetInnerHTML={{__html:
|
||||
utils.urlify(
|
||||
(result && _.isNumber(result.step)) ?
|
||||
utils.highlightTestStep(description, result.step)
|
||||
:
|
||||
description
|
||||
)
|
||||
}}>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
<td className='healthcheck-col-duration'>
|
||||
<div className='healthcheck-duration'>{test.get('duration') || ''}</div>
|
||||
</td>
|
||||
<td className='healthcheck-col-duration'>
|
||||
{(status != 'running' && result && _.isNumber(result.taken)) ?
|
||||
<div className='healthcheck-duration'>{result.taken.toFixed(1)}</div>
|
||||
:
|
||||
<div className='healthcheck-status healthcheck-status-unknown'>—</div>
|
||||
}
|
||||
</td>
|
||||
<td className='healthcheck-col-status'>
|
||||
<div className={currentStatusClassName}>
|
||||
{iconClasses[status] ? <i className={iconClasses[status]} /> : String.fromCharCode(0x2014)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default HealthCheckTab;
|
||||
export default HealthCheckTab;
|
||||
|
|
|
@ -24,398 +24,398 @@ import {pollingMixin} from 'component_mixins';
|
|||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ReactFragment from 'react-addons-create-fragment';
|
||||
|
||||
var LogsTab = React.createClass({
|
||||
mixins: [
|
||||
pollingMixin(5)
|
||||
],
|
||||
shouldDataBeFetched() {
|
||||
return this.state.to && this.state.logsEntries;
|
||||
var LogsTab = React.createClass({
|
||||
mixins: [
|
||||
pollingMixin(5)
|
||||
],
|
||||
shouldDataBeFetched() {
|
||||
return this.state.to && this.state.logsEntries;
|
||||
},
|
||||
fetchData() {
|
||||
var request,
|
||||
logsEntries = this.state.logsEntries,
|
||||
from = this.state.from,
|
||||
to = this.state.to;
|
||||
request = this.fetchLogs({from: from, to: to})
|
||||
.done((data) => {
|
||||
this.setState({
|
||||
logsEntries: data.entries.concat(logsEntries),
|
||||
from: data.from,
|
||||
to: data.to
|
||||
});
|
||||
});
|
||||
return $.when(request);
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
showMoreLogsLink: false,
|
||||
loading: 'loading',
|
||||
loadingError: null,
|
||||
from: -1,
|
||||
to: 0
|
||||
};
|
||||
},
|
||||
fetchLogs(data) {
|
||||
return $.ajax({
|
||||
url: '/api/logs',
|
||||
dataType: 'json',
|
||||
data: _.extend(_.omit(this.props.selectedLogs, 'type'), data),
|
||||
headers: {
|
||||
'X-Auth-Token': app.keystoneClient.token
|
||||
}
|
||||
});
|
||||
},
|
||||
showLogs(params) {
|
||||
this.stopPolling();
|
||||
var logOptions = this.props.selectedLogs.type == 'remote' ? _.extend({}, this.props.selectedLogs) : _.omit(this.props.selectedLogs, 'node');
|
||||
logOptions.level = logOptions.level.toLowerCase();
|
||||
app.navigate('#cluster/' + this.props.cluster.id + '/logs/' + utils.serializeTabOptions(logOptions), {trigger: false, replace: true});
|
||||
params = params || {};
|
||||
this.fetchLogs(params)
|
||||
.done((data) => {
|
||||
var logsEntries = this.state.logsEntries || [];
|
||||
this.setState({
|
||||
showMoreLogsLink: data.has_more || false,
|
||||
logsEntries: params.fetch_older ? logsEntries.concat(data.entries) : data.entries,
|
||||
loading: 'done',
|
||||
from: data.from,
|
||||
to: data.to
|
||||
});
|
||||
this.startPolling();
|
||||
})
|
||||
.fail((response) => {
|
||||
this.setState({
|
||||
logsEntries: undefined,
|
||||
loading: 'fail',
|
||||
loadingError: utils.getResponseText(response, i18n('cluster_page.logs_tab.log_alert'))
|
||||
});
|
||||
});
|
||||
},
|
||||
onShowButtonClick() {
|
||||
this.setState({
|
||||
loading: 'loading',
|
||||
loadingError: null
|
||||
}, this.showLogs);
|
||||
},
|
||||
onShowMoreClick(value) {
|
||||
this.showLogs({max_entries: value, fetch_older: true, from: this.state.from});
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div className='row'>
|
||||
<div className='title'>{i18n('cluster_page.logs_tab.title')}</div>
|
||||
<div className='col-xs-12 content-elements'>
|
||||
<LogFilterBar
|
||||
{... _.pick(this.props, 'selectedLogs', 'changeLogSelection')}
|
||||
nodes={this.props.cluster.get('nodes')}
|
||||
showLogs={this.showLogs}
|
||||
onShowButtonClick={this.onShowButtonClick}
|
||||
/>
|
||||
{this.state.loading == 'fail' &&
|
||||
<div className='logs-fetch-error alert alert-danger'>
|
||||
{this.state.loadingError}
|
||||
</div>
|
||||
}
|
||||
{this.state.loading == 'loading' && <ProgressBar />}
|
||||
{this.state.logsEntries &&
|
||||
<LogsTable
|
||||
logsEntries={this.state.logsEntries}
|
||||
showMoreLogsLink={this.state.showMoreLogsLink}
|
||||
onShowMoreClick={this.onShowMoreClick}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var LogFilterBar = React.createClass({
|
||||
// PureRenderMixin added for prevention the rerender LogFilterBar (because of polling) in Mozilla browser
|
||||
mixins: [PureRenderMixin],
|
||||
getInitialState() {
|
||||
return _.extend({}, this.props.selectedLogs, {
|
||||
sourcesLoadingState: 'loading',
|
||||
sourcesLoadingError: null,
|
||||
sources: [],
|
||||
locked: true
|
||||
});
|
||||
},
|
||||
fetchSources(type, nodeId) {
|
||||
var nodes = this.props.nodes,
|
||||
chosenNodeId = nodeId || (nodes.length ? nodes.first().id : null);
|
||||
this.sources = new models.LogSources();
|
||||
this.sources.deferred = (type == 'remote' && chosenNodeId) ?
|
||||
this.sources.fetch({url: '/api/logs/sources/nodes/' + chosenNodeId})
|
||||
:
|
||||
this.sources.fetch();
|
||||
this.sources.deferred.done(() => {
|
||||
var filteredSources = this.sources.filter((source) => source.get('remote') == (type != 'local')),
|
||||
chosenSource = _.findWhere(filteredSources, {id: this.state.source}) || _.first(filteredSources),
|
||||
chosenLevelId = chosenSource ? _.contains(chosenSource.get('levels'), this.state.level) ? this.state.level : _.first(chosenSource.get('levels')) : null;
|
||||
this.setState({
|
||||
type: type,
|
||||
sources: this.sources,
|
||||
sourcesLoadingState: 'done',
|
||||
node: chosenNodeId && type == 'remote' ? chosenNodeId : null,
|
||||
source: chosenSource ? chosenSource.id : null,
|
||||
level: chosenLevelId,
|
||||
locked: false
|
||||
});
|
||||
});
|
||||
this.sources.deferred.fail((response) => {
|
||||
this.setState({
|
||||
type: type,
|
||||
sources: {},
|
||||
sourcesLoadingState: 'fail',
|
||||
sourcesLoadingError: utils.getResponseText(response, i18n('cluster_page.logs_tab.source_alert')),
|
||||
locked: false
|
||||
});
|
||||
});
|
||||
return this.sources.deferred;
|
||||
},
|
||||
componentDidMount() {
|
||||
this.fetchSources(this.state.type, this.state.node)
|
||||
.done(() => {
|
||||
this.setState({locked: true});
|
||||
this.props.showLogs();
|
||||
});
|
||||
},
|
||||
onTypeChange(name, value) {
|
||||
this.fetchSources(value);
|
||||
},
|
||||
onNodeChange(name, value) {
|
||||
this.fetchSources('remote', value);
|
||||
},
|
||||
onLevelChange(name, value) {
|
||||
this.setState({
|
||||
level: value,
|
||||
locked: false
|
||||
});
|
||||
},
|
||||
onSourceChange(name, value) {
|
||||
var levels = this.state.sources.get(value).get('levels'),
|
||||
data = {locked: false, source: value};
|
||||
if (!_.contains(levels, this.state.level)) data.level = _.first(levels);
|
||||
this.setState(data);
|
||||
},
|
||||
getLocalSources() {
|
||||
return this.state.sources.map((source) => {
|
||||
if (!source.get('remote')) {
|
||||
return <option value={source.id} key={source.id}>{source.get('name')}</option>;
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
getRemoteSources() {
|
||||
var options = {},
|
||||
groups = [''],
|
||||
sourcesByGroup = {'': []},
|
||||
sources = this.state.sources;
|
||||
if (sources.length) {
|
||||
sources.each((source) => {
|
||||
var group = source.get('group') || '';
|
||||
if (!_.has(sourcesByGroup, group)) {
|
||||
sourcesByGroup[group] = [];
|
||||
groups.push(group);
|
||||
}
|
||||
sourcesByGroup[group].push(source);
|
||||
});
|
||||
_.each(groups, (group) => {
|
||||
if (sourcesByGroup[group].length) {
|
||||
var option = sourcesByGroup[group].map((source) => {
|
||||
return <option value={source.id} key={source.id}>{source.get('name')}</option>;
|
||||
});
|
||||
options[group] = group ? <optgroup label={group}>{option}</optgroup> : option;
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
return ReactFragment(options);
|
||||
},
|
||||
handleShowButtonClick() {
|
||||
this.setState({locked: true});
|
||||
this.props.changeLogSelection(_.pick(this.state, 'type', 'node', 'source', 'level'));
|
||||
this.props.onShowButtonClick();
|
||||
},
|
||||
render() {
|
||||
var isRemote = this.state.type == 'remote';
|
||||
return (
|
||||
<div className='well well-sm'>
|
||||
<div className='sticker row'>
|
||||
{this.renderTypeSelect()}
|
||||
{isRemote && this.renderNodeSelect()}
|
||||
{this.renderSourceSelect()}
|
||||
{this.renderLevelSelect()}
|
||||
{this.renderFilterButton(isRemote)}
|
||||
</div>
|
||||
{this.state.sourcesLoadingState == 'fail' &&
|
||||
<div className='node-sources-error alert alert-danger'>
|
||||
{this.state.sourcesLoadingError}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
renderFilterButton(isRemote) {
|
||||
return <div className={utils.classNames({
|
||||
'form-group': true,
|
||||
'col-md-4 col-sm-12': isRemote,
|
||||
'col-md-6 col-sm-3': !isRemote
|
||||
})}>
|
||||
<label />
|
||||
<button
|
||||
className='btn btn-default pull-right'
|
||||
onClick={this.handleShowButtonClick}
|
||||
disabled={!this.state.source || this.state.locked}
|
||||
>
|
||||
{i18n('cluster_page.logs_tab.show')}
|
||||
</button>
|
||||
</div>;
|
||||
},
|
||||
renderTypeSelect() {
|
||||
var types = [['local', 'Fuel Master']];
|
||||
if (this.props.nodes.length) {
|
||||
types.push(['remote', 'Other servers']);
|
||||
}
|
||||
var typeOptions = types.map((type) => {
|
||||
return <option value={type[0]} key={type[0]}>{type[1]}</option>;
|
||||
});
|
||||
return <div className='col-md-2 col-sm-3'>
|
||||
<Input
|
||||
type='select'
|
||||
label={i18n('cluster_page.logs_tab.logs')}
|
||||
value={this.state.type}
|
||||
wrapperClassName='filter-bar-item log-type-filter'
|
||||
name='type'
|
||||
onChange={this.onTypeChange}
|
||||
children={typeOptions}
|
||||
/>
|
||||
</div>;
|
||||
},
|
||||
renderNodeSelect() {
|
||||
var sortedNodes = this.props.nodes.models.sort(_.partialRight(utils.compare, {attr: 'name'})),
|
||||
nodeOptions = sortedNodes.map((node) => {
|
||||
return <option value={node.id} key={node.id}>{node.get('name') || node.get('mac')}</option>;
|
||||
});
|
||||
|
||||
return <div className='col-md-2 col-sm-3'>
|
||||
<Input
|
||||
type='select'
|
||||
label={i18n('cluster_page.logs_tab.node')}
|
||||
value={this.state.node}
|
||||
wrapperClassName='filter-bar-item log-node-filter'
|
||||
name='node'
|
||||
onChange={this.onNodeChange}
|
||||
children={nodeOptions}
|
||||
/>
|
||||
</div>;
|
||||
},
|
||||
renderSourceSelect() {
|
||||
var sourceOptions = this.state.type == 'local' ? this.getLocalSources() : this.getRemoteSources();
|
||||
return <div className='col-md-2 col-sm-3'>
|
||||
<Input
|
||||
type='select'
|
||||
label={i18n('cluster_page.logs_tab.source')}
|
||||
value={this.state.source}
|
||||
wrapperClassName='filter-bar-item log-source-filter'
|
||||
name='source'
|
||||
onChange={this.onSourceChange}
|
||||
disabled={!this.state.source}
|
||||
children={sourceOptions}
|
||||
/>
|
||||
</div>;
|
||||
},
|
||||
renderLevelSelect() {
|
||||
var levelOptions = [];
|
||||
if (this.state.source && this.state.sources.length) {
|
||||
levelOptions = this.state.sources.get(this.state.source).get('levels').map((level) => {
|
||||
return <option value={level} key={level}>{level}</option>;
|
||||
});
|
||||
}
|
||||
return <div className='col-md-2 col-sm-3'>
|
||||
<Input
|
||||
type='select'
|
||||
label={i18n('cluster_page.logs_tab.min_level')}
|
||||
value={this.state.level}
|
||||
wrapperClassName='filter-bar-item log-level-filter'
|
||||
name='level'
|
||||
onChange={this.onLevelChange}
|
||||
disabled={!this.state.level}
|
||||
children={levelOptions}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
var LogsTable = React.createClass({
|
||||
handleShowMoreClick(value) {
|
||||
return this.props.onShowMoreClick(value);
|
||||
},
|
||||
getLevelClass(level) {
|
||||
return {
|
||||
DEBUG: 'debug',
|
||||
INFO: 'info',
|
||||
NOTICE: 'notice',
|
||||
WARNING: 'warning',
|
||||
ERROR: 'error',
|
||||
ERR: 'error',
|
||||
CRITICAL: 'critical',
|
||||
CRIT: 'critical',
|
||||
ALERT: 'alert',
|
||||
EMERG: 'emerg'
|
||||
}[level];
|
||||
},
|
||||
render() {
|
||||
var tabRows = [],
|
||||
logsEntries = this.props.logsEntries;
|
||||
if (logsEntries && logsEntries.length) {
|
||||
tabRows = _.map(
|
||||
logsEntries,
|
||||
function(entry, index) {
|
||||
var key = logsEntries.length - index;
|
||||
return <tr key={key} className={this.getLevelClass(entry[1])}>
|
||||
<td>{entry[0]}</td>
|
||||
<td>{entry[1]}</td>
|
||||
<td>{entry[2]}</td>
|
||||
</tr>;
|
||||
},
|
||||
fetchData() {
|
||||
var request,
|
||||
logsEntries = this.state.logsEntries,
|
||||
from = this.state.from,
|
||||
to = this.state.to;
|
||||
request = this.fetchLogs({from: from, to: to})
|
||||
.done((data) => {
|
||||
this.setState({
|
||||
logsEntries: data.entries.concat(logsEntries),
|
||||
from: data.from,
|
||||
to: data.to
|
||||
});
|
||||
});
|
||||
return $.when(request);
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
showMoreLogsLink: false,
|
||||
loading: 'loading',
|
||||
loadingError: null,
|
||||
from: -1,
|
||||
to: 0
|
||||
};
|
||||
},
|
||||
fetchLogs(data) {
|
||||
return $.ajax({
|
||||
url: '/api/logs',
|
||||
dataType: 'json',
|
||||
data: _.extend(_.omit(this.props.selectedLogs, 'type'), data),
|
||||
headers: {
|
||||
'X-Auth-Token': app.keystoneClient.token
|
||||
}
|
||||
});
|
||||
},
|
||||
showLogs(params) {
|
||||
this.stopPolling();
|
||||
var logOptions = this.props.selectedLogs.type == 'remote' ? _.extend({}, this.props.selectedLogs) : _.omit(this.props.selectedLogs, 'node');
|
||||
logOptions.level = logOptions.level.toLowerCase();
|
||||
app.navigate('#cluster/' + this.props.cluster.id + '/logs/' + utils.serializeTabOptions(logOptions), {trigger: false, replace: true});
|
||||
params = params || {};
|
||||
this.fetchLogs(params)
|
||||
.done((data) => {
|
||||
var logsEntries = this.state.logsEntries || [];
|
||||
this.setState({
|
||||
showMoreLogsLink: data.has_more || false,
|
||||
logsEntries: params.fetch_older ? logsEntries.concat(data.entries) : data.entries,
|
||||
loading: 'done',
|
||||
from: data.from,
|
||||
to: data.to
|
||||
});
|
||||
this.startPolling();
|
||||
})
|
||||
.fail((response) => {
|
||||
this.setState({
|
||||
logsEntries: undefined,
|
||||
loading: 'fail',
|
||||
loadingError: utils.getResponseText(response, i18n('cluster_page.logs_tab.log_alert'))
|
||||
});
|
||||
});
|
||||
},
|
||||
onShowButtonClick() {
|
||||
this.setState({
|
||||
loading: 'loading',
|
||||
loadingError: null
|
||||
}, this.showLogs);
|
||||
},
|
||||
onShowMoreClick(value) {
|
||||
this.showLogs({max_entries: value, fetch_older: true, from: this.state.from});
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div className='row'>
|
||||
<div className='title'>{i18n('cluster_page.logs_tab.title')}</div>
|
||||
<div className='col-xs-12 content-elements'>
|
||||
<LogFilterBar
|
||||
{... _.pick(this.props, 'selectedLogs', 'changeLogSelection')}
|
||||
nodes={this.props.cluster.get('nodes')}
|
||||
showLogs={this.showLogs}
|
||||
onShowButtonClick={this.onShowButtonClick}
|
||||
/>
|
||||
{this.state.loading == 'fail' &&
|
||||
<div className='logs-fetch-error alert alert-danger'>
|
||||
{this.state.loadingError}
|
||||
</div>
|
||||
}
|
||||
{this.state.loading == 'loading' && <ProgressBar />}
|
||||
{this.state.logsEntries &&
|
||||
<LogsTable
|
||||
logsEntries={this.state.logsEntries}
|
||||
showMoreLogsLink={this.state.showMoreLogsLink}
|
||||
onShowMoreClick={this.onShowMoreClick}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
this
|
||||
);
|
||||
}
|
||||
return logsEntries.length ?
|
||||
<table className='table log-entries'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className='col-date'>{i18n('cluster_page.logs_tab.date')}</th>
|
||||
<th className='col-level'>{i18n('cluster_page.logs_tab.level')}</th>
|
||||
<th className='col-message'>{i18n('cluster_page.logs_tab.message')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tabRows}
|
||||
</tbody>
|
||||
{this.props.showMoreLogsLink &&
|
||||
<tfoot className='entries-skipped-msg'>
|
||||
<tr>
|
||||
<td colSpan='3' className='text-center'>
|
||||
<div>
|
||||
<span>{i18n('cluster_page.logs_tab.bottom_text')}</span>:
|
||||
{
|
||||
[100, 500, 1000, 5000].map(
|
||||
function(count) {
|
||||
return <button className='btn btn-link show-more-entries' onClick={_.bind(this.handleShowMoreClick, this, count)} key={count}>{count} </button>;
|
||||
},
|
||||
this
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
}
|
||||
});
|
||||
</table>
|
||||
:
|
||||
<div className='no-logs-msg'>{i18n('cluster_page.logs_tab.no_log_text')}</div>;
|
||||
}
|
||||
});
|
||||
|
||||
var LogFilterBar = React.createClass({
|
||||
// PureRenderMixin added for prevention the rerender LogFilterBar (because of polling) in Mozilla browser
|
||||
mixins: [PureRenderMixin],
|
||||
getInitialState() {
|
||||
return _.extend({}, this.props.selectedLogs, {
|
||||
sourcesLoadingState: 'loading',
|
||||
sourcesLoadingError: null,
|
||||
sources: [],
|
||||
locked: true
|
||||
});
|
||||
},
|
||||
fetchSources(type, nodeId) {
|
||||
var nodes = this.props.nodes,
|
||||
chosenNodeId = nodeId || (nodes.length ? nodes.first().id : null);
|
||||
this.sources = new models.LogSources();
|
||||
this.sources.deferred = (type == 'remote' && chosenNodeId) ?
|
||||
this.sources.fetch({url: '/api/logs/sources/nodes/' + chosenNodeId})
|
||||
:
|
||||
this.sources.fetch();
|
||||
this.sources.deferred.done(() => {
|
||||
var filteredSources = this.sources.filter((source) => source.get('remote') == (type != 'local')),
|
||||
chosenSource = _.findWhere(filteredSources, {id: this.state.source}) || _.first(filteredSources),
|
||||
chosenLevelId = chosenSource ? _.contains(chosenSource.get('levels'), this.state.level) ? this.state.level : _.first(chosenSource.get('levels')) : null;
|
||||
this.setState({
|
||||
type: type,
|
||||
sources: this.sources,
|
||||
sourcesLoadingState: 'done',
|
||||
node: chosenNodeId && type == 'remote' ? chosenNodeId : null,
|
||||
source: chosenSource ? chosenSource.id : null,
|
||||
level: chosenLevelId,
|
||||
locked: false
|
||||
});
|
||||
});
|
||||
this.sources.deferred.fail((response) => {
|
||||
this.setState({
|
||||
type: type,
|
||||
sources: {},
|
||||
sourcesLoadingState: 'fail',
|
||||
sourcesLoadingError: utils.getResponseText(response, i18n('cluster_page.logs_tab.source_alert')),
|
||||
locked: false
|
||||
});
|
||||
});
|
||||
return this.sources.deferred;
|
||||
},
|
||||
componentDidMount() {
|
||||
this.fetchSources(this.state.type, this.state.node)
|
||||
.done(() => {
|
||||
this.setState({locked: true});
|
||||
this.props.showLogs();
|
||||
});
|
||||
},
|
||||
onTypeChange(name, value) {
|
||||
this.fetchSources(value);
|
||||
},
|
||||
onNodeChange(name, value) {
|
||||
this.fetchSources('remote', value);
|
||||
},
|
||||
onLevelChange(name, value) {
|
||||
this.setState({
|
||||
level: value,
|
||||
locked: false
|
||||
});
|
||||
},
|
||||
onSourceChange(name, value) {
|
||||
var levels = this.state.sources.get(value).get('levels'),
|
||||
data = {locked: false, source: value};
|
||||
if (!_.contains(levels, this.state.level)) data.level = _.first(levels);
|
||||
this.setState(data);
|
||||
},
|
||||
getLocalSources() {
|
||||
return this.state.sources.map((source) => {
|
||||
if (!source.get('remote')) {
|
||||
return <option value={source.id} key={source.id}>{source.get('name')}</option>;
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
getRemoteSources() {
|
||||
var options = {},
|
||||
groups = [''],
|
||||
sourcesByGroup = {'': []},
|
||||
sources = this.state.sources;
|
||||
if (sources.length) {
|
||||
sources.each((source) => {
|
||||
var group = source.get('group') || '';
|
||||
if (!_.has(sourcesByGroup, group)) {
|
||||
sourcesByGroup[group] = [];
|
||||
groups.push(group);
|
||||
}
|
||||
sourcesByGroup[group].push(source);
|
||||
});
|
||||
_.each(groups, (group) => {
|
||||
if (sourcesByGroup[group].length) {
|
||||
var option = sourcesByGroup[group].map((source) => {
|
||||
return <option value={source.id} key={source.id}>{source.get('name')}</option>;
|
||||
});
|
||||
options[group] = group ? <optgroup label={group}>{option}</optgroup> : option;
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
return ReactFragment(options);
|
||||
},
|
||||
handleShowButtonClick() {
|
||||
this.setState({locked: true});
|
||||
this.props.changeLogSelection(_.pick(this.state, 'type', 'node', 'source', 'level'));
|
||||
this.props.onShowButtonClick();
|
||||
},
|
||||
render() {
|
||||
var isRemote = this.state.type == 'remote';
|
||||
return (
|
||||
<div className='well well-sm'>
|
||||
<div className='sticker row'>
|
||||
{this.renderTypeSelect()}
|
||||
{isRemote && this.renderNodeSelect()}
|
||||
{this.renderSourceSelect()}
|
||||
{this.renderLevelSelect()}
|
||||
{this.renderFilterButton(isRemote)}
|
||||
</div>
|
||||
{this.state.sourcesLoadingState == 'fail' &&
|
||||
<div className='node-sources-error alert alert-danger'>
|
||||
{this.state.sourcesLoadingError}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
renderFilterButton(isRemote) {
|
||||
return <div className={utils.classNames({
|
||||
'form-group': true,
|
||||
'col-md-4 col-sm-12': isRemote,
|
||||
'col-md-6 col-sm-3': !isRemote
|
||||
})}>
|
||||
<label />
|
||||
<button
|
||||
className='btn btn-default pull-right'
|
||||
onClick={this.handleShowButtonClick}
|
||||
disabled={!this.state.source || this.state.locked}
|
||||
>
|
||||
{i18n('cluster_page.logs_tab.show')}
|
||||
</button>
|
||||
</div>;
|
||||
},
|
||||
renderTypeSelect() {
|
||||
var types = [['local', 'Fuel Master']];
|
||||
if (this.props.nodes.length) {
|
||||
types.push(['remote', 'Other servers']);
|
||||
}
|
||||
var typeOptions = types.map((type) => {
|
||||
return <option value={type[0]} key={type[0]}>{type[1]}</option>;
|
||||
});
|
||||
return <div className='col-md-2 col-sm-3'>
|
||||
<Input
|
||||
type='select'
|
||||
label={i18n('cluster_page.logs_tab.logs')}
|
||||
value={this.state.type}
|
||||
wrapperClassName='filter-bar-item log-type-filter'
|
||||
name='type'
|
||||
onChange={this.onTypeChange}
|
||||
children={typeOptions}
|
||||
/>
|
||||
</div>;
|
||||
},
|
||||
renderNodeSelect() {
|
||||
var sortedNodes = this.props.nodes.models.sort(_.partialRight(utils.compare, {attr: 'name'})),
|
||||
nodeOptions = sortedNodes.map((node) => {
|
||||
return <option value={node.id} key={node.id}>{node.get('name') || node.get('mac')}</option>;
|
||||
});
|
||||
|
||||
return <div className='col-md-2 col-sm-3'>
|
||||
<Input
|
||||
type='select'
|
||||
label={i18n('cluster_page.logs_tab.node')}
|
||||
value={this.state.node}
|
||||
wrapperClassName='filter-bar-item log-node-filter'
|
||||
name='node'
|
||||
onChange={this.onNodeChange}
|
||||
children={nodeOptions}
|
||||
/>
|
||||
</div>;
|
||||
},
|
||||
renderSourceSelect() {
|
||||
var sourceOptions = this.state.type == 'local' ? this.getLocalSources() : this.getRemoteSources();
|
||||
return <div className='col-md-2 col-sm-3'>
|
||||
<Input
|
||||
type='select'
|
||||
label={i18n('cluster_page.logs_tab.source')}
|
||||
value={this.state.source}
|
||||
wrapperClassName='filter-bar-item log-source-filter'
|
||||
name='source'
|
||||
onChange={this.onSourceChange}
|
||||
disabled={!this.state.source}
|
||||
children={sourceOptions}
|
||||
/>
|
||||
</div>;
|
||||
},
|
||||
renderLevelSelect() {
|
||||
var levelOptions = [];
|
||||
if (this.state.source && this.state.sources.length) {
|
||||
levelOptions = this.state.sources.get(this.state.source).get('levels').map((level) => {
|
||||
return <option value={level} key={level}>{level}</option>;
|
||||
});
|
||||
}
|
||||
return <div className='col-md-2 col-sm-3'>
|
||||
<Input
|
||||
type='select'
|
||||
label={i18n('cluster_page.logs_tab.min_level')}
|
||||
value={this.state.level}
|
||||
wrapperClassName='filter-bar-item log-level-filter'
|
||||
name='level'
|
||||
onChange={this.onLevelChange}
|
||||
disabled={!this.state.level}
|
||||
children={levelOptions}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
var LogsTable = React.createClass({
|
||||
handleShowMoreClick(value) {
|
||||
return this.props.onShowMoreClick(value);
|
||||
},
|
||||
getLevelClass(level) {
|
||||
return {
|
||||
DEBUG: 'debug',
|
||||
INFO: 'info',
|
||||
NOTICE: 'notice',
|
||||
WARNING: 'warning',
|
||||
ERROR: 'error',
|
||||
ERR: 'error',
|
||||
CRITICAL: 'critical',
|
||||
CRIT: 'critical',
|
||||
ALERT: 'alert',
|
||||
EMERG: 'emerg'
|
||||
}[level];
|
||||
},
|
||||
render() {
|
||||
var tabRows = [],
|
||||
logsEntries = this.props.logsEntries;
|
||||
if (logsEntries && logsEntries.length) {
|
||||
tabRows = _.map(
|
||||
logsEntries,
|
||||
function(entry, index) {
|
||||
var key = logsEntries.length - index;
|
||||
return <tr key={key} className={this.getLevelClass(entry[1])}>
|
||||
<td>{entry[0]}</td>
|
||||
<td>{entry[1]}</td>
|
||||
<td>{entry[2]}</td>
|
||||
</tr>;
|
||||
},
|
||||
this
|
||||
);
|
||||
}
|
||||
return logsEntries.length ?
|
||||
<table className='table log-entries'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className='col-date'>{i18n('cluster_page.logs_tab.date')}</th>
|
||||
<th className='col-level'>{i18n('cluster_page.logs_tab.level')}</th>
|
||||
<th className='col-message'>{i18n('cluster_page.logs_tab.message')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tabRows}
|
||||
</tbody>
|
||||
{this.props.showMoreLogsLink &&
|
||||
<tfoot className='entries-skipped-msg'>
|
||||
<tr>
|
||||
<td colSpan='3' className='text-center'>
|
||||
<div>
|
||||
<span>{i18n('cluster_page.logs_tab.bottom_text')}</span>:
|
||||
{
|
||||
[100, 500, 1000, 5000].map(
|
||||
function(count) {
|
||||
return <button className='btn btn-link show-more-entries' onClick={_.bind(this.handleShowMoreClick, this, count)} key={count}>{count} </button>;
|
||||
},
|
||||
this
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
}
|
||||
</table>
|
||||
:
|
||||
<div className='no-logs-msg'>{i18n('cluster_page.logs_tab.no_log_text')}</div>;
|
||||
}
|
||||
});
|
||||
|
||||
export default LogsTab;
|
||||
export default LogsTab;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -26,120 +26,120 @@ import EditNodeDisksScreen from 'views/cluster_page_tabs/nodes_tab_screens/edit_
|
|||
import EditNodeInterfacesScreen from 'views/cluster_page_tabs/nodes_tab_screens/edit_node_interfaces_screen';
|
||||
import ReactTransitionGroup from 'react-addons-transition-group';
|
||||
|
||||
var NodesTab = React.createClass({
|
||||
getInitialState() {
|
||||
var screen = this.getScreen();
|
||||
return {
|
||||
loading: this.shouldScreenDataBeLoaded(screen),
|
||||
screen: screen,
|
||||
screenOptions: this.getScreenOptions(),
|
||||
screenData: {}
|
||||
};
|
||||
},
|
||||
getScreenConstructor(screen) {
|
||||
return {
|
||||
list: ClusterNodesScreen,
|
||||
add: AddNodesScreen,
|
||||
edit: EditNodesScreen,
|
||||
disks: EditNodeDisksScreen,
|
||||
interfaces: EditNodeInterfacesScreen
|
||||
}[screen];
|
||||
},
|
||||
checkScreenExists(screen) {
|
||||
if (!this.getScreenConstructor(screen || this.state.screen)) {
|
||||
app.navigate('cluster/' + this.props.cluster.id + '/nodes', {trigger: true, replace: true});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
loadScreenData(screen, screenOptions) {
|
||||
return this.getScreenConstructor(screen || this.state.screen)
|
||||
.fetchData({
|
||||
cluster: this.props.cluster,
|
||||
screenOptions: screenOptions || this.state.screenOptions
|
||||
})
|
||||
.done((data) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
screenData: data || {}
|
||||
});
|
||||
})
|
||||
.fail(() => {
|
||||
app.navigate('#cluster/' + this.props.cluster.id + '/nodes', {trigger: true, replace: true});
|
||||
});
|
||||
},
|
||||
getScreen(props) {
|
||||
return (props || this.props).tabOptions[0] || 'list';
|
||||
},
|
||||
getScreenOptions(props) {
|
||||
return (props || this.props).tabOptions.slice(1);
|
||||
},
|
||||
shouldScreenDataBeLoaded(screen) {
|
||||
return !!this.getScreenConstructor(screen).fetchData;
|
||||
},
|
||||
componentDidMount() {
|
||||
if (this.checkScreenExists() && this.state.loading) this.loadScreenData();
|
||||
},
|
||||
componentWillReceiveProps(newProps) {
|
||||
var screen = this.getScreen(newProps);
|
||||
if (this.state.screen != screen && this.checkScreenExists(screen)) {
|
||||
var screenOptions = this.getScreenOptions(newProps),
|
||||
newState = {
|
||||
screen: screen,
|
||||
screenOptions: screenOptions,
|
||||
screenData: {}
|
||||
};
|
||||
if (this.shouldScreenDataBeLoaded(screen)) {
|
||||
this.setState(_.extend(newState, {loading: true}));
|
||||
this.loadScreenData(screen, screenOptions);
|
||||
} else {
|
||||
this.setState(_.extend(newState, {loading: false}));
|
||||
}
|
||||
}
|
||||
},
|
||||
render() {
|
||||
var Screen = this.getScreenConstructor(this.state.screen) || {};
|
||||
return (
|
||||
<ReactTransitionGroup
|
||||
component='div'
|
||||
className='wrapper'
|
||||
transitionName='screen'
|
||||
>
|
||||
<ScreenTransitionWrapper
|
||||
key={this.state.screen}
|
||||
loading={this.state.loading}
|
||||
>
|
||||
<Screen
|
||||
{...this.state.screenData}
|
||||
{... _.pick(this.props, 'cluster', 'nodeNetworkGroups', 'selectedNodeIds', 'selectNodes')}
|
||||
ref='screen'
|
||||
screenOptions={this.state.screenOptions}
|
||||
/>
|
||||
</ScreenTransitionWrapper>
|
||||
</ReactTransitionGroup>
|
||||
);
|
||||
}
|
||||
});
|
||||
var NodesTab = React.createClass({
|
||||
getInitialState() {
|
||||
var screen = this.getScreen();
|
||||
return {
|
||||
loading: this.shouldScreenDataBeLoaded(screen),
|
||||
screen: screen,
|
||||
screenOptions: this.getScreenOptions(),
|
||||
screenData: {}
|
||||
};
|
||||
},
|
||||
getScreenConstructor(screen) {
|
||||
return {
|
||||
list: ClusterNodesScreen,
|
||||
add: AddNodesScreen,
|
||||
edit: EditNodesScreen,
|
||||
disks: EditNodeDisksScreen,
|
||||
interfaces: EditNodeInterfacesScreen
|
||||
}[screen];
|
||||
},
|
||||
checkScreenExists(screen) {
|
||||
if (!this.getScreenConstructor(screen || this.state.screen)) {
|
||||
app.navigate('cluster/' + this.props.cluster.id + '/nodes', {trigger: true, replace: true});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
loadScreenData(screen, screenOptions) {
|
||||
return this.getScreenConstructor(screen || this.state.screen)
|
||||
.fetchData({
|
||||
cluster: this.props.cluster,
|
||||
screenOptions: screenOptions || this.state.screenOptions
|
||||
})
|
||||
.done((data) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
screenData: data || {}
|
||||
});
|
||||
})
|
||||
.fail(() => {
|
||||
app.navigate('#cluster/' + this.props.cluster.id + '/nodes', {trigger: true, replace: true});
|
||||
});
|
||||
},
|
||||
getScreen(props) {
|
||||
return (props || this.props).tabOptions[0] || 'list';
|
||||
},
|
||||
getScreenOptions(props) {
|
||||
return (props || this.props).tabOptions.slice(1);
|
||||
},
|
||||
shouldScreenDataBeLoaded(screen) {
|
||||
return !!this.getScreenConstructor(screen).fetchData;
|
||||
},
|
||||
componentDidMount() {
|
||||
if (this.checkScreenExists() && this.state.loading) this.loadScreenData();
|
||||
},
|
||||
componentWillReceiveProps(newProps) {
|
||||
var screen = this.getScreen(newProps);
|
||||
if (this.state.screen != screen && this.checkScreenExists(screen)) {
|
||||
var screenOptions = this.getScreenOptions(newProps),
|
||||
newState = {
|
||||
screen: screen,
|
||||
screenOptions: screenOptions,
|
||||
screenData: {}
|
||||
};
|
||||
if (this.shouldScreenDataBeLoaded(screen)) {
|
||||
this.setState(_.extend(newState, {loading: true}));
|
||||
this.loadScreenData(screen, screenOptions);
|
||||
} else {
|
||||
this.setState(_.extend(newState, {loading: false}));
|
||||
}
|
||||
}
|
||||
},
|
||||
render() {
|
||||
var Screen = this.getScreenConstructor(this.state.screen) || {};
|
||||
return (
|
||||
<ReactTransitionGroup
|
||||
component='div'
|
||||
className='wrapper'
|
||||
transitionName='screen'
|
||||
>
|
||||
<ScreenTransitionWrapper
|
||||
key={this.state.screen}
|
||||
loading={this.state.loading}
|
||||
>
|
||||
<Screen
|
||||
{...this.state.screenData}
|
||||
{... _.pick(this.props, 'cluster', 'nodeNetworkGroups', 'selectedNodeIds', 'selectNodes')}
|
||||
ref='screen'
|
||||
screenOptions={this.state.screenOptions}
|
||||
/>
|
||||
</ScreenTransitionWrapper>
|
||||
</ReactTransitionGroup>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// additional screen wrapper to keep ref to screen in the tab component
|
||||
// see https://github.com/facebook/react/issues/1950 for more info
|
||||
var ScreenTransitionWrapper = React.createClass({
|
||||
componentWillEnter(cb) {
|
||||
$(ReactDOM.findDOMNode(this)).hide().delay('fast').fadeIn('fast', cb);
|
||||
},
|
||||
componentWillLeave(cb) {
|
||||
$(ReactDOM.findDOMNode(this)).fadeOut('fast', cb);
|
||||
},
|
||||
render() {
|
||||
if (this.props.loading) return (
|
||||
<div className='row'>
|
||||
<div className='col-xs-12' style={{paddingTop: '40px'}}>
|
||||
<ProgressBar />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <div>{this.props.children}</div>;
|
||||
}
|
||||
});
|
||||
// additional screen wrapper to keep ref to screen in the tab component
|
||||
// see https://github.com/facebook/react/issues/1950 for more info
|
||||
var ScreenTransitionWrapper = React.createClass({
|
||||
componentWillEnter(cb) {
|
||||
$(ReactDOM.findDOMNode(this)).hide().delay('fast').fadeIn('fast', cb);
|
||||
},
|
||||
componentWillLeave(cb) {
|
||||
$(ReactDOM.findDOMNode(this)).fadeOut('fast', cb);
|
||||
},
|
||||
render() {
|
||||
if (this.props.loading) return (
|
||||
<div className='row'>
|
||||
<div className='col-xs-12' style={{paddingTop: '40px'}}>
|
||||
<ProgressBar />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <div>{this.props.children}</div>;
|
||||
}
|
||||
});
|
||||
|
||||
export default NodesTab;
|
||||
export default NodesTab;
|
||||
|
|
|
@ -19,36 +19,36 @@ import React from 'react';
|
|||
import models from 'models';
|
||||
import NodeListScreen from 'views/cluster_page_tabs/nodes_tab_screens/node_list_screen';
|
||||
|
||||
var AddNodesScreen = React.createClass({
|
||||
statics: {
|
||||
fetchData(options) {
|
||||
var nodes = new models.Nodes();
|
||||
nodes.fetch = function(options) {
|
||||
return this.constructor.__super__.fetch.call(this, _.extend({data: {cluster_id: ''}}, options));
|
||||
};
|
||||
return $.when(nodes.fetch(), options.cluster.get('roles').fetch(),
|
||||
options.cluster.get('settings').fetch({cache: true})).then(() => ({nodes: nodes}));
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return <NodeListScreen {... _.omit(this.props, 'screenOptions')}
|
||||
ref='screen'
|
||||
mode='add'
|
||||
roles={this.props.cluster.get('roles')}
|
||||
sorters={_.without(models.Nodes.prototype.sorters, 'cluster', 'roles', 'group_id')}
|
||||
defaultSorting={[{status: 'asc'}]}
|
||||
filters={_.without(models.Nodes.prototype.filters, 'cluster', 'roles', 'group_id')}
|
||||
statusesToFilter={_.without(models.Node.prototype.statuses,
|
||||
'ready',
|
||||
'pending_addition',
|
||||
'pending_deletion',
|
||||
'provisioned',
|
||||
'provisioning',
|
||||
'deploying'
|
||||
)}
|
||||
defaultFilters={{status: []}}
|
||||
/>;
|
||||
}
|
||||
});
|
||||
var AddNodesScreen = React.createClass({
|
||||
statics: {
|
||||
fetchData(options) {
|
||||
var nodes = new models.Nodes();
|
||||
nodes.fetch = function(options) {
|
||||
return this.constructor.__super__.fetch.call(this, _.extend({data: {cluster_id: ''}}, options));
|
||||
};
|
||||
return $.when(nodes.fetch(), options.cluster.get('roles').fetch(),
|
||||
options.cluster.get('settings').fetch({cache: true})).then(() => ({nodes: nodes}));
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return <NodeListScreen {... _.omit(this.props, 'screenOptions')}
|
||||
ref='screen'
|
||||
mode='add'
|
||||
roles={this.props.cluster.get('roles')}
|
||||
sorters={_.without(models.Nodes.prototype.sorters, 'cluster', 'roles', 'group_id')}
|
||||
defaultSorting={[{status: 'asc'}]}
|
||||
filters={_.without(models.Nodes.prototype.filters, 'cluster', 'roles', 'group_id')}
|
||||
statusesToFilter={_.without(models.Node.prototype.statuses,
|
||||
'ready',
|
||||
'pending_addition',
|
||||
'pending_deletion',
|
||||
'provisioned',
|
||||
'provisioning',
|
||||
'deploying'
|
||||
)}
|
||||
defaultFilters={{status: []}}
|
||||
/>;
|
||||
}
|
||||
});
|
||||
|
||||
export default AddNodesScreen;
|
||||
export default AddNodesScreen;
|
||||
|
|
|
@ -18,20 +18,20 @@ import React from 'react';
|
|||
import models from 'models';
|
||||
import NodeListScreen from 'views/cluster_page_tabs/nodes_tab_screens/node_list_screen';
|
||||
|
||||
var ClusterNodesScreen = React.createClass({
|
||||
render() {
|
||||
return <NodeListScreen {... _.omit(this.props, 'screenOptions')}
|
||||
ref='screen'
|
||||
mode='list'
|
||||
nodes={this.props.cluster.get('nodes')}
|
||||
roles={this.props.cluster.get('roles')}
|
||||
sorters={_.without(models.Nodes.prototype.sorters, 'cluster')}
|
||||
defaultSorting={[{roles: 'asc'}]}
|
||||
filters={_.without(models.Nodes.prototype.filters, 'cluster')}
|
||||
statusesToFilter={_.without(models.Node.prototype.statuses, 'discover')}
|
||||
defaultFilters={{roles: [], status: []}}
|
||||
/>;
|
||||
}
|
||||
});
|
||||
var ClusterNodesScreen = React.createClass({
|
||||
render() {
|
||||
return <NodeListScreen {... _.omit(this.props, 'screenOptions')}
|
||||
ref='screen'
|
||||
mode='list'
|
||||
nodes={this.props.cluster.get('nodes')}
|
||||
roles={this.props.cluster.get('roles')}
|
||||
sorters={_.without(models.Nodes.prototype.sorters, 'cluster')}
|
||||
defaultSorting={[{roles: 'asc'}]}
|
||||
filters={_.without(models.Nodes.prototype.filters, 'cluster')}
|
||||
statusesToFilter={_.without(models.Node.prototype.statuses, 'discover')}
|
||||
defaultFilters={{roles: [], status: []}}
|
||||
/>;
|
||||
}
|
||||
});
|
||||
|
||||
export default ClusterNodesScreen;
|
||||
export default ClusterNodesScreen;
|
||||
|
|
|
@ -24,345 +24,345 @@ import models from 'models';
|
|||
import {backboneMixin, unsavedChangesMixin} from 'component_mixins';
|
||||
import {Input} from 'views/controls';
|
||||
|
||||
var EditNodeDisksScreen = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin('cluster', 'change:status change:nodes sync'),
|
||||
backboneMixin('nodes', 'change sync'),
|
||||
backboneMixin('disks', 'reset change'),
|
||||
unsavedChangesMixin
|
||||
],
|
||||
statics: {
|
||||
fetchData(options) {
|
||||
var nodes = utils.getNodeListFromTabOptions(options);
|
||||
var EditNodeDisksScreen = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin('cluster', 'change:status change:nodes sync'),
|
||||
backboneMixin('nodes', 'change sync'),
|
||||
backboneMixin('disks', 'reset change'),
|
||||
unsavedChangesMixin
|
||||
],
|
||||
statics: {
|
||||
fetchData(options) {
|
||||
var nodes = utils.getNodeListFromTabOptions(options);
|
||||
|
||||
if (!nodes || !nodes.areDisksConfigurable()) {
|
||||
return $.Deferred().reject();
|
||||
if (!nodes || !nodes.areDisksConfigurable()) {
|
||||
return $.Deferred().reject();
|
||||
}
|
||||
|
||||
var volumes = new models.Volumes();
|
||||
volumes.url = _.result(nodes.at(0), 'url') + '/volumes';
|
||||
return $.when(...nodes.map((node) => {
|
||||
node.disks = new models.Disks();
|
||||
return node.disks.fetch({url: _.result(node, 'url') + '/disks'});
|
||||
}, this).concat(volumes.fetch()))
|
||||
.then(() => {
|
||||
var disks = new models.Disks(_.cloneDeep(nodes.at(0).disks.toJSON()), {parse: true});
|
||||
return {
|
||||
disks: disks,
|
||||
nodes: nodes,
|
||||
volumes: volumes
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
getInitialState() {
|
||||
return {actionInProgress: false};
|
||||
},
|
||||
componentWillMount() {
|
||||
this.updateInitialData();
|
||||
},
|
||||
isLocked() {
|
||||
return !!this.props.cluster.task({group: 'deployment', active: true}) ||
|
||||
!_.all(this.props.nodes.invoke('areDisksConfigurable'));
|
||||
},
|
||||
updateInitialData() {
|
||||
this.setState({initialDisks: _.cloneDeep(this.props.nodes.at(0).disks.toJSON())});
|
||||
},
|
||||
hasChanges() {
|
||||
return !this.isLocked() && !_.isEqual(_.pluck(this.props.disks.toJSON(), 'volumes'), _.pluck(this.state.initialDisks, 'volumes'));
|
||||
},
|
||||
loadDefaults() {
|
||||
this.setState({actionInProgress: true});
|
||||
this.props.disks.fetch({url: _.result(this.props.nodes.at(0), 'url') + '/disks/defaults/'})
|
||||
.fail((response) => {
|
||||
var ns = 'cluster_page.nodes_tab.configure_disks.configuration_error.';
|
||||
utils.showErrorDialog({
|
||||
title: i18n(ns + 'title'),
|
||||
message: utils.getResponseText(response) || i18n(ns + 'load_defaults_warning')
|
||||
});
|
||||
})
|
||||
.always(() => {
|
||||
this.setState({actionInProgress: false});
|
||||
});
|
||||
},
|
||||
revertChanges() {
|
||||
this.props.disks.reset(_.cloneDeep(this.state.initialDisks), {parse: true});
|
||||
},
|
||||
applyChanges() {
|
||||
if (!this.isSavingPossible()) return $.Deferred().reject();
|
||||
|
||||
this.setState({actionInProgress: true});
|
||||
return $.when(...this.props.nodes.map(function(node) {
|
||||
node.disks.each(function(disk, index) {
|
||||
disk.set({volumes: new models.Volumes(this.props.disks.at(index).get('volumes').toJSON())});
|
||||
}, this);
|
||||
return Backbone.sync('update', node.disks, {url: _.result(node, 'url') + '/disks'});
|
||||
}, this))
|
||||
.done(this.updateInitialData)
|
||||
.fail((response) => {
|
||||
var ns = 'cluster_page.nodes_tab.configure_disks.configuration_error.';
|
||||
utils.showErrorDialog({
|
||||
title: i18n(ns + 'title'),
|
||||
message: utils.getResponseText(response) || i18n(ns + 'saving_warning')
|
||||
});
|
||||
})
|
||||
.always(() => {
|
||||
this.setState({actionInProgress: false});
|
||||
});
|
||||
},
|
||||
getDiskMetaData(disk) {
|
||||
var result,
|
||||
disksMetaData = this.props.nodes.at(0).get('meta').disks;
|
||||
// try to find disk metadata by matching "extra" field
|
||||
// if at least one entry presents both in disk and metadata entry,
|
||||
// this metadata entry is for our disk
|
||||
var extra = disk.get('extra') || [];
|
||||
result = _.find(disksMetaData, (diskMetaData) =>
|
||||
_.isArray(diskMetaData.extra) && _.intersection(diskMetaData.extra, extra).length
|
||||
);
|
||||
// if matching "extra" fields doesn't work, try to search by disk id
|
||||
if (!result) {
|
||||
result = _.find(disksMetaData, {disk: disk.id});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
getVolumesInfo(disk) {
|
||||
var volumes = {},
|
||||
unallocatedWidth = 100;
|
||||
disk.get('volumes').each(function(volume) {
|
||||
var size = volume.get('size') || 0,
|
||||
width = this.getVolumeWidth(disk, size),
|
||||
name = volume.get('name');
|
||||
unallocatedWidth -= width;
|
||||
volumes[name] = {
|
||||
size: size,
|
||||
width: width,
|
||||
max: volume.getMaxSize(),
|
||||
min: volume.getMinimalSize(this.props.volumes.findWhere({name: name}).get('min_size')),
|
||||
error: volume.validationError
|
||||
};
|
||||
}, this);
|
||||
volumes.unallocated = {
|
||||
size: disk.getUnallocatedSpace(),
|
||||
width: unallocatedWidth
|
||||
};
|
||||
return volumes;
|
||||
},
|
||||
getVolumeWidth(disk, size) {
|
||||
return disk.get('size') ? utils.floor(size / disk.get('size') * 100, 2) : 0;
|
||||
},
|
||||
hasErrors() {
|
||||
return this.props.disks.any((disk) =>
|
||||
disk.get('volumes').any('validationError')
|
||||
);
|
||||
},
|
||||
isSavingPossible() {
|
||||
return !this.state.actionInProgress && this.hasChanges() && !this.hasErrors();
|
||||
},
|
||||
render() {
|
||||
var hasChanges = this.hasChanges(),
|
||||
locked = this.isLocked(),
|
||||
loadDefaultsDisabled = !!this.state.actionInProgress,
|
||||
revertChangesDisabled = !!this.state.actionInProgress || !hasChanges;
|
||||
return (
|
||||
<div className='edit-node-disks-screen'>
|
||||
<div className='row'>
|
||||
<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')})}
|
||||
</div>
|
||||
<div className='col-xs-12 node-disks'>
|
||||
{this.props.disks.length ?
|
||||
this.props.disks.map(function(disk, index) {
|
||||
return (<NodeDisk
|
||||
disk={disk}
|
||||
key={index}
|
||||
disabled={locked || this.state.actionInProgress}
|
||||
volumes={this.props.volumes}
|
||||
volumesInfo={this.getVolumesInfo(disk)}
|
||||
diskMetaData={this.getDiskMetaData(disk)}
|
||||
/>);
|
||||
}, this)
|
||||
:
|
||||
<div className='alert alert-warning'>
|
||||
{i18n('cluster_page.nodes_tab.configure_disks.no_disks', {count: this.props.nodes.length})}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className='col-xs-12 page-buttons content-elements'>
|
||||
<div className='well clearfix'>
|
||||
<div className='btn-group'>
|
||||
<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')}
|
||||
</a>
|
||||
</div>
|
||||
{!locked && !!this.props.disks.length &&
|
||||
<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 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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var NodeDisk = React.createClass({
|
||||
getInitialState() {
|
||||
return {collapsed: true};
|
||||
},
|
||||
componentDidMount() {
|
||||
$('.disk-details', ReactDOM.findDOMNode(this))
|
||||
.on('show.bs.collapse', this.setState.bind(this, {collapsed: true}, null))
|
||||
.on('hide.bs.collapse', this.setState.bind(this, {collapsed: false}, null));
|
||||
},
|
||||
updateDisk(name, value) {
|
||||
var size = parseInt(value, 10) || 0,
|
||||
volumeInfo = this.props.volumesInfo[name];
|
||||
if (size > volumeInfo.max) {
|
||||
size = volumeInfo.max;
|
||||
}
|
||||
this.props.disk.get('volumes').findWhere({name: name}).set({size: size}).isValid({minimum: volumeInfo.min});
|
||||
this.props.disk.trigger('change', this.props.disk);
|
||||
},
|
||||
toggleDisk(name) {
|
||||
$(ReactDOM.findDOMNode(this.refs[name])).collapse('toggle');
|
||||
},
|
||||
render() {
|
||||
var disk = this.props.disk,
|
||||
volumesInfo = this.props.volumesInfo,
|
||||
diskMetaData = this.props.diskMetaData,
|
||||
requiredDiskSize = _.sum(disk.get('volumes').map(function(volume) {
|
||||
return volume.getMinimalSize(this.props.volumes.findWhere({name: volume.get('name')}).get('min_size'));
|
||||
}, this)),
|
||||
diskError = disk.get('size') < requiredDiskSize,
|
||||
sortOrder = ['name', 'model', 'size'],
|
||||
ns = 'cluster_page.nodes_tab.configure_disks.';
|
||||
|
||||
return (
|
||||
<div className='col-xs-12 disk-box' data-disk={disk.id} key={this.props.key}>
|
||||
<div className='row'>
|
||||
<h4 className='col-xs-6'>
|
||||
{disk.get('name')} ({disk.id})
|
||||
</h4>
|
||||
<h4 className='col-xs-6 text-right'>
|
||||
{i18n(ns + 'total_space')} : {utils.showDiskSize(disk.get('size'), 2)}
|
||||
</h4>
|
||||
</div>
|
||||
<div className='row disk-visual clearfix'>
|
||||
{this.props.volumes.map(function(volume, index) {
|
||||
var volumeName = volume.get('name');
|
||||
return (
|
||||
<div
|
||||
key={'volume_' + volumeName}
|
||||
ref={'volume_' + volumeName}
|
||||
className={'volume-group pull-left volume-type-' + (index + 1)}
|
||||
data-volume={volumeName}
|
||||
style={{width: volumesInfo[volumeName].width + '%'}}
|
||||
>
|
||||
<div className='text-center toggle' onClick={_.partial(this.toggleDisk, disk.get('name'))}>
|
||||
<div>{volume.get('label')}</div>
|
||||
<div className='volume-group-size'>
|
||||
{utils.showDiskSize(volumesInfo[volumeName].size, 2)}
|
||||
</div>
|
||||
</div>
|
||||
{!this.props.disabled && volumesInfo[volumeName].min <= 0 && this.state.collapsed &&
|
||||
<div className='close-btn' onClick={_.partial(this.updateDisk, volumeName, 0)}>×</div>
|
||||
}
|
||||
|
||||
var volumes = new models.Volumes();
|
||||
volumes.url = _.result(nodes.at(0), 'url') + '/volumes';
|
||||
return $.when(...nodes.map((node) => {
|
||||
node.disks = new models.Disks();
|
||||
return node.disks.fetch({url: _.result(node, 'url') + '/disks'});
|
||||
}, this).concat(volumes.fetch()))
|
||||
.then(() => {
|
||||
var disks = new models.Disks(_.cloneDeep(nodes.at(0).disks.toJSON()), {parse: true});
|
||||
return {
|
||||
disks: disks,
|
||||
nodes: nodes,
|
||||
volumes: volumes
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
getInitialState() {
|
||||
return {actionInProgress: false};
|
||||
},
|
||||
componentWillMount() {
|
||||
this.updateInitialData();
|
||||
},
|
||||
isLocked() {
|
||||
return !!this.props.cluster.task({group: 'deployment', active: true}) ||
|
||||
!_.all(this.props.nodes.invoke('areDisksConfigurable'));
|
||||
},
|
||||
updateInitialData() {
|
||||
this.setState({initialDisks: _.cloneDeep(this.props.nodes.at(0).disks.toJSON())});
|
||||
},
|
||||
hasChanges() {
|
||||
return !this.isLocked() && !_.isEqual(_.pluck(this.props.disks.toJSON(), 'volumes'), _.pluck(this.state.initialDisks, 'volumes'));
|
||||
},
|
||||
loadDefaults() {
|
||||
this.setState({actionInProgress: true});
|
||||
this.props.disks.fetch({url: _.result(this.props.nodes.at(0), 'url') + '/disks/defaults/'})
|
||||
.fail((response) => {
|
||||
var ns = 'cluster_page.nodes_tab.configure_disks.configuration_error.';
|
||||
utils.showErrorDialog({
|
||||
title: i18n(ns + 'title'),
|
||||
message: utils.getResponseText(response) || i18n(ns + 'load_defaults_warning')
|
||||
});
|
||||
})
|
||||
.always(() => {
|
||||
this.setState({actionInProgress: false});
|
||||
});
|
||||
},
|
||||
revertChanges() {
|
||||
this.props.disks.reset(_.cloneDeep(this.state.initialDisks), {parse: true});
|
||||
},
|
||||
applyChanges() {
|
||||
if (!this.isSavingPossible()) return $.Deferred().reject();
|
||||
|
||||
this.setState({actionInProgress: true});
|
||||
return $.when(...this.props.nodes.map(function(node) {
|
||||
node.disks.each(function(disk, index) {
|
||||
disk.set({volumes: new models.Volumes(this.props.disks.at(index).get('volumes').toJSON())});
|
||||
}, this);
|
||||
return Backbone.sync('update', node.disks, {url: _.result(node, 'url') + '/disks'});
|
||||
}, this))
|
||||
.done(this.updateInitialData)
|
||||
.fail((response) => {
|
||||
var ns = 'cluster_page.nodes_tab.configure_disks.configuration_error.';
|
||||
utils.showErrorDialog({
|
||||
title: i18n(ns + 'title'),
|
||||
message: utils.getResponseText(response) || i18n(ns + 'saving_warning')
|
||||
});
|
||||
})
|
||||
.always(() => {
|
||||
this.setState({actionInProgress: false});
|
||||
});
|
||||
},
|
||||
getDiskMetaData(disk) {
|
||||
var result,
|
||||
disksMetaData = this.props.nodes.at(0).get('meta').disks;
|
||||
// try to find disk metadata by matching "extra" field
|
||||
// if at least one entry presents both in disk and metadata entry,
|
||||
// this metadata entry is for our disk
|
||||
var extra = disk.get('extra') || [];
|
||||
result = _.find(disksMetaData, (diskMetaData) =>
|
||||
_.isArray(diskMetaData.extra) && _.intersection(diskMetaData.extra, extra).length
|
||||
</div>
|
||||
);
|
||||
// if matching "extra" fields doesn't work, try to search by disk id
|
||||
if (!result) {
|
||||
result = _.find(disksMetaData, {disk: disk.id});
|
||||
}, this)}
|
||||
<div 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-size'>{utils.showDiskSize(volumesInfo.unallocated.size, 2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='row collapse disk-details' id={disk.get('name')} key='diskDetails' ref={disk.get('name')}>
|
||||
<div className='col-xs-5'>
|
||||
{diskMetaData &&
|
||||
<div>
|
||||
<h5>{i18n(ns + 'disk_information')}</h5>
|
||||
<div className='form-horizontal disk-info-box'>
|
||||
{_.map(utils.sortEntryProperties(diskMetaData, sortOrder), (propertyName) => {
|
||||
return (
|
||||
<div className='form-group' key={'property_' + propertyName}>
|
||||
<label className='col-xs-2'>{propertyName.replace(/_/g, ' ')}</label>
|
||||
<div className='col-xs-10'>
|
||||
<p className='form-control-static'>
|
||||
{propertyName == 'size' ? utils.showDiskSize(diskMetaData[propertyName]) : diskMetaData[propertyName]}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
return result;
|
||||
},
|
||||
getVolumesInfo(disk) {
|
||||
var volumes = {},
|
||||
unallocatedWidth = 100;
|
||||
disk.get('volumes').each(function(volume) {
|
||||
var size = volume.get('size') || 0,
|
||||
width = this.getVolumeWidth(disk, size),
|
||||
name = volume.get('name');
|
||||
unallocatedWidth -= width;
|
||||
volumes[name] = {
|
||||
size: size,
|
||||
width: width,
|
||||
max: volume.getMaxSize(),
|
||||
min: volume.getMinimalSize(this.props.volumes.findWhere({name: name}).get('min_size')),
|
||||
error: volume.validationError
|
||||
</div>
|
||||
<div className='col-xs-7'>
|
||||
<h5>{i18n(ns + 'volume_groups')}</h5>
|
||||
<div className='form-horizontal disk-utility-box'>
|
||||
{this.props.volumes.map(function(volume, index) {
|
||||
var volumeName = volume.get('name'),
|
||||
value = volumesInfo[volumeName].size,
|
||||
currentMaxSize = volumesInfo[volumeName].max,
|
||||
currentMinSize = _.max([volumesInfo[volumeName].min, 0]),
|
||||
validationError = volumesInfo[volumeName].error;
|
||||
|
||||
var props = {
|
||||
name: volumeName,
|
||||
min: currentMinSize,
|
||||
max: currentMaxSize,
|
||||
disabled: this.props.disabled || currentMaxSize <= currentMinSize
|
||||
};
|
||||
}, this);
|
||||
volumes.unallocated = {
|
||||
size: disk.getUnallocatedSpace(),
|
||||
width: unallocatedWidth
|
||||
};
|
||||
return volumes;
|
||||
},
|
||||
getVolumeWidth(disk, size) {
|
||||
return disk.get('size') ? utils.floor(size / disk.get('size') * 100, 2) : 0;
|
||||
},
|
||||
hasErrors() {
|
||||
return this.props.disks.any((disk) =>
|
||||
disk.get('volumes').any('validationError')
|
||||
);
|
||||
},
|
||||
isSavingPossible() {
|
||||
return !this.state.actionInProgress && this.hasChanges() && !this.hasErrors();
|
||||
},
|
||||
render() {
|
||||
var hasChanges = this.hasChanges(),
|
||||
locked = this.isLocked(),
|
||||
loadDefaultsDisabled = !!this.state.actionInProgress,
|
||||
revertChangesDisabled = !!this.state.actionInProgress || !hasChanges;
|
||||
return (
|
||||
<div className='edit-node-disks-screen'>
|
||||
<div className='row'>
|
||||
<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')})}
|
||||
</div>
|
||||
<div className='col-xs-12 node-disks'>
|
||||
{this.props.disks.length ?
|
||||
this.props.disks.map(function(disk, index) {
|
||||
return (<NodeDisk
|
||||
disk={disk}
|
||||
key={index}
|
||||
disabled={locked || this.state.actionInProgress}
|
||||
volumes={this.props.volumes}
|
||||
volumesInfo={this.getVolumesInfo(disk)}
|
||||
diskMetaData={this.getDiskMetaData(disk)}
|
||||
/>);
|
||||
}, this)
|
||||
:
|
||||
<div className='alert alert-warning'>
|
||||
{i18n('cluster_page.nodes_tab.configure_disks.no_disks', {count: this.props.nodes.length})}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className='col-xs-12 page-buttons content-elements'>
|
||||
<div className='well clearfix'>
|
||||
<div className='btn-group'>
|
||||
<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')}
|
||||
</a>
|
||||
</div>
|
||||
{!locked && !!this.props.disks.length &&
|
||||
<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 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>
|
||||
|
||||
return (
|
||||
<div key={'edit_' + volumeName} data-volume={volumeName}>
|
||||
<div className='form-group volume-group row'>
|
||||
<label className='col-xs-4 volume-group-label'>
|
||||
<span ref={'volume-group-flag ' + volumeName} className={'volume-type-' + (index + 1)}> </span>
|
||||
{volume.get('label')}
|
||||
</label>
|
||||
<div className='col-xs-4 volume-group-range'>
|
||||
<Input {...props}
|
||||
type='range'
|
||||
ref={'range-' + volumeName}
|
||||
onChange={_.partialRight(this.updateDisk)}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
<Input {...props}
|
||||
type='number'
|
||||
wrapperClassName='col-xs-3 volume-group-input'
|
||||
onChange={_.partialRight(this.updateDisk)}
|
||||
error={validationError && ''}
|
||||
value={value}
|
||||
/>
|
||||
<div className='col-xs-1 volume-group-size-label'>{i18n('common.size.mb')}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
{!!value && value == currentMinSize &&
|
||||
<div className='volume-group-notice text-info'>{i18n(ns + 'minimum_reached')}</div>
|
||||
}
|
||||
{validationError &&
|
||||
<div className='volume-group-notice text-danger'>{validationError}</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}, this)}
|
||||
{diskError &&
|
||||
<div className='volume-group-notice text-danger'>{i18n(ns + 'not_enough_space')}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var NodeDisk = React.createClass({
|
||||
getInitialState() {
|
||||
return {collapsed: true};
|
||||
},
|
||||
componentDidMount() {
|
||||
$('.disk-details', ReactDOM.findDOMNode(this))
|
||||
.on('show.bs.collapse', this.setState.bind(this, {collapsed: true}, null))
|
||||
.on('hide.bs.collapse', this.setState.bind(this, {collapsed: false}, null));
|
||||
},
|
||||
updateDisk(name, value) {
|
||||
var size = parseInt(value, 10) || 0,
|
||||
volumeInfo = this.props.volumesInfo[name];
|
||||
if (size > volumeInfo.max) {
|
||||
size = volumeInfo.max;
|
||||
}
|
||||
this.props.disk.get('volumes').findWhere({name: name}).set({size: size}).isValid({minimum: volumeInfo.min});
|
||||
this.props.disk.trigger('change', this.props.disk);
|
||||
},
|
||||
toggleDisk(name) {
|
||||
$(ReactDOM.findDOMNode(this.refs[name])).collapse('toggle');
|
||||
},
|
||||
render() {
|
||||
var disk = this.props.disk,
|
||||
volumesInfo = this.props.volumesInfo,
|
||||
diskMetaData = this.props.diskMetaData,
|
||||
requiredDiskSize = _.sum(disk.get('volumes').map(function(volume) {
|
||||
return volume.getMinimalSize(this.props.volumes.findWhere({name: volume.get('name')}).get('min_size'));
|
||||
}, this)),
|
||||
diskError = disk.get('size') < requiredDiskSize,
|
||||
sortOrder = ['name', 'model', 'size'],
|
||||
ns = 'cluster_page.nodes_tab.configure_disks.';
|
||||
|
||||
return (
|
||||
<div className='col-xs-12 disk-box' data-disk={disk.id} key={this.props.key}>
|
||||
<div className='row'>
|
||||
<h4 className='col-xs-6'>
|
||||
{disk.get('name')} ({disk.id})
|
||||
</h4>
|
||||
<h4 className='col-xs-6 text-right'>
|
||||
{i18n(ns + 'total_space')} : {utils.showDiskSize(disk.get('size'), 2)}
|
||||
</h4>
|
||||
</div>
|
||||
<div className='row disk-visual clearfix'>
|
||||
{this.props.volumes.map(function(volume, index) {
|
||||
var volumeName = volume.get('name');
|
||||
return (
|
||||
<div
|
||||
key={'volume_' + volumeName}
|
||||
ref={'volume_' + volumeName}
|
||||
className={'volume-group pull-left volume-type-' + (index + 1)}
|
||||
data-volume={volumeName}
|
||||
style={{width: volumesInfo[volumeName].width + '%'}}
|
||||
>
|
||||
<div className='text-center toggle' onClick={_.partial(this.toggleDisk, disk.get('name'))}>
|
||||
<div>{volume.get('label')}</div>
|
||||
<div className='volume-group-size'>
|
||||
{utils.showDiskSize(volumesInfo[volumeName].size, 2)}
|
||||
</div>
|
||||
</div>
|
||||
{!this.props.disabled && volumesInfo[volumeName].min <= 0 && this.state.collapsed &&
|
||||
<div className='close-btn' onClick={_.partial(this.updateDisk, volumeName, 0)}>×</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}, this)}
|
||||
<div 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-size'>{utils.showDiskSize(volumesInfo.unallocated.size, 2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='row collapse disk-details' id={disk.get('name')} key='diskDetails' ref={disk.get('name')}>
|
||||
<div className='col-xs-5'>
|
||||
{diskMetaData &&
|
||||
<div>
|
||||
<h5>{i18n(ns + 'disk_information')}</h5>
|
||||
<div className='form-horizontal disk-info-box'>
|
||||
{_.map(utils.sortEntryProperties(diskMetaData, sortOrder), (propertyName) => {
|
||||
return (
|
||||
<div className='form-group' key={'property_' + propertyName}>
|
||||
<label className='col-xs-2'>{propertyName.replace(/_/g, ' ')}</label>
|
||||
<div className='col-xs-10'>
|
||||
<p className='form-control-static'>
|
||||
{propertyName == 'size' ? utils.showDiskSize(diskMetaData[propertyName]) : diskMetaData[propertyName]}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className='col-xs-7'>
|
||||
<h5>{i18n(ns + 'volume_groups')}</h5>
|
||||
<div className='form-horizontal disk-utility-box'>
|
||||
{this.props.volumes.map(function(volume, index) {
|
||||
var volumeName = volume.get('name'),
|
||||
value = volumesInfo[volumeName].size,
|
||||
currentMaxSize = volumesInfo[volumeName].max,
|
||||
currentMinSize = _.max([volumesInfo[volumeName].min, 0]),
|
||||
validationError = volumesInfo[volumeName].error;
|
||||
|
||||
var props = {
|
||||
name: volumeName,
|
||||
min: currentMinSize,
|
||||
max: currentMaxSize,
|
||||
disabled: this.props.disabled || currentMaxSize <= currentMinSize
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={'edit_' + volumeName} data-volume={volumeName}>
|
||||
<div className='form-group volume-group row'>
|
||||
<label className='col-xs-4 volume-group-label'>
|
||||
<span ref={'volume-group-flag ' + volumeName} className={'volume-type-' + (index + 1)}> </span>
|
||||
{volume.get('label')}
|
||||
</label>
|
||||
<div className='col-xs-4 volume-group-range'>
|
||||
<Input {...props}
|
||||
type='range'
|
||||
ref={'range-' + volumeName}
|
||||
onChange={_.partialRight(this.updateDisk)}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
<Input {...props}
|
||||
type='number'
|
||||
wrapperClassName='col-xs-3 volume-group-input'
|
||||
onChange={_.partialRight(this.updateDisk)}
|
||||
error={validationError && ''}
|
||||
value={value}
|
||||
/>
|
||||
<div className='col-xs-1 volume-group-size-label'>{i18n('common.size.mb')}</div>
|
||||
</div>
|
||||
{!!value && value == currentMinSize &&
|
||||
<div className='volume-group-notice text-info'>{i18n(ns + 'minimum_reached')}</div>
|
||||
}
|
||||
{validationError &&
|
||||
<div className='volume-group-notice text-danger'>{validationError}</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}, this)}
|
||||
{diskError &&
|
||||
<div className='volume-group-notice text-danger'>{i18n(ns + 'not_enough_space')}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default EditNodeDisksScreen;
|
||||
export default EditNodeDisksScreen;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -19,36 +19,36 @@ import React from 'react';
|
|||
import utils from 'utils';
|
||||
import NodeListScreen from 'views/cluster_page_tabs/nodes_tab_screens/node_list_screen';
|
||||
|
||||
var EditNodesScreen = React.createClass({
|
||||
statics: {
|
||||
fetchData(options) {
|
||||
var cluster = options.cluster,
|
||||
nodes = utils.getNodeListFromTabOptions(options);
|
||||
var EditNodesScreen = React.createClass({
|
||||
statics: {
|
||||
fetchData(options) {
|
||||
var cluster = options.cluster,
|
||||
nodes = utils.getNodeListFromTabOptions(options);
|
||||
|
||||
if (!nodes) {
|
||||
return $.Deferred().reject();
|
||||
}
|
||||
if (!nodes) {
|
||||
return $.Deferred().reject();
|
||||
}
|
||||
|
||||
nodes.fetch = function(options) {
|
||||
return this.constructor.__super__.fetch.call(this, _.extend({data: {cluster_id: cluster.id}}, options));
|
||||
};
|
||||
nodes.parse = function() {
|
||||
return this.getByIds(nodes.pluck('id'));
|
||||
};
|
||||
return $.when(options.cluster.get('roles').fetch(),
|
||||
cluster.get('settings').fetch({cache: true})).then(() => ({nodes: nodes}));
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<NodeListScreen
|
||||
{... _.omit(this.props, 'screenOptions')}
|
||||
ref='screen'
|
||||
mode='edit'
|
||||
roles={this.props.cluster.get('roles')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
nodes.fetch = function(options) {
|
||||
return this.constructor.__super__.fetch.call(this, _.extend({data: {cluster_id: cluster.id}}, options));
|
||||
};
|
||||
nodes.parse = function() {
|
||||
return this.getByIds(nodes.pluck('id'));
|
||||
};
|
||||
return $.when(options.cluster.get('roles').fetch(),
|
||||
cluster.get('settings').fetch({cache: true})).then(() => ({nodes: nodes}));
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<NodeListScreen
|
||||
{... _.omit(this.props, 'screenOptions')}
|
||||
ref='screen'
|
||||
mode='edit'
|
||||
roles={this.props.cluster.get('roles')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default EditNodesScreen;
|
||||
export default EditNodesScreen;
|
||||
|
|
|
@ -24,444 +24,444 @@ import {Input, Popover, Tooltip} from 'views/controls';
|
|||
import {DeleteNodesDialog, RemoveOfflineNodeDialog, ShowNodeInfoDialog} from 'views/dialogs';
|
||||
import {renamingMixin} from 'component_mixins';
|
||||
|
||||
var Node = React.createClass({
|
||||
mixins: [renamingMixin('name')],
|
||||
getInitialState() {
|
||||
return {
|
||||
actionInProgress: false,
|
||||
extendedView: false,
|
||||
labelsPopoverVisible: false
|
||||
};
|
||||
},
|
||||
componentDidUpdate() {
|
||||
if (!this.props.node.get('cluster') && !this.props.checked) {
|
||||
this.props.node.set({pending_roles: []}, {assign: true});
|
||||
var Node = React.createClass({
|
||||
mixins: [renamingMixin('name')],
|
||||
getInitialState() {
|
||||
return {
|
||||
actionInProgress: false,
|
||||
extendedView: false,
|
||||
labelsPopoverVisible: false
|
||||
};
|
||||
},
|
||||
componentDidUpdate() {
|
||||
if (!this.props.node.get('cluster') && !this.props.checked) {
|
||||
this.props.node.set({pending_roles: []}, {assign: true});
|
||||
}
|
||||
},
|
||||
getNodeLogsLink() {
|
||||
var status = this.props.node.get('status'),
|
||||
error = this.props.node.get('error_type'),
|
||||
options = {type: 'remote', node: this.props.node.id};
|
||||
if (status == 'discover') {
|
||||
options.source = 'bootstrap/messages';
|
||||
} else if (status == 'provisioning' || status == 'provisioned' || (status == 'error' && error == 'provision')) {
|
||||
options.source = 'install/fuel-agent';
|
||||
} else if (status == 'deploying' || status == 'ready' || (status == 'error' && error == 'deploy')) {
|
||||
options.source = 'install/puppet';
|
||||
}
|
||||
return '#cluster/' + this.props.node.get('cluster') + '/logs/' + utils.serializeTabOptions(options);
|
||||
},
|
||||
applyNewNodeName(newName) {
|
||||
if (newName && newName != this.props.node.get('name')) {
|
||||
this.setState({actionInProgress: true});
|
||||
this.props.node.save({name: newName}, {patch: true, wait: true}).always(this.endRenaming);
|
||||
} else {
|
||||
this.endRenaming();
|
||||
}
|
||||
},
|
||||
onNodeNameInputKeydown(e) {
|
||||
if (e.key == 'Enter') {
|
||||
this.applyNewNodeName(this.refs.name.getInputDOMNode().value);
|
||||
} else if (e.key == 'Escape') {
|
||||
this.endRenaming();
|
||||
}
|
||||
},
|
||||
discardNodeDeletion(e) {
|
||||
e.preventDefault();
|
||||
if (this.state.actionInProgress) return;
|
||||
this.setState({actionInProgress: true});
|
||||
var node = new models.Node(this.props.node.attributes),
|
||||
data = {pending_deletion: false};
|
||||
node.save(data, {patch: true})
|
||||
.done(() => {
|
||||
this.props.cluster.fetchRelated('nodes').done(() => {
|
||||
this.setState({actionInProgress: false});
|
||||
});
|
||||
})
|
||||
.fail((response) => {
|
||||
utils.showErrorDialog({
|
||||
title: i18n('cluster_page.nodes_tab.node.cant_discard'),
|
||||
response: response
|
||||
});
|
||||
});
|
||||
},
|
||||
removeNode(e) {
|
||||
e.preventDefault();
|
||||
if (this.props.viewMode == 'compact') this.toggleExtendedNodePanel();
|
||||
RemoveOfflineNodeDialog
|
||||
.show()
|
||||
.done(() => {
|
||||
// sync('delete') is used instead of node.destroy() because we want
|
||||
// to keep showing the 'Removing' status until the node is truly removed
|
||||
// Otherwise this node would disappear and might reappear again upon
|
||||
// cluster nodes refetch with status 'Removing' which would look ugly
|
||||
// to the end user
|
||||
return Backbone
|
||||
.sync('delete', this.props.node)
|
||||
.then(
|
||||
(task) => {
|
||||
dispatcher.trigger('networkConfigurationUpdated updateNodeStats updateNotifications labelsConfigurationUpdated');
|
||||
if (task.status == 'ready') {
|
||||
// Do not send the 'DELETE' request again, just get rid
|
||||
// of this node.
|
||||
this.props.node.trigger('destroy', this.props.node);
|
||||
return;
|
||||
}
|
||||
if (this.props.cluster) {
|
||||
this.props.cluster.get('tasks').add(new models.Task(task), {parse: true});
|
||||
}
|
||||
this.props.node.set('status', 'removing');
|
||||
},
|
||||
(response) => {
|
||||
utils.showErrorDialog({response: response});
|
||||
}
|
||||
},
|
||||
getNodeLogsLink() {
|
||||
var status = this.props.node.get('status'),
|
||||
error = this.props.node.get('error_type'),
|
||||
options = {type: 'remote', node: this.props.node.id};
|
||||
if (status == 'discover') {
|
||||
options.source = 'bootstrap/messages';
|
||||
} else if (status == 'provisioning' || status == 'provisioned' || (status == 'error' && error == 'provision')) {
|
||||
options.source = 'install/fuel-agent';
|
||||
} else if (status == 'deploying' || status == 'ready' || (status == 'error' && error == 'deploy')) {
|
||||
options.source = 'install/puppet';
|
||||
}
|
||||
return '#cluster/' + this.props.node.get('cluster') + '/logs/' + utils.serializeTabOptions(options);
|
||||
},
|
||||
applyNewNodeName(newName) {
|
||||
if (newName && newName != this.props.node.get('name')) {
|
||||
this.setState({actionInProgress: true});
|
||||
this.props.node.save({name: newName}, {patch: true, wait: true}).always(this.endRenaming);
|
||||
} else {
|
||||
this.endRenaming();
|
||||
}
|
||||
},
|
||||
onNodeNameInputKeydown(e) {
|
||||
if (e.key == 'Enter') {
|
||||
this.applyNewNodeName(this.refs.name.getInputDOMNode().value);
|
||||
} else if (e.key == 'Escape') {
|
||||
this.endRenaming();
|
||||
}
|
||||
},
|
||||
discardNodeDeletion(e) {
|
||||
e.preventDefault();
|
||||
if (this.state.actionInProgress) return;
|
||||
this.setState({actionInProgress: true});
|
||||
var node = new models.Node(this.props.node.attributes),
|
||||
data = {pending_deletion: false};
|
||||
node.save(data, {patch: true})
|
||||
.done(() => {
|
||||
this.props.cluster.fetchRelated('nodes').done(() => {
|
||||
this.setState({actionInProgress: false});
|
||||
});
|
||||
})
|
||||
.fail((response) => {
|
||||
utils.showErrorDialog({
|
||||
title: i18n('cluster_page.nodes_tab.node.cant_discard'),
|
||||
response: response
|
||||
});
|
||||
});
|
||||
},
|
||||
removeNode(e) {
|
||||
e.preventDefault();
|
||||
if (this.props.viewMode == 'compact') this.toggleExtendedNodePanel();
|
||||
RemoveOfflineNodeDialog
|
||||
.show()
|
||||
.done(() => {
|
||||
// sync('delete') is used instead of node.destroy() because we want
|
||||
// to keep showing the 'Removing' status until the node is truly removed
|
||||
// Otherwise this node would disappear and might reappear again upon
|
||||
// cluster nodes refetch with status 'Removing' which would look ugly
|
||||
// to the end user
|
||||
return Backbone
|
||||
.sync('delete', this.props.node)
|
||||
.then(
|
||||
(task) => {
|
||||
dispatcher.trigger('networkConfigurationUpdated updateNodeStats updateNotifications labelsConfigurationUpdated');
|
||||
if (task.status == 'ready') {
|
||||
// Do not send the 'DELETE' request again, just get rid
|
||||
// of this node.
|
||||
this.props.node.trigger('destroy', this.props.node);
|
||||
return;
|
||||
}
|
||||
if (this.props.cluster) {
|
||||
this.props.cluster.get('tasks').add(new models.Task(task), {parse: true});
|
||||
}
|
||||
this.props.node.set('status', 'removing');
|
||||
},
|
||||
(response) => {
|
||||
utils.showErrorDialog({response: response});
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
showNodeDetails(e) {
|
||||
e.preventDefault();
|
||||
if (this.state.extendedView) this.toggleExtendedNodePanel();
|
||||
ShowNodeInfoDialog.show({
|
||||
node: this.props.node,
|
||||
cluster: this.props.cluster,
|
||||
nodeNetworkGroup: this.props.nodeNetworkGroups.get(this.props.node.get('group_id')),
|
||||
renderActionButtons: this.props.renderActionButtons
|
||||
});
|
||||
},
|
||||
toggleExtendedNodePanel() {
|
||||
var states = this.state.extendedView ? {extendedView: false, isRenaming: false} : {extendedView: true};
|
||||
this.setState(states);
|
||||
},
|
||||
renderNameControl() {
|
||||
if (this.state.isRenaming) return (
|
||||
<Input
|
||||
ref='name'
|
||||
type='text'
|
||||
name='node-name'
|
||||
defaultValue={this.props.node.get('name')}
|
||||
inputClassName='form-control node-name-input'
|
||||
disabled={this.state.actionInProgress}
|
||||
onKeyDown={this.onNodeNameInputKeydown}
|
||||
maxLength='100'
|
||||
selectOnFocus
|
||||
autoFocus
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Tooltip text={i18n('cluster_page.nodes_tab.node.edit_name')}>
|
||||
<p onClick={!this.state.actionInProgress && this.startRenaming}>
|
||||
{this.props.node.get('name') || this.props.node.get('mac')}
|
||||
</p>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
renderStatusLabel(status) {
|
||||
return (
|
||||
<span>
|
||||
{i18n('cluster_page.nodes_tab.node.status.' + status, {
|
||||
os: this.props.cluster && this.props.cluster.get('release').get('operating_system') || 'OS'
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
renderNodeProgress(status) {
|
||||
var nodeProgress = this.props.node.get('progress');
|
||||
return (
|
||||
<div className='progress'>
|
||||
{status &&
|
||||
<div className='progress-bar-title'>
|
||||
{this.renderStatusLabel(status)}
|
||||
{': ' + nodeProgress + '%'}
|
||||
</div>
|
||||
}
|
||||
<div className='progress-bar' role='progressbar' style={{width: _.max([nodeProgress, 3]) + '%'}}></div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
renderNodeHardwareSummary() {
|
||||
var htCores = this.props.node.resource('ht_cores'),
|
||||
hdd = this.props.node.resource('hdd'),
|
||||
ram = this.props.node.resource('ram');
|
||||
return (
|
||||
<div className='node-hardware'>
|
||||
<span>{i18n('node_details.cpu')}: {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>
|
||||
);
|
||||
},
|
||||
renderLogsLink(iconRepresentation) {
|
||||
return (
|
||||
<Tooltip 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')}
|
||||
</a>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
renderNodeCheckbox() {
|
||||
return (
|
||||
<Input
|
||||
type='checkbox'
|
||||
name={this.props.node.id}
|
||||
checked={this.props.checked}
|
||||
disabled={this.props.locked || !this.props.node.isSelectable() || this.props.mode == 'edit'}
|
||||
onChange={this.props.mode != 'edit' ? this.props.onNodeSelection : _.noop}
|
||||
wrapperClassName='pull-left'
|
||||
/>
|
||||
);
|
||||
},
|
||||
renderRemoveButton() {
|
||||
return (
|
||||
<button onClick={this.removeNode} className='btn node-remove-button'>
|
||||
{i18n('cluster_page.nodes_tab.node.remove')}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
renderRoleList(roles) {
|
||||
return (
|
||||
<ul>
|
||||
{_.map(roles, function(role) {
|
||||
return (
|
||||
<li
|
||||
key={this.props.node.id + role}
|
||||
className={utils.classNames({'text-success': !this.props.node.get('roles').length})}
|
||||
>
|
||||
{role}
|
||||
</li>
|
||||
);
|
||||
}, this)}
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
showDeleteNodesDialog(e) {
|
||||
e.preventDefault();
|
||||
if (this.props.viewMode == 'compact') this.toggleExtendedNodePanel();
|
||||
DeleteNodesDialog
|
||||
.show({
|
||||
nodes: new models.Nodes(this.props.node),
|
||||
cluster: this.props.cluster
|
||||
})
|
||||
.done(this.props.onNodeSelection);
|
||||
},
|
||||
renderLabels() {
|
||||
var labels = this.props.node.get('labels');
|
||||
if (_.isEmpty(labels)) return null;
|
||||
return (
|
||||
<ul>
|
||||
{_.map(_.keys(labels).sort(_.partialRight(utils.natsort, {insensitive: true})), (key) => {
|
||||
var value = labels[key];
|
||||
return (
|
||||
<li key={key + value} className='label'>
|
||||
{key + (_.isNull(value) ? '' : ' "' + value + '"')}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
toggleLabelsPopover(visible) {
|
||||
this.setState({
|
||||
labelsPopoverVisible: _.isBoolean(visible) ? visible : !this.state.labelsPopoverVisible
|
||||
});
|
||||
},
|
||||
render() {
|
||||
var ns = 'cluster_page.nodes_tab.node.',
|
||||
node = this.props.node,
|
||||
isSelectable = node.isSelectable() && !this.props.locked && this.props.mode != 'edit',
|
||||
status = node.getStatusSummary(),
|
||||
roles = this.props.cluster ? node.sortedRoles(this.props.cluster.get('roles').pluck('name')) : [];
|
||||
|
||||
// compose classes
|
||||
var nodePanelClasses = {
|
||||
node: true,
|
||||
selected: this.props.checked,
|
||||
'col-xs-12': this.props.viewMode != 'compact',
|
||||
unavailable: !isSelectable
|
||||
};
|
||||
nodePanelClasses[status] = status;
|
||||
|
||||
var manufacturer = node.get('manufacturer') || '',
|
||||
logoClasses = {
|
||||
'manufacturer-logo': true
|
||||
};
|
||||
logoClasses[manufacturer.toLowerCase()] = manufacturer;
|
||||
|
||||
var statusClasses = {
|
||||
'node-status': true
|
||||
},
|
||||
statusClass = {
|
||||
pending_addition: 'text-success',
|
||||
pending_deletion: 'text-warning',
|
||||
error: 'text-danger',
|
||||
ready: 'text-info',
|
||||
provisioning: 'text-info',
|
||||
deploying: 'text-success',
|
||||
provisioned: 'text-info'
|
||||
}[status];
|
||||
statusClasses[statusClass] = true;
|
||||
|
||||
if (this.props.viewMode == 'compact') return (
|
||||
<div className='compact-node'>
|
||||
<div className={utils.classNames(nodePanelClasses)}>
|
||||
<label className='node-box'>
|
||||
<div
|
||||
className='node-box-inner clearfix'
|
||||
onClick={isSelectable && _.partial(this.props.onNodeSelection, null, !this.props.checked)}
|
||||
>
|
||||
<div className='node-checkbox'>
|
||||
{this.props.checked && <i className='glyphicon glyphicon-ok' />}
|
||||
</div>
|
||||
<div className='node-name'>
|
||||
<p>{node.get('name') || node.get('mac')}</p>
|
||||
</div>
|
||||
<div className={utils.classNames(statusClasses)}>
|
||||
{_.contains(['provisioning', 'deploying'], status) ?
|
||||
this.renderNodeProgress()
|
||||
:
|
||||
this.renderStatusLabel(status)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className='node-hardware'>
|
||||
<p>
|
||||
<span>
|
||||
{node.resource('cores') || '0'} ({node.resource('ht_cores') || '?'})
|
||||
</span> / <span>
|
||||
{node.resource('hdd') ? utils.showDiskSize(node.resource('hdd')) : '?' + i18n('common.size.gb')}
|
||||
</span> / <span>
|
||||
{node.resource('ram') ? utils.showMemorySize(node.resource('ram')) : '?' + i18n('common.size.gb')}
|
||||
</span>
|
||||
</p>
|
||||
<p className='btn btn-link' onClick={this.toggleExtendedNodePanel}>
|
||||
{i18n(ns + 'more_info')}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
{this.state.extendedView &&
|
||||
<Popover className='node-popover' toggle={this.toggleExtendedNodePanel}>
|
||||
<div>
|
||||
<div className='node-name clearfix'>
|
||||
{this.renderNodeCheckbox()}
|
||||
<div className='name pull-left'>
|
||||
{this.renderNameControl()}
|
||||
</div>
|
||||
</div>
|
||||
<div className='node-stats'>
|
||||
{!!roles.length &&
|
||||
<div className='role-list'>
|
||||
<i className='glyphicon glyphicon-pushpin' />
|
||||
{this.renderRoleList(roles)}
|
||||
</div>
|
||||
}
|
||||
{!_.isEmpty(this.props.node.get('labels')) &&
|
||||
<div className='node-labels'>
|
||||
<i className='glyphicon glyphicon-tags pull-left' />
|
||||
{this.renderLabels()}
|
||||
</div>
|
||||
}
|
||||
<div className={utils.classNames(statusClasses)}>
|
||||
<i className='glyphicon glyphicon-time' />
|
||||
{_.contains(['provisioning', 'deploying'], status) ?
|
||||
<div>
|
||||
{this.renderStatusLabel(status)}
|
||||
<div className='node-buttons'>
|
||||
{this.renderLogsLink()}
|
||||
</div>
|
||||
{this.renderNodeProgress(status)}
|
||||
</div>
|
||||
:
|
||||
<div>
|
||||
{this.renderStatusLabel(status)}
|
||||
<div className='node-buttons'>
|
||||
{status == 'offline' && this.renderRemoveButton()}
|
||||
{[
|
||||
!!node.get('cluster') && this.renderLogsLink(),
|
||||
this.props.renderActionButtons && node.hasChanges() && !this.props.locked &&
|
||||
<button
|
||||
className='btn btn-discard'
|
||||
key='btn-discard'
|
||||
onClick={node.get('pending_deletion') ? this.discardNodeDeletion : this.showDeleteNodesDialog}
|
||||
>
|
||||
{i18n(ns + (node.get('pending_deletion') ? 'discard_deletion' : 'delete_node'))}
|
||||
</button>
|
||||
]}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className='hardware-info clearfix'>
|
||||
<div className={utils.classNames(logoClasses)} />
|
||||
{this.renderNodeHardwareSummary()}
|
||||
</div>
|
||||
<div className='node-popover-buttons'>
|
||||
<button className='btn btn-default node-settings' onClick={this.showNodeDetails}>Details</button>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={utils.classNames(nodePanelClasses)}>
|
||||
<label className='node-box'>
|
||||
{this.renderNodeCheckbox()}
|
||||
<div className={utils.classNames(logoClasses)} />
|
||||
<div className='node-name'>
|
||||
<div className='name'>
|
||||
{this.renderNameControl()}
|
||||
</div>
|
||||
<div className='role-list'>
|
||||
{this.renderRoleList(roles)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='node-labels'>
|
||||
{!_.isEmpty(node.get('labels')) &&
|
||||
<button className='btn btn-link' onClick={this.toggleLabelsPopover}>
|
||||
<i className='glyphicon glyphicon-tag-alt' />
|
||||
{_.keys(node.get('labels')).length}
|
||||
</button>
|
||||
}
|
||||
{this.state.labelsPopoverVisible &&
|
||||
<Popover className='node-labels-popover' toggle={this.toggleLabelsPopover}>
|
||||
{this.renderLabels()}
|
||||
</Popover>
|
||||
}
|
||||
</div>
|
||||
<div className='node-action'>
|
||||
{[
|
||||
!!node.get('cluster') && this.renderLogsLink(true),
|
||||
this.props.renderActionButtons && node.hasChanges() && !this.props.locked &&
|
||||
<Tooltip
|
||||
key={'pending_addition_' + node.id}
|
||||
text={i18n(ns + (node.get('pending_deletion') ? 'discard_deletion' : 'delete_node'))}
|
||||
>
|
||||
<div
|
||||
className='icon btn-discard'
|
||||
onClick={node.get('pending_deletion') ? this.discardNodeDeletion : this.showDeleteNodesDialog}
|
||||
/>
|
||||
</Tooltip>
|
||||
]}
|
||||
</div>
|
||||
<div className={utils.classNames(statusClasses)}>
|
||||
{_.contains(['provisioning', 'deploying'], status) ?
|
||||
this.renderNodeProgress(status)
|
||||
:
|
||||
<div>
|
||||
{this.renderStatusLabel(status)}
|
||||
{status == 'offline' && this.renderRemoveButton()}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{this.renderNodeHardwareSummary()}
|
||||
<div className='node-settings' onClick={this.showNodeDetails} />
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
showNodeDetails(e) {
|
||||
e.preventDefault();
|
||||
if (this.state.extendedView) this.toggleExtendedNodePanel();
|
||||
ShowNodeInfoDialog.show({
|
||||
node: this.props.node,
|
||||
cluster: this.props.cluster,
|
||||
nodeNetworkGroup: this.props.nodeNetworkGroups.get(this.props.node.get('group_id')),
|
||||
renderActionButtons: this.props.renderActionButtons
|
||||
});
|
||||
},
|
||||
toggleExtendedNodePanel() {
|
||||
var states = this.state.extendedView ? {extendedView: false, isRenaming: false} : {extendedView: true};
|
||||
this.setState(states);
|
||||
},
|
||||
renderNameControl() {
|
||||
if (this.state.isRenaming) return (
|
||||
<Input
|
||||
ref='name'
|
||||
type='text'
|
||||
name='node-name'
|
||||
defaultValue={this.props.node.get('name')}
|
||||
inputClassName='form-control node-name-input'
|
||||
disabled={this.state.actionInProgress}
|
||||
onKeyDown={this.onNodeNameInputKeydown}
|
||||
maxLength='100'
|
||||
selectOnFocus
|
||||
autoFocus
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Tooltip text={i18n('cluster_page.nodes_tab.node.edit_name')}>
|
||||
<p onClick={!this.state.actionInProgress && this.startRenaming}>
|
||||
{this.props.node.get('name') || this.props.node.get('mac')}
|
||||
</p>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
renderStatusLabel(status) {
|
||||
return (
|
||||
<span>
|
||||
{i18n('cluster_page.nodes_tab.node.status.' + status, {
|
||||
os: this.props.cluster && this.props.cluster.get('release').get('operating_system') || 'OS'
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
renderNodeProgress(status) {
|
||||
var nodeProgress = this.props.node.get('progress');
|
||||
return (
|
||||
<div className='progress'>
|
||||
{status &&
|
||||
<div className='progress-bar-title'>
|
||||
{this.renderStatusLabel(status)}
|
||||
{': ' + nodeProgress + '%'}
|
||||
</div>
|
||||
}
|
||||
<div className='progress-bar' role='progressbar' style={{width: _.max([nodeProgress, 3]) + '%'}}></div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
renderNodeHardwareSummary() {
|
||||
var htCores = this.props.node.resource('ht_cores'),
|
||||
hdd = this.props.node.resource('hdd'),
|
||||
ram = this.props.node.resource('ram');
|
||||
return (
|
||||
<div className='node-hardware'>
|
||||
<span>{i18n('node_details.cpu')}: {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>
|
||||
);
|
||||
},
|
||||
renderLogsLink(iconRepresentation) {
|
||||
return (
|
||||
<Tooltip 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')}
|
||||
</a>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
renderNodeCheckbox() {
|
||||
return (
|
||||
<Input
|
||||
type='checkbox'
|
||||
name={this.props.node.id}
|
||||
checked={this.props.checked}
|
||||
disabled={this.props.locked || !this.props.node.isSelectable() || this.props.mode == 'edit'}
|
||||
onChange={this.props.mode != 'edit' ? this.props.onNodeSelection : _.noop}
|
||||
wrapperClassName='pull-left'
|
||||
/>
|
||||
);
|
||||
},
|
||||
renderRemoveButton() {
|
||||
return (
|
||||
<button onClick={this.removeNode} className='btn node-remove-button'>
|
||||
{i18n('cluster_page.nodes_tab.node.remove')}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
renderRoleList(roles) {
|
||||
return (
|
||||
<ul>
|
||||
{_.map(roles, function(role) {
|
||||
return (
|
||||
<li
|
||||
key={this.props.node.id + role}
|
||||
className={utils.classNames({'text-success': !this.props.node.get('roles').length})}
|
||||
>
|
||||
{role}
|
||||
</li>
|
||||
);
|
||||
}, this)}
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
showDeleteNodesDialog(e) {
|
||||
e.preventDefault();
|
||||
if (this.props.viewMode == 'compact') this.toggleExtendedNodePanel();
|
||||
DeleteNodesDialog
|
||||
.show({
|
||||
nodes: new models.Nodes(this.props.node),
|
||||
cluster: this.props.cluster
|
||||
})
|
||||
.done(this.props.onNodeSelection);
|
||||
},
|
||||
renderLabels() {
|
||||
var labels = this.props.node.get('labels');
|
||||
if (_.isEmpty(labels)) return null;
|
||||
return (
|
||||
<ul>
|
||||
{_.map(_.keys(labels).sort(_.partialRight(utils.natsort, {insensitive: true})), (key) => {
|
||||
var value = labels[key];
|
||||
return (
|
||||
<li key={key + value} className='label'>
|
||||
{key + (_.isNull(value) ? '' : ' "' + value + '"')}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
toggleLabelsPopover(visible) {
|
||||
this.setState({
|
||||
labelsPopoverVisible: _.isBoolean(visible) ? visible : !this.state.labelsPopoverVisible
|
||||
});
|
||||
},
|
||||
render() {
|
||||
var ns = 'cluster_page.nodes_tab.node.',
|
||||
node = this.props.node,
|
||||
isSelectable = node.isSelectable() && !this.props.locked && this.props.mode != 'edit',
|
||||
status = node.getStatusSummary(),
|
||||
roles = this.props.cluster ? node.sortedRoles(this.props.cluster.get('roles').pluck('name')) : [];
|
||||
|
||||
export default Node;
|
||||
// compose classes
|
||||
var nodePanelClasses = {
|
||||
node: true,
|
||||
selected: this.props.checked,
|
||||
'col-xs-12': this.props.viewMode != 'compact',
|
||||
unavailable: !isSelectable
|
||||
};
|
||||
nodePanelClasses[status] = status;
|
||||
|
||||
var manufacturer = node.get('manufacturer') || '',
|
||||
logoClasses = {
|
||||
'manufacturer-logo': true
|
||||
};
|
||||
logoClasses[manufacturer.toLowerCase()] = manufacturer;
|
||||
|
||||
var statusClasses = {
|
||||
'node-status': true
|
||||
},
|
||||
statusClass = {
|
||||
pending_addition: 'text-success',
|
||||
pending_deletion: 'text-warning',
|
||||
error: 'text-danger',
|
||||
ready: 'text-info',
|
||||
provisioning: 'text-info',
|
||||
deploying: 'text-success',
|
||||
provisioned: 'text-info'
|
||||
}[status];
|
||||
statusClasses[statusClass] = true;
|
||||
|
||||
if (this.props.viewMode == 'compact') return (
|
||||
<div className='compact-node'>
|
||||
<div className={utils.classNames(nodePanelClasses)}>
|
||||
<label className='node-box'>
|
||||
<div
|
||||
className='node-box-inner clearfix'
|
||||
onClick={isSelectable && _.partial(this.props.onNodeSelection, null, !this.props.checked)}
|
||||
>
|
||||
<div className='node-checkbox'>
|
||||
{this.props.checked && <i className='glyphicon glyphicon-ok' />}
|
||||
</div>
|
||||
<div className='node-name'>
|
||||
<p>{node.get('name') || node.get('mac')}</p>
|
||||
</div>
|
||||
<div className={utils.classNames(statusClasses)}>
|
||||
{_.contains(['provisioning', 'deploying'], status) ?
|
||||
this.renderNodeProgress()
|
||||
:
|
||||
this.renderStatusLabel(status)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className='node-hardware'>
|
||||
<p>
|
||||
<span>
|
||||
{node.resource('cores') || '0'} ({node.resource('ht_cores') || '?'})
|
||||
</span> / <span>
|
||||
{node.resource('hdd') ? utils.showDiskSize(node.resource('hdd')) : '?' + i18n('common.size.gb')}
|
||||
</span> / <span>
|
||||
{node.resource('ram') ? utils.showMemorySize(node.resource('ram')) : '?' + i18n('common.size.gb')}
|
||||
</span>
|
||||
</p>
|
||||
<p className='btn btn-link' onClick={this.toggleExtendedNodePanel}>
|
||||
{i18n(ns + 'more_info')}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
{this.state.extendedView &&
|
||||
<Popover className='node-popover' toggle={this.toggleExtendedNodePanel}>
|
||||
<div>
|
||||
<div className='node-name clearfix'>
|
||||
{this.renderNodeCheckbox()}
|
||||
<div className='name pull-left'>
|
||||
{this.renderNameControl()}
|
||||
</div>
|
||||
</div>
|
||||
<div className='node-stats'>
|
||||
{!!roles.length &&
|
||||
<div className='role-list'>
|
||||
<i className='glyphicon glyphicon-pushpin' />
|
||||
{this.renderRoleList(roles)}
|
||||
</div>
|
||||
}
|
||||
{!_.isEmpty(this.props.node.get('labels')) &&
|
||||
<div className='node-labels'>
|
||||
<i className='glyphicon glyphicon-tags pull-left' />
|
||||
{this.renderLabels()}
|
||||
</div>
|
||||
}
|
||||
<div className={utils.classNames(statusClasses)}>
|
||||
<i className='glyphicon glyphicon-time' />
|
||||
{_.contains(['provisioning', 'deploying'], status) ?
|
||||
<div>
|
||||
{this.renderStatusLabel(status)}
|
||||
<div className='node-buttons'>
|
||||
{this.renderLogsLink()}
|
||||
</div>
|
||||
{this.renderNodeProgress(status)}
|
||||
</div>
|
||||
:
|
||||
<div>
|
||||
{this.renderStatusLabel(status)}
|
||||
<div className='node-buttons'>
|
||||
{status == 'offline' && this.renderRemoveButton()}
|
||||
{[
|
||||
!!node.get('cluster') && this.renderLogsLink(),
|
||||
this.props.renderActionButtons && node.hasChanges() && !this.props.locked &&
|
||||
<button
|
||||
className='btn btn-discard'
|
||||
key='btn-discard'
|
||||
onClick={node.get('pending_deletion') ? this.discardNodeDeletion : this.showDeleteNodesDialog}
|
||||
>
|
||||
{i18n(ns + (node.get('pending_deletion') ? 'discard_deletion' : 'delete_node'))}
|
||||
</button>
|
||||
]}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className='hardware-info clearfix'>
|
||||
<div className={utils.classNames(logoClasses)} />
|
||||
{this.renderNodeHardwareSummary()}
|
||||
</div>
|
||||
<div className='node-popover-buttons'>
|
||||
<button className='btn btn-default node-settings' onClick={this.showNodeDetails}>Details</button>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={utils.classNames(nodePanelClasses)}>
|
||||
<label className='node-box'>
|
||||
{this.renderNodeCheckbox()}
|
||||
<div className={utils.classNames(logoClasses)} />
|
||||
<div className='node-name'>
|
||||
<div className='name'>
|
||||
{this.renderNameControl()}
|
||||
</div>
|
||||
<div className='role-list'>
|
||||
{this.renderRoleList(roles)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='node-labels'>
|
||||
{!_.isEmpty(node.get('labels')) &&
|
||||
<button className='btn btn-link' onClick={this.toggleLabelsPopover}>
|
||||
<i className='glyphicon glyphicon-tag-alt' />
|
||||
{_.keys(node.get('labels')).length}
|
||||
</button>
|
||||
}
|
||||
{this.state.labelsPopoverVisible &&
|
||||
<Popover className='node-labels-popover' toggle={this.toggleLabelsPopover}>
|
||||
{this.renderLabels()}
|
||||
</Popover>
|
||||
}
|
||||
</div>
|
||||
<div className='node-action'>
|
||||
{[
|
||||
!!node.get('cluster') && this.renderLogsLink(true),
|
||||
this.props.renderActionButtons && node.hasChanges() && !this.props.locked &&
|
||||
<Tooltip
|
||||
key={'pending_addition_' + node.id}
|
||||
text={i18n(ns + (node.get('pending_deletion') ? 'discard_deletion' : 'delete_node'))}
|
||||
>
|
||||
<div
|
||||
className='icon btn-discard'
|
||||
onClick={node.get('pending_deletion') ? this.discardNodeDeletion : this.showDeleteNodesDialog}
|
||||
/>
|
||||
</Tooltip>
|
||||
]}
|
||||
</div>
|
||||
<div className={utils.classNames(statusClasses)}>
|
||||
{_.contains(['provisioning', 'deploying'], status) ?
|
||||
this.renderNodeProgress(status)
|
||||
:
|
||||
<div>
|
||||
{this.renderStatusLabel(status)}
|
||||
{status == 'offline' && this.renderRemoveButton()}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{this.renderNodeHardwareSummary()}
|
||||
<div className='node-settings' onClick={this.showNodeDetails} />
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default Node;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -18,168 +18,169 @@ import i18n from 'i18n';
|
|||
import React from 'react';
|
||||
import utils from 'utils';
|
||||
|
||||
var ns = 'cluster_page.nodes_tab.configure_interfaces.',
|
||||
OffloadingModesControl = React.createClass({
|
||||
propTypes: {
|
||||
interface: React.PropTypes.object
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
isVisible: false
|
||||
};
|
||||
},
|
||||
toggleVisibility() {
|
||||
this.setState({isVisible: !this.state.isVisible});
|
||||
},
|
||||
setModeState(mode, state) {
|
||||
mode.state = state;
|
||||
_.each(mode.sub, (mode) => this.setModeState(mode, state));
|
||||
},
|
||||
checkModes(mode, sub) {
|
||||
var changedState = sub.reduce((state, childMode) => {
|
||||
if (!_.isEmpty(childMode.sub)) {
|
||||
this.checkModes(childMode, childMode.sub);
|
||||
}
|
||||
return (state === 0 || state === childMode.state) ? childMode.state : -1;
|
||||
},
|
||||
0
|
||||
),
|
||||
oldState;
|
||||
var ns = 'cluster_page.nodes_tab.configure_interfaces.';
|
||||
|
||||
if (mode && mode.state != changedState) {
|
||||
oldState = mode.state;
|
||||
mode.state = oldState === false ? null : (changedState === false ? false : oldState);
|
||||
}
|
||||
},
|
||||
findMode(name, modes) {
|
||||
var result,
|
||||
mode,
|
||||
index = 0,
|
||||
modesLength = modes.length;
|
||||
for (; index < modesLength; index++) {
|
||||
mode = modes[index];
|
||||
if (mode.name == name) {
|
||||
return mode;
|
||||
} else if (!_.isEmpty(mode.sub)) {
|
||||
result = this.findMode(name, mode.sub);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
onModeStateChange(name, state) {
|
||||
var modes = _.cloneDeep(this.props.interface.get('offloading_modes') || []),
|
||||
mode = this.findMode(name, modes);
|
||||
|
||||
return () => {
|
||||
if (mode) {
|
||||
this.setModeState(mode, state);
|
||||
this.checkModes(null, modes);
|
||||
} else {
|
||||
// handle All Modes click
|
||||
_.each(modes, function(mode) {
|
||||
return this.setModeState(mode, state);
|
||||
}, this);
|
||||
}
|
||||
this.props.interface.set('offloading_modes', modes);
|
||||
};
|
||||
},
|
||||
makeOffloadingModesExcerpt() {
|
||||
var states = {
|
||||
true: i18n(ns + 'offloading_enabled'),
|
||||
false: i18n(ns + 'offloading_disabled'),
|
||||
null: i18n(ns + 'offloading_default')
|
||||
},
|
||||
ifcModes = this.props.interface.get('offloading_modes');
|
||||
|
||||
if (_.uniq(_.pluck(ifcModes, 'state')).length == 1) {
|
||||
return states[ifcModes[0].state];
|
||||
}
|
||||
|
||||
var lastState,
|
||||
added = 0,
|
||||
excerpt = [];
|
||||
_.each(ifcModes,
|
||||
(mode) => {
|
||||
if (!_.isNull(mode.state) && mode.state !== lastState) {
|
||||
lastState = mode.state;
|
||||
added++;
|
||||
excerpt.push((added > 1 ? ', ' : '') + mode.name + ' ' + states[mode.state]);
|
||||
}
|
||||
// show no more than two modes in the button
|
||||
if (added == 2) return false;
|
||||
}
|
||||
);
|
||||
if (added < ifcModes.length) excerpt.push(', ...');
|
||||
return excerpt;
|
||||
},
|
||||
renderChildModes(modes, level) {
|
||||
return modes.map((mode) => {
|
||||
var lines = [
|
||||
<tr key={mode.name} className={'level' + level}>
|
||||
<td>{mode.name}</td>
|
||||
{[true, false, null].map((modeState) => {
|
||||
var styles = {
|
||||
'btn-link': true,
|
||||
active: mode.state === modeState
|
||||
};
|
||||
return (
|
||||
<td key={mode.name + modeState}>
|
||||
<button
|
||||
className={utils.classNames(styles)}
|
||||
disabled={this.props.disabled}
|
||||
onClick={this.onModeStateChange(mode.name, modeState)}>
|
||||
<i className='glyphicon glyphicon-ok'></i>
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
];
|
||||
if (mode.sub) {
|
||||
return _.union([lines, this.renderChildModes(mode.sub, level + 1)]);
|
||||
}
|
||||
return lines;
|
||||
});
|
||||
},
|
||||
render() {
|
||||
var modes = [],
|
||||
ifcModes = this.props.interface.get('offloading_modes');
|
||||
if (ifcModes) {
|
||||
modes.push({
|
||||
name: i18n(ns + 'all_modes'),
|
||||
state: _.uniq(_.pluck(ifcModes, 'state')).length == 1 ? ifcModes[0].state : undefined,
|
||||
sub: ifcModes
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='offloading-modes'>
|
||||
<div>
|
||||
<button className='btn btn-default' onClick={this.toggleVisibility}>
|
||||
{i18n(ns + 'offloading_modes')}: {this.makeOffloadingModesExcerpt()}
|
||||
</button>
|
||||
{this.state.isVisible &&
|
||||
<table className='table'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{i18n(ns + 'offloading_mode')}</th>
|
||||
<th>{i18n(ns + 'offloading_enabled')}</th>
|
||||
<th>{i18n(ns + 'offloading_disabled')}</th>
|
||||
<th>{i18n(ns + 'offloading_default')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.renderChildModes(modes, 1)}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
var OffloadingModesControl = React.createClass({
|
||||
propTypes: {
|
||||
interface: React.PropTypes.object
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
isVisible: false
|
||||
};
|
||||
},
|
||||
toggleVisibility() {
|
||||
this.setState({isVisible: !this.state.isVisible});
|
||||
},
|
||||
setModeState(mode, state) {
|
||||
mode.state = state;
|
||||
_.each(mode.sub, (mode) => this.setModeState(mode, state));
|
||||
},
|
||||
checkModes(mode, sub) {
|
||||
var changedState = sub.reduce((state, childMode) => {
|
||||
if (!_.isEmpty(childMode.sub)) {
|
||||
this.checkModes(childMode, childMode.sub);
|
||||
}
|
||||
});
|
||||
return (state === 0 || state === childMode.state) ? childMode.state : -1;
|
||||
},
|
||||
0
|
||||
),
|
||||
oldState;
|
||||
|
||||
export default OffloadingModesControl;
|
||||
if (mode && mode.state != changedState) {
|
||||
oldState = mode.state;
|
||||
mode.state = oldState === false ? null : (changedState === false ? false : oldState);
|
||||
}
|
||||
},
|
||||
findMode(name, modes) {
|
||||
var result,
|
||||
mode,
|
||||
index = 0,
|
||||
modesLength = modes.length;
|
||||
for (; index < modesLength; index++) {
|
||||
mode = modes[index];
|
||||
if (mode.name == name) {
|
||||
return mode;
|
||||
} else if (!_.isEmpty(mode.sub)) {
|
||||
result = this.findMode(name, mode.sub);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
onModeStateChange(name, state) {
|
||||
var modes = _.cloneDeep(this.props.interface.get('offloading_modes') || []),
|
||||
mode = this.findMode(name, modes);
|
||||
|
||||
return () => {
|
||||
if (mode) {
|
||||
this.setModeState(mode, state);
|
||||
this.checkModes(null, modes);
|
||||
} else {
|
||||
// handle All Modes click
|
||||
_.each(modes, function(mode) {
|
||||
return this.setModeState(mode, state);
|
||||
}, this);
|
||||
}
|
||||
this.props.interface.set('offloading_modes', modes);
|
||||
};
|
||||
},
|
||||
makeOffloadingModesExcerpt() {
|
||||
var states = {
|
||||
true: i18n(ns + 'offloading_enabled'),
|
||||
false: i18n(ns + 'offloading_disabled'),
|
||||
null: i18n(ns + 'offloading_default')
|
||||
},
|
||||
ifcModes = this.props.interface.get('offloading_modes');
|
||||
|
||||
if (_.uniq(_.pluck(ifcModes, 'state')).length == 1) {
|
||||
return states[ifcModes[0].state];
|
||||
}
|
||||
|
||||
var lastState,
|
||||
added = 0,
|
||||
excerpt = [];
|
||||
_.each(ifcModes,
|
||||
(mode) => {
|
||||
if (!_.isNull(mode.state) && mode.state !== lastState) {
|
||||
lastState = mode.state;
|
||||
added++;
|
||||
excerpt.push((added > 1 ? ', ' : '') + mode.name + ' ' + states[mode.state]);
|
||||
}
|
||||
// show no more than two modes in the button
|
||||
if (added == 2) return false;
|
||||
}
|
||||
);
|
||||
if (added < ifcModes.length) excerpt.push(', ...');
|
||||
return excerpt;
|
||||
},
|
||||
renderChildModes(modes, level) {
|
||||
return modes.map((mode) => {
|
||||
var lines = [
|
||||
<tr key={mode.name} className={'level' + level}>
|
||||
<td>{mode.name}</td>
|
||||
{[true, false, null].map((modeState) => {
|
||||
var styles = {
|
||||
'btn-link': true,
|
||||
active: mode.state === modeState
|
||||
};
|
||||
return (
|
||||
<td key={mode.name + modeState}>
|
||||
<button
|
||||
className={utils.classNames(styles)}
|
||||
disabled={this.props.disabled}
|
||||
onClick={this.onModeStateChange(mode.name, modeState)}>
|
||||
<i className='glyphicon glyphicon-ok'></i>
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
];
|
||||
if (mode.sub) {
|
||||
return _.union([lines, this.renderChildModes(mode.sub, level + 1)]);
|
||||
}
|
||||
return lines;
|
||||
});
|
||||
},
|
||||
render() {
|
||||
var modes = [],
|
||||
ifcModes = this.props.interface.get('offloading_modes');
|
||||
if (ifcModes) {
|
||||
modes.push({
|
||||
name: i18n(ns + 'all_modes'),
|
||||
state: _.uniq(_.pluck(ifcModes, 'state')).length == 1 ? ifcModes[0].state : undefined,
|
||||
sub: ifcModes
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='offloading-modes'>
|
||||
<div>
|
||||
<button className='btn btn-default' onClick={this.toggleVisibility}>
|
||||
{i18n(ns + 'offloading_modes')}: {this.makeOffloadingModesExcerpt()}
|
||||
</button>
|
||||
{this.state.isVisible &&
|
||||
<table className='table'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{i18n(ns + 'offloading_mode')}</th>
|
||||
<th>{i18n(ns + 'offloading_enabled')}</th>
|
||||
<th>{i18n(ns + 'offloading_disabled')}</th>
|
||||
<th>{i18n(ns + 'offloading_default')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.renderChildModes(modes, 1)}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default OffloadingModesControl;
|
||||
|
|
|
@ -21,269 +21,269 @@ import Expression from 'expression';
|
|||
import {Input, RadioGroup} from 'views/controls';
|
||||
import customControls from 'views/custom_controls';
|
||||
|
||||
var SettingSection = React.createClass({
|
||||
processRestrictions(setting, settingName) {
|
||||
var result = false,
|
||||
restrictionsCheck = this.props.checkRestrictions('disable', setting),
|
||||
messagesCheck = this.props.checkRestrictions('none', setting),
|
||||
messages = _.compact([restrictionsCheck.message, messagesCheck.message]);
|
||||
var SettingSection = React.createClass({
|
||||
processRestrictions(setting, settingName) {
|
||||
var result = false,
|
||||
restrictionsCheck = this.props.checkRestrictions('disable', setting),
|
||||
messagesCheck = this.props.checkRestrictions('none', setting),
|
||||
messages = _.compact([restrictionsCheck.message, messagesCheck.message]);
|
||||
|
||||
// FIXME: hack for #1442475 to lock images_ceph in env with controllers
|
||||
if (settingName == 'images_ceph') {
|
||||
if (_.contains(_.flatten(this.props.cluster.get('nodes').pluck('pending_roles')), 'controller')) {
|
||||
result = true;
|
||||
messages.push(i18n('cluster_page.settings_tab.images_ceph_warning'));
|
||||
}
|
||||
}
|
||||
// FIXME: hack for #1442475 to lock images_ceph in env with controllers
|
||||
if (settingName == 'images_ceph') {
|
||||
if (_.contains(_.flatten(this.props.cluster.get('nodes').pluck('pending_roles')), 'controller')) {
|
||||
result = true;
|
||||
messages.push(i18n('cluster_page.settings_tab.images_ceph_warning'));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
result: result || restrictionsCheck.result,
|
||||
message: messages.join(' ')
|
||||
};
|
||||
},
|
||||
checkDependencies(sectionName, settingName) {
|
||||
var messages = [],
|
||||
dependentRoles = this.checkDependentRoles(sectionName, settingName),
|
||||
dependentSettings = this.checkDependentSettings(sectionName, settingName);
|
||||
return {
|
||||
result: result || restrictionsCheck.result,
|
||||
message: messages.join(' ')
|
||||
};
|
||||
},
|
||||
checkDependencies(sectionName, settingName) {
|
||||
var messages = [],
|
||||
dependentRoles = this.checkDependentRoles(sectionName, settingName),
|
||||
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 (dependentSettings.length) messages.push(i18n('cluster_page.settings_tab.dependent_settings_warning', {settings: dependentSettings.join(', '), count: dependentSettings.length}));
|
||||
if (dependentRoles.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 {
|
||||
result: !!dependentRoles.length || !!dependentSettings.length,
|
||||
message: messages.join(' ')
|
||||
};
|
||||
},
|
||||
areCalculationsPossible(setting) {
|
||||
return setting.toggleable || _.contains(['checkbox', 'radio'], setting.type);
|
||||
},
|
||||
getValuesToCheck(setting, valueAttribute) {
|
||||
return setting.values ? _.without(_.pluck(setting.values, 'data'), setting[valueAttribute]) : [!setting[valueAttribute]];
|
||||
},
|
||||
checkValues(values, path, currentValue, restriction) {
|
||||
var extraModels = {settings: this.props.settingsForChecks};
|
||||
var result = _.all(values, function(value) {
|
||||
this.props.settingsForChecks.set(path, value);
|
||||
return new Expression(restriction.condition, this.props.configModels, restriction).evaluate(extraModels);
|
||||
}, this);
|
||||
this.props.settingsForChecks.set(path, currentValue);
|
||||
return result;
|
||||
},
|
||||
checkDependentRoles(sectionName, settingName) {
|
||||
if (!this.props.allocatedRoles.length) return [];
|
||||
var path = this.props.makePath(sectionName, settingName),
|
||||
setting = this.props.settings.get(path);
|
||||
if (!this.areCalculationsPossible(setting)) return [];
|
||||
var valueAttribute = this.props.getValueAttribute(settingName),
|
||||
valuesToCheck = this.getValuesToCheck(setting, valueAttribute),
|
||||
pathToCheck = this.props.makePath(path, valueAttribute),
|
||||
roles = this.props.cluster.get('roles');
|
||||
return _.compact(this.props.allocatedRoles.map(function(roleName) {
|
||||
var role = roles.findWhere({name: roleName});
|
||||
if (_.any(role.get('restrictions'), (restriction) => {
|
||||
restriction = utils.expandRestriction(restriction);
|
||||
if (_.contains(restriction.condition, 'settings:' + path) && !(new Expression(restriction.condition, this.props.configModels, restriction).evaluate())) {
|
||||
return this.checkValues(valuesToCheck, pathToCheck, setting[valueAttribute], restriction);
|
||||
}
|
||||
})) return role.get('label');
|
||||
}, this));
|
||||
},
|
||||
checkDependentSettings(sectionName, settingName) {
|
||||
var path = this.props.makePath(sectionName, settingName),
|
||||
currentSetting = this.props.settings.get(path);
|
||||
if (!this.areCalculationsPossible(currentSetting)) return [];
|
||||
var dependentRestrictions = {};
|
||||
var addDependentRestrictions = (setting, label) => {
|
||||
var result = _.filter(_.map(setting.restrictions, utils.expandRestriction),
|
||||
(restriction) => restriction.action == 'disable' && _.contains(restriction.condition, 'settings:' + path)
|
||||
);
|
||||
if (result.length) {
|
||||
dependentRestrictions[label] = result.concat(dependentRestrictions[label] || []);
|
||||
}
|
||||
};
|
||||
// collect dependencies
|
||||
_.each(this.props.settings.attributes, function(section, sectionName) {
|
||||
// don't take into account hidden dependent settings
|
||||
if (this.props.checkRestrictions('hide', section.metadata).result) return;
|
||||
_.each(section, function(setting, settingName) {
|
||||
// we support dependecies on checkboxes, toggleable setting groups, dropdowns and radio groups
|
||||
if (!this.areCalculationsPossible(setting) ||
|
||||
this.props.makePath(sectionName, settingName) == path ||
|
||||
this.props.checkRestrictions('hide', setting).result
|
||||
) return;
|
||||
if (setting[this.props.getValueAttribute(settingName)] == true) {
|
||||
addDependentRestrictions(setting, setting.label);
|
||||
} else {
|
||||
var activeOption = _.find(setting.values, {data: setting.value});
|
||||
if (activeOption) addDependentRestrictions(activeOption, setting.label);
|
||||
}
|
||||
}, this);
|
||||
}, this);
|
||||
// evaluate dependencies
|
||||
if (!_.isEmpty(dependentRestrictions)) {
|
||||
var valueAttribute = this.props.getValueAttribute(settingName),
|
||||
pathToCheck = this.props.makePath(path, valueAttribute),
|
||||
valuesToCheck = this.getValuesToCheck(currentSetting, valueAttribute),
|
||||
checkValues = _.partial(this.checkValues, valuesToCheck, pathToCheck, currentSetting[valueAttribute]);
|
||||
return _.compact(_.map(dependentRestrictions, (restrictions, label) => {
|
||||
if (_.any(restrictions, checkValues)) return label;
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
},
|
||||
composeOptions(values) {
|
||||
return _.map(values, (value, index) => {
|
||||
return (
|
||||
<option key={index} value={value.data} disabled={value.disabled}>
|
||||
{value.label}
|
||||
</option>
|
||||
);
|
||||
});
|
||||
},
|
||||
onPluginVersionChange(pluginName, version) {
|
||||
var settings = this.props.settings;
|
||||
// FIXME: the following hacks cause we can't pass {validate: true} option to set method
|
||||
// this form of validation isn't supported in Backbone DeepModel
|
||||
settings.validationError = null;
|
||||
settings.set(this.props.makePath(pluginName, 'metadata', 'chosen_id'), Number(version));
|
||||
settings.mergePluginSettings();
|
||||
settings.isValid({models: this.props.configModels});
|
||||
this.props.settingsForChecks.set(_.cloneDeep(settings.attributes));
|
||||
},
|
||||
togglePlugin(pluginName, settingName, enabled) {
|
||||
this.props.onChange(settingName, enabled);
|
||||
var pluginMetadata = this.props.settings.get(pluginName).metadata;
|
||||
if (enabled) {
|
||||
// check for editable plugin version
|
||||
var chosenVersionData = _.find(pluginMetadata.versions, (version) => version.metadata.plugin_id == pluginMetadata.chosen_id);
|
||||
if (this.props.lockedCluster && !chosenVersionData.metadata.always_editable) {
|
||||
var editableVersion = _.find(pluginMetadata.versions, (version) => version.metadata.always_editable).metadata.plugin_id;
|
||||
this.onPluginVersionChange(pluginName, editableVersion);
|
||||
}
|
||||
} else {
|
||||
var initialVersion = this.props.initialAttributes[pluginName].metadata.chosen_id;
|
||||
if (pluginMetadata.chosen_id !== initialVersion) this.onPluginVersionChange(pluginName, initialVersion);
|
||||
}
|
||||
},
|
||||
render() {
|
||||
var {settings, sectionName} = this.props,
|
||||
section = settings.get(sectionName),
|
||||
isPlugin = settings.isPlugin(section),
|
||||
metadata = section.metadata,
|
||||
sortedSettings = _.sortBy(this.props.settingsToDisplay, (settingName) => section[settingName].weight),
|
||||
processedGroupRestrictions = this.processRestrictions(metadata),
|
||||
processedGroupDependencies = this.checkDependencies(sectionName, 'metadata'),
|
||||
isGroupAlwaysEditable = isPlugin ? _.any(metadata.versions, (version) => version.metadata.always_editable) : metadata.always_editable,
|
||||
isGroupDisabled = this.props.locked || (this.props.lockedCluster && !isGroupAlwaysEditable) || processedGroupRestrictions.result,
|
||||
showSettingGroupWarning = !this.props.lockedCluster || metadata.always_editable,
|
||||
groupWarning = _.compact([processedGroupRestrictions.message, processedGroupDependencies.message]).join(' ');
|
||||
|
||||
return (
|
||||
<div className={'setting-section setting-section-' + sectionName}>
|
||||
<h3>
|
||||
{metadata.toggleable ?
|
||||
<Input
|
||||
type='checkbox'
|
||||
name='metadata'
|
||||
label={metadata.label || sectionName}
|
||||
defaultChecked={metadata.enabled}
|
||||
disabled={isGroupDisabled || processedGroupDependencies.result}
|
||||
tooltipText={showSettingGroupWarning && groupWarning}
|
||||
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>
|
||||
}
|
||||
</h3>
|
||||
<div>
|
||||
{isPlugin &&
|
||||
<div className='plugin-versions clearfix'>
|
||||
<RadioGroup
|
||||
key={metadata.chosen_id}
|
||||
name={sectionName}
|
||||
label={i18n('cluster_page.settings_tab.plugin_versions')}
|
||||
values={_.map(metadata.versions, (version) => {
|
||||
return {
|
||||
data: version.metadata.plugin_id,
|
||||
label: version.metadata.plugin_version,
|
||||
defaultChecked: version.metadata.plugin_id == metadata.chosen_id,
|
||||
disabled: this.props.locked || (this.props.lockedCluster && !version.metadata.always_editable) || processedGroupRestrictions.result || (metadata.toggleable && !metadata.enabled)
|
||||
};
|
||||
}, this)}
|
||||
onChange={this.onPluginVersionChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{_.map(sortedSettings, function(settingName) {
|
||||
var setting = section[settingName],
|
||||
settingKey = settingName + (isPlugin ? '-' + metadata.chosen_id : ''),
|
||||
path = this.props.makePath(sectionName, settingName),
|
||||
error = (settings.validationError || {})[path],
|
||||
processedSettingRestrictions = this.processRestrictions(setting, settingName),
|
||||
processedSettingDependencies = this.checkDependencies(sectionName, settingName),
|
||||
isSettingDisabled = isGroupDisabled || (metadata.toggleable && !metadata.enabled) || processedSettingRestrictions.result || processedSettingDependencies.result,
|
||||
showSettingWarning = showSettingGroupWarning && !isGroupDisabled && (!metadata.toggleable || metadata.enabled),
|
||||
settingWarning = _.compact([processedSettingRestrictions.message, processedSettingDependencies.message]).join(' ');
|
||||
|
||||
// support of custom controls
|
||||
var CustomControl = customControls[setting.type];
|
||||
if (CustomControl) {
|
||||
return <CustomControl
|
||||
{...setting}
|
||||
{... _.pick(this.props, 'cluster', 'settings', 'configModels')}
|
||||
key={settingKey}
|
||||
path={path}
|
||||
error={error}
|
||||
disabled={isSettingDisabled}
|
||||
tooltipText={showSettingWarning && settingWarning}
|
||||
/>;
|
||||
}
|
||||
|
||||
if (setting.values) {
|
||||
var values = _.chain(_.cloneDeep(setting.values))
|
||||
.map(function(value) {
|
||||
var processedValueRestrictions = this.props.checkRestrictions('disable', value);
|
||||
if (!this.props.checkRestrictions('hide', value).result) {
|
||||
value.disabled = isSettingDisabled || processedValueRestrictions.result;
|
||||
value.defaultChecked = value.data == setting.value;
|
||||
value.tooltipText = showSettingWarning && processedValueRestrictions.message;
|
||||
return value;
|
||||
}
|
||||
}, this)
|
||||
.compact()
|
||||
.value();
|
||||
if (setting.type == 'radio') return <RadioGroup {...this.props}
|
||||
key={settingKey}
|
||||
name={settingName}
|
||||
label={setting.label}
|
||||
values={values}
|
||||
error={error}
|
||||
tooltipText={showSettingWarning && settingWarning}
|
||||
/>;
|
||||
}
|
||||
|
||||
var settingDescription = setting.description &&
|
||||
<span dangerouslySetInnerHTML={{__html: utils.urlify(_.escape(setting.description))}} />;
|
||||
return <Input
|
||||
{... _.pick(setting, 'type', 'label')}
|
||||
key={settingKey}
|
||||
name={settingName}
|
||||
description={settingDescription}
|
||||
children={setting.type == 'select' ? this.composeOptions(setting.values) : null}
|
||||
debounce={setting.type == 'text' || setting.type == 'password' || setting.type == 'textarea'}
|
||||
defaultValue={setting.value}
|
||||
defaultChecked={_.isBoolean(setting.value) ? setting.value : false}
|
||||
toggleable={setting.type == 'password'}
|
||||
error={error}
|
||||
disabled={isSettingDisabled}
|
||||
tooltipText={showSettingWarning && settingWarning}
|
||||
onChange={this.props.onChange}
|
||||
/>;
|
||||
}, this)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return {
|
||||
result: !!dependentRoles.length || !!dependentSettings.length,
|
||||
message: messages.join(' ')
|
||||
};
|
||||
},
|
||||
areCalculationsPossible(setting) {
|
||||
return setting.toggleable || _.contains(['checkbox', 'radio'], setting.type);
|
||||
},
|
||||
getValuesToCheck(setting, valueAttribute) {
|
||||
return setting.values ? _.without(_.pluck(setting.values, 'data'), setting[valueAttribute]) : [!setting[valueAttribute]];
|
||||
},
|
||||
checkValues(values, path, currentValue, restriction) {
|
||||
var extraModels = {settings: this.props.settingsForChecks};
|
||||
var result = _.all(values, function(value) {
|
||||
this.props.settingsForChecks.set(path, value);
|
||||
return new Expression(restriction.condition, this.props.configModels, restriction).evaluate(extraModels);
|
||||
}, this);
|
||||
this.props.settingsForChecks.set(path, currentValue);
|
||||
return result;
|
||||
},
|
||||
checkDependentRoles(sectionName, settingName) {
|
||||
if (!this.props.allocatedRoles.length) return [];
|
||||
var path = this.props.makePath(sectionName, settingName),
|
||||
setting = this.props.settings.get(path);
|
||||
if (!this.areCalculationsPossible(setting)) return [];
|
||||
var valueAttribute = this.props.getValueAttribute(settingName),
|
||||
valuesToCheck = this.getValuesToCheck(setting, valueAttribute),
|
||||
pathToCheck = this.props.makePath(path, valueAttribute),
|
||||
roles = this.props.cluster.get('roles');
|
||||
return _.compact(this.props.allocatedRoles.map(function(roleName) {
|
||||
var role = roles.findWhere({name: roleName});
|
||||
if (_.any(role.get('restrictions'), (restriction) => {
|
||||
restriction = utils.expandRestriction(restriction);
|
||||
if (_.contains(restriction.condition, 'settings:' + path) && !(new Expression(restriction.condition, this.props.configModels, restriction).evaluate())) {
|
||||
return this.checkValues(valuesToCheck, pathToCheck, setting[valueAttribute], restriction);
|
||||
}
|
||||
})) return role.get('label');
|
||||
}, this));
|
||||
},
|
||||
checkDependentSettings(sectionName, settingName) {
|
||||
var path = this.props.makePath(sectionName, settingName),
|
||||
currentSetting = this.props.settings.get(path);
|
||||
if (!this.areCalculationsPossible(currentSetting)) return [];
|
||||
var dependentRestrictions = {};
|
||||
var addDependentRestrictions = (setting, label) => {
|
||||
var result = _.filter(_.map(setting.restrictions, utils.expandRestriction),
|
||||
(restriction) => restriction.action == 'disable' && _.contains(restriction.condition, 'settings:' + path)
|
||||
);
|
||||
if (result.length) {
|
||||
dependentRestrictions[label] = result.concat(dependentRestrictions[label] || []);
|
||||
}
|
||||
};
|
||||
// collect dependencies
|
||||
_.each(this.props.settings.attributes, function(section, sectionName) {
|
||||
// don't take into account hidden dependent settings
|
||||
if (this.props.checkRestrictions('hide', section.metadata).result) return;
|
||||
_.each(section, function(setting, settingName) {
|
||||
// we support dependecies on checkboxes, toggleable setting groups, dropdowns and radio groups
|
||||
if (!this.areCalculationsPossible(setting) ||
|
||||
this.props.makePath(sectionName, settingName) == path ||
|
||||
this.props.checkRestrictions('hide', setting).result
|
||||
) return;
|
||||
if (setting[this.props.getValueAttribute(settingName)] == true) {
|
||||
addDependentRestrictions(setting, setting.label);
|
||||
} else {
|
||||
var activeOption = _.find(setting.values, {data: setting.value});
|
||||
if (activeOption) addDependentRestrictions(activeOption, setting.label);
|
||||
}
|
||||
}, this);
|
||||
}, this);
|
||||
// evaluate dependencies
|
||||
if (!_.isEmpty(dependentRestrictions)) {
|
||||
var valueAttribute = this.props.getValueAttribute(settingName),
|
||||
pathToCheck = this.props.makePath(path, valueAttribute),
|
||||
valuesToCheck = this.getValuesToCheck(currentSetting, valueAttribute),
|
||||
checkValues = _.partial(this.checkValues, valuesToCheck, pathToCheck, currentSetting[valueAttribute]);
|
||||
return _.compact(_.map(dependentRestrictions, (restrictions, label) => {
|
||||
if (_.any(restrictions, checkValues)) return label;
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
},
|
||||
composeOptions(values) {
|
||||
return _.map(values, (value, index) => {
|
||||
return (
|
||||
<option key={index} value={value.data} disabled={value.disabled}>
|
||||
{value.label}
|
||||
</option>
|
||||
);
|
||||
});
|
||||
},
|
||||
onPluginVersionChange(pluginName, version) {
|
||||
var settings = this.props.settings;
|
||||
// FIXME: the following hacks cause we can't pass {validate: true} option to set method
|
||||
// this form of validation isn't supported in Backbone DeepModel
|
||||
settings.validationError = null;
|
||||
settings.set(this.props.makePath(pluginName, 'metadata', 'chosen_id'), Number(version));
|
||||
settings.mergePluginSettings();
|
||||
settings.isValid({models: this.props.configModels});
|
||||
this.props.settingsForChecks.set(_.cloneDeep(settings.attributes));
|
||||
},
|
||||
togglePlugin(pluginName, settingName, enabled) {
|
||||
this.props.onChange(settingName, enabled);
|
||||
var pluginMetadata = this.props.settings.get(pluginName).metadata;
|
||||
if (enabled) {
|
||||
// check for editable plugin version
|
||||
var chosenVersionData = _.find(pluginMetadata.versions, (version) => version.metadata.plugin_id == pluginMetadata.chosen_id);
|
||||
if (this.props.lockedCluster && !chosenVersionData.metadata.always_editable) {
|
||||
var editableVersion = _.find(pluginMetadata.versions, (version) => version.metadata.always_editable).metadata.plugin_id;
|
||||
this.onPluginVersionChange(pluginName, editableVersion);
|
||||
}
|
||||
} else {
|
||||
var initialVersion = this.props.initialAttributes[pluginName].metadata.chosen_id;
|
||||
if (pluginMetadata.chosen_id !== initialVersion) this.onPluginVersionChange(pluginName, initialVersion);
|
||||
}
|
||||
},
|
||||
render() {
|
||||
var {settings, sectionName} = this.props,
|
||||
section = settings.get(sectionName),
|
||||
isPlugin = settings.isPlugin(section),
|
||||
metadata = section.metadata,
|
||||
sortedSettings = _.sortBy(this.props.settingsToDisplay, (settingName) => section[settingName].weight),
|
||||
processedGroupRestrictions = this.processRestrictions(metadata),
|
||||
processedGroupDependencies = this.checkDependencies(sectionName, 'metadata'),
|
||||
isGroupAlwaysEditable = isPlugin ? _.any(metadata.versions, (version) => version.metadata.always_editable) : metadata.always_editable,
|
||||
isGroupDisabled = this.props.locked || (this.props.lockedCluster && !isGroupAlwaysEditable) || processedGroupRestrictions.result,
|
||||
showSettingGroupWarning = !this.props.lockedCluster || metadata.always_editable,
|
||||
groupWarning = _.compact([processedGroupRestrictions.message, processedGroupDependencies.message]).join(' ');
|
||||
|
||||
export default SettingSection;
|
||||
return (
|
||||
<div className={'setting-section setting-section-' + sectionName}>
|
||||
<h3>
|
||||
{metadata.toggleable ?
|
||||
<Input
|
||||
type='checkbox'
|
||||
name='metadata'
|
||||
label={metadata.label || sectionName}
|
||||
defaultChecked={metadata.enabled}
|
||||
disabled={isGroupDisabled || processedGroupDependencies.result}
|
||||
tooltipText={showSettingGroupWarning && groupWarning}
|
||||
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>
|
||||
}
|
||||
</h3>
|
||||
<div>
|
||||
{isPlugin &&
|
||||
<div className='plugin-versions clearfix'>
|
||||
<RadioGroup
|
||||
key={metadata.chosen_id}
|
||||
name={sectionName}
|
||||
label={i18n('cluster_page.settings_tab.plugin_versions')}
|
||||
values={_.map(metadata.versions, (version) => {
|
||||
return {
|
||||
data: version.metadata.plugin_id,
|
||||
label: version.metadata.plugin_version,
|
||||
defaultChecked: version.metadata.plugin_id == metadata.chosen_id,
|
||||
disabled: this.props.locked || (this.props.lockedCluster && !version.metadata.always_editable) || processedGroupRestrictions.result || (metadata.toggleable && !metadata.enabled)
|
||||
};
|
||||
}, this)}
|
||||
onChange={this.onPluginVersionChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{_.map(sortedSettings, function(settingName) {
|
||||
var setting = section[settingName],
|
||||
settingKey = settingName + (isPlugin ? '-' + metadata.chosen_id : ''),
|
||||
path = this.props.makePath(sectionName, settingName),
|
||||
error = (settings.validationError || {})[path],
|
||||
processedSettingRestrictions = this.processRestrictions(setting, settingName),
|
||||
processedSettingDependencies = this.checkDependencies(sectionName, settingName),
|
||||
isSettingDisabled = isGroupDisabled || (metadata.toggleable && !metadata.enabled) || processedSettingRestrictions.result || processedSettingDependencies.result,
|
||||
showSettingWarning = showSettingGroupWarning && !isGroupDisabled && (!metadata.toggleable || metadata.enabled),
|
||||
settingWarning = _.compact([processedSettingRestrictions.message, processedSettingDependencies.message]).join(' ');
|
||||
|
||||
// support of custom controls
|
||||
var CustomControl = customControls[setting.type];
|
||||
if (CustomControl) {
|
||||
return <CustomControl
|
||||
{...setting}
|
||||
{... _.pick(this.props, 'cluster', 'settings', 'configModels')}
|
||||
key={settingKey}
|
||||
path={path}
|
||||
error={error}
|
||||
disabled={isSettingDisabled}
|
||||
tooltipText={showSettingWarning && settingWarning}
|
||||
/>;
|
||||
}
|
||||
|
||||
if (setting.values) {
|
||||
var values = _.chain(_.cloneDeep(setting.values))
|
||||
.map(function(value) {
|
||||
var processedValueRestrictions = this.props.checkRestrictions('disable', value);
|
||||
if (!this.props.checkRestrictions('hide', value).result) {
|
||||
value.disabled = isSettingDisabled || processedValueRestrictions.result;
|
||||
value.defaultChecked = value.data == setting.value;
|
||||
value.tooltipText = showSettingWarning && processedValueRestrictions.message;
|
||||
return value;
|
||||
}
|
||||
}, this)
|
||||
.compact()
|
||||
.value();
|
||||
if (setting.type == 'radio') return <RadioGroup {...this.props}
|
||||
key={settingKey}
|
||||
name={settingName}
|
||||
label={setting.label}
|
||||
values={values}
|
||||
error={error}
|
||||
tooltipText={showSettingWarning && settingWarning}
|
||||
/>;
|
||||
}
|
||||
|
||||
var settingDescription = setting.description &&
|
||||
<span dangerouslySetInnerHTML={{__html: utils.urlify(_.escape(setting.description))}} />;
|
||||
return <Input
|
||||
{... _.pick(setting, 'type', 'label')}
|
||||
key={settingKey}
|
||||
name={settingName}
|
||||
description={settingDescription}
|
||||
children={setting.type == 'select' ? this.composeOptions(setting.values) : null}
|
||||
debounce={setting.type == 'text' || setting.type == 'password' || setting.type == 'textarea'}
|
||||
defaultValue={setting.value}
|
||||
defaultChecked={_.isBoolean(setting.value) ? setting.value : false}
|
||||
toggleable={setting.type == 'password'}
|
||||
error={error}
|
||||
disabled={isSettingDisabled}
|
||||
tooltipText={showSettingWarning && settingWarning}
|
||||
onChange={this.props.onChange}
|
||||
/>;
|
||||
}, this)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default SettingSection;
|
||||
|
|
|
@ -23,324 +23,324 @@ import {backboneMixin, unsavedChangesMixin} from 'component_mixins';
|
|||
import SettingSection from 'views/cluster_page_tabs/setting_section';
|
||||
import CSSTransitionGroup from 'react-addons-transition-group';
|
||||
|
||||
var SettingsTab = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin('cluster', 'change:status'),
|
||||
backboneMixin({
|
||||
modelOrCollection(props) {
|
||||
return props.cluster.get('settings');
|
||||
},
|
||||
renderOn: 'change invalid'
|
||||
}),
|
||||
backboneMixin({modelOrCollection(props) {
|
||||
return props.cluster.get('tasks');
|
||||
}}),
|
||||
backboneMixin({modelOrCollection(props) {
|
||||
return props.cluster.task({group: 'deployment', active: true});
|
||||
}}),
|
||||
unsavedChangesMixin
|
||||
],
|
||||
statics: {
|
||||
fetchData(options) {
|
||||
return $.when(options.cluster.get('settings').fetch({cache: true}),
|
||||
options.cluster.get('networkConfiguration').fetch({cache: true})).then(() => ({}));
|
||||
var SettingsTab = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin('cluster', 'change:status'),
|
||||
backboneMixin({
|
||||
modelOrCollection(props) {
|
||||
return props.cluster.get('settings');
|
||||
},
|
||||
renderOn: 'change invalid'
|
||||
}),
|
||||
backboneMixin({modelOrCollection(props) {
|
||||
return props.cluster.get('tasks');
|
||||
}}),
|
||||
backboneMixin({modelOrCollection(props) {
|
||||
return props.cluster.task({group: 'deployment', active: true});
|
||||
}}),
|
||||
unsavedChangesMixin
|
||||
],
|
||||
statics: {
|
||||
fetchData(options) {
|
||||
return $.when(options.cluster.get('settings').fetch({cache: true}),
|
||||
options.cluster.get('networkConfiguration').fetch({cache: true})).then(() => ({}));
|
||||
}
|
||||
},
|
||||
getInitialState() {
|
||||
var settings = this.props.cluster.get('settings');
|
||||
return {
|
||||
configModels: {
|
||||
cluster: this.props.cluster,
|
||||
settings: settings,
|
||||
networking_parameters: this.props.cluster.get('networkConfiguration').get('networking_parameters'),
|
||||
version: app.version,
|
||||
release: this.props.cluster.get('release'),
|
||||
default: settings
|
||||
},
|
||||
settingsForChecks: new models.Settings(_.cloneDeep(settings.attributes)),
|
||||
initialAttributes: _.cloneDeep(settings.attributes),
|
||||
actionInProgress: false
|
||||
};
|
||||
},
|
||||
componentDidMount() {
|
||||
this.props.cluster.get('settings').isValid({models: this.state.configModels});
|
||||
},
|
||||
componentWillUnmount() {
|
||||
this.loadInitialSettings();
|
||||
},
|
||||
hasChanges() {
|
||||
return this.props.cluster.get('settings').hasChanges(this.state.initialAttributes, this.state.configModels);
|
||||
},
|
||||
applyChanges() {
|
||||
if (!this.isSavingPossible()) return $.Deferred().reject();
|
||||
|
||||
// collecting data to save
|
||||
var settings = this.props.cluster.get('settings'),
|
||||
dataToSave = this.props.cluster.isAvailableForSettingsChanges() ? settings.attributes :
|
||||
_.pick(settings.attributes, (group) => (group.metadata || {}).always_editable);
|
||||
var options = {url: settings.url, patch: true, wait: true, validate: false},
|
||||
deferred = new models.Settings(_.cloneDeep(dataToSave)).save(null, options);
|
||||
if (deferred) {
|
||||
this.setState({actionInProgress: true});
|
||||
deferred
|
||||
.done(() => {
|
||||
this.setState({initialAttributes: _.cloneDeep(settings.attributes)});
|
||||
// some networks may have restrictions which are processed by nailgun,
|
||||
// so networks need to be refetched after updating cluster attributes
|
||||
this.props.cluster.get('networkConfiguration').cancelThrottling();
|
||||
})
|
||||
.always(() => {
|
||||
this.setState({
|
||||
actionInProgress: false,
|
||||
key: _.now()
|
||||
});
|
||||
this.props.cluster.fetch();
|
||||
})
|
||||
.fail((response) => {
|
||||
utils.showErrorDialog({
|
||||
title: i18n('cluster_page.settings_tab.settings_error.title'),
|
||||
message: i18n('cluster_page.settings_tab.settings_error.saving_warning'),
|
||||
response: response
|
||||
});
|
||||
});
|
||||
}
|
||||
return deferred;
|
||||
},
|
||||
loadDefaults() {
|
||||
var settings = this.props.cluster.get('settings'),
|
||||
lockedCluster = !this.props.cluster.isAvailableForSettingsChanges(),
|
||||
defaultSettings = new models.Settings(),
|
||||
deferred = defaultSettings.fetch({url: _.result(this.props.cluster, 'url') + '/attributes/defaults'});
|
||||
|
||||
if (deferred) {
|
||||
this.setState({actionInProgress: true});
|
||||
deferred
|
||||
.done(() => {
|
||||
_.each(settings.attributes, (section, sectionName) => {
|
||||
if ((!lockedCluster || section.metadata.always_editable) && section.metadata.group != 'network') {
|
||||
_.each(section, (setting, settingName) => {
|
||||
// do not update hidden settings (hack for #1442143),
|
||||
// the same for settings with group network
|
||||
if (setting.type == 'hidden' || setting.group == 'network') return;
|
||||
var path = settings.makePath(sectionName, settingName);
|
||||
settings.set(path, defaultSettings.get(path), {silent: true});
|
||||
});
|
||||
}
|
||||
},
|
||||
getInitialState() {
|
||||
var settings = this.props.cluster.get('settings');
|
||||
return {
|
||||
configModels: {
|
||||
cluster: this.props.cluster,
|
||||
settings: settings,
|
||||
networking_parameters: this.props.cluster.get('networkConfiguration').get('networking_parameters'),
|
||||
version: app.version,
|
||||
release: this.props.cluster.get('release'),
|
||||
default: settings
|
||||
},
|
||||
settingsForChecks: new models.Settings(_.cloneDeep(settings.attributes)),
|
||||
initialAttributes: _.cloneDeep(settings.attributes),
|
||||
actionInProgress: false
|
||||
};
|
||||
},
|
||||
componentDidMount() {
|
||||
this.props.cluster.get('settings').isValid({models: this.state.configModels});
|
||||
},
|
||||
componentWillUnmount() {
|
||||
this.loadInitialSettings();
|
||||
},
|
||||
hasChanges() {
|
||||
return this.props.cluster.get('settings').hasChanges(this.state.initialAttributes, this.state.configModels);
|
||||
},
|
||||
applyChanges() {
|
||||
if (!this.isSavingPossible()) return $.Deferred().reject();
|
||||
});
|
||||
settings.mergePluginSettings();
|
||||
settings.isValid({models: this.state.configModels});
|
||||
this.setState({
|
||||
actionInProgress: false,
|
||||
key: _.now()
|
||||
});
|
||||
})
|
||||
.fail((response) => {
|
||||
utils.showErrorDialog({
|
||||
title: i18n('cluster_page.settings_tab.settings_error.title'),
|
||||
message: i18n('cluster_page.settings_tab.settings_error.load_defaults_warning'),
|
||||
response: response
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
revertChanges() {
|
||||
this.loadInitialSettings();
|
||||
this.setState({key: _.now()});
|
||||
},
|
||||
loadInitialSettings() {
|
||||
var settings = this.props.cluster.get('settings');
|
||||
settings.set(_.cloneDeep(this.state.initialAttributes), {silent: true, validate: false});
|
||||
settings.mergePluginSettings();
|
||||
settings.isValid({models: this.state.configModels});
|
||||
},
|
||||
onChange(groupName, settingName, value) {
|
||||
var settings = this.props.cluster.get('settings'),
|
||||
name = settings.makePath(groupName, settingName, settings.getValueAttribute(settingName));
|
||||
this.state.settingsForChecks.set(name, value);
|
||||
// FIXME: the following hacks cause we can't pass {validate: true} option to set method
|
||||
// this form of validation isn't supported in Backbone DeepModel
|
||||
settings.validationError = null;
|
||||
settings.set(name, value);
|
||||
settings.isValid({models: this.state.configModels});
|
||||
},
|
||||
checkRestrictions(action, setting) {
|
||||
return this.props.cluster.get('settings').checkRestrictions(this.state.configModels, action, setting);
|
||||
},
|
||||
isSavingPossible() {
|
||||
var settings = this.props.cluster.get('settings'),
|
||||
locked = this.state.actionInProgress || !!this.props.cluster.task({group: 'deployment', active: true}),
|
||||
// network settings are shown on Networks tab, so they should not block
|
||||
// saving of changes on Settings tab
|
||||
areSettingsValid = !_.any(_.keys(settings.validationError), (settingPath) => {
|
||||
var settingSection = settingPath.split('.')[0];
|
||||
return settings.get(settingSection).metadata.group != 'network' &&
|
||||
settings.get(settingPath).group != 'network';
|
||||
});
|
||||
return !locked && this.hasChanges() && areSettingsValid;
|
||||
},
|
||||
render() {
|
||||
var cluster = this.props.cluster,
|
||||
settings = cluster.get('settings'),
|
||||
settingsGroupList = settings.getGroupList(),
|
||||
locked = this.state.actionInProgress || !!cluster.task({group: 'deployment', active: true}),
|
||||
lockedCluster = !cluster.isAvailableForSettingsChanges(),
|
||||
someSettingsEditable = _.any(settings.attributes, (group) => group.metadata.always_editable),
|
||||
hasChanges = this.hasChanges(),
|
||||
allocatedRoles = _.uniq(_.flatten(_.union(cluster.get('nodes').pluck('roles'), cluster.get('nodes').pluck('pending_roles')))),
|
||||
classes = {
|
||||
row: true,
|
||||
'changes-locked': lockedCluster
|
||||
};
|
||||
|
||||
// collecting data to save
|
||||
var settings = this.props.cluster.get('settings'),
|
||||
dataToSave = this.props.cluster.isAvailableForSettingsChanges() ? settings.attributes :
|
||||
_.pick(settings.attributes, (group) => (group.metadata || {}).always_editable);
|
||||
var options = {url: settings.url, patch: true, wait: true, validate: false},
|
||||
deferred = new models.Settings(_.cloneDeep(dataToSave)).save(null, options);
|
||||
if (deferred) {
|
||||
this.setState({actionInProgress: true});
|
||||
deferred
|
||||
.done(() => {
|
||||
this.setState({initialAttributes: _.cloneDeep(settings.attributes)});
|
||||
// some networks may have restrictions which are processed by nailgun,
|
||||
// so networks need to be refetched after updating cluster attributes
|
||||
this.props.cluster.get('networkConfiguration').cancelThrottling();
|
||||
})
|
||||
.always(() => {
|
||||
this.setState({
|
||||
actionInProgress: false,
|
||||
key: _.now()
|
||||
});
|
||||
this.props.cluster.fetch();
|
||||
})
|
||||
.fail((response) => {
|
||||
utils.showErrorDialog({
|
||||
title: i18n('cluster_page.settings_tab.settings_error.title'),
|
||||
message: i18n('cluster_page.settings_tab.settings_error.saving_warning'),
|
||||
response: response
|
||||
});
|
||||
});
|
||||
}
|
||||
return deferred;
|
||||
},
|
||||
loadDefaults() {
|
||||
var settings = this.props.cluster.get('settings'),
|
||||
lockedCluster = !this.props.cluster.isAvailableForSettingsChanges(),
|
||||
defaultSettings = new models.Settings(),
|
||||
deferred = defaultSettings.fetch({url: _.result(this.props.cluster, 'url') + '/attributes/defaults'});
|
||||
|
||||
if (deferred) {
|
||||
this.setState({actionInProgress: true});
|
||||
deferred
|
||||
.done(() => {
|
||||
_.each(settings.attributes, (section, sectionName) => {
|
||||
if ((!lockedCluster || section.metadata.always_editable) && section.metadata.group != 'network') {
|
||||
_.each(section, (setting, settingName) => {
|
||||
// do not update hidden settings (hack for #1442143),
|
||||
// the same for settings with group network
|
||||
if (setting.type == 'hidden' || setting.group == 'network') return;
|
||||
var path = settings.makePath(sectionName, settingName);
|
||||
settings.set(path, defaultSettings.get(path), {silent: true});
|
||||
});
|
||||
}
|
||||
});
|
||||
settings.mergePluginSettings();
|
||||
settings.isValid({models: this.state.configModels});
|
||||
this.setState({
|
||||
actionInProgress: false,
|
||||
key: _.now()
|
||||
});
|
||||
})
|
||||
.fail((response) => {
|
||||
utils.showErrorDialog({
|
||||
title: i18n('cluster_page.settings_tab.settings_error.title'),
|
||||
message: i18n('cluster_page.settings_tab.settings_error.load_defaults_warning'),
|
||||
response: response
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
revertChanges() {
|
||||
this.loadInitialSettings();
|
||||
this.setState({key: _.now()});
|
||||
},
|
||||
loadInitialSettings() {
|
||||
var settings = this.props.cluster.get('settings');
|
||||
settings.set(_.cloneDeep(this.state.initialAttributes), {silent: true, validate: false});
|
||||
settings.mergePluginSettings();
|
||||
settings.isValid({models: this.state.configModels});
|
||||
},
|
||||
onChange(groupName, settingName, value) {
|
||||
var settings = this.props.cluster.get('settings'),
|
||||
name = settings.makePath(groupName, settingName, settings.getValueAttribute(settingName));
|
||||
this.state.settingsForChecks.set(name, value);
|
||||
// FIXME: the following hacks cause we can't pass {validate: true} option to set method
|
||||
// this form of validation isn't supported in Backbone DeepModel
|
||||
settings.validationError = null;
|
||||
settings.set(name, value);
|
||||
settings.isValid({models: this.state.configModels});
|
||||
},
|
||||
checkRestrictions(action, setting) {
|
||||
return this.props.cluster.get('settings').checkRestrictions(this.state.configModels, action, setting);
|
||||
},
|
||||
isSavingPossible() {
|
||||
var settings = this.props.cluster.get('settings'),
|
||||
locked = this.state.actionInProgress || !!this.props.cluster.task({group: 'deployment', active: true}),
|
||||
// network settings are shown on Networks tab, so they should not block
|
||||
// saving of changes on Settings tab
|
||||
areSettingsValid = !_.any(_.keys(settings.validationError), (settingPath) => {
|
||||
var settingSection = settingPath.split('.')[0];
|
||||
return settings.get(settingSection).metadata.group != 'network' &&
|
||||
settings.get(settingPath).group != 'network';
|
||||
});
|
||||
return !locked && this.hasChanges() && areSettingsValid;
|
||||
},
|
||||
render() {
|
||||
var cluster = this.props.cluster,
|
||||
settings = cluster.get('settings'),
|
||||
settingsGroupList = settings.getGroupList(),
|
||||
locked = this.state.actionInProgress || !!cluster.task({group: 'deployment', active: true}),
|
||||
lockedCluster = !cluster.isAvailableForSettingsChanges(),
|
||||
someSettingsEditable = _.any(settings.attributes, (group) => group.metadata.always_editable),
|
||||
hasChanges = this.hasChanges(),
|
||||
allocatedRoles = _.uniq(_.flatten(_.union(cluster.get('nodes').pluck('roles'), cluster.get('nodes').pluck('pending_roles')))),
|
||||
classes = {
|
||||
row: true,
|
||||
'changes-locked': lockedCluster
|
||||
};
|
||||
|
||||
var invalidSections = {};
|
||||
_.each(settings.validationError, (error, key) => {
|
||||
invalidSections[_.first(key.split('.'))] = true;
|
||||
});
|
||||
|
||||
// Prepare list of settings organized by groups
|
||||
var groupedSettings = {};
|
||||
_.each(settingsGroupList, (group) => groupedSettings[group] = {});
|
||||
_.each(settings.attributes, function(section, sectionName) {
|
||||
var isHidden = this.checkRestrictions('hide', section.metadata).result;
|
||||
if (!isHidden) {
|
||||
var group = section.metadata.group,
|
||||
hasErrors = invalidSections[sectionName];
|
||||
if (group) {
|
||||
if (group != 'network') {
|
||||
groupedSettings[settings.sanitizeGroup(group)][sectionName] = {invalid: hasErrors};
|
||||
}
|
||||
} else {
|
||||
// Settings like 'Common' can be splitted to different groups
|
||||
var settingGroups = _.chain(section)
|
||||
.omit('metadata')
|
||||
.pluck('group')
|
||||
.unique()
|
||||
.without('network')
|
||||
.value();
|
||||
|
||||
// to support plugins without settings (just for user to be able to switch its version)
|
||||
if (!settingGroups.length && settings.isPlugin(section)) {
|
||||
groupedSettings.other[sectionName] = {settings: [], invalid: hasErrors};
|
||||
}
|
||||
|
||||
_.each(settingGroups, function(settingGroup) {
|
||||
var calculatedGroup = settings.sanitizeGroup(settingGroup),
|
||||
pickedSettings = _.compact(_.map(section, function(setting, settingName) {
|
||||
if (
|
||||
settingName != 'metadata' &&
|
||||
setting.type != 'hidden' &&
|
||||
settings.sanitizeGroup(setting.group) == calculatedGroup &&
|
||||
!this.checkRestrictions('hide', setting).result
|
||||
) return settingName;
|
||||
}, this)),
|
||||
hasErrors = _.any(pickedSettings, (settingName) => {
|
||||
return (settings.validationError || {})[settings.makePath(sectionName, settingName)];
|
||||
});
|
||||
if (!_.isEmpty(pickedSettings)) {
|
||||
groupedSettings[calculatedGroup][sectionName] = {settings: pickedSettings, invalid: hasErrors};
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
groupedSettings = _.omit(groupedSettings, _.isEmpty);
|
||||
|
||||
return (
|
||||
<div key={this.state.key} className={utils.classNames(classes)}>
|
||||
<div className='title'>{i18n('cluster_page.settings_tab.title')}</div>
|
||||
<SettingSubtabs
|
||||
settings={settings}
|
||||
settingsGroupList={settingsGroupList}
|
||||
groupedSettings={groupedSettings}
|
||||
makePath={settings.makePath}
|
||||
configModels={this.state.configModels}
|
||||
setActiveSettingsGroupName={this.props.setActiveSettingsGroupName}
|
||||
activeSettingsSectionName={this.props.activeSettingsSectionName}
|
||||
checkRestrictions={this.checkRestrictions}
|
||||
/>
|
||||
{_.map(groupedSettings, function(selectedGroup, groupName) {
|
||||
if (groupName != this.props.activeSettingsSectionName) return null;
|
||||
|
||||
var sortedSections = _.sortBy(_.keys(selectedGroup), (name) => settings.get(name + '.metadata.weight'));
|
||||
return (
|
||||
<div className={'col-xs-10 forms-box ' + groupName} key={groupName}>
|
||||
{_.map(sortedSections, function(sectionName) {
|
||||
var settingsToDisplay = selectedGroup[sectionName].settings ||
|
||||
_.compact(_.map(settings.get(sectionName), function(setting, settingName) {
|
||||
if (
|
||||
settingName != 'metadata' &&
|
||||
setting.type != 'hidden' &&
|
||||
!this.checkRestrictions('hide', setting).result
|
||||
) return settingName;
|
||||
}, this));
|
||||
return <SettingSection
|
||||
{... _.pick(this.state, 'initialAttributes', 'settingsForChecks', 'configModels')}
|
||||
key={sectionName}
|
||||
cluster={this.props.cluster}
|
||||
sectionName={sectionName}
|
||||
settingsToDisplay={settingsToDisplay}
|
||||
onChange={_.bind(this.onChange, this, sectionName)}
|
||||
allocatedRoles={allocatedRoles}
|
||||
settings={settings}
|
||||
makePath={settings.makePath}
|
||||
getValueAttribute={settings.getValueAttribute}
|
||||
locked={locked}
|
||||
lockedCluster={lockedCluster}
|
||||
checkRestrictions={this.checkRestrictions}
|
||||
/>;
|
||||
}, this)}
|
||||
</div>
|
||||
);
|
||||
}, this)}
|
||||
<div className='col-xs-12 page-buttons content-elements'>
|
||||
<div className='well clearfix'>
|
||||
<div className='btn-group pull-right'>
|
||||
<button className='btn btn-default btn-load-defaults' onClick={this.loadDefaults} disabled={locked || (lockedCluster && !someSettingsEditable)}>
|
||||
{i18n('common.load_defaults_button')}
|
||||
</button>
|
||||
<button className='btn btn-default btn-revert-changes' onClick={this.revertChanges} disabled={locked || !hasChanges}>
|
||||
{i18n('common.cancel_changes_button')}
|
||||
</button>
|
||||
<button className='btn btn-success btn-apply-changes' onClick={this.applyChanges} disabled={!this.isSavingPossible()}>
|
||||
{i18n('common.save_settings_button')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
var invalidSections = {};
|
||||
_.each(settings.validationError, (error, key) => {
|
||||
invalidSections[_.first(key.split('.'))] = true;
|
||||
});
|
||||
|
||||
var SettingSubtabs = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<div className='col-xs-2'>
|
||||
<CSSTransitionGroup component='ul' transitionName='subtab-item' className='nav nav-pills nav-stacked'>
|
||||
{
|
||||
this.props.settingsGroupList.map(function(groupName) {
|
||||
if (!this.props.groupedSettings[groupName]) return null;
|
||||
// Prepare list of settings organized by groups
|
||||
var groupedSettings = {};
|
||||
_.each(settingsGroupList, (group) => groupedSettings[group] = {});
|
||||
_.each(settings.attributes, function(section, sectionName) {
|
||||
var isHidden = this.checkRestrictions('hide', section.metadata).result;
|
||||
if (!isHidden) {
|
||||
var group = section.metadata.group,
|
||||
hasErrors = invalidSections[sectionName];
|
||||
if (group) {
|
||||
if (group != 'network') {
|
||||
groupedSettings[settings.sanitizeGroup(group)][sectionName] = {invalid: hasErrors};
|
||||
}
|
||||
} else {
|
||||
// Settings like 'Common' can be splitted to different groups
|
||||
var settingGroups = _.chain(section)
|
||||
.omit('metadata')
|
||||
.pluck('group')
|
||||
.unique()
|
||||
.without('network')
|
||||
.value();
|
||||
|
||||
var hasErrors = _.any(_.pluck(this.props.groupedSettings[groupName], 'invalid'));
|
||||
return (
|
||||
<li
|
||||
key={groupName}
|
||||
role='presentation'
|
||||
className={utils.classNames({active: groupName == this.props.activeSettingsSectionName})}
|
||||
onClick={_.partial(this.props.setActiveSettingsGroupName, groupName)}
|
||||
>
|
||||
<a className={'subtab-link-' + groupName}>
|
||||
{hasErrors && <i className='subtab-icon glyphicon-danger-sign'/>}
|
||||
{i18n('cluster_page.settings_tab.groups.' + groupName, {defaultValue: groupName})}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}, this)
|
||||
}
|
||||
</CSSTransitionGroup>
|
||||
</div>
|
||||
);
|
||||
// to support plugins without settings (just for user to be able to switch its version)
|
||||
if (!settingGroups.length && settings.isPlugin(section)) {
|
||||
groupedSettings.other[sectionName] = {settings: [], invalid: hasErrors};
|
||||
}
|
||||
|
||||
_.each(settingGroups, function(settingGroup) {
|
||||
var calculatedGroup = settings.sanitizeGroup(settingGroup),
|
||||
pickedSettings = _.compact(_.map(section, function(setting, settingName) {
|
||||
if (
|
||||
settingName != 'metadata' &&
|
||||
setting.type != 'hidden' &&
|
||||
settings.sanitizeGroup(setting.group) == calculatedGroup &&
|
||||
!this.checkRestrictions('hide', setting).result
|
||||
) return settingName;
|
||||
}, this)),
|
||||
hasErrors = _.any(pickedSettings, (settingName) => {
|
||||
return (settings.validationError || {})[settings.makePath(sectionName, settingName)];
|
||||
});
|
||||
if (!_.isEmpty(pickedSettings)) {
|
||||
groupedSettings[calculatedGroup][sectionName] = {settings: pickedSettings, invalid: hasErrors};
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, this);
|
||||
groupedSettings = _.omit(groupedSettings, _.isEmpty);
|
||||
|
||||
export default SettingsTab;
|
||||
return (
|
||||
<div key={this.state.key} className={utils.classNames(classes)}>
|
||||
<div className='title'>{i18n('cluster_page.settings_tab.title')}</div>
|
||||
<SettingSubtabs
|
||||
settings={settings}
|
||||
settingsGroupList={settingsGroupList}
|
||||
groupedSettings={groupedSettings}
|
||||
makePath={settings.makePath}
|
||||
configModels={this.state.configModels}
|
||||
setActiveSettingsGroupName={this.props.setActiveSettingsGroupName}
|
||||
activeSettingsSectionName={this.props.activeSettingsSectionName}
|
||||
checkRestrictions={this.checkRestrictions}
|
||||
/>
|
||||
{_.map(groupedSettings, function(selectedGroup, groupName) {
|
||||
if (groupName != this.props.activeSettingsSectionName) return null;
|
||||
|
||||
var sortedSections = _.sortBy(_.keys(selectedGroup), (name) => settings.get(name + '.metadata.weight'));
|
||||
return (
|
||||
<div className={'col-xs-10 forms-box ' + groupName} key={groupName}>
|
||||
{_.map(sortedSections, function(sectionName) {
|
||||
var settingsToDisplay = selectedGroup[sectionName].settings ||
|
||||
_.compact(_.map(settings.get(sectionName), function(setting, settingName) {
|
||||
if (
|
||||
settingName != 'metadata' &&
|
||||
setting.type != 'hidden' &&
|
||||
!this.checkRestrictions('hide', setting).result
|
||||
) return settingName;
|
||||
}, this));
|
||||
return <SettingSection
|
||||
{... _.pick(this.state, 'initialAttributes', 'settingsForChecks', 'configModels')}
|
||||
key={sectionName}
|
||||
cluster={this.props.cluster}
|
||||
sectionName={sectionName}
|
||||
settingsToDisplay={settingsToDisplay}
|
||||
onChange={_.bind(this.onChange, this, sectionName)}
|
||||
allocatedRoles={allocatedRoles}
|
||||
settings={settings}
|
||||
makePath={settings.makePath}
|
||||
getValueAttribute={settings.getValueAttribute}
|
||||
locked={locked}
|
||||
lockedCluster={lockedCluster}
|
||||
checkRestrictions={this.checkRestrictions}
|
||||
/>;
|
||||
}, this)}
|
||||
</div>
|
||||
);
|
||||
}, this)}
|
||||
<div className='col-xs-12 page-buttons content-elements'>
|
||||
<div className='well clearfix'>
|
||||
<div className='btn-group pull-right'>
|
||||
<button className='btn btn-default btn-load-defaults' onClick={this.loadDefaults} disabled={locked || (lockedCluster && !someSettingsEditable)}>
|
||||
{i18n('common.load_defaults_button')}
|
||||
</button>
|
||||
<button className='btn btn-default btn-revert-changes' onClick={this.revertChanges} disabled={locked || !hasChanges}>
|
||||
{i18n('common.cancel_changes_button')}
|
||||
</button>
|
||||
<button className='btn btn-success btn-apply-changes' onClick={this.applyChanges} disabled={!this.isSavingPossible()}>
|
||||
{i18n('common.save_settings_button')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var SettingSubtabs = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<div className='col-xs-2'>
|
||||
<CSSTransitionGroup component='ul' transitionName='subtab-item' className='nav nav-pills nav-stacked'>
|
||||
{
|
||||
this.props.settingsGroupList.map(function(groupName) {
|
||||
if (!this.props.groupedSettings[groupName]) return null;
|
||||
|
||||
var hasErrors = _.any(_.pluck(this.props.groupedSettings[groupName], 'invalid'));
|
||||
return (
|
||||
<li
|
||||
key={groupName}
|
||||
role='presentation'
|
||||
className={utils.classNames({active: groupName == this.props.activeSettingsSectionName})}
|
||||
onClick={_.partial(this.props.setActiveSettingsGroupName, groupName)}
|
||||
>
|
||||
<a className={'subtab-link-' + groupName}>
|
||||
{hasErrors && <i className='subtab-icon glyphicon-danger-sign'/>}
|
||||
{i18n('cluster_page.settings_tab.groups.' + groupName, {defaultValue: groupName})}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}, this)
|
||||
}
|
||||
</CSSTransitionGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default SettingsTab;
|
||||
|
|
|
@ -24,149 +24,149 @@ import dispatcher from 'dispatcher';
|
|||
import {backboneMixin, pollingMixin} from 'component_mixins';
|
||||
import CreateClusterWizard from 'views/wizard';
|
||||
|
||||
var ClustersPage, ClusterList, Cluster;
|
||||
var ClustersPage, ClusterList, Cluster;
|
||||
|
||||
ClustersPage = React.createClass({
|
||||
statics: {
|
||||
title: i18n('clusters_page.title'),
|
||||
navbarActiveElement: 'clusters',
|
||||
breadcrumbsPath: [['home', '#'], 'environments'],
|
||||
fetchData() {
|
||||
var clusters = new models.Clusters();
|
||||
var nodes = new models.Nodes();
|
||||
var tasks = new models.Tasks();
|
||||
return $.when(clusters.fetch(), nodes.fetch(), tasks.fetch()).done(() => {
|
||||
clusters.each((cluster) => {
|
||||
cluster.set('nodes', new models.Nodes(nodes.where({cluster: cluster.id})));
|
||||
cluster.set('tasks', new models.Tasks(tasks.where({cluster: cluster.id})));
|
||||
}, this);
|
||||
}).then(() => ({clusters: clusters}));
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div className='clusters-page'>
|
||||
<div className='page-title'>
|
||||
<h1 className='title'>{i18n('clusters_page.title')}</h1>
|
||||
</div>
|
||||
<ClusterList clusters={this.props.clusters} />
|
||||
</div>
|
||||
);
|
||||
ClustersPage = React.createClass({
|
||||
statics: {
|
||||
title: i18n('clusters_page.title'),
|
||||
navbarActiveElement: 'clusters',
|
||||
breadcrumbsPath: [['home', '#'], 'environments'],
|
||||
fetchData() {
|
||||
var clusters = new models.Clusters();
|
||||
var nodes = new models.Nodes();
|
||||
var tasks = new models.Tasks();
|
||||
return $.when(clusters.fetch(), nodes.fetch(), tasks.fetch()).done(() => {
|
||||
clusters.each((cluster) => {
|
||||
cluster.set('nodes', new models.Nodes(nodes.where({cluster: cluster.id})));
|
||||
cluster.set('tasks', new models.Tasks(tasks.where({cluster: cluster.id})));
|
||||
}, this);
|
||||
}).then(() => ({clusters: clusters}));
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div className='clusters-page'>
|
||||
<div className='page-title'>
|
||||
<h1 className='title'>{i18n('clusters_page.title')}</h1>
|
||||
</div>
|
||||
<ClusterList clusters={this.props.clusters} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
ClusterList = React.createClass({
|
||||
mixins: [backboneMixin('clusters')],
|
||||
createCluster() {
|
||||
CreateClusterWizard.show({clusters: this.props.clusters, modalClass: 'wizard', backdrop: 'static'});
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div className='row'>
|
||||
{this.props.clusters.map((cluster) => {
|
||||
return <Cluster key={cluster.id} cluster={cluster} />;
|
||||
}, this)}
|
||||
<div key='create-cluster' className='col-xs-3'>
|
||||
<button className='btn-link create-cluster' onClick={this.createCluster}>
|
||||
<span>{i18n('clusters_page.create_cluster_text')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Cluster = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin('cluster'),
|
||||
backboneMixin({modelOrCollection(props) {
|
||||
return props.cluster.get('nodes');
|
||||
}}),
|
||||
backboneMixin({modelOrCollection(props) {
|
||||
return props.cluster.get('tasks');
|
||||
}}),
|
||||
backboneMixin({modelOrCollection(props) {
|
||||
return props.cluster.task({group: 'deployment', active: true});
|
||||
}}),
|
||||
pollingMixin(3)
|
||||
],
|
||||
shouldDataBeFetched() {
|
||||
return this.props.cluster.task({group: 'deployment', active: true}) ||
|
||||
this.props.cluster.task({name: 'cluster_deletion', active: true}) ||
|
||||
this.props.cluster.task({name: 'cluster_deletion', status: 'ready'});
|
||||
},
|
||||
fetchData() {
|
||||
var request, requests = [];
|
||||
var deletionTask = this.props.cluster.task('cluster_deletion');
|
||||
if (deletionTask) {
|
||||
request = deletionTask.fetch();
|
||||
request.fail((response) => {
|
||||
if (response.status == 404) {
|
||||
this.props.cluster.collection.remove(this.props.cluster);
|
||||
dispatcher.trigger('updateNodeStats');
|
||||
}
|
||||
});
|
||||
|
||||
ClusterList = React.createClass({
|
||||
mixins: [backboneMixin('clusters')],
|
||||
createCluster() {
|
||||
CreateClusterWizard.show({clusters: this.props.clusters, modalClass: 'wizard', backdrop: 'static'});
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div className='row'>
|
||||
{this.props.clusters.map((cluster) => {
|
||||
return <Cluster key={cluster.id} cluster={cluster} />;
|
||||
}, this)}
|
||||
<div key='create-cluster' className='col-xs-3'>
|
||||
<button className='btn-link create-cluster' onClick={this.createCluster}>
|
||||
<span>{i18n('clusters_page.create_cluster_text')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
requests.push(request);
|
||||
}
|
||||
var deploymentTask = this.props.cluster.task({group: 'deployment', active: true});
|
||||
if (deploymentTask) {
|
||||
request = deploymentTask.fetch();
|
||||
request.done(() => {
|
||||
if (deploymentTask.match({active: false})) {
|
||||
this.props.cluster.fetch();
|
||||
dispatcher.trigger('updateNodeStats');
|
||||
}
|
||||
});
|
||||
|
||||
Cluster = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin('cluster'),
|
||||
backboneMixin({modelOrCollection(props) {
|
||||
return props.cluster.get('nodes');
|
||||
}}),
|
||||
backboneMixin({modelOrCollection(props) {
|
||||
return props.cluster.get('tasks');
|
||||
}}),
|
||||
backboneMixin({modelOrCollection(props) {
|
||||
return props.cluster.task({group: 'deployment', active: true});
|
||||
}}),
|
||||
pollingMixin(3)
|
||||
],
|
||||
shouldDataBeFetched() {
|
||||
return this.props.cluster.task({group: 'deployment', active: true}) ||
|
||||
this.props.cluster.task({name: 'cluster_deletion', active: true}) ||
|
||||
this.props.cluster.task({name: 'cluster_deletion', status: 'ready'});
|
||||
},
|
||||
fetchData() {
|
||||
var request, requests = [];
|
||||
var deletionTask = this.props.cluster.task('cluster_deletion');
|
||||
if (deletionTask) {
|
||||
request = deletionTask.fetch();
|
||||
request.fail((response) => {
|
||||
if (response.status == 404) {
|
||||
this.props.cluster.collection.remove(this.props.cluster);
|
||||
dispatcher.trigger('updateNodeStats');
|
||||
}
|
||||
});
|
||||
requests.push(request);
|
||||
});
|
||||
requests.push(request);
|
||||
}
|
||||
return $.when(...requests);
|
||||
},
|
||||
render() {
|
||||
var cluster = this.props.cluster;
|
||||
var status = cluster.get('status');
|
||||
var nodes = cluster.get('nodes');
|
||||
var deletionTask = cluster.task({name: 'cluster_deletion', active: true}) || cluster.task({name: 'cluster_deletion', status: 'ready'});
|
||||
var deploymentTask = cluster.task({group: 'deployment', active: true});
|
||||
return (
|
||||
<div className='col-xs-3'>
|
||||
<a className={utils.classNames({clusterbox: true, 'cluster-disabled': !!deletionTask})} href={!deletionTask ? '#cluster/' + cluster.id : 'javascript:void 0'}>
|
||||
<div className='name'>{cluster.get('name')}</div>
|
||||
<div className='tech-info'>
|
||||
<div key='nodes-title' className='item'>{i18n('clusters_page.cluster_hardware_nodes')}</div>
|
||||
<div key='nodes-value' className='value'>{nodes.length}</div>
|
||||
{!!nodes.length && [
|
||||
<div key='cpu-title' className='item'>{i18n('clusters_page.cluster_hardware_cpu')}</div>,
|
||||
<div key='cpu-value' className='value'>{nodes.resources('cores')} ({nodes.resources('ht_cores')})</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 className='status text-info'>
|
||||
{deploymentTask ?
|
||||
<div className='progress'>
|
||||
<div
|
||||
className={utils.classNames({
|
||||
'progress-bar': true,
|
||||
'progress-bar-warning': _.contains(['stop_deployment', 'reset_environment'], deploymentTask.get('name'))
|
||||
})}
|
||||
style={{width: (deploymentTask.get('progress') > 3 ? deploymentTask.get('progress') : 3) + '%'}}
|
||||
></div>
|
||||
</div>
|
||||
:
|
||||
<span className={utils.classNames({
|
||||
'text-danger': status == 'error' || status == 'update_error',
|
||||
'text-success': status == 'operational'
|
||||
})}>
|
||||
{i18n('cluster.status.' + status, {defaultValue: status})}
|
||||
</span>
|
||||
}
|
||||
var deploymentTask = this.props.cluster.task({group: 'deployment', active: true});
|
||||
if (deploymentTask) {
|
||||
request = deploymentTask.fetch();
|
||||
request.done(() => {
|
||||
if (deploymentTask.match({active: false})) {
|
||||
this.props.cluster.fetch();
|
||||
dispatcher.trigger('updateNodeStats');
|
||||
}
|
||||
});
|
||||
requests.push(request);
|
||||
}
|
||||
return $.when(...requests);
|
||||
},
|
||||
render() {
|
||||
var cluster = this.props.cluster;
|
||||
var status = cluster.get('status');
|
||||
var nodes = cluster.get('nodes');
|
||||
var deletionTask = cluster.task({name: 'cluster_deletion', active: true}) || cluster.task({name: 'cluster_deletion', status: 'ready'});
|
||||
var deploymentTask = cluster.task({group: 'deployment', active: true});
|
||||
return (
|
||||
<div className='col-xs-3'>
|
||||
<a className={utils.classNames({clusterbox: true, 'cluster-disabled': !!deletionTask})} href={!deletionTask ? '#cluster/' + cluster.id : 'javascript:void 0'}>
|
||||
<div className='name'>{cluster.get('name')}</div>
|
||||
<div className='tech-info'>
|
||||
<div key='nodes-title' className='item'>{i18n('clusters_page.cluster_hardware_nodes')}</div>
|
||||
<div key='nodes-value' className='value'>{nodes.length}</div>
|
||||
{!!nodes.length && [
|
||||
<div key='cpu-title' className='item'>{i18n('clusters_page.cluster_hardware_cpu')}</div>,
|
||||
<div key='cpu-value' className='value'>{nodes.resources('cores')} ({nodes.resources('ht_cores')})</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 className='status text-info'>
|
||||
{deploymentTask ?
|
||||
<div className='progress'>
|
||||
<div
|
||||
className={utils.classNames({
|
||||
'progress-bar': true,
|
||||
'progress-bar-warning': _.contains(['stop_deployment', 'reset_environment'], deploymentTask.get('name'))
|
||||
})}
|
||||
style={{width: (deploymentTask.get('progress') > 3 ? deploymentTask.get('progress') : 3) + '%'}}
|
||||
></div>
|
||||
</div>
|
||||
:
|
||||
<span className={utils.classNames({
|
||||
'text-danger': status == 'error' || status == 'update_error',
|
||||
'text-success': status == 'operational'
|
||||
})}>
|
||||
{i18n('cluster.status.' + status, {defaultValue: status})}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default ClustersPage;
|
||||
export default ClustersPage;
|
||||
|
|
|
@ -28,357 +28,357 @@ import ReactDOM from 'react-dom';
|
|||
import utils from 'utils';
|
||||
import {outerClickMixin} from 'component_mixins';
|
||||
|
||||
export var Input = React.createClass({
|
||||
propTypes: {
|
||||
type: React.PropTypes.oneOf(['text', 'password', 'textarea', 'checkbox', 'radio', 'select', 'hidden', 'number', 'range', 'file']).isRequired,
|
||||
name: React.PropTypes.node,
|
||||
label: React.PropTypes.node,
|
||||
debounce: React.PropTypes.bool,
|
||||
description: React.PropTypes.node,
|
||||
disabled: React.PropTypes.bool,
|
||||
inputClassName: React.PropTypes.node,
|
||||
wrapperClassName: React.PropTypes.node,
|
||||
tooltipPlacement: React.PropTypes.oneOf(['left', 'right', 'top', 'bottom']),
|
||||
tooltipIcon: React.PropTypes.node,
|
||||
tooltipText: React.PropTypes.node,
|
||||
toggleable: React.PropTypes.bool,
|
||||
onChange: React.PropTypes.func,
|
||||
extraContent: React.PropTypes.node
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
visible: false,
|
||||
fileName: this.props.defaultValue && this.props.defaultValue.name || null,
|
||||
content: this.props.defaultValue && this.props.defaultValue.content || null
|
||||
};
|
||||
},
|
||||
getDefaultProps() {
|
||||
return {
|
||||
tooltipIcon: 'glyphicon-warning-sign',
|
||||
tooltipPlacement: 'right'
|
||||
};
|
||||
},
|
||||
togglePassword() {
|
||||
this.setState({visible: !this.state.visible});
|
||||
},
|
||||
isCheckboxOrRadio() {
|
||||
return this.props.type == 'radio' || this.props.type == 'checkbox';
|
||||
},
|
||||
getInputDOMNode() {
|
||||
return ReactDOM.findDOMNode(this.refs.input);
|
||||
},
|
||||
debouncedChange: _.debounce(function() {
|
||||
return this.onChange();
|
||||
}, 200, {leading: true}),
|
||||
pickFile() {
|
||||
if (!this.props.disabled) {
|
||||
this.getInputDOMNode().click();
|
||||
}
|
||||
},
|
||||
saveFile(fileName, content) {
|
||||
this.setState({
|
||||
fileName: fileName,
|
||||
content: content
|
||||
});
|
||||
return this.props.onChange(
|
||||
this.props.name,
|
||||
{name: fileName, content: content}
|
||||
);
|
||||
},
|
||||
removeFile() {
|
||||
if (!this.props.disabled) {
|
||||
ReactDOM.findDOMNode(this.refs.form).reset();
|
||||
this.saveFile(null, null);
|
||||
}
|
||||
},
|
||||
readFile() {
|
||||
var reader = new FileReader(),
|
||||
input = this.getInputDOMNode();
|
||||
|
||||
if (input.files.length) {
|
||||
reader.onload = (function() {
|
||||
return this.saveFile(input.value.replace(/^.*[\\\/]/g, ''), reader.result);
|
||||
}).bind(this);
|
||||
reader.readAsBinaryString(input.files[0]);
|
||||
}
|
||||
},
|
||||
onChange() {
|
||||
if (this.props.onChange) {
|
||||
var input = this.getInputDOMNode();
|
||||
return this.props.onChange(
|
||||
this.props.name,
|
||||
this.props.type == 'checkbox' ? input.checked : input.value
|
||||
);
|
||||
}
|
||||
},
|
||||
handleFocus(e) {
|
||||
e.target.select();
|
||||
},
|
||||
renderInput() {
|
||||
var classes = {'form-control': this.props.type != 'range'};
|
||||
classes[this.props.inputClassName] = this.props.inputClassName;
|
||||
var props = {
|
||||
ref: 'input',
|
||||
key: 'input',
|
||||
onFocus: this.props.selectOnFocus && this.handleFocus,
|
||||
type: (this.props.toggleable && this.state.visible) ? 'text' : this.props.type,
|
||||
className: utils.classNames(classes)
|
||||
};
|
||||
if (this.props.type == 'file') {
|
||||
props.onChange = this.readFile;
|
||||
} else {
|
||||
props.onChange = this.props.debounce ? this.debouncedChange : this.onChange;
|
||||
}
|
||||
var Tag = _.contains(['select', 'textarea'], this.props.type) ? this.props.type : 'input',
|
||||
input = <Tag {...this.props} {...props}>{this.props.children}</Tag>,
|
||||
isCheckboxOrRadio = this.isCheckboxOrRadio(),
|
||||
inputWrapperClasses = {
|
||||
'input-group': this.props.toggleable,
|
||||
'custom-tumbler': isCheckboxOrRadio,
|
||||
textarea: this.props.type == 'textarea'
|
||||
};
|
||||
if (this.props.type == 'file') {
|
||||
input = <form ref='form'>{input}</form>;
|
||||
}
|
||||
return (
|
||||
<div key='input-group' className={utils.classNames(inputWrapperClasses)}>
|
||||
{input}
|
||||
{this.props.type == 'file' &&
|
||||
<div className='input-group'>
|
||||
<input
|
||||
className='form-control file-name'
|
||||
type='text'
|
||||
placeholder={i18n('file.placeholder')}
|
||||
value={this.state.fileName && '[' + utils.showSize(this.state.content.length) + '] ' + this.state.fileName}
|
||||
onClick={this.pickFile}
|
||||
disabled={this.props.disabled}
|
||||
readOnly
|
||||
/>
|
||||
<div 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>
|
||||
}
|
||||
{this.props.toggleable &&
|
||||
<div className='input-group-addon' onClick={this.togglePassword}>
|
||||
<i className={this.state.visible ? 'glyphicon glyphicon-eye-close' : 'glyphicon glyphicon-eye-open'} />
|
||||
</div>
|
||||
}
|
||||
{isCheckboxOrRadio && <span> </span>}
|
||||
{this.props.extraContent}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
renderLabel(children) {
|
||||
if (!this.props.label && !children) return null;
|
||||
return (
|
||||
<label key='label' htmlFor={this.props.id}>
|
||||
{children}
|
||||
{this.props.label}
|
||||
{this.props.tooltipText &&
|
||||
<Tooltip text={this.props.tooltipText} placement={this.props.tooltipPlacement}>
|
||||
<i className={utils.classNames('glyphicon tooltip-icon', this.props.tooltipIcon)} />
|
||||
</Tooltip>
|
||||
}
|
||||
</label>
|
||||
);
|
||||
},
|
||||
renderDescription() {
|
||||
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>;
|
||||
},
|
||||
renderWrapper(children) {
|
||||
var isCheckboxOrRadio = this.isCheckboxOrRadio(),
|
||||
classes = {
|
||||
'form-group': !isCheckboxOrRadio,
|
||||
'checkbox-group': isCheckboxOrRadio,
|
||||
'has-error': !_.isUndefined(this.props.error) && !_.isNull(this.props.error),
|
||||
disabled: this.props.disabled
|
||||
};
|
||||
classes[this.props.wrapperClassName] = this.props.wrapperClassName;
|
||||
return (<div className={utils.classNames(classes)}>{children}</div>);
|
||||
},
|
||||
render() {
|
||||
if (this.props.type == 'hidden' && !this.props.description && !this.props.label) return null;
|
||||
return this.renderWrapper(this.isCheckboxOrRadio() ?
|
||||
[
|
||||
this.renderLabel(this.renderInput()),
|
||||
this.renderDescription()
|
||||
] : [
|
||||
this.renderLabel(),
|
||||
this.renderInput(),
|
||||
this.renderDescription()
|
||||
]
|
||||
);
|
||||
}
|
||||
export var Input = React.createClass({
|
||||
propTypes: {
|
||||
type: React.PropTypes.oneOf(['text', 'password', 'textarea', 'checkbox', 'radio', 'select', 'hidden', 'number', 'range', 'file']).isRequired,
|
||||
name: React.PropTypes.node,
|
||||
label: React.PropTypes.node,
|
||||
debounce: React.PropTypes.bool,
|
||||
description: React.PropTypes.node,
|
||||
disabled: React.PropTypes.bool,
|
||||
inputClassName: React.PropTypes.node,
|
||||
wrapperClassName: React.PropTypes.node,
|
||||
tooltipPlacement: React.PropTypes.oneOf(['left', 'right', 'top', 'bottom']),
|
||||
tooltipIcon: React.PropTypes.node,
|
||||
tooltipText: React.PropTypes.node,
|
||||
toggleable: React.PropTypes.bool,
|
||||
onChange: React.PropTypes.func,
|
||||
extraContent: React.PropTypes.node
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
visible: false,
|
||||
fileName: this.props.defaultValue && this.props.defaultValue.name || null,
|
||||
content: this.props.defaultValue && this.props.defaultValue.content || null
|
||||
};
|
||||
},
|
||||
getDefaultProps() {
|
||||
return {
|
||||
tooltipIcon: 'glyphicon-warning-sign',
|
||||
tooltipPlacement: 'right'
|
||||
};
|
||||
},
|
||||
togglePassword() {
|
||||
this.setState({visible: !this.state.visible});
|
||||
},
|
||||
isCheckboxOrRadio() {
|
||||
return this.props.type == 'radio' || this.props.type == 'checkbox';
|
||||
},
|
||||
getInputDOMNode() {
|
||||
return ReactDOM.findDOMNode(this.refs.input);
|
||||
},
|
||||
debouncedChange: _.debounce(function() {
|
||||
return this.onChange();
|
||||
}, 200, {leading: true}),
|
||||
pickFile() {
|
||||
if (!this.props.disabled) {
|
||||
this.getInputDOMNode().click();
|
||||
}
|
||||
},
|
||||
saveFile(fileName, content) {
|
||||
this.setState({
|
||||
fileName: fileName,
|
||||
content: content
|
||||
});
|
||||
return this.props.onChange(
|
||||
this.props.name,
|
||||
{name: fileName, content: content}
|
||||
);
|
||||
},
|
||||
removeFile() {
|
||||
if (!this.props.disabled) {
|
||||
ReactDOM.findDOMNode(this.refs.form).reset();
|
||||
this.saveFile(null, null);
|
||||
}
|
||||
},
|
||||
readFile() {
|
||||
var reader = new FileReader(),
|
||||
input = this.getInputDOMNode();
|
||||
|
||||
export var RadioGroup = React.createClass({
|
||||
propTypes: {
|
||||
name: React.PropTypes.string,
|
||||
values: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
|
||||
label: React.PropTypes.node,
|
||||
tooltipText: React.PropTypes.node
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div className='radio-group'>
|
||||
{this.props.label &&
|
||||
<h4>
|
||||
{this.props.label}
|
||||
{this.props.tooltipText &&
|
||||
<Tooltip text={this.props.tooltipText} placement='right'>
|
||||
<i className='glyphicon glyphicon-warning-sign tooltip-icon' />
|
||||
</Tooltip>
|
||||
}
|
||||
</h4>
|
||||
}
|
||||
{_.map(this.props.values, function(value) {
|
||||
return <Input
|
||||
{...this.props}
|
||||
{...value}
|
||||
type='radio'
|
||||
key={value.data}
|
||||
value={value.data}
|
||||
/>;
|
||||
}, this)}
|
||||
</div>
|
||||
);
|
||||
if (input.files.length) {
|
||||
reader.onload = (function() {
|
||||
return this.saveFile(input.value.replace(/^.*[\\\/]/g, ''), reader.result);
|
||||
}).bind(this);
|
||||
reader.readAsBinaryString(input.files[0]);
|
||||
}
|
||||
},
|
||||
onChange() {
|
||||
if (this.props.onChange) {
|
||||
var input = this.getInputDOMNode();
|
||||
return this.props.onChange(
|
||||
this.props.name,
|
||||
this.props.type == 'checkbox' ? input.checked : input.value
|
||||
);
|
||||
}
|
||||
},
|
||||
handleFocus(e) {
|
||||
e.target.select();
|
||||
},
|
||||
renderInput() {
|
||||
var classes = {'form-control': this.props.type != 'range'};
|
||||
classes[this.props.inputClassName] = this.props.inputClassName;
|
||||
var props = {
|
||||
ref: 'input',
|
||||
key: 'input',
|
||||
onFocus: this.props.selectOnFocus && this.handleFocus,
|
||||
type: (this.props.toggleable && this.state.visible) ? 'text' : this.props.type,
|
||||
className: utils.classNames(classes)
|
||||
};
|
||||
if (this.props.type == 'file') {
|
||||
props.onChange = this.readFile;
|
||||
} else {
|
||||
props.onChange = this.props.debounce ? this.debouncedChange : this.onChange;
|
||||
}
|
||||
var Tag = _.contains(['select', 'textarea'], this.props.type) ? this.props.type : 'input',
|
||||
input = <Tag {...this.props} {...props}>{this.props.children}</Tag>,
|
||||
isCheckboxOrRadio = this.isCheckboxOrRadio(),
|
||||
inputWrapperClasses = {
|
||||
'input-group': this.props.toggleable,
|
||||
'custom-tumbler': isCheckboxOrRadio,
|
||||
textarea: this.props.type == 'textarea'
|
||||
};
|
||||
if (this.props.type == 'file') {
|
||||
input = <form ref='form'>{input}</form>;
|
||||
}
|
||||
return (
|
||||
<div key='input-group' className={utils.classNames(inputWrapperClasses)}>
|
||||
{input}
|
||||
{this.props.type == 'file' &&
|
||||
<div className='input-group'>
|
||||
<input
|
||||
className='form-control file-name'
|
||||
type='text'
|
||||
placeholder={i18n('file.placeholder')}
|
||||
value={this.state.fileName && '[' + utils.showSize(this.state.content.length) + '] ' + this.state.fileName}
|
||||
onClick={this.pickFile}
|
||||
disabled={this.props.disabled}
|
||||
readOnly
|
||||
/>
|
||||
<div 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>
|
||||
}
|
||||
});
|
||||
|
||||
export var ProgressBar = React.createClass({
|
||||
propTypes: {
|
||||
wrapperClassName: React.PropTypes.node,
|
||||
progress: React.PropTypes.number
|
||||
},
|
||||
render() {
|
||||
var wrapperClasses = {
|
||||
progress: true
|
||||
};
|
||||
wrapperClasses[this.props.wrapperClassName] = this.props.wrapperClassName;
|
||||
|
||||
var isInfinite = !_.isNumber(this.props.progress);
|
||||
var progressClasses = {
|
||||
'progress-bar': true,
|
||||
'progress-bar-striped active': isInfinite
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={utils.classNames(wrapperClasses)}>
|
||||
<div
|
||||
className={utils.classNames(progressClasses)}
|
||||
role='progressbar'
|
||||
style={{width: isInfinite ? '100%' : _.max([this.props.progress, 3]) + '%'}}
|
||||
>
|
||||
{!isInfinite && this.props.progress + '%'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
{this.props.toggleable &&
|
||||
<div className='input-group-addon' onClick={this.togglePassword}>
|
||||
<i className={this.state.visible ? 'glyphicon glyphicon-eye-close' : 'glyphicon glyphicon-eye-open'} />
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
export var Table = React.createClass({
|
||||
propTypes: {
|
||||
tableClassName: React.PropTypes.node,
|
||||
head: React.PropTypes.array,
|
||||
body: React.PropTypes.array
|
||||
},
|
||||
render() {
|
||||
var tableClasses = {'table table-bordered': true, 'table-striped': !this.props.noStripes};
|
||||
tableClasses[this.props.tableClassName] = this.props.tableClassName;
|
||||
return (
|
||||
<table className={utils.classNames(tableClasses)}>
|
||||
<thead>
|
||||
<tr>
|
||||
{_.map(this.props.head, (column, index) => {
|
||||
var classes = {};
|
||||
classes[column.className] = column.className;
|
||||
return <th key={index} className={utils.classNames(classes)}>{column.label}</th>;
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{_.map(this.props.body, (row, rowIndex) => {
|
||||
return <tr key={rowIndex}>
|
||||
{_.map(row, (column, columnIndex) => {
|
||||
return <td key={columnIndex} className='enable-selection'>{column}</td>;
|
||||
})}
|
||||
</tr>;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
{isCheckboxOrRadio && <span> </span>}
|
||||
{this.props.extraContent}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
renderLabel(children) {
|
||||
if (!this.props.label && !children) return null;
|
||||
return (
|
||||
<label key='label' htmlFor={this.props.id}>
|
||||
{children}
|
||||
{this.props.label}
|
||||
{this.props.tooltipText &&
|
||||
<Tooltip text={this.props.tooltipText} placement={this.props.tooltipPlacement}>
|
||||
<i className={utils.classNames('glyphicon tooltip-icon', this.props.tooltipIcon)} />
|
||||
</Tooltip>
|
||||
}
|
||||
});
|
||||
</label>
|
||||
);
|
||||
},
|
||||
renderDescription() {
|
||||
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>;
|
||||
},
|
||||
renderWrapper(children) {
|
||||
var isCheckboxOrRadio = this.isCheckboxOrRadio(),
|
||||
classes = {
|
||||
'form-group': !isCheckboxOrRadio,
|
||||
'checkbox-group': isCheckboxOrRadio,
|
||||
'has-error': !_.isUndefined(this.props.error) && !_.isNull(this.props.error),
|
||||
disabled: this.props.disabled
|
||||
};
|
||||
classes[this.props.wrapperClassName] = this.props.wrapperClassName;
|
||||
return (<div className={utils.classNames(classes)}>{children}</div>);
|
||||
},
|
||||
render() {
|
||||
if (this.props.type == 'hidden' && !this.props.description && !this.props.label) return null;
|
||||
return this.renderWrapper(this.isCheckboxOrRadio() ?
|
||||
[
|
||||
this.renderLabel(this.renderInput()),
|
||||
this.renderDescription()
|
||||
] : [
|
||||
this.renderLabel(),
|
||||
this.renderInput(),
|
||||
this.renderDescription()
|
||||
]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export var Popover = React.createClass({
|
||||
mixins: [outerClickMixin],
|
||||
propTypes: {
|
||||
className: React.PropTypes.node,
|
||||
placement: React.PropTypes.node
|
||||
},
|
||||
getDefaultProps() {
|
||||
return {placement: 'bottom'};
|
||||
},
|
||||
render() {
|
||||
var classes = {'popover in': true};
|
||||
classes[this.props.placement] = true;
|
||||
classes[this.props.className] = true;
|
||||
return (
|
||||
<div className={utils.classNames(classes)}>
|
||||
<div className='arrow' />
|
||||
<div className='popover-content'>{this.props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export var Tooltip = React.createClass({
|
||||
propTypes: {
|
||||
container: React.PropTypes.node,
|
||||
placement: React.PropTypes.node,
|
||||
text: React.PropTypes.node
|
||||
},
|
||||
getDefaultProps() {
|
||||
return {
|
||||
placement: 'top',
|
||||
container: 'body',
|
||||
wrapperClassName: 'tooltip-wrapper'
|
||||
};
|
||||
},
|
||||
componentDidMount() {
|
||||
if (this.props.text) this.addTooltip();
|
||||
},
|
||||
componentDidUpdate() {
|
||||
if (this.props.text) {
|
||||
this.updateTooltipTitle();
|
||||
} else {
|
||||
this.removeTooltip();
|
||||
export var RadioGroup = React.createClass({
|
||||
propTypes: {
|
||||
name: React.PropTypes.string,
|
||||
values: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
|
||||
label: React.PropTypes.node,
|
||||
tooltipText: React.PropTypes.node
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div className='radio-group'>
|
||||
{this.props.label &&
|
||||
<h4>
|
||||
{this.props.label}
|
||||
{this.props.tooltipText &&
|
||||
<Tooltip text={this.props.tooltipText} placement='right'>
|
||||
<i className='glyphicon glyphicon-warning-sign tooltip-icon' />
|
||||
</Tooltip>
|
||||
}
|
||||
},
|
||||
componentWillUnmount() {
|
||||
this.removeTooltip();
|
||||
},
|
||||
addTooltip() {
|
||||
$(ReactDOM.findDOMNode(this.refs.tooltip)).tooltip({
|
||||
container: this.props.container,
|
||||
placement: this.props.placement,
|
||||
title: this.props.text
|
||||
});
|
||||
},
|
||||
updateTooltipTitle() {
|
||||
$(ReactDOM.findDOMNode(this.refs.tooltip)).attr('title', this.props.text).tooltip('fixTitle');
|
||||
},
|
||||
removeTooltip() {
|
||||
$(ReactDOM.findDOMNode(this.refs.tooltip)).tooltip('destroy');
|
||||
},
|
||||
render() {
|
||||
if (!this.props.wrap) return React.cloneElement(React.Children.only(this.props.children), {ref: 'tooltip'});
|
||||
return (
|
||||
<div className={this.props.wrapperClassName} ref='tooltip'>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
</h4>
|
||||
}
|
||||
{_.map(this.props.values, function(value) {
|
||||
return <Input
|
||||
{...this.props}
|
||||
{...value}
|
||||
type='radio'
|
||||
key={value.data}
|
||||
value={value.data}
|
||||
/>;
|
||||
}, this)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export var ProgressBar = React.createClass({
|
||||
propTypes: {
|
||||
wrapperClassName: React.PropTypes.node,
|
||||
progress: React.PropTypes.number
|
||||
},
|
||||
render() {
|
||||
var wrapperClasses = {
|
||||
progress: true
|
||||
};
|
||||
wrapperClasses[this.props.wrapperClassName] = this.props.wrapperClassName;
|
||||
|
||||
var isInfinite = !_.isNumber(this.props.progress);
|
||||
var progressClasses = {
|
||||
'progress-bar': true,
|
||||
'progress-bar-striped active': isInfinite
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={utils.classNames(wrapperClasses)}>
|
||||
<div
|
||||
className={utils.classNames(progressClasses)}
|
||||
role='progressbar'
|
||||
style={{width: isInfinite ? '100%' : _.max([this.props.progress, 3]) + '%'}}
|
||||
>
|
||||
{!isInfinite && this.props.progress + '%'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export var Table = React.createClass({
|
||||
propTypes: {
|
||||
tableClassName: React.PropTypes.node,
|
||||
head: React.PropTypes.array,
|
||||
body: React.PropTypes.array
|
||||
},
|
||||
render() {
|
||||
var tableClasses = {'table table-bordered': true, 'table-striped': !this.props.noStripes};
|
||||
tableClasses[this.props.tableClassName] = this.props.tableClassName;
|
||||
return (
|
||||
<table className={utils.classNames(tableClasses)}>
|
||||
<thead>
|
||||
<tr>
|
||||
{_.map(this.props.head, (column, index) => {
|
||||
var classes = {};
|
||||
classes[column.className] = column.className;
|
||||
return <th key={index} className={utils.classNames(classes)}>{column.label}</th>;
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{_.map(this.props.body, (row, rowIndex) => {
|
||||
return <tr key={rowIndex}>
|
||||
{_.map(row, (column, columnIndex) => {
|
||||
return <td key={columnIndex} className='enable-selection'>{column}</td>;
|
||||
})}
|
||||
</tr>;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export var Popover = React.createClass({
|
||||
mixins: [outerClickMixin],
|
||||
propTypes: {
|
||||
className: React.PropTypes.node,
|
||||
placement: React.PropTypes.node
|
||||
},
|
||||
getDefaultProps() {
|
||||
return {placement: 'bottom'};
|
||||
},
|
||||
render() {
|
||||
var classes = {'popover in': true};
|
||||
classes[this.props.placement] = true;
|
||||
classes[this.props.className] = true;
|
||||
return (
|
||||
<div className={utils.classNames(classes)}>
|
||||
<div className='arrow' />
|
||||
<div className='popover-content'>{this.props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export var Tooltip = React.createClass({
|
||||
propTypes: {
|
||||
container: React.PropTypes.node,
|
||||
placement: React.PropTypes.node,
|
||||
text: React.PropTypes.node
|
||||
},
|
||||
getDefaultProps() {
|
||||
return {
|
||||
placement: 'top',
|
||||
container: 'body',
|
||||
wrapperClassName: 'tooltip-wrapper'
|
||||
};
|
||||
},
|
||||
componentDidMount() {
|
||||
if (this.props.text) this.addTooltip();
|
||||
},
|
||||
componentDidUpdate() {
|
||||
if (this.props.text) {
|
||||
this.updateTooltipTitle();
|
||||
} else {
|
||||
this.removeTooltip();
|
||||
}
|
||||
},
|
||||
componentWillUnmount() {
|
||||
this.removeTooltip();
|
||||
},
|
||||
addTooltip() {
|
||||
$(ReactDOM.findDOMNode(this.refs.tooltip)).tooltip({
|
||||
container: this.props.container,
|
||||
placement: this.props.placement,
|
||||
title: this.props.text
|
||||
});
|
||||
},
|
||||
updateTooltipTitle() {
|
||||
$(ReactDOM.findDOMNode(this.refs.tooltip)).attr('title', this.props.text).tooltip('fixTitle');
|
||||
},
|
||||
removeTooltip() {
|
||||
$(ReactDOM.findDOMNode(this.refs.tooltip)).tooltip('destroy');
|
||||
},
|
||||
render() {
|
||||
if (!this.props.wrap) return React.cloneElement(React.Children.only(this.props.children), {ref: 'tooltip'});
|
||||
return (
|
||||
<div className={this.props.wrapperClassName} ref='tooltip'>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -19,168 +19,168 @@ import React from 'react';
|
|||
import utils from 'utils';
|
||||
import {Input} from 'views/controls';
|
||||
|
||||
var customControls = {};
|
||||
var customControls = {};
|
||||
|
||||
customControls.custom_repo_configuration = React.createClass({
|
||||
statics: {
|
||||
// validate method represented as static method to support cluster settings validation
|
||||
validate(setting, models) {
|
||||
var ns = 'cluster_page.settings_tab.custom_repo_configuration.errors.',
|
||||
nameRegexp = /^[\w-.]+$/,
|
||||
os = models.release.get('operating_system');
|
||||
var errors = setting.value.map(function(repo) {
|
||||
var error = {},
|
||||
value = this.repoToString(repo, os);
|
||||
if (!repo.name) {
|
||||
error.name = i18n(ns + 'empty_name');
|
||||
} else if (!repo.name.match(nameRegexp)) {
|
||||
error.name = i18n(ns + 'invalid_name');
|
||||
}
|
||||
if (!value || !value.match(this.defaultProps.repoRegexes[os])) {
|
||||
error.uri = i18n(ns + 'invalid_repo');
|
||||
}
|
||||
var priority = repo.priority;
|
||||
if (_.isNaN(priority) || !_.isNull(priority) && (!(priority == _.parseInt(priority, 10)) || os == 'CentOS' && (priority < 1 || priority > 99))) {
|
||||
error.priority = i18n(ns + 'invalid_priority');
|
||||
}
|
||||
return _.isEmpty(error) ? null : error;
|
||||
}, this);
|
||||
return _.compact(errors).length ? errors : null;
|
||||
},
|
||||
repoToString(repo, os) {
|
||||
var repoData = _.compact(this.defaultProps.repoAttributes[os].map((attribute) => repo[attribute]));
|
||||
if (!repoData.length) return ''; // in case of new repo
|
||||
return repoData.join(' ');
|
||||
}
|
||||
},
|
||||
getInitialState() {
|
||||
return {};
|
||||
},
|
||||
getDefaultProps() {
|
||||
return {
|
||||
repoRegexes: {
|
||||
Ubuntu: /^(deb|deb-src)\s+(\w+:\/\/[\w\-.\/]+(?::\d+)?[\w\-.\/]+)\s+([\w\-.\/]+)(?:\s+([\w\-.\/\s]+))?$/i,
|
||||
CentOS: /^(\w+:\/\/[\w\-.\/]+(?::\d+)?[\w\-.\/]+)\s*$/i
|
||||
},
|
||||
repoAttributes: {
|
||||
Ubuntu: ['type', 'uri', 'suite', 'section'],
|
||||
CentOS: ['uri']
|
||||
}
|
||||
};
|
||||
},
|
||||
changeRepos(method, index, value) {
|
||||
value = _.trim(value).replace(/\s+/g, ' ');
|
||||
var repos = _.cloneDeep(this.props.value),
|
||||
os = this.props.cluster.get('release').get('operating_system');
|
||||
switch (method) {
|
||||
case 'add':
|
||||
var data = {
|
||||
name: '',
|
||||
type: '',
|
||||
uri: '',
|
||||
priority: this.props.extra_priority
|
||||
};
|
||||
if (os == 'Ubuntu') {
|
||||
data.suite = '';
|
||||
data.section = '';
|
||||
} else {
|
||||
data.type = 'rpm';
|
||||
}
|
||||
repos.push(data);
|
||||
break;
|
||||
case 'delete':
|
||||
repos.splice(index, 1);
|
||||
this.setState({key: _.now()});
|
||||
break;
|
||||
case 'change_name':
|
||||
repos[index].name = value;
|
||||
break;
|
||||
case 'change_priority':
|
||||
repos[index].priority = value == '' ? null : Number(value);
|
||||
break;
|
||||
default:
|
||||
var repo = repos[index],
|
||||
match = value.match(this.props.repoRegexes[os]);
|
||||
if (match) {
|
||||
_.each(this.props.repoAttributes[os], (attribute, index) => {
|
||||
repo[attribute] = match[index + 1] || '';
|
||||
});
|
||||
} else {
|
||||
repo.uri = value;
|
||||
}
|
||||
}
|
||||
var path = this.props.settings.makePath(this.props.path, 'value');
|
||||
this.props.settings.set(path, repos);
|
||||
this.props.settings.isValid({models: this.props.configModels});
|
||||
},
|
||||
renderDeleteButton(index) {
|
||||
return (
|
||||
<button
|
||||
className='btn btn-link'
|
||||
onClick={this.changeRepos.bind(this, 'delete', index)}
|
||||
disabled={this.props.disabled}
|
||||
>
|
||||
<i className='glyphicon glyphicon-minus-sign' />
|
||||
</button>
|
||||
);
|
||||
},
|
||||
render() {
|
||||
var ns = 'cluster_page.settings_tab.custom_repo_configuration.',
|
||||
os = this.props.cluster.get('release').get('operating_system');
|
||||
return (
|
||||
<div className='repos' key={this.state.key}>
|
||||
{this.props.description &&
|
||||
<span className='help-block' dangerouslySetInnerHTML={{__html: utils.urlify(utils.linebreaks(_.escape(this.props.description)))}} />
|
||||
}
|
||||
{this.props.value.map(function(repo, index) {
|
||||
var error = (this.props.error || {})[index],
|
||||
props = {
|
||||
name: index,
|
||||
type: 'text',
|
||||
disabled: this.props.disabled
|
||||
};
|
||||
return (
|
||||
<div className='form-inline' key={'repo-' + index}>
|
||||
<Input
|
||||
{...props}
|
||||
defaultValue={repo.name}
|
||||
error={error && error.name}
|
||||
wrapperClassName='repo-name'
|
||||
onChange={this.changeRepos.bind(this, 'change_name')}
|
||||
label={index == 0 && i18n(ns + 'labels.name')}
|
||||
debounce
|
||||
/>
|
||||
<Input
|
||||
{...props}
|
||||
defaultValue={this.constructor.repoToString(repo, os)}
|
||||
error={error && (error.uri ? error.name ? '' : error.uri : null)}
|
||||
onChange={this.changeRepos.bind(this, null)}
|
||||
label={index == 0 && i18n(ns + 'labels.uri')}
|
||||
wrapperClassName='repo-uri'
|
||||
debounce
|
||||
/>
|
||||
<Input
|
||||
{...props}
|
||||
defaultValue={repo.priority}
|
||||
error={error && (error.priority ? (error.name || error.uri) ? '' : error.priority : null)}
|
||||
wrapperClassName='repo-priority'
|
||||
onChange={this.changeRepos.bind(this, 'change_priority')}
|
||||
extraContent={index > 0 && this.renderDeleteButton(index)}
|
||||
label={index == 0 && i18n(ns + 'labels.priority')}
|
||||
placeholder={i18n(ns + 'placeholders.priority')}
|
||||
debounce
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}, this)}
|
||||
<div className='buttons'>
|
||||
<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')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
customControls.custom_repo_configuration = React.createClass({
|
||||
statics: {
|
||||
// validate method represented as static method to support cluster settings validation
|
||||
validate(setting, models) {
|
||||
var ns = 'cluster_page.settings_tab.custom_repo_configuration.errors.',
|
||||
nameRegexp = /^[\w-.]+$/,
|
||||
os = models.release.get('operating_system');
|
||||
var errors = setting.value.map(function(repo) {
|
||||
var error = {},
|
||||
value = this.repoToString(repo, os);
|
||||
if (!repo.name) {
|
||||
error.name = i18n(ns + 'empty_name');
|
||||
} else if (!repo.name.match(nameRegexp)) {
|
||||
error.name = i18n(ns + 'invalid_name');
|
||||
}
|
||||
});
|
||||
if (!value || !value.match(this.defaultProps.repoRegexes[os])) {
|
||||
error.uri = i18n(ns + 'invalid_repo');
|
||||
}
|
||||
var priority = repo.priority;
|
||||
if (_.isNaN(priority) || !_.isNull(priority) && (!(priority == _.parseInt(priority, 10)) || os == 'CentOS' && (priority < 1 || priority > 99))) {
|
||||
error.priority = i18n(ns + 'invalid_priority');
|
||||
}
|
||||
return _.isEmpty(error) ? null : error;
|
||||
}, this);
|
||||
return _.compact(errors).length ? errors : null;
|
||||
},
|
||||
repoToString(repo, os) {
|
||||
var repoData = _.compact(this.defaultProps.repoAttributes[os].map((attribute) => repo[attribute]));
|
||||
if (!repoData.length) return ''; // in case of new repo
|
||||
return repoData.join(' ');
|
||||
}
|
||||
},
|
||||
getInitialState() {
|
||||
return {};
|
||||
},
|
||||
getDefaultProps() {
|
||||
return {
|
||||
repoRegexes: {
|
||||
Ubuntu: /^(deb|deb-src)\s+(\w+:\/\/[\w\-.\/]+(?::\d+)?[\w\-.\/]+)\s+([\w\-.\/]+)(?:\s+([\w\-.\/\s]+))?$/i,
|
||||
CentOS: /^(\w+:\/\/[\w\-.\/]+(?::\d+)?[\w\-.\/]+)\s*$/i
|
||||
},
|
||||
repoAttributes: {
|
||||
Ubuntu: ['type', 'uri', 'suite', 'section'],
|
||||
CentOS: ['uri']
|
||||
}
|
||||
};
|
||||
},
|
||||
changeRepos(method, index, value) {
|
||||
value = _.trim(value).replace(/\s+/g, ' ');
|
||||
var repos = _.cloneDeep(this.props.value),
|
||||
os = this.props.cluster.get('release').get('operating_system');
|
||||
switch (method) {
|
||||
case 'add':
|
||||
var data = {
|
||||
name: '',
|
||||
type: '',
|
||||
uri: '',
|
||||
priority: this.props.extra_priority
|
||||
};
|
||||
if (os == 'Ubuntu') {
|
||||
data.suite = '';
|
||||
data.section = '';
|
||||
} else {
|
||||
data.type = 'rpm';
|
||||
}
|
||||
repos.push(data);
|
||||
break;
|
||||
case 'delete':
|
||||
repos.splice(index, 1);
|
||||
this.setState({key: _.now()});
|
||||
break;
|
||||
case 'change_name':
|
||||
repos[index].name = value;
|
||||
break;
|
||||
case 'change_priority':
|
||||
repos[index].priority = value == '' ? null : Number(value);
|
||||
break;
|
||||
default:
|
||||
var repo = repos[index],
|
||||
match = value.match(this.props.repoRegexes[os]);
|
||||
if (match) {
|
||||
_.each(this.props.repoAttributes[os], (attribute, index) => {
|
||||
repo[attribute] = match[index + 1] || '';
|
||||
});
|
||||
} else {
|
||||
repo.uri = value;
|
||||
}
|
||||
}
|
||||
var path = this.props.settings.makePath(this.props.path, 'value');
|
||||
this.props.settings.set(path, repos);
|
||||
this.props.settings.isValid({models: this.props.configModels});
|
||||
},
|
||||
renderDeleteButton(index) {
|
||||
return (
|
||||
<button
|
||||
className='btn btn-link'
|
||||
onClick={this.changeRepos.bind(this, 'delete', index)}
|
||||
disabled={this.props.disabled}
|
||||
>
|
||||
<i className='glyphicon glyphicon-minus-sign' />
|
||||
</button>
|
||||
);
|
||||
},
|
||||
render() {
|
||||
var ns = 'cluster_page.settings_tab.custom_repo_configuration.',
|
||||
os = this.props.cluster.get('release').get('operating_system');
|
||||
return (
|
||||
<div className='repos' key={this.state.key}>
|
||||
{this.props.description &&
|
||||
<span className='help-block' dangerouslySetInnerHTML={{__html: utils.urlify(utils.linebreaks(_.escape(this.props.description)))}} />
|
||||
}
|
||||
{this.props.value.map(function(repo, index) {
|
||||
var error = (this.props.error || {})[index],
|
||||
props = {
|
||||
name: index,
|
||||
type: 'text',
|
||||
disabled: this.props.disabled
|
||||
};
|
||||
return (
|
||||
<div className='form-inline' key={'repo-' + index}>
|
||||
<Input
|
||||
{...props}
|
||||
defaultValue={repo.name}
|
||||
error={error && error.name}
|
||||
wrapperClassName='repo-name'
|
||||
onChange={this.changeRepos.bind(this, 'change_name')}
|
||||
label={index == 0 && i18n(ns + 'labels.name')}
|
||||
debounce
|
||||
/>
|
||||
<Input
|
||||
{...props}
|
||||
defaultValue={this.constructor.repoToString(repo, os)}
|
||||
error={error && (error.uri ? error.name ? '' : error.uri : null)}
|
||||
onChange={this.changeRepos.bind(this, null)}
|
||||
label={index == 0 && i18n(ns + 'labels.uri')}
|
||||
wrapperClassName='repo-uri'
|
||||
debounce
|
||||
/>
|
||||
<Input
|
||||
{...props}
|
||||
defaultValue={repo.priority}
|
||||
error={error && (error.priority ? (error.name || error.uri) ? '' : error.priority : null)}
|
||||
wrapperClassName='repo-priority'
|
||||
onChange={this.changeRepos.bind(this, 'change_priority')}
|
||||
extraContent={index > 0 && this.renderDeleteButton(index)}
|
||||
label={index == 0 && i18n(ns + 'labels.priority')}
|
||||
placeholder={i18n(ns + 'placeholders.priority')}
|
||||
debounce
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}, this)}
|
||||
<div className='buttons'>
|
||||
<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')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default customControls;
|
||||
export default customControls;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -22,129 +22,129 @@ import models from 'models';
|
|||
import {backboneMixin} from 'component_mixins';
|
||||
import NodeListScreen from 'views/cluster_page_tabs/nodes_tab_screens/node_list_screen';
|
||||
|
||||
var EquipmentPage, PluginLinks;
|
||||
var EquipmentPage, PluginLinks;
|
||||
|
||||
EquipmentPage = React.createClass({
|
||||
mixins: [backboneMixin('nodes')],
|
||||
statics: {
|
||||
title: i18n('equipment_page.title'),
|
||||
navbarActiveElement: 'equipment',
|
||||
breadcrumbsPath: [['home', '#'], 'equipment'],
|
||||
fetchData() {
|
||||
var nodes = new models.Nodes(),
|
||||
clusters = new models.Clusters(),
|
||||
plugins = new models.Plugins(),
|
||||
{releases, nodeNetworkGroups, fuelSettings} = app;
|
||||
EquipmentPage = React.createClass({
|
||||
mixins: [backboneMixin('nodes')],
|
||||
statics: {
|
||||
title: i18n('equipment_page.title'),
|
||||
navbarActiveElement: 'equipment',
|
||||
breadcrumbsPath: [['home', '#'], 'equipment'],
|
||||
fetchData() {
|
||||
var nodes = new models.Nodes(),
|
||||
clusters = new models.Clusters(),
|
||||
plugins = new models.Plugins(),
|
||||
{releases, nodeNetworkGroups, fuelSettings} = app;
|
||||
|
||||
return $.when(
|
||||
nodes.fetch(),
|
||||
clusters.fetch(),
|
||||
releases.fetch({cache: true}),
|
||||
nodeNetworkGroups.fetch({cache: true}),
|
||||
fuelSettings.fetch({cache: true}),
|
||||
plugins.fetch()
|
||||
).then(() => {
|
||||
clusters.each(
|
||||
(cluster) => cluster.set({
|
||||
release: releases.get(cluster.get('release_id'))
|
||||
})
|
||||
);
|
||||
var requests = clusters.map((cluster) => {
|
||||
var roles = new models.Roles();
|
||||
roles.url = _.result(cluster, 'url') + '/roles';
|
||||
cluster.set({roles: roles});
|
||||
return roles.fetch();
|
||||
});
|
||||
requests = requests.concat(
|
||||
plugins
|
||||
.filter((plugin) => _.contains(plugin.get('groups'), 'equipment'))
|
||||
.map((plugin) => {
|
||||
var pluginLinks = new models.PluginLinks();
|
||||
pluginLinks.url = _.result(plugin, 'url') + '/links';
|
||||
plugin.set({links: pluginLinks});
|
||||
return pluginLinks.fetch();
|
||||
})
|
||||
);
|
||||
return $.when(...requests);
|
||||
})
|
||||
.then(() => {
|
||||
var links = new models.PluginLinks();
|
||||
plugins.each(
|
||||
(plugin) => links.add(plugin.get('links') && plugin.get('links').models)
|
||||
);
|
||||
return $.when(
|
||||
nodes.fetch(),
|
||||
clusters.fetch(),
|
||||
releases.fetch({cache: true}),
|
||||
nodeNetworkGroups.fetch({cache: true}),
|
||||
fuelSettings.fetch({cache: true}),
|
||||
plugins.fetch()
|
||||
).then(() => {
|
||||
clusters.each(
|
||||
(cluster) => cluster.set({
|
||||
release: releases.get(cluster.get('release_id'))
|
||||
})
|
||||
);
|
||||
var requests = clusters.map((cluster) => {
|
||||
var roles = new models.Roles();
|
||||
roles.url = _.result(cluster, 'url') + '/roles';
|
||||
cluster.set({roles: roles});
|
||||
return roles.fetch();
|
||||
});
|
||||
requests = requests.concat(
|
||||
plugins
|
||||
.filter((plugin) => _.contains(plugin.get('groups'), 'equipment'))
|
||||
.map((plugin) => {
|
||||
var pluginLinks = new models.PluginLinks();
|
||||
pluginLinks.url = _.result(plugin, 'url') + '/links';
|
||||
plugin.set({links: pluginLinks});
|
||||
return pluginLinks.fetch();
|
||||
})
|
||||
);
|
||||
return $.when(...requests);
|
||||
})
|
||||
.then(() => {
|
||||
var links = new models.PluginLinks();
|
||||
plugins.each(
|
||||
(plugin) => links.add(plugin.get('links') && plugin.get('links').models)
|
||||
);
|
||||
|
||||
return {nodes, clusters, nodeNetworkGroups, fuelSettings, links};
|
||||
});
|
||||
}
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
selectedNodeIds: []
|
||||
};
|
||||
},
|
||||
selectNodes(ids = [], checked = false) {
|
||||
var nodeSelection = {};
|
||||
if (ids.length) {
|
||||
nodeSelection = this.state.selectedNodeIds;
|
||||
_.each(ids, (id) => {
|
||||
if (checked) {
|
||||
nodeSelection[id] = true;
|
||||
} else {
|
||||
delete nodeSelection[id];
|
||||
}
|
||||
});
|
||||
}
|
||||
this.setState({selectedNodeIds: nodeSelection});
|
||||
},
|
||||
render() {
|
||||
var roles = new models.Roles();
|
||||
this.props.clusters.each((cluster) => {
|
||||
roles.add(
|
||||
cluster.get('roles').filter((role) => !roles.findWhere({name: role.get('name')}))
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className='equipment-page'>
|
||||
<div className='page-title'>
|
||||
<h1 className='title'>{i18n('equipment_page.title')}</h1>
|
||||
</div>
|
||||
<div className='content-box'>
|
||||
<PluginLinks links={this.props.links} />
|
||||
<NodeListScreen {...this.props}
|
||||
ref='screen'
|
||||
selectedNodeIds={this.state.selectedNodeIds}
|
||||
selectNodes={this.selectNodes}
|
||||
roles={roles}
|
||||
sorters={models.Nodes.prototype.sorters}
|
||||
defaultSorting={[{status: 'asc'}]}
|
||||
filters={models.Nodes.prototype.filters}
|
||||
statusesToFilter={models.Node.prototype.statuses}
|
||||
defaultFilters={{status: []}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return {nodes, clusters, nodeNetworkGroups, fuelSettings, links};
|
||||
});
|
||||
}
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
selectedNodeIds: []
|
||||
};
|
||||
},
|
||||
selectNodes(ids = [], checked = false) {
|
||||
var nodeSelection = {};
|
||||
if (ids.length) {
|
||||
nodeSelection = this.state.selectedNodeIds;
|
||||
_.each(ids, (id) => {
|
||||
if (checked) {
|
||||
nodeSelection[id] = true;
|
||||
} else {
|
||||
delete nodeSelection[id];
|
||||
}
|
||||
});
|
||||
}
|
||||
this.setState({selectedNodeIds: nodeSelection});
|
||||
},
|
||||
render() {
|
||||
var roles = new models.Roles();
|
||||
this.props.clusters.each((cluster) => {
|
||||
roles.add(
|
||||
cluster.get('roles').filter((role) => !roles.findWhere({name: role.get('name')}))
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className='equipment-page'>
|
||||
<div className='page-title'>
|
||||
<h1 className='title'>{i18n('equipment_page.title')}</h1>
|
||||
</div>
|
||||
<div className='content-box'>
|
||||
<PluginLinks links={this.props.links} />
|
||||
<NodeListScreen {...this.props}
|
||||
ref='screen'
|
||||
selectedNodeIds={this.state.selectedNodeIds}
|
||||
selectNodes={this.selectNodes}
|
||||
roles={roles}
|
||||
sorters={models.Nodes.prototype.sorters}
|
||||
defaultSorting={[{status: 'asc'}]}
|
||||
filters={models.Nodes.prototype.filters}
|
||||
statusesToFilter={models.Node.prototype.statuses}
|
||||
defaultFilters={{status: []}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
PluginLinks = React.createClass({
|
||||
render() {
|
||||
if (!this.props.links.length) return null;
|
||||
return (
|
||||
<div className='row'>
|
||||
<div className='plugin-links-block clearfix'>
|
||||
{this.props.links.map((link, index) =>
|
||||
<div className='link-block col-xs-12' key={index}>
|
||||
<div className='title'>
|
||||
<a href={link.get('url')} target='_blank'>{link.get('title')}</a>
|
||||
</div>
|
||||
<div className='description'>{link.get('description')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
PluginLinks = React.createClass({
|
||||
render() {
|
||||
if (!this.props.links.length) return null;
|
||||
return (
|
||||
<div className='row'>
|
||||
<div className='plugin-links-block clearfix'>
|
||||
{this.props.links.map((link, index) =>
|
||||
<div className='link-block col-xs-12' key={index}>
|
||||
<div className='title'>
|
||||
<a href={link.get('url')} target='_blank'>{link.get('title')}</a>
|
||||
</div>
|
||||
<div className='description'>{link.get('description')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default EquipmentPage;
|
||||
export default EquipmentPage;
|
||||
|
|
|
@ -25,390 +25,390 @@ import {backboneMixin, pollingMixin, dispatcherMixin} from 'component_mixins';
|
|||
import {Popover} from 'views/controls';
|
||||
import {ChangePasswordDialog, ShowNodeInfoDialog} from 'views/dialogs';
|
||||
|
||||
export var Navbar = React.createClass({
|
||||
mixins: [
|
||||
dispatcherMixin('updateNodeStats', 'updateNodeStats'),
|
||||
dispatcherMixin('updateNotifications', 'updateNotifications'),
|
||||
backboneMixin('user'),
|
||||
backboneMixin('version'),
|
||||
backboneMixin('statistics'),
|
||||
backboneMixin('notifications', 'update change:status'),
|
||||
pollingMixin(20)
|
||||
],
|
||||
togglePopover(popoverName) {
|
||||
return _.memoize((visible) => {
|
||||
this.setState((previousState) => {
|
||||
var nextState = {};
|
||||
var key = popoverName + 'PopoverVisible';
|
||||
nextState[key] = _.isBoolean(visible) ? visible : !previousState[key];
|
||||
return nextState;
|
||||
});
|
||||
});
|
||||
},
|
||||
setActive(url) {
|
||||
this.setState({activeElement: url});
|
||||
},
|
||||
shouldDataBeFetched() {
|
||||
return this.props.user.get('authenticated');
|
||||
},
|
||||
fetchData() {
|
||||
return $.when(this.props.statistics.fetch(), this.props.notifications.fetch({limit: this.props.notificationsDisplayCount}));
|
||||
},
|
||||
updateNodeStats() {
|
||||
return this.props.statistics.fetch();
|
||||
},
|
||||
updateNotifications() {
|
||||
return this.props.notifications.fetch({limit: this.props.notificationsDisplayCount});
|
||||
},
|
||||
componentDidMount() {
|
||||
this.props.user.on('change:authenticated', function(model, value) {
|
||||
if (value) {
|
||||
this.startPolling();
|
||||
} else {
|
||||
this.stopPolling();
|
||||
this.props.statistics.clear();
|
||||
this.props.notifications.reset();
|
||||
export var Navbar = React.createClass({
|
||||
mixins: [
|
||||
dispatcherMixin('updateNodeStats', 'updateNodeStats'),
|
||||
dispatcherMixin('updateNotifications', 'updateNotifications'),
|
||||
backboneMixin('user'),
|
||||
backboneMixin('version'),
|
||||
backboneMixin('statistics'),
|
||||
backboneMixin('notifications', 'update change:status'),
|
||||
pollingMixin(20)
|
||||
],
|
||||
togglePopover(popoverName) {
|
||||
return _.memoize((visible) => {
|
||||
this.setState((previousState) => {
|
||||
var nextState = {};
|
||||
var key = popoverName + 'PopoverVisible';
|
||||
nextState[key] = _.isBoolean(visible) ? visible : !previousState[key];
|
||||
return nextState;
|
||||
});
|
||||
});
|
||||
},
|
||||
setActive(url) {
|
||||
this.setState({activeElement: url});
|
||||
},
|
||||
shouldDataBeFetched() {
|
||||
return this.props.user.get('authenticated');
|
||||
},
|
||||
fetchData() {
|
||||
return $.when(this.props.statistics.fetch(), this.props.notifications.fetch({limit: this.props.notificationsDisplayCount}));
|
||||
},
|
||||
updateNodeStats() {
|
||||
return this.props.statistics.fetch();
|
||||
},
|
||||
updateNotifications() {
|
||||
return this.props.notifications.fetch({limit: this.props.notificationsDisplayCount});
|
||||
},
|
||||
componentDidMount() {
|
||||
this.props.user.on('change:authenticated', function(model, value) {
|
||||
if (value) {
|
||||
this.startPolling();
|
||||
} else {
|
||||
this.stopPolling();
|
||||
this.props.statistics.clear();
|
||||
this.props.notifications.reset();
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
getDefaultProps() {
|
||||
return {
|
||||
notificationsDisplayCount: 5,
|
||||
elements: [
|
||||
{label: 'environments', url: '#clusters'},
|
||||
{label: 'equipment', url: '#equipment'},
|
||||
{label: 'releases', url: '#releases'},
|
||||
{label: 'plugins', url: '#plugins'},
|
||||
{label: 'support', url: '#support'}
|
||||
]
|
||||
};
|
||||
},
|
||||
getInitialState() {
|
||||
return {};
|
||||
},
|
||||
scrollToTop() {
|
||||
$('html, body').animate({scrollTop: 0}, 'fast');
|
||||
},
|
||||
render() {
|
||||
var unreadNotificationsCount = this.props.notifications.where({status: 'unread'}).length;
|
||||
var authenticationEnabled = this.props.version.get('auth_required') && this.props.user.get('authenticated');
|
||||
return (
|
||||
<div className='navigation-box'>
|
||||
<div className='navbar-bg'></div>
|
||||
<nav className='navbar navbar-default' role='navigation'>
|
||||
<div className='row'>
|
||||
<div className='navbar-header col-xs-2'>
|
||||
<a className='navbar-logo' href='#'></a>
|
||||
</div>
|
||||
<div className='col-xs-6'>
|
||||
<ul className='nav navbar-nav pull-left'>
|
||||
{_.map(this.props.elements, function(element) {
|
||||
return (
|
||||
<li className={utils.classNames({active: this.props.activeElement == element.url.slice(1)})} key={element.label}>
|
||||
<a href={element.url}>
|
||||
{i18n('navbar.' + element.label, {defaultValue: element.label})}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}, this)}
|
||||
</ul>
|
||||
</div>
|
||||
<div className='col-xs-4'>
|
||||
<ul className={utils.classNames({
|
||||
'nav navbar-icons pull-right': true,
|
||||
'with-auth': authenticationEnabled,
|
||||
'without-auth': !authenticationEnabled
|
||||
})}>
|
||||
<li
|
||||
key='language-icon'
|
||||
className='language-icon'
|
||||
onClick={this.togglePopover('language')}
|
||||
>
|
||||
<div className='language-text'>{i18n.getLocaleName(i18n.getCurrentLocale())}</div>
|
||||
</li>
|
||||
<li
|
||||
key='statistics-icon'
|
||||
className={'statistics-icon ' + (this.props.statistics.get('unallocated') ? '' : 'no-unallocated')}
|
||||
onClick={this.togglePopover('statistics')}
|
||||
>
|
||||
{!!this.props.statistics.get('unallocated') &&
|
||||
<div className='unallocated'>{this.props.statistics.get('unallocated')}</div>
|
||||
}
|
||||
<div className='total'>{this.props.statistics.get('total')}</div>
|
||||
</li>
|
||||
{authenticationEnabled &&
|
||||
<li
|
||||
key='user-icon'
|
||||
className='user-icon'
|
||||
onClick={this.togglePopover('user')}
|
||||
></li>
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
getDefaultProps() {
|
||||
return {
|
||||
notificationsDisplayCount: 5,
|
||||
elements: [
|
||||
{label: 'environments', url: '#clusters'},
|
||||
{label: 'equipment', url: '#equipment'},
|
||||
{label: 'releases', url: '#releases'},
|
||||
{label: 'plugins', url: '#plugins'},
|
||||
{label: 'support', url: '#support'}
|
||||
]
|
||||
};
|
||||
},
|
||||
getInitialState() {
|
||||
return {};
|
||||
},
|
||||
scrollToTop() {
|
||||
$('html, body').animate({scrollTop: 0}, 'fast');
|
||||
},
|
||||
render() {
|
||||
var unreadNotificationsCount = this.props.notifications.where({status: 'unread'}).length;
|
||||
var authenticationEnabled = this.props.version.get('auth_required') && this.props.user.get('authenticated');
|
||||
return (
|
||||
<div className='navigation-box'>
|
||||
<div className='navbar-bg'></div>
|
||||
<nav className='navbar navbar-default' role='navigation'>
|
||||
<div className='row'>
|
||||
<div className='navbar-header col-xs-2'>
|
||||
<a className='navbar-logo' href='#'></a>
|
||||
</div>
|
||||
<div className='col-xs-6'>
|
||||
<ul className='nav navbar-nav pull-left'>
|
||||
{_.map(this.props.elements, function(element) {
|
||||
return (
|
||||
<li className={utils.classNames({active: this.props.activeElement == element.url.slice(1)})} key={element.label}>
|
||||
<a href={element.url}>
|
||||
{i18n('navbar.' + element.label, {defaultValue: element.label})}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}, this)}
|
||||
</ul>
|
||||
</div>
|
||||
<div className='col-xs-4'>
|
||||
<ul className={utils.classNames({
|
||||
'nav navbar-icons pull-right': true,
|
||||
'with-auth': authenticationEnabled,
|
||||
'without-auth': !authenticationEnabled
|
||||
})}>
|
||||
<li
|
||||
key='language-icon'
|
||||
className='language-icon'
|
||||
onClick={this.togglePopover('language')}
|
||||
>
|
||||
<div className='language-text'>{i18n.getLocaleName(i18n.getCurrentLocale())}</div>
|
||||
</li>
|
||||
<li
|
||||
key='statistics-icon'
|
||||
className={'statistics-icon ' + (this.props.statistics.get('unallocated') ? '' : 'no-unallocated')}
|
||||
onClick={this.togglePopover('statistics')}
|
||||
>
|
||||
{!!this.props.statistics.get('unallocated') &&
|
||||
<div className='unallocated'>{this.props.statistics.get('unallocated')}</div>
|
||||
}
|
||||
<div className='total'>{this.props.statistics.get('total')}</div>
|
||||
</li>
|
||||
{authenticationEnabled &&
|
||||
<li
|
||||
key='user-icon'
|
||||
className='user-icon'
|
||||
onClick={this.togglePopover('user')}
|
||||
></li>
|
||||
}
|
||||
<li
|
||||
key='notifications-icon'
|
||||
className='notifications-icon'
|
||||
onClick={this.togglePopover('notifications')}
|
||||
>
|
||||
<span className={utils.classNames({badge: true, visible: unreadNotificationsCount})}>
|
||||
{unreadNotificationsCount}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
key='notifications-icon'
|
||||
className='notifications-icon'
|
||||
onClick={this.togglePopover('notifications')}
|
||||
>
|
||||
<span className={utils.classNames({badge: true, visible: unreadNotificationsCount})}>
|
||||
{unreadNotificationsCount}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
{this.state.languagePopoverVisible &&
|
||||
<LanguagePopover
|
||||
key='language-popover'
|
||||
toggle={this.togglePopover('language')}
|
||||
/>
|
||||
}
|
||||
{this.state.statisticsPopoverVisible &&
|
||||
<StatisticsPopover
|
||||
key='statistics-popover'
|
||||
statistics={this.props.statistics}
|
||||
toggle={this.togglePopover('statistics')}
|
||||
/>
|
||||
}
|
||||
{this.state.userPopoverVisible &&
|
||||
<UserPopover
|
||||
key='user-popover'
|
||||
user={this.props.user}
|
||||
toggle={this.togglePopover('user')}
|
||||
/>
|
||||
}
|
||||
{this.state.notificationsPopoverVisible &&
|
||||
<NotificationsPopover
|
||||
key='notifications-popover'
|
||||
notifications={this.props.notifications}
|
||||
displayCount={this.props.notificationsDisplayCount}
|
||||
toggle={this.togglePopover('notifications')}
|
||||
/>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div className='page-up' onClick={this.scrollToTop} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
{this.state.languagePopoverVisible &&
|
||||
<LanguagePopover
|
||||
key='language-popover'
|
||||
toggle={this.togglePopover('language')}
|
||||
/>
|
||||
}
|
||||
{this.state.statisticsPopoverVisible &&
|
||||
<StatisticsPopover
|
||||
key='statistics-popover'
|
||||
statistics={this.props.statistics}
|
||||
toggle={this.togglePopover('statistics')}
|
||||
/>
|
||||
}
|
||||
{this.state.userPopoverVisible &&
|
||||
<UserPopover
|
||||
key='user-popover'
|
||||
user={this.props.user}
|
||||
toggle={this.togglePopover('user')}
|
||||
/>
|
||||
}
|
||||
{this.state.notificationsPopoverVisible &&
|
||||
<NotificationsPopover
|
||||
key='notifications-popover'
|
||||
notifications={this.props.notifications}
|
||||
displayCount={this.props.notificationsDisplayCount}
|
||||
toggle={this.togglePopover('notifications')}
|
||||
/>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div className='page-up' onClick={this.scrollToTop} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var LanguagePopover = React.createClass({
|
||||
changeLocale(locale, e) {
|
||||
e.preventDefault();
|
||||
this.props.toggle(false);
|
||||
_.defer(() => {
|
||||
i18n.setLocale(locale);
|
||||
location.reload();
|
||||
});
|
||||
},
|
||||
render() {
|
||||
var currentLocale = i18n.getCurrentLocale();
|
||||
return (
|
||||
<Popover {...this.props} className='language-popover'>
|
||||
<ul className='nav nav-pills nav-stacked'>
|
||||
{_.map(i18n.getAvailableLocales(), function(locale) {
|
||||
return (
|
||||
<li key={locale} className={utils.classNames({active: locale == currentLocale})}>
|
||||
<a onClick={_.partial(this.changeLocale, locale)}>
|
||||
{i18n.getLanguageName(locale)}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}, this)}
|
||||
</ul>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
var LanguagePopover = React.createClass({
|
||||
changeLocale(locale, e) {
|
||||
e.preventDefault();
|
||||
this.props.toggle(false);
|
||||
_.defer(() => {
|
||||
i18n.setLocale(locale);
|
||||
location.reload();
|
||||
});
|
||||
},
|
||||
render() {
|
||||
var currentLocale = i18n.getCurrentLocale();
|
||||
return (
|
||||
<Popover {...this.props} className='language-popover'>
|
||||
<ul className='nav nav-pills nav-stacked'>
|
||||
{_.map(i18n.getAvailableLocales(), function(locale) {
|
||||
return (
|
||||
<li key={locale} className={utils.classNames({active: locale == currentLocale})}>
|
||||
<a onClick={_.partial(this.changeLocale, locale)}>
|
||||
{i18n.getLanguageName(locale)}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}, this)}
|
||||
</ul>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var StatisticsPopover = React.createClass({
|
||||
mixins: [backboneMixin('statistics')],
|
||||
render() {
|
||||
return (
|
||||
<Popover {...this.props} className='statistics-popover'>
|
||||
<div className='list-group'>
|
||||
<li className='list-group-item'>
|
||||
<span className='badge'>{this.props.statistics.get('unallocated')}</span>
|
||||
{i18n('navbar.stats.unallocated', {count: this.props.statistics.get('unallocated')})}
|
||||
</li>
|
||||
<li className='list-group-item text-success font-semibold'>
|
||||
<span className='badge bg-green'>{this.props.statistics.get('total')}</span>
|
||||
<a href='#equipment'>
|
||||
{i18n('navbar.stats.total', {count: this.props.statistics.get('total')})}
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
});
|
||||
var StatisticsPopover = React.createClass({
|
||||
mixins: [backboneMixin('statistics')],
|
||||
render() {
|
||||
return (
|
||||
<Popover {...this.props} className='statistics-popover'>
|
||||
<div className='list-group'>
|
||||
<li className='list-group-item'>
|
||||
<span className='badge'>{this.props.statistics.get('unallocated')}</span>
|
||||
{i18n('navbar.stats.unallocated', {count: this.props.statistics.get('unallocated')})}
|
||||
</li>
|
||||
<li className='list-group-item text-success font-semibold'>
|
||||
<span className='badge bg-green'>{this.props.statistics.get('total')}</span>
|
||||
<a href='#equipment'>
|
||||
{i18n('navbar.stats.total', {count: this.props.statistics.get('total')})}
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var UserPopover = React.createClass({
|
||||
mixins: [backboneMixin('user')],
|
||||
showChangePasswordDialog() {
|
||||
this.props.toggle(false);
|
||||
ChangePasswordDialog.show();
|
||||
},
|
||||
logout() {
|
||||
this.props.toggle(false);
|
||||
app.logout();
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<Popover {...this.props} className='user-popover'>
|
||||
<div className='username'>{i18n('common.username')}:</div>
|
||||
<h3 className='name'>{this.props.user.get('username')}</h3>
|
||||
<div className='clearfix'>
|
||||
<button className='btn btn-default btn-sm pull-left' onClick={this.showChangePasswordDialog}>
|
||||
<i className='glyphicon glyphicon-user'></i>
|
||||
{i18n('common.change_password')}
|
||||
</button>
|
||||
<button className='btn btn-info btn-sm pull-right btn-logout' onClick={this.logout}>
|
||||
<i className='glyphicon glyphicon-off'></i>
|
||||
{i18n('common.logout')}
|
||||
</button>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
});
|
||||
var UserPopover = React.createClass({
|
||||
mixins: [backboneMixin('user')],
|
||||
showChangePasswordDialog() {
|
||||
this.props.toggle(false);
|
||||
ChangePasswordDialog.show();
|
||||
},
|
||||
logout() {
|
||||
this.props.toggle(false);
|
||||
app.logout();
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<Popover {...this.props} className='user-popover'>
|
||||
<div className='username'>{i18n('common.username')}:</div>
|
||||
<h3 className='name'>{this.props.user.get('username')}</h3>
|
||||
<div className='clearfix'>
|
||||
<button className='btn btn-default btn-sm pull-left' onClick={this.showChangePasswordDialog}>
|
||||
<i className='glyphicon glyphicon-user'></i>
|
||||
{i18n('common.change_password')}
|
||||
</button>
|
||||
<button className='btn btn-info btn-sm pull-right btn-logout' onClick={this.logout}>
|
||||
<i className='glyphicon glyphicon-off'></i>
|
||||
{i18n('common.logout')}
|
||||
</button>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var NotificationsPopover = React.createClass({
|
||||
mixins: [backboneMixin('notifications')],
|
||||
showNodeInfo(id) {
|
||||
this.props.toggle(false);
|
||||
var node = new models.Node({id: id});
|
||||
node.fetch();
|
||||
ShowNodeInfoDialog.show({node: node});
|
||||
},
|
||||
markAsRead() {
|
||||
var notificationsToMark = new models.Notifications(this.props.notifications.where({status: 'unread'}));
|
||||
if (notificationsToMark.length) {
|
||||
this.setState({unreadNotificationsIds: notificationsToMark.pluck('id')});
|
||||
notificationsToMark.toJSON = function() {
|
||||
return notificationsToMark.map((notification) => {
|
||||
notification.set({status: 'read'});
|
||||
return _.pick(notification.attributes, 'id', 'status');
|
||||
}, this);
|
||||
};
|
||||
Backbone.sync('update', notificationsToMark);
|
||||
}
|
||||
},
|
||||
componentDidMount() {
|
||||
this.markAsRead();
|
||||
},
|
||||
getInitialState() {
|
||||
return {unreadNotificationsIds: []};
|
||||
},
|
||||
renderNotification(notification) {
|
||||
var topic = notification.get('topic'),
|
||||
nodeId = notification.get('node_id'),
|
||||
notificationClasses = {
|
||||
notification: true,
|
||||
'text-danger': topic == 'error',
|
||||
'text-warning': topic == 'warning',
|
||||
clickable: nodeId,
|
||||
unread: notification.get('status') == 'unread' || _.contains(this.state.unreadNotificationsIds, notification.id)
|
||||
},
|
||||
iconClass = {
|
||||
error: 'glyphicon-exclamation-sign',
|
||||
warning: 'glyphicon-warning-sign',
|
||||
discover: 'glyphicon-bell'
|
||||
}[topic] || 'glyphicon-info-sign';
|
||||
return (
|
||||
<div key={notification.id} className={utils.classNames(notificationClasses)}>
|
||||
<i className={'glyphicon ' + iconClass}></i>
|
||||
<p
|
||||
dangerouslySetInnerHTML={{__html: utils.urlify(notification.escape('message'))}}
|
||||
onClick={nodeId && _.partial(this.showNodeInfo, nodeId)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render() {
|
||||
var showMore = Backbone.history.getHash() != 'notifications';
|
||||
var notifications = this.props.notifications.take(this.props.displayCount);
|
||||
return (
|
||||
<Popover {...this.props} className='notifications-popover'>
|
||||
{_.map(notifications, this.renderNotification)}
|
||||
{showMore &&
|
||||
<div className='show-more'>
|
||||
<a href='#notifications'>{i18n('notifications_popover.view_all_button')}</a>
|
||||
</div>
|
||||
}
|
||||
</Popover>
|
||||
);
|
||||
var NotificationsPopover = React.createClass({
|
||||
mixins: [backboneMixin('notifications')],
|
||||
showNodeInfo(id) {
|
||||
this.props.toggle(false);
|
||||
var node = new models.Node({id: id});
|
||||
node.fetch();
|
||||
ShowNodeInfoDialog.show({node: node});
|
||||
},
|
||||
markAsRead() {
|
||||
var notificationsToMark = new models.Notifications(this.props.notifications.where({status: 'unread'}));
|
||||
if (notificationsToMark.length) {
|
||||
this.setState({unreadNotificationsIds: notificationsToMark.pluck('id')});
|
||||
notificationsToMark.toJSON = function() {
|
||||
return notificationsToMark.map((notification) => {
|
||||
notification.set({status: 'read'});
|
||||
return _.pick(notification.attributes, 'id', 'status');
|
||||
}, this);
|
||||
};
|
||||
Backbone.sync('update', notificationsToMark);
|
||||
}
|
||||
},
|
||||
componentDidMount() {
|
||||
this.markAsRead();
|
||||
},
|
||||
getInitialState() {
|
||||
return {unreadNotificationsIds: []};
|
||||
},
|
||||
renderNotification(notification) {
|
||||
var topic = notification.get('topic'),
|
||||
nodeId = notification.get('node_id'),
|
||||
notificationClasses = {
|
||||
notification: true,
|
||||
'text-danger': topic == 'error',
|
||||
'text-warning': topic == 'warning',
|
||||
clickable: nodeId,
|
||||
unread: notification.get('status') == 'unread' || _.contains(this.state.unreadNotificationsIds, notification.id)
|
||||
},
|
||||
iconClass = {
|
||||
error: 'glyphicon-exclamation-sign',
|
||||
warning: 'glyphicon-warning-sign',
|
||||
discover: 'glyphicon-bell'
|
||||
}[topic] || 'glyphicon-info-sign';
|
||||
return (
|
||||
<div key={notification.id} className={utils.classNames(notificationClasses)}>
|
||||
<i className={'glyphicon ' + iconClass}></i>
|
||||
<p
|
||||
dangerouslySetInnerHTML={{__html: utils.urlify(notification.escape('message'))}}
|
||||
onClick={nodeId && _.partial(this.showNodeInfo, nodeId)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render() {
|
||||
var showMore = Backbone.history.getHash() != 'notifications';
|
||||
var notifications = this.props.notifications.take(this.props.displayCount);
|
||||
return (
|
||||
<Popover {...this.props} className='notifications-popover'>
|
||||
{_.map(notifications, this.renderNotification)}
|
||||
{showMore &&
|
||||
<div className='show-more'>
|
||||
<a href='#notifications'>{i18n('notifications_popover.view_all_button')}</a>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export var Footer = React.createClass({
|
||||
mixins: [backboneMixin('version')],
|
||||
render() {
|
||||
var version = this.props.version;
|
||||
return (
|
||||
<div className='footer'>
|
||||
{_.contains(version.get('feature_groups'), 'mirantis') && [
|
||||
<a key='logo' className='mirantis-logo-white' href='http://www.mirantis.com/' target='_blank'></a>,
|
||||
<div key='copyright'>{i18n('common.copyright')}</div>
|
||||
]}
|
||||
<div key='version'>{i18n('common.version')}: {version.get('release')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
export var Footer = React.createClass({
|
||||
mixins: [backboneMixin('version')],
|
||||
render() {
|
||||
var version = this.props.version;
|
||||
return (
|
||||
<div className='footer'>
|
||||
{_.contains(version.get('feature_groups'), 'mirantis') && [
|
||||
<a key='logo' className='mirantis-logo-white' href='http://www.mirantis.com/' target='_blank'></a>,
|
||||
<div key='copyright'>{i18n('common.copyright')}</div>
|
||||
]}
|
||||
<div key='version'>{i18n('common.version')}: {version.get('release')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export var Breadcrumbs = React.createClass({
|
||||
mixins: [
|
||||
dispatcherMixin('updatePageLayout', 'refresh')
|
||||
],
|
||||
getInitialState() {
|
||||
return {path: this.getBreadcrumbsPath()};
|
||||
},
|
||||
getBreadcrumbsPath() {
|
||||
var page = this.props.Page;
|
||||
return _.isFunction(page.breadcrumbsPath) ? page.breadcrumbsPath(this.props.pageOptions) : page.breadcrumbsPath;
|
||||
},
|
||||
refresh() {
|
||||
this.setState({path: this.getBreadcrumbsPath()});
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<ol className='breadcrumb'>
|
||||
{_.map(this.state.path, (breadcrumb, index) => {
|
||||
if (!_.isArray(breadcrumb)) breadcrumb = [breadcrumb, null, {active: true}];
|
||||
var text = breadcrumb[0];
|
||||
var link = breadcrumb[1];
|
||||
var options = breadcrumb[2] || {};
|
||||
if (!options.skipTranslation) {
|
||||
text = i18n('breadcrumbs.' + text, {defaultValue: text});
|
||||
}
|
||||
if (options.active) {
|
||||
return <li key={index} className='active'>{text}</li>;
|
||||
} else {
|
||||
return <li key={index}><a href={link}>{text}</a></li>;
|
||||
}
|
||||
})}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
});
|
||||
export var Breadcrumbs = React.createClass({
|
||||
mixins: [
|
||||
dispatcherMixin('updatePageLayout', 'refresh')
|
||||
],
|
||||
getInitialState() {
|
||||
return {path: this.getBreadcrumbsPath()};
|
||||
},
|
||||
getBreadcrumbsPath() {
|
||||
var page = this.props.Page;
|
||||
return _.isFunction(page.breadcrumbsPath) ? page.breadcrumbsPath(this.props.pageOptions) : page.breadcrumbsPath;
|
||||
},
|
||||
refresh() {
|
||||
this.setState({path: this.getBreadcrumbsPath()});
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<ol className='breadcrumb'>
|
||||
{_.map(this.state.path, (breadcrumb, index) => {
|
||||
if (!_.isArray(breadcrumb)) breadcrumb = [breadcrumb, null, {active: true}];
|
||||
var text = breadcrumb[0];
|
||||
var link = breadcrumb[1];
|
||||
var options = breadcrumb[2] || {};
|
||||
if (!options.skipTranslation) {
|
||||
text = i18n('breadcrumbs.' + text, {defaultValue: text});
|
||||
}
|
||||
if (options.active) {
|
||||
return <li key={index} className='active'>{text}</li>;
|
||||
} else {
|
||||
return <li key={index}><a href={link}>{text}</a></li>;
|
||||
}
|
||||
})}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export var DefaultPasswordWarning = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<div className='alert global-alert alert-warning'>
|
||||
<button className='close' onClick={this.props.close}>×</button>
|
||||
{i18n('common.default_password_warning')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
export var DefaultPasswordWarning = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<div className='alert global-alert alert-warning'>
|
||||
<button className='close' onClick={this.props.close}>×</button>
|
||||
{i18n('common.default_password_warning')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export var BootstrapError = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<div className='alert global-alert alert-danger'>
|
||||
{this.props.text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
export var BootstrapError = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<div className='alert global-alert alert-danger'>
|
||||
{this.props.text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -22,150 +22,150 @@ import ReactDOM from 'react-dom';
|
|||
import utils from 'utils';
|
||||
import dispatcher from 'dispatcher';
|
||||
|
||||
var LoginPage = React.createClass({
|
||||
statics: {
|
||||
title: i18n('login_page.title'),
|
||||
hiddenLayout: true
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div className='login-page'>
|
||||
<div className='container col-md-4 col-md-offset-4 col-xs-10 col-xs-offset-1'>
|
||||
<div className='box'>
|
||||
<div className='logo-circle'></div>
|
||||
<div className='logo'></div>
|
||||
<div className='fields-box'>
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='footer col-xs-12'>
|
||||
{_.contains(app.version.get('feature_groups'), 'mirantis') &&
|
||||
<p className='text-center'>{i18n('common.copyright')}</p>
|
||||
}
|
||||
<p className='text-center'>{i18n('common.version')}: {app.version.get('release')}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
var LoginPage = React.createClass({
|
||||
statics: {
|
||||
title: i18n('login_page.title'),
|
||||
hiddenLayout: true
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div className='login-page'>
|
||||
<div className='container col-md-4 col-md-offset-4 col-xs-10 col-xs-offset-1'>
|
||||
<div className='box'>
|
||||
<div className='logo-circle'></div>
|
||||
<div className='logo'></div>
|
||||
<div className='fields-box'>
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='footer col-xs-12'>
|
||||
{_.contains(app.version.get('feature_groups'), 'mirantis') &&
|
||||
<p className='text-center'>{i18n('common.copyright')}</p>
|
||||
}
|
||||
<p className='text-center'>{i18n('common.version')}: {app.version.get('release')}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var LoginForm = React.createClass({
|
||||
login(username, password) {
|
||||
var keystoneClient = app.keystoneClient;
|
||||
|
||||
return keystoneClient.authenticate(username, password, {force: true})
|
||||
.fail((xhr) => {
|
||||
$(ReactDOM.findDOMNode(this.refs.username)).focus();
|
||||
|
||||
var status = xhr && xhr.status;
|
||||
var error = 'login_error';
|
||||
if (status == 401) {
|
||||
error = 'credentials_error';
|
||||
} else if (!status || String(status)[0] == '5') { // no status (connection refused) or 5xx error
|
||||
error = 'keystone_unavailable_error';
|
||||
}
|
||||
});
|
||||
this.setState({error: i18n('login_page.' + error)});
|
||||
})
|
||||
.then(() => {
|
||||
app.user.set({
|
||||
authenticated: true,
|
||||
username: username,
|
||||
token: keystoneClient.token
|
||||
});
|
||||
|
||||
var LoginForm = React.createClass({
|
||||
login(username, password) {
|
||||
var keystoneClient = app.keystoneClient;
|
||||
|
||||
return keystoneClient.authenticate(username, password, {force: true})
|
||||
.fail((xhr) => {
|
||||
$(ReactDOM.findDOMNode(this.refs.username)).focus();
|
||||
|
||||
var status = xhr && xhr.status;
|
||||
var error = 'login_error';
|
||||
if (status == 401) {
|
||||
error = 'credentials_error';
|
||||
} else if (!status || String(status)[0] == '5') { // no status (connection refused) or 5xx error
|
||||
error = 'keystone_unavailable_error';
|
||||
}
|
||||
this.setState({error: i18n('login_page.' + error)});
|
||||
})
|
||||
.then(() => {
|
||||
app.user.set({
|
||||
authenticated: true,
|
||||
username: username,
|
||||
token: keystoneClient.token
|
||||
});
|
||||
|
||||
if (password == keystoneClient.DEFAULT_PASSWORD) {
|
||||
dispatcher.trigger('showDefaultPasswordWarning');
|
||||
}
|
||||
|
||||
return app.fuelSettings.fetch({cache: true});
|
||||
})
|
||||
.then(() => {
|
||||
var nextUrl = '';
|
||||
if (app.router.returnUrl) {
|
||||
nextUrl = app.router.returnUrl;
|
||||
delete app.router.returnUrl;
|
||||
}
|
||||
app.navigate(nextUrl, {trigger: true});
|
||||
});
|
||||
},
|
||||
componentDidMount() {
|
||||
$(ReactDOM.findDOMNode(this.refs.username)).focus();
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
actionInProgress: false,
|
||||
error: null
|
||||
};
|
||||
},
|
||||
onChange() {
|
||||
this.setState({error: null});
|
||||
},
|
||||
onSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var username = ReactDOM.findDOMNode(this.refs.username).value;
|
||||
var password = ReactDOM.findDOMNode(this.refs.password).value;
|
||||
|
||||
this.setState({actionInProgress: true});
|
||||
|
||||
this.login(username, password)
|
||||
.fail(() => {
|
||||
this.setState({actionInProgress: false});
|
||||
});
|
||||
},
|
||||
render() {
|
||||
var httpsUsed = location.protocol == 'https:';
|
||||
var httpsPort = 8443;
|
||||
var httpsLink = 'https://' + location.hostname + ':' + httpsPort;
|
||||
|
||||
return (
|
||||
<form className='form-horizontal' onSubmit={this.onSubmit}>
|
||||
<div className='form-group'>
|
||||
<label className='control-label col-xs-2'>
|
||||
<i className='glyphicon glyphicon-user'></i>
|
||||
</label>
|
||||
<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} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label className='control-label col-xs-2'>
|
||||
<i className='glyphicon glyphicon-lock'></i>
|
||||
</label>
|
||||
<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} />
|
||||
</div>
|
||||
</div>
|
||||
{!httpsUsed &&
|
||||
<div className='http-warning'>
|
||||
<i className='glyphicon glyphicon-warning-sign'></i>
|
||||
{i18n('login_page.http_warning')}
|
||||
<br/>
|
||||
<a href={httpsLink}>{i18n('login_page.http_warning_link')}</a>
|
||||
</div>
|
||||
}
|
||||
{this.state.error &&
|
||||
<div className='login-error'>{this.state.error}</div>
|
||||
}
|
||||
<div className='form-group'>
|
||||
<div className='col-xs-12 text-center'>
|
||||
<button
|
||||
type='submit'
|
||||
className={utils.classNames({
|
||||
'btn login-btn': true,
|
||||
'btn-success': httpsUsed,
|
||||
'btn-warning': !httpsUsed
|
||||
})}
|
||||
disabled={this.state.actionInProgress}
|
||||
>
|
||||
{i18n('login_page.log_in')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
if (password == keystoneClient.DEFAULT_PASSWORD) {
|
||||
dispatcher.trigger('showDefaultPasswordWarning');
|
||||
}
|
||||
});
|
||||
|
||||
export default LoginPage;
|
||||
return app.fuelSettings.fetch({cache: true});
|
||||
})
|
||||
.then(() => {
|
||||
var nextUrl = '';
|
||||
if (app.router.returnUrl) {
|
||||
nextUrl = app.router.returnUrl;
|
||||
delete app.router.returnUrl;
|
||||
}
|
||||
app.navigate(nextUrl, {trigger: true});
|
||||
});
|
||||
},
|
||||
componentDidMount() {
|
||||
$(ReactDOM.findDOMNode(this.refs.username)).focus();
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
actionInProgress: false,
|
||||
error: null
|
||||
};
|
||||
},
|
||||
onChange() {
|
||||
this.setState({error: null});
|
||||
},
|
||||
onSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var username = ReactDOM.findDOMNode(this.refs.username).value;
|
||||
var password = ReactDOM.findDOMNode(this.refs.password).value;
|
||||
|
||||
this.setState({actionInProgress: true});
|
||||
|
||||
this.login(username, password)
|
||||
.fail(() => {
|
||||
this.setState({actionInProgress: false});
|
||||
});
|
||||
},
|
||||
render() {
|
||||
var httpsUsed = location.protocol == 'https:';
|
||||
var httpsPort = 8443;
|
||||
var httpsLink = 'https://' + location.hostname + ':' + httpsPort;
|
||||
|
||||
return (
|
||||
<form className='form-horizontal' onSubmit={this.onSubmit}>
|
||||
<div className='form-group'>
|
||||
<label className='control-label col-xs-2'>
|
||||
<i className='glyphicon glyphicon-user'></i>
|
||||
</label>
|
||||
<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} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label className='control-label col-xs-2'>
|
||||
<i className='glyphicon glyphicon-lock'></i>
|
||||
</label>
|
||||
<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} />
|
||||
</div>
|
||||
</div>
|
||||
{!httpsUsed &&
|
||||
<div className='http-warning'>
|
||||
<i className='glyphicon glyphicon-warning-sign'></i>
|
||||
{i18n('login_page.http_warning')}
|
||||
<br/>
|
||||
<a href={httpsLink}>{i18n('login_page.http_warning_link')}</a>
|
||||
</div>
|
||||
}
|
||||
{this.state.error &&
|
||||
<div className='login-error'>{this.state.error}</div>
|
||||
}
|
||||
<div className='form-group'>
|
||||
<div className='col-xs-12 text-center'>
|
||||
<button
|
||||
type='submit'
|
||||
className={utils.classNames({
|
||||
'btn login-btn': true,
|
||||
'btn-success': httpsUsed,
|
||||
'btn-warning': !httpsUsed
|
||||
})}
|
||||
disabled={this.state.actionInProgress}
|
||||
>
|
||||
{i18n('login_page.log_in')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default LoginPage;
|
||||
|
|
|
@ -21,100 +21,100 @@ import models from 'models';
|
|||
import {ShowNodeInfoDialog} from 'views/dialogs';
|
||||
import {backboneMixin} from 'component_mixins';
|
||||
|
||||
var NotificationsPage, Notification;
|
||||
var NotificationsPage, Notification;
|
||||
|
||||
NotificationsPage = React.createClass({
|
||||
mixins: [backboneMixin('notifications')],
|
||||
statics: {
|
||||
title: i18n('notifications_page.title'),
|
||||
navbarActiveElement: null,
|
||||
breadcrumbsPath: [['home', '#'], 'notifications'],
|
||||
fetchData() {
|
||||
var notifications = app.notifications;
|
||||
return notifications.fetch().then(() =>
|
||||
({notifications: notifications})
|
||||
);
|
||||
}
|
||||
},
|
||||
checkDateIsToday(date) {
|
||||
var today = new Date();
|
||||
return [today.getDate(), today.getMonth() + 1, today.getFullYear()].join('-') == date;
|
||||
},
|
||||
render() {
|
||||
var notificationGroups = this.props.notifications.groupBy('date');
|
||||
NotificationsPage = React.createClass({
|
||||
mixins: [backboneMixin('notifications')],
|
||||
statics: {
|
||||
title: i18n('notifications_page.title'),
|
||||
navbarActiveElement: null,
|
||||
breadcrumbsPath: [['home', '#'], 'notifications'],
|
||||
fetchData() {
|
||||
var notifications = app.notifications;
|
||||
return notifications.fetch().then(() =>
|
||||
({notifications: notifications})
|
||||
);
|
||||
}
|
||||
},
|
||||
checkDateIsToday(date) {
|
||||
var today = new Date();
|
||||
return [today.getDate(), today.getMonth() + 1, today.getFullYear()].join('-') == date;
|
||||
},
|
||||
render() {
|
||||
var notificationGroups = this.props.notifications.groupBy('date');
|
||||
return (
|
||||
<div className='notifications-page'>
|
||||
<div className='page-title'>
|
||||
<h1 className='title'>{i18n('notifications_page.title')}</h1>
|
||||
</div>
|
||||
<div className='content-box'>
|
||||
{_.map(notificationGroups, function(notifications, date) {
|
||||
return (
|
||||
<div className='notifications-page'>
|
||||
<div className='page-title'>
|
||||
<h1 className='title'>{i18n('notifications_page.title')}</h1>
|
||||
</div>
|
||||
<div className='content-box'>
|
||||
{_.map(notificationGroups, function(notifications, date) {
|
||||
return (
|
||||
<div className='row notification-group' key={date}>
|
||||
<div className='title col-xs-12'>
|
||||
{this.checkDateIsToday(date) ? i18n('notifications_page.today') : date}
|
||||
</div>
|
||||
{_.map(notifications, (notification) => {
|
||||
return <Notification
|
||||
key={notification.id}
|
||||
notification={notification}
|
||||
/>;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}, this)}
|
||||
</div>
|
||||
<div className='row notification-group' key={date}>
|
||||
<div className='title col-xs-12'>
|
||||
{this.checkDateIsToday(date) ? i18n('notifications_page.today') : date}
|
||||
</div>
|
||||
{_.map(notifications, (notification) => {
|
||||
return <Notification
|
||||
key={notification.id}
|
||||
notification={notification}
|
||||
/>;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
}, this)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Notification = React.createClass({
|
||||
mixins: [backboneMixin('notification')],
|
||||
showNodeInfo(id) {
|
||||
var node = new models.Node({id: id});
|
||||
node.fetch();
|
||||
ShowNodeInfoDialog.show({node: node});
|
||||
},
|
||||
markAsRead() {
|
||||
var notification = this.props.notification;
|
||||
notification.toJSON = function() {
|
||||
return notification.pick('id', 'status');
|
||||
};
|
||||
notification.save({status: 'read'});
|
||||
},
|
||||
onNotificationClick() {
|
||||
if (this.props.notification.get('status') == 'unread') {
|
||||
this.markAsRead();
|
||||
}
|
||||
var nodeId = this.props.notification.get('node_id');
|
||||
if (nodeId) {
|
||||
this.showNodeInfo(nodeId);
|
||||
}
|
||||
},
|
||||
render() {
|
||||
var topic = this.props.notification.get('topic'),
|
||||
notificationClasses = {
|
||||
'col-xs-12 notification': true,
|
||||
'text-danger': topic == 'error',
|
||||
'text-warning': topic == 'warning',
|
||||
unread: this.props.notification.get('status') == 'unread'
|
||||
},
|
||||
iconClass = {
|
||||
error: 'glyphicon-exclamation-sign',
|
||||
warning: 'glyphicon-warning-sign',
|
||||
discover: 'glyphicon-bell'
|
||||
}[topic] || 'glyphicon-info-sign';
|
||||
return (
|
||||
<div className={utils.classNames(notificationClasses)} onClick={this.onNotificationClick}>
|
||||
<div className='notification-time'>{this.props.notification.get('time')}</div>
|
||||
<div className='notification-type'><i className={'glyphicon ' + iconClass} /></div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
Notification = React.createClass({
|
||||
mixins: [backboneMixin('notification')],
|
||||
showNodeInfo(id) {
|
||||
var node = new models.Node({id: id});
|
||||
node.fetch();
|
||||
ShowNodeInfoDialog.show({node: node});
|
||||
},
|
||||
markAsRead() {
|
||||
var notification = this.props.notification;
|
||||
notification.toJSON = function() {
|
||||
return notification.pick('id', 'status');
|
||||
};
|
||||
notification.save({status: 'read'});
|
||||
},
|
||||
onNotificationClick() {
|
||||
if (this.props.notification.get('status') == 'unread') {
|
||||
this.markAsRead();
|
||||
}
|
||||
var nodeId = this.props.notification.get('node_id');
|
||||
if (nodeId) {
|
||||
this.showNodeInfo(nodeId);
|
||||
}
|
||||
},
|
||||
render() {
|
||||
var topic = this.props.notification.get('topic'),
|
||||
notificationClasses = {
|
||||
'col-xs-12 notification': true,
|
||||
'text-danger': topic == 'error',
|
||||
'text-warning': topic == 'warning',
|
||||
unread: this.props.notification.get('status') == 'unread'
|
||||
},
|
||||
iconClass = {
|
||||
error: 'glyphicon-exclamation-sign',
|
||||
warning: 'glyphicon-warning-sign',
|
||||
discover: 'glyphicon-bell'
|
||||
}[topic] || 'glyphicon-info-sign';
|
||||
return (
|
||||
<div className={utils.classNames(notificationClasses)} onClick={this.onNotificationClick}>
|
||||
<div className='notification-time'>{this.props.notification.get('time')}</div>
|
||||
<div className='notification-type'><i className={'glyphicon ' + iconClass} /></div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default NotificationsPage;
|
||||
export default NotificationsPage;
|
||||
|
|
|
@ -21,122 +21,122 @@ import React from 'react';
|
|||
import utils from 'utils';
|
||||
import models from 'models';
|
||||
|
||||
var PluginsPage = React.createClass({
|
||||
statics: {
|
||||
title: i18n('plugins_page.title'),
|
||||
navbarActiveElement: 'plugins',
|
||||
breadcrumbsPath: [['home', '#'], 'plugins'],
|
||||
fetchData() {
|
||||
var plugins = new models.Plugins();
|
||||
return plugins.fetch()
|
||||
.then(() => {
|
||||
return $.when(...plugins.map((plugin) => {
|
||||
var links = new models.PluginLinks();
|
||||
links.url = _.result(plugin, 'url') + '/links';
|
||||
plugin.set({links: links});
|
||||
return links.fetch();
|
||||
}));
|
||||
})
|
||||
.then(() => ({plugins}));
|
||||
}
|
||||
},
|
||||
getDefaultProps() {
|
||||
return {
|
||||
details: [
|
||||
'version',
|
||||
'description',
|
||||
'homepage',
|
||||
'authors',
|
||||
'licenses',
|
||||
'releases',
|
||||
'links'
|
||||
]
|
||||
};
|
||||
},
|
||||
processPluginData(plugin, attribute) {
|
||||
var data = plugin.get(attribute);
|
||||
if (attribute == 'releases') {
|
||||
return _.map(_.groupBy(data, 'os'), (osReleases, osName) =>
|
||||
<div key={osName}>
|
||||
{i18n('plugins_page.' + osName) + ': '}
|
||||
{_.pluck(osReleases, 'version').join(', ')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (attribute == 'homepage') {
|
||||
return <span dangerouslySetInnerHTML={{__html: utils.composeLink(data)}} />;
|
||||
}
|
||||
if (attribute == 'links') {
|
||||
return data.map((link) =>
|
||||
<div key={link.get('url')} className='plugin-link'>
|
||||
<a href={link.get('url')} target='_blank'>{link.get('title')}</a>
|
||||
{link.get('description')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (_.isArray(data)) return data.join(', ');
|
||||
return data;
|
||||
},
|
||||
renderPlugin(plugin, index) {
|
||||
return (
|
||||
<div key={index} className='plugin'>
|
||||
<div className='row'>
|
||||
<div className='col-xs-2' />
|
||||
<h3 className='col-xs-10'>
|
||||
{plugin.get('title')}
|
||||
</h3>
|
||||
</div>
|
||||
{_.map(this.props.details, (attribute) => {
|
||||
var data = this.processPluginData(plugin, attribute);
|
||||
if (data.length) return (
|
||||
<div className='row' key={attribute}>
|
||||
<div className='col-xs-2 detail-title text-right'>
|
||||
{i18n('plugins_page.' + attribute)}:
|
||||
</div>
|
||||
<div className='col-xs-10'>{data}</div>
|
||||
</div>
|
||||
);
|
||||
}, this)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render() {
|
||||
var isMirantisIso = _.contains(app.version.get('feature_groups'), 'mirantis'),
|
||||
links = {
|
||||
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')
|
||||
};
|
||||
return (
|
||||
<div className='plugins-page'>
|
||||
<div className='page-title'>
|
||||
<h1 className='title'>{i18n('plugins_page.title')}</h1>
|
||||
</div>
|
||||
<div className='content-box'>
|
||||
<div className='row'>
|
||||
<div className='col-xs-12'>
|
||||
{this.props.plugins.map(this.renderPlugin)}
|
||||
<div className={utils.classNames({
|
||||
'plugin-page-links': !!this.props.plugins.length,
|
||||
'text-center': true
|
||||
})}>
|
||||
{!this.props.plugins.length && i18n('plugins_page.no_plugins')}{' '}
|
||||
<span>
|
||||
{i18n('plugins_page.more_info')}{' '}
|
||||
<a href={links.catalog} target='_blank'>
|
||||
{i18n('plugins_page.plugins_catalog')}
|
||||
</a>{' '}
|
||||
{i18n('common.and')}{' '}
|
||||
<a href={links.documentation} target='_blank'>
|
||||
{i18n('plugins_page.plugins_documentation')}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
var PluginsPage = React.createClass({
|
||||
statics: {
|
||||
title: i18n('plugins_page.title'),
|
||||
navbarActiveElement: 'plugins',
|
||||
breadcrumbsPath: [['home', '#'], 'plugins'],
|
||||
fetchData() {
|
||||
var plugins = new models.Plugins();
|
||||
return plugins.fetch()
|
||||
.then(() => {
|
||||
return $.when(...plugins.map((plugin) => {
|
||||
var links = new models.PluginLinks();
|
||||
links.url = _.result(plugin, 'url') + '/links';
|
||||
plugin.set({links: links});
|
||||
return links.fetch();
|
||||
}));
|
||||
})
|
||||
.then(() => ({plugins}));
|
||||
}
|
||||
},
|
||||
getDefaultProps() {
|
||||
return {
|
||||
details: [
|
||||
'version',
|
||||
'description',
|
||||
'homepage',
|
||||
'authors',
|
||||
'licenses',
|
||||
'releases',
|
||||
'links'
|
||||
]
|
||||
};
|
||||
},
|
||||
processPluginData(plugin, attribute) {
|
||||
var data = plugin.get(attribute);
|
||||
if (attribute == 'releases') {
|
||||
return _.map(_.groupBy(data, 'os'), (osReleases, osName) =>
|
||||
<div key={osName}>
|
||||
{i18n('plugins_page.' + osName) + ': '}
|
||||
{_.pluck(osReleases, 'version').join(', ')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (attribute == 'homepage') {
|
||||
return <span dangerouslySetInnerHTML={{__html: utils.composeLink(data)}} />;
|
||||
}
|
||||
if (attribute == 'links') {
|
||||
return data.map((link) =>
|
||||
<div key={link.get('url')} className='plugin-link'>
|
||||
<a href={link.get('url')} target='_blank'>{link.get('title')}</a>
|
||||
{link.get('description')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (_.isArray(data)) return data.join(', ');
|
||||
return data;
|
||||
},
|
||||
renderPlugin(plugin, index) {
|
||||
return (
|
||||
<div key={index} className='plugin'>
|
||||
<div className='row'>
|
||||
<div className='col-xs-2' />
|
||||
<h3 className='col-xs-10'>
|
||||
{plugin.get('title')}
|
||||
</h3>
|
||||
</div>
|
||||
{_.map(this.props.details, (attribute) => {
|
||||
var data = this.processPluginData(plugin, attribute);
|
||||
if (data.length) return (
|
||||
<div className='row' key={attribute}>
|
||||
<div className='col-xs-2 detail-title text-right'>
|
||||
{i18n('plugins_page.' + attribute)}:
|
||||
</div>
|
||||
<div className='col-xs-10'>{data}</div>
|
||||
</div>
|
||||
);
|
||||
}, this)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render() {
|
||||
var isMirantisIso = _.contains(app.version.get('feature_groups'), 'mirantis'),
|
||||
links = {
|
||||
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')
|
||||
};
|
||||
return (
|
||||
<div className='plugins-page'>
|
||||
<div className='page-title'>
|
||||
<h1 className='title'>{i18n('plugins_page.title')}</h1>
|
||||
</div>
|
||||
<div className='content-box'>
|
||||
<div className='row'>
|
||||
<div className='col-xs-12'>
|
||||
{this.props.plugins.map(this.renderPlugin)}
|
||||
<div className={utils.classNames({
|
||||
'plugin-page-links': !!this.props.plugins.length,
|
||||
'text-center': true
|
||||
})}>
|
||||
{!this.props.plugins.length && i18n('plugins_page.no_plugins')}{' '}
|
||||
<span>
|
||||
{i18n('plugins_page.more_info')}{' '}
|
||||
<a href={links.catalog} target='_blank'>
|
||||
{i18n('plugins_page.plugins_catalog')}
|
||||
</a>{' '}
|
||||
{i18n('common.and')}{' '}
|
||||
<a href={links.documentation} target='_blank'>
|
||||
{i18n('plugins_page.plugins_documentation')}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default PluginsPage;
|
||||
export default PluginsPage;
|
||||
|
|
|
@ -19,49 +19,49 @@ import React from 'react';
|
|||
import {Table} from 'views/controls';
|
||||
import {backboneMixin} from 'component_mixins';
|
||||
|
||||
var ReleasesPage = React.createClass({
|
||||
mixins: [backboneMixin('releases')],
|
||||
getDefaultProps() {
|
||||
return {columns: ['name', 'version', 'state']};
|
||||
},
|
||||
statics: {
|
||||
title: i18n('release_page.title'),
|
||||
navbarActiveElement: 'releases',
|
||||
breadcrumbsPath: [['home', '#'], 'releases'],
|
||||
fetchData() {
|
||||
var releases = app.releases;
|
||||
return releases.fetch({cache: true}).then(() => ({releases}));
|
||||
}
|
||||
},
|
||||
getReleaseData(release) {
|
||||
return _.map(this.props.columns, (attr) => {
|
||||
if (attr == 'state') {
|
||||
return i18n('release_page.release.' + (release.get(attr)));
|
||||
}
|
||||
return release.get(attr) || i18n('common.not_available');
|
||||
});
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div className='releases-page'>
|
||||
<div className='page-title'>
|
||||
<h1 className='title'>{i18n('release_page.title')}</h1>
|
||||
</div>
|
||||
<div className='content-box'>
|
||||
<div className='row'>
|
||||
<div className='col-xs-12 content-elements'>
|
||||
<Table
|
||||
head={_.map(this.props.columns, (column) => {
|
||||
return ({label: i18n('release_page.' + column), className: column});
|
||||
})}
|
||||
body={this.props.releases.map(this.getReleaseData)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
var ReleasesPage = React.createClass({
|
||||
mixins: [backboneMixin('releases')],
|
||||
getDefaultProps() {
|
||||
return {columns: ['name', 'version', 'state']};
|
||||
},
|
||||
statics: {
|
||||
title: i18n('release_page.title'),
|
||||
navbarActiveElement: 'releases',
|
||||
breadcrumbsPath: [['home', '#'], 'releases'],
|
||||
fetchData() {
|
||||
var releases = app.releases;
|
||||
return releases.fetch({cache: true}).then(() => ({releases}));
|
||||
}
|
||||
},
|
||||
getReleaseData(release) {
|
||||
return _.map(this.props.columns, (attr) => {
|
||||
if (attr == 'state') {
|
||||
return i18n('release_page.release.' + (release.get(attr)));
|
||||
}
|
||||
return release.get(attr) || i18n('common.not_available');
|
||||
});
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div className='releases-page'>
|
||||
<div className='page-title'>
|
||||
<h1 className='title'>{i18n('release_page.title')}</h1>
|
||||
</div>
|
||||
<div className='content-box'>
|
||||
<div className='row'>
|
||||
<div className='col-xs-12 content-elements'>
|
||||
<Table
|
||||
head={_.map(this.props.columns, (column) => {
|
||||
return ({label: i18n('release_page.' + column), className: column});
|
||||
})}
|
||||
body={this.props.releases.map(this.getReleaseData)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default ReleasesPage;
|
||||
export default ReleasesPage;
|
||||
|
|
|
@ -24,64 +24,64 @@ import {Navbar, Breadcrumbs, DefaultPasswordWarning, BootstrapError, Footer} fro
|
|||
import {DragDropContext} from 'react-dnd';
|
||||
import HTML5Backend from 'react-dnd-html5-backend';
|
||||
|
||||
var RootComponent = React.createClass({
|
||||
mixins: [
|
||||
dispatcherMixin('updatePageLayout', 'updateTitle'),
|
||||
dispatcherMixin('showDefaultPasswordWarning', 'showDefaultPasswordWarning'),
|
||||
dispatcherMixin('hideDefaultPasswordWarning', 'hideDefaultPasswordWarning')
|
||||
],
|
||||
showDefaultPasswordWarning() {
|
||||
this.setState({showDefaultPasswordWarning: true});
|
||||
},
|
||||
hideDefaultPasswordWarning() {
|
||||
this.setState({showDefaultPasswordWarning: false});
|
||||
},
|
||||
getInitialState() {
|
||||
return {showDefaultPasswordWarning: false};
|
||||
},
|
||||
setPage(Page, pageOptions) {
|
||||
this.setState({
|
||||
Page: Page,
|
||||
pageOptions: pageOptions
|
||||
});
|
||||
return this.refs.page;
|
||||
},
|
||||
updateTitle() {
|
||||
var Page = this.state.Page,
|
||||
title = _.isFunction(Page.title) ? Page.title(this.state.pageOptions) : Page.title;
|
||||
document.title = i18n('common.title') + (title ? ' - ' + title : '');
|
||||
},
|
||||
componentDidUpdate() {
|
||||
dispatcher.trigger('updatePageLayout');
|
||||
},
|
||||
render() {
|
||||
var {Page, showDefaultPasswordWarning} = this.state;
|
||||
var {fuelSettings, version} = this.props;
|
||||
|
||||
if (!Page) return null;
|
||||
var layoutClasses = {
|
||||
clamp: true,
|
||||
'fixed-width-layout': !Page.hiddenLayout
|
||||
};
|
||||
|
||||
return (
|
||||
<div id='content-wrapper'>
|
||||
<div className={utils.classNames(layoutClasses)}>
|
||||
{!Page.hiddenLayout && [
|
||||
<Navbar key='navbar' ref='navbar' activeElement={Page.navbarActiveElement} {...this.props} />,
|
||||
<Breadcrumbs key='breadcrumbs' ref='breadcrumbs' {...this.state} />,
|
||||
showDefaultPasswordWarning && <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'>
|
||||
<Page ref='page' {...this.state.pageOptions} />
|
||||
</div>
|
||||
{!Page.hiddenLayout && <div id='footer-spacer'></div>}
|
||||
</div>
|
||||
{!Page.hiddenLayout && <Footer version={version} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
var RootComponent = React.createClass({
|
||||
mixins: [
|
||||
dispatcherMixin('updatePageLayout', 'updateTitle'),
|
||||
dispatcherMixin('showDefaultPasswordWarning', 'showDefaultPasswordWarning'),
|
||||
dispatcherMixin('hideDefaultPasswordWarning', 'hideDefaultPasswordWarning')
|
||||
],
|
||||
showDefaultPasswordWarning() {
|
||||
this.setState({showDefaultPasswordWarning: true});
|
||||
},
|
||||
hideDefaultPasswordWarning() {
|
||||
this.setState({showDefaultPasswordWarning: false});
|
||||
},
|
||||
getInitialState() {
|
||||
return {showDefaultPasswordWarning: false};
|
||||
},
|
||||
setPage(Page, pageOptions) {
|
||||
this.setState({
|
||||
Page: Page,
|
||||
pageOptions: pageOptions
|
||||
});
|
||||
return this.refs.page;
|
||||
},
|
||||
updateTitle() {
|
||||
var Page = this.state.Page,
|
||||
title = _.isFunction(Page.title) ? Page.title(this.state.pageOptions) : Page.title;
|
||||
document.title = i18n('common.title') + (title ? ' - ' + title : '');
|
||||
},
|
||||
componentDidUpdate() {
|
||||
dispatcher.trigger('updatePageLayout');
|
||||
},
|
||||
render() {
|
||||
var {Page, showDefaultPasswordWarning} = this.state;
|
||||
var {fuelSettings, version} = this.props;
|
||||
|
||||
export default DragDropContext(HTML5Backend)(RootComponent);
|
||||
if (!Page) return null;
|
||||
var layoutClasses = {
|
||||
clamp: true,
|
||||
'fixed-width-layout': !Page.hiddenLayout
|
||||
};
|
||||
|
||||
return (
|
||||
<div id='content-wrapper'>
|
||||
<div className={utils.classNames(layoutClasses)}>
|
||||
{!Page.hiddenLayout && [
|
||||
<Navbar key='navbar' ref='navbar' activeElement={Page.navbarActiveElement} {...this.props} />,
|
||||
<Breadcrumbs key='breadcrumbs' ref='breadcrumbs' {...this.state} />,
|
||||
showDefaultPasswordWarning && <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'>
|
||||
<Page ref='page' {...this.state.pageOptions} />
|
||||
</div>
|
||||
{!Page.hiddenLayout && <div id='footer-spacer'></div>}
|
||||
</div>
|
||||
{!Page.hiddenLayout && <Footer version={version} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default DragDropContext(HTML5Backend)(RootComponent);
|
||||
|
|
|
@ -21,266 +21,266 @@ import models from 'models';
|
|||
import {Input, ProgressBar} from 'views/controls';
|
||||
import {RegistrationDialog, RetrievePasswordDialog} from 'views/dialogs';
|
||||
|
||||
export default {
|
||||
propTypes: {
|
||||
settings: React.PropTypes.object.isRequired
|
||||
},
|
||||
getDefaultProps() {
|
||||
return {statsCheckboxes: ['send_anonymous_statistic', 'send_user_info']};
|
||||
},
|
||||
getInitialState() {
|
||||
var tracking = this.props.settings.get('tracking');
|
||||
return {
|
||||
isConnected: !!(tracking.email.value && tracking.password.value),
|
||||
actionInProgress: false,
|
||||
remoteLoginForm: new models.MirantisLoginForm(),
|
||||
registrationForm: new models.MirantisRegistrationForm(),
|
||||
remoteRetrievePasswordForm: new models.MirantisRetrievePasswordForm()
|
||||
};
|
||||
},
|
||||
setConnected() {
|
||||
this.setState({isConnected: true});
|
||||
},
|
||||
saveSettings(initialAttributes) {
|
||||
var settings = this.props.settings;
|
||||
this.setState({actionInProgress: true});
|
||||
return settings.save(null, {patch: true, wait: true, validate: false})
|
||||
.fail((response) => {
|
||||
if (initialAttributes) settings.set(initialAttributes);
|
||||
utils.showErrorDialog({response: response});
|
||||
})
|
||||
.always(() => {
|
||||
this.setState({actionInProgress: false});
|
||||
});
|
||||
},
|
||||
prepareStatisticsToSave() {
|
||||
var currentAttributes = _.cloneDeep(this.props.settings.attributes);
|
||||
// We're saving only two checkboxes
|
||||
_.each(this.props.statsCheckboxes, function(field) {
|
||||
var path = this.props.settings.makePath('statistics', field, 'value');
|
||||
this.props.settings.set(path, this.props.statistics.get(path));
|
||||
}, this);
|
||||
return this.saveSettings(currentAttributes);
|
||||
},
|
||||
prepareTrackingToSave(response) {
|
||||
var currentAttributes = _.cloneDeep(this.props.settings.attributes);
|
||||
// Saving user contact data to Statistics section
|
||||
_.each(response, function(value, name) {
|
||||
if (name != 'password') {
|
||||
var path = this.props.settings.makePath('statistics', name, 'value');
|
||||
this.props.settings.set(path, value);
|
||||
this.props.tracking.set(path, value);
|
||||
}
|
||||
}, this);
|
||||
// Saving email and password to Tracking section
|
||||
_.each(this.props.tracking.get('tracking'), function(data, inputName) {
|
||||
var path = this.props.settings.makePath('tracking', inputName, 'value');
|
||||
this.props.settings.set(path, this.props.tracking.get(path));
|
||||
}, this);
|
||||
this.saveSettings(currentAttributes).done(this.setConnected);
|
||||
},
|
||||
showResponseErrors(response) {
|
||||
var jsonObj,
|
||||
error = '';
|
||||
try {
|
||||
jsonObj = JSON.parse(response.responseText);
|
||||
error = jsonObj.message;
|
||||
} catch (e) {
|
||||
error = i18n('welcome_page.register.connection_error');
|
||||
}
|
||||
this.setState({error: error});
|
||||
},
|
||||
connectToMirantis() {
|
||||
this.setState({error: null});
|
||||
var tracking = this.props.tracking.get('tracking');
|
||||
if (this.props.tracking.isValid({models: this.configModels})) {
|
||||
var remoteLoginForm = this.state.remoteLoginForm;
|
||||
this.setState({actionInProgress: true});
|
||||
_.each(tracking, (data, inputName) => {
|
||||
var name = remoteLoginForm.makePath('credentials', inputName, 'value');
|
||||
remoteLoginForm.set(name, tracking[inputName].value);
|
||||
});
|
||||
remoteLoginForm.save()
|
||||
.done(this.prepareTrackingToSave)
|
||||
.fail(this.showResponseErrors)
|
||||
.always(() => {
|
||||
this.setState({actionInProgress: false});
|
||||
});
|
||||
}
|
||||
},
|
||||
checkRestrictions(name, action = 'disable') {
|
||||
return this.props.settings.checkRestrictions(this.configModels, action, this.props.settings.get('statistics').name);
|
||||
},
|
||||
componentWillMount() {
|
||||
var model = this.props.statistics || this.props.tracking;
|
||||
this.configModels = {
|
||||
fuel_settings: model,
|
||||
version: app.version,
|
||||
default: model
|
||||
};
|
||||
},
|
||||
getError(model, name) {
|
||||
return (model.validationError || {})[model.makePath('statistics', name)];
|
||||
},
|
||||
getText(key) {
|
||||
if (_.contains(app.version.get('feature_groups'), 'mirantis')) return i18n(key);
|
||||
return i18n(key + '_community');
|
||||
},
|
||||
renderInput(settingName, wrapperClassName, disabledState) {
|
||||
var model = this.props.statistics || this.props.tracking,
|
||||
setting = model.get(model.makePath('statistics', settingName));
|
||||
if (this.checkRestrictions('metadata', 'hide').result || this.checkRestrictions(settingName, 'hide').result || setting.type == 'hidden') return null;
|
||||
var error = this.getError(model, settingName),
|
||||
disabled = this.checkRestrictions('metadata').result || this.checkRestrictions(settingName).result || disabledState;
|
||||
return <Input
|
||||
key={settingName}
|
||||
type={setting.type}
|
||||
name={settingName}
|
||||
label={setting.label && this.getText(setting.label)}
|
||||
checked={!disabled && setting.value}
|
||||
value={setting.value}
|
||||
disabled={disabled}
|
||||
inputClassName={setting.type == 'text' && 'input-xlarge'}
|
||||
wrapperClassName={wrapperClassName}
|
||||
onChange={this.onCheckboxChange}
|
||||
error={error && i18n(error)}
|
||||
/>;
|
||||
},
|
||||
renderList(list, key) {
|
||||
return (
|
||||
<div key={key}>
|
||||
{i18n('statistics.' + key + '_title')}
|
||||
<ul>
|
||||
{_.map(list, (item) => {
|
||||
return <li key={item}>{i18n('statistics.' + key + '.' + item)}</li>;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
renderIntro() {
|
||||
var ns = 'statistics.',
|
||||
isMirantisIso = _.contains(app.version.get('feature_groups'), 'mirantis'),
|
||||
lists = {
|
||||
actions: [
|
||||
'operation_type',
|
||||
'operation_time',
|
||||
'actual_time',
|
||||
'network_verification',
|
||||
'ostf_results'
|
||||
],
|
||||
settings: [
|
||||
'envronments_amount',
|
||||
'distribution',
|
||||
'network_type',
|
||||
'kernel_parameters',
|
||||
'admin_network_parameters',
|
||||
'pxe_parameters',
|
||||
'dns_parameters',
|
||||
'storage_options',
|
||||
'related_projects',
|
||||
'modified_settings',
|
||||
'networking_configuration'
|
||||
],
|
||||
node_settings: [
|
||||
'deployed_nodes_amount',
|
||||
'deployed_roles',
|
||||
'disk_layout',
|
||||
'interfaces_configuration'
|
||||
],
|
||||
system_info: [
|
||||
'hypervisor',
|
||||
'hardware_info',
|
||||
'fuel_version',
|
||||
'openstack_version'
|
||||
]
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<div className='statistics-text-box'>
|
||||
<div className={utils.classNames({notice: isMirantisIso})}>{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'>
|
||||
<p>{i18n(ns + 'statistics_includes_full')}</p>
|
||||
{_.map(lists, this.renderList)}
|
||||
<p>{i18n(ns + 'statistics_user_info')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
onCheckboxChange(name, value) {
|
||||
var model = this.props.statistics || this.props.tracking;
|
||||
model.set(model.makePath('statistics', name, 'value'), value);
|
||||
},
|
||||
onTrackingSettingChange(name, value) {
|
||||
this.setState({error: null});
|
||||
var path = this.props.tracking.makePath('tracking', name);
|
||||
delete (this.props.tracking.validationError || {})[path];
|
||||
this.props.tracking.set(this.props.tracking.makePath(path, 'value'), value);
|
||||
},
|
||||
clearRegistrationForm() {
|
||||
if (!this.state.isConnected) {
|
||||
var tracking = this.props.tracking,
|
||||
initialData = this.props.settings.get('tracking');
|
||||
_.each(tracking.get('tracking'), (data, name) => {
|
||||
var path = tracking.makePath('tracking', name, 'value');
|
||||
tracking.set(path, initialData[name].value);
|
||||
});
|
||||
tracking.validationError = null;
|
||||
}
|
||||
},
|
||||
renderRegistrationForm(model, disabled, error, showProgressBar) {
|
||||
var tracking = model.get('tracking'),
|
||||
sortedFields = _.chain(_.keys(tracking))
|
||||
.without('metadata')
|
||||
.sortBy((inputName) => tracking[inputName].weight)
|
||||
.value();
|
||||
return (
|
||||
<div>
|
||||
{error &&
|
||||
<div className='text-danger'>
|
||||
<i className='glyphicon glyphicon-warning-sign' />
|
||||
{error}
|
||||
</div>
|
||||
}
|
||||
<div className='connection-form'>
|
||||
{showProgressBar && <ProgressBar />}
|
||||
{_.map(sortedFields, function(inputName) {
|
||||
return <Input
|
||||
ref={inputName}
|
||||
key={inputName}
|
||||
name={inputName}
|
||||
disabled={disabled}
|
||||
{... _.pick(tracking[inputName], 'type', 'label', 'value')}
|
||||
onChange={this.onTrackingSettingChange}
|
||||
error={(model.validationError || {})[model.makePath('tracking', inputName)]}
|
||||
/>;
|
||||
}, this)}
|
||||
<div className='links-container'>
|
||||
<button className='btn btn-link create-account pull-left' onClick={this.showRegistrationDialog}>
|
||||
{i18n('welcome_page.register.create_account')}
|
||||
</button>
|
||||
<button className='btn btn-link retrive-password pull-right' onClick={this.showRetrievePasswordDialog}>
|
||||
{i18n('welcome_page.register.retrieve_password')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
showRegistrationDialog() {
|
||||
RegistrationDialog.show({
|
||||
registrationForm: this.state.registrationForm,
|
||||
setConnected: this.setConnected,
|
||||
settings: this.props.settings,
|
||||
tracking: this.props.tracking,
|
||||
saveSettings: this.saveSettings
|
||||
});
|
||||
},
|
||||
showRetrievePasswordDialog() {
|
||||
RetrievePasswordDialog.show({
|
||||
remoteRetrievePasswordForm: this.state.remoteRetrievePasswordForm
|
||||
});
|
||||
}
|
||||
export default {
|
||||
propTypes: {
|
||||
settings: React.PropTypes.object.isRequired
|
||||
},
|
||||
getDefaultProps() {
|
||||
return {statsCheckboxes: ['send_anonymous_statistic', 'send_user_info']};
|
||||
},
|
||||
getInitialState() {
|
||||
var tracking = this.props.settings.get('tracking');
|
||||
return {
|
||||
isConnected: !!(tracking.email.value && tracking.password.value),
|
||||
actionInProgress: false,
|
||||
remoteLoginForm: new models.MirantisLoginForm(),
|
||||
registrationForm: new models.MirantisRegistrationForm(),
|
||||
remoteRetrievePasswordForm: new models.MirantisRetrievePasswordForm()
|
||||
};
|
||||
},
|
||||
setConnected() {
|
||||
this.setState({isConnected: true});
|
||||
},
|
||||
saveSettings(initialAttributes) {
|
||||
var settings = this.props.settings;
|
||||
this.setState({actionInProgress: true});
|
||||
return settings.save(null, {patch: true, wait: true, validate: false})
|
||||
.fail((response) => {
|
||||
if (initialAttributes) settings.set(initialAttributes);
|
||||
utils.showErrorDialog({response: response});
|
||||
})
|
||||
.always(() => {
|
||||
this.setState({actionInProgress: false});
|
||||
});
|
||||
},
|
||||
prepareStatisticsToSave() {
|
||||
var currentAttributes = _.cloneDeep(this.props.settings.attributes);
|
||||
// We're saving only two checkboxes
|
||||
_.each(this.props.statsCheckboxes, function(field) {
|
||||
var path = this.props.settings.makePath('statistics', field, 'value');
|
||||
this.props.settings.set(path, this.props.statistics.get(path));
|
||||
}, this);
|
||||
return this.saveSettings(currentAttributes);
|
||||
},
|
||||
prepareTrackingToSave(response) {
|
||||
var currentAttributes = _.cloneDeep(this.props.settings.attributes);
|
||||
// Saving user contact data to Statistics section
|
||||
_.each(response, function(value, name) {
|
||||
if (name != 'password') {
|
||||
var path = this.props.settings.makePath('statistics', name, 'value');
|
||||
this.props.settings.set(path, value);
|
||||
this.props.tracking.set(path, value);
|
||||
}
|
||||
}, this);
|
||||
// Saving email and password to Tracking section
|
||||
_.each(this.props.tracking.get('tracking'), function(data, inputName) {
|
||||
var path = this.props.settings.makePath('tracking', inputName, 'value');
|
||||
this.props.settings.set(path, this.props.tracking.get(path));
|
||||
}, this);
|
||||
this.saveSettings(currentAttributes).done(this.setConnected);
|
||||
},
|
||||
showResponseErrors(response) {
|
||||
var jsonObj,
|
||||
error = '';
|
||||
try {
|
||||
jsonObj = JSON.parse(response.responseText);
|
||||
error = jsonObj.message;
|
||||
} catch (e) {
|
||||
error = i18n('welcome_page.register.connection_error');
|
||||
}
|
||||
this.setState({error: error});
|
||||
},
|
||||
connectToMirantis() {
|
||||
this.setState({error: null});
|
||||
var tracking = this.props.tracking.get('tracking');
|
||||
if (this.props.tracking.isValid({models: this.configModels})) {
|
||||
var remoteLoginForm = this.state.remoteLoginForm;
|
||||
this.setState({actionInProgress: true});
|
||||
_.each(tracking, (data, inputName) => {
|
||||
var name = remoteLoginForm.makePath('credentials', inputName, 'value');
|
||||
remoteLoginForm.set(name, tracking[inputName].value);
|
||||
});
|
||||
remoteLoginForm.save()
|
||||
.done(this.prepareTrackingToSave)
|
||||
.fail(this.showResponseErrors)
|
||||
.always(() => {
|
||||
this.setState({actionInProgress: false});
|
||||
});
|
||||
}
|
||||
},
|
||||
checkRestrictions(name, action = 'disable') {
|
||||
return this.props.settings.checkRestrictions(this.configModels, action, this.props.settings.get('statistics').name);
|
||||
},
|
||||
componentWillMount() {
|
||||
var model = this.props.statistics || this.props.tracking;
|
||||
this.configModels = {
|
||||
fuel_settings: model,
|
||||
version: app.version,
|
||||
default: model
|
||||
};
|
||||
},
|
||||
getError(model, name) {
|
||||
return (model.validationError || {})[model.makePath('statistics', name)];
|
||||
},
|
||||
getText(key) {
|
||||
if (_.contains(app.version.get('feature_groups'), 'mirantis')) return i18n(key);
|
||||
return i18n(key + '_community');
|
||||
},
|
||||
renderInput(settingName, wrapperClassName, disabledState) {
|
||||
var model = this.props.statistics || this.props.tracking,
|
||||
setting = model.get(model.makePath('statistics', settingName));
|
||||
if (this.checkRestrictions('metadata', 'hide').result || this.checkRestrictions(settingName, 'hide').result || setting.type == 'hidden') return null;
|
||||
var error = this.getError(model, settingName),
|
||||
disabled = this.checkRestrictions('metadata').result || this.checkRestrictions(settingName).result || disabledState;
|
||||
return <Input
|
||||
key={settingName}
|
||||
type={setting.type}
|
||||
name={settingName}
|
||||
label={setting.label && this.getText(setting.label)}
|
||||
checked={!disabled && setting.value}
|
||||
value={setting.value}
|
||||
disabled={disabled}
|
||||
inputClassName={setting.type == 'text' && 'input-xlarge'}
|
||||
wrapperClassName={wrapperClassName}
|
||||
onChange={this.onCheckboxChange}
|
||||
error={error && i18n(error)}
|
||||
/>;
|
||||
},
|
||||
renderList(list, key) {
|
||||
return (
|
||||
<div key={key}>
|
||||
{i18n('statistics.' + key + '_title')}
|
||||
<ul>
|
||||
{_.map(list, (item) => {
|
||||
return <li key={item}>{i18n('statistics.' + key + '.' + item)}</li>;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
renderIntro() {
|
||||
var ns = 'statistics.',
|
||||
isMirantisIso = _.contains(app.version.get('feature_groups'), 'mirantis'),
|
||||
lists = {
|
||||
actions: [
|
||||
'operation_type',
|
||||
'operation_time',
|
||||
'actual_time',
|
||||
'network_verification',
|
||||
'ostf_results'
|
||||
],
|
||||
settings: [
|
||||
'envronments_amount',
|
||||
'distribution',
|
||||
'network_type',
|
||||
'kernel_parameters',
|
||||
'admin_network_parameters',
|
||||
'pxe_parameters',
|
||||
'dns_parameters',
|
||||
'storage_options',
|
||||
'related_projects',
|
||||
'modified_settings',
|
||||
'networking_configuration'
|
||||
],
|
||||
node_settings: [
|
||||
'deployed_nodes_amount',
|
||||
'deployed_roles',
|
||||
'disk_layout',
|
||||
'interfaces_configuration'
|
||||
],
|
||||
system_info: [
|
||||
'hypervisor',
|
||||
'hardware_info',
|
||||
'fuel_version',
|
||||
'openstack_version'
|
||||
]
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<div className='statistics-text-box'>
|
||||
<div className={utils.classNames({notice: isMirantisIso})}>{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'>
|
||||
<p>{i18n(ns + 'statistics_includes_full')}</p>
|
||||
{_.map(lists, this.renderList)}
|
||||
<p>{i18n(ns + 'statistics_user_info')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
onCheckboxChange(name, value) {
|
||||
var model = this.props.statistics || this.props.tracking;
|
||||
model.set(model.makePath('statistics', name, 'value'), value);
|
||||
},
|
||||
onTrackingSettingChange(name, value) {
|
||||
this.setState({error: null});
|
||||
var path = this.props.tracking.makePath('tracking', name);
|
||||
delete (this.props.tracking.validationError || {})[path];
|
||||
this.props.tracking.set(this.props.tracking.makePath(path, 'value'), value);
|
||||
},
|
||||
clearRegistrationForm() {
|
||||
if (!this.state.isConnected) {
|
||||
var tracking = this.props.tracking,
|
||||
initialData = this.props.settings.get('tracking');
|
||||
_.each(tracking.get('tracking'), (data, name) => {
|
||||
var path = tracking.makePath('tracking', name, 'value');
|
||||
tracking.set(path, initialData[name].value);
|
||||
});
|
||||
tracking.validationError = null;
|
||||
}
|
||||
},
|
||||
renderRegistrationForm(model, disabled, error, showProgressBar) {
|
||||
var tracking = model.get('tracking'),
|
||||
sortedFields = _.chain(_.keys(tracking))
|
||||
.without('metadata')
|
||||
.sortBy((inputName) => tracking[inputName].weight)
|
||||
.value();
|
||||
return (
|
||||
<div>
|
||||
{error &&
|
||||
<div className='text-danger'>
|
||||
<i className='glyphicon glyphicon-warning-sign' />
|
||||
{error}
|
||||
</div>
|
||||
}
|
||||
<div className='connection-form'>
|
||||
{showProgressBar && <ProgressBar />}
|
||||
{_.map(sortedFields, function(inputName) {
|
||||
return <Input
|
||||
ref={inputName}
|
||||
key={inputName}
|
||||
name={inputName}
|
||||
disabled={disabled}
|
||||
{... _.pick(tracking[inputName], 'type', 'label', 'value')}
|
||||
onChange={this.onTrackingSettingChange}
|
||||
error={(model.validationError || {})[model.makePath('tracking', inputName)]}
|
||||
/>;
|
||||
}, this)}
|
||||
<div className='links-container'>
|
||||
<button className='btn btn-link create-account pull-left' onClick={this.showRegistrationDialog}>
|
||||
{i18n('welcome_page.register.create_account')}
|
||||
</button>
|
||||
<button className='btn btn-link retrive-password pull-right' onClick={this.showRetrievePasswordDialog}>
|
||||
{i18n('welcome_page.register.retrieve_password')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
showRegistrationDialog() {
|
||||
RegistrationDialog.show({
|
||||
registrationForm: this.state.registrationForm,
|
||||
setConnected: this.setConnected,
|
||||
settings: this.props.settings,
|
||||
tracking: this.props.tracking,
|
||||
saveSettings: this.saveSettings
|
||||
});
|
||||
},
|
||||
showRetrievePasswordDialog() {
|
||||
RetrievePasswordDialog.show({
|
||||
remoteRetrievePasswordForm: this.state.remoteRetrievePasswordForm
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -21,281 +21,281 @@ import models from 'models';
|
|||
import {backboneMixin, pollingMixin, unsavedChangesMixin} from 'component_mixins';
|
||||
import statisticsMixin from 'views/statistics_mixin';
|
||||
|
||||
var SupportPage = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin('tasks')
|
||||
],
|
||||
statics: {
|
||||
title: i18n('support_page.title'),
|
||||
navbarActiveElement: 'support',
|
||||
breadcrumbsPath: [['home', '#'], 'support'],
|
||||
fetchData() {
|
||||
var tasks = new models.Tasks();
|
||||
return $.when(app.fuelSettings.fetch({cache: true}), tasks.fetch()).then(() => {
|
||||
var tracking = new models.FuelSettings(_.cloneDeep(app.fuelSettings.attributes)),
|
||||
statistics = new models.FuelSettings(_.cloneDeep(app.fuelSettings.attributes));
|
||||
return {
|
||||
tasks: tasks,
|
||||
settings: app.fuelSettings,
|
||||
tracking: tracking,
|
||||
statistics: statistics
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
render() {
|
||||
var elements = [
|
||||
<DocumentationLink key='DocumentationLink' />,
|
||||
<DiagnosticSnapshot key='DiagnosticSnapshot' tasks={this.props.tasks} task={this.props.tasks.findTask({name: 'dump'})} />,
|
||||
<CapacityAudit key='CapacityAudit' />
|
||||
];
|
||||
if (_.contains(app.version.get('feature_groups'), 'mirantis')) {
|
||||
elements.unshift(
|
||||
<RegistrationInfo key='RegistrationInfo' settings={this.props.settings} tracking={this.props.tracking}/>,
|
||||
<StatisticsSettings key='StatisticsSettings' settings={this.props.settings} statistics={this.props.statistics}/>,
|
||||
<SupportContacts key='SupportContacts' />
|
||||
);
|
||||
} else {
|
||||
elements.push(<StatisticsSettings key='StatisticsSettings' settings={this.props.settings} statistics={this.props.statistics}/>);
|
||||
}
|
||||
return (
|
||||
<div className='support-page'>
|
||||
<div className='page-title'>
|
||||
<h1 className='title'>{i18n('support_page.title')}</h1>
|
||||
</div>
|
||||
<div className='content-box'>
|
||||
{_.reduce(elements, (result, element, index) => {
|
||||
if (index) result.push(<hr key={index} />);
|
||||
result.push(element);
|
||||
return result;
|
||||
}, [])}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
var SupportPage = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin('tasks')
|
||||
],
|
||||
statics: {
|
||||
title: i18n('support_page.title'),
|
||||
navbarActiveElement: 'support',
|
||||
breadcrumbsPath: [['home', '#'], 'support'],
|
||||
fetchData() {
|
||||
var tasks = new models.Tasks();
|
||||
return $.when(app.fuelSettings.fetch({cache: true}), tasks.fetch()).then(() => {
|
||||
var tracking = new models.FuelSettings(_.cloneDeep(app.fuelSettings.attributes)),
|
||||
statistics = new models.FuelSettings(_.cloneDeep(app.fuelSettings.attributes));
|
||||
return {
|
||||
tasks: tasks,
|
||||
settings: app.fuelSettings,
|
||||
tracking: tracking,
|
||||
statistics: statistics
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
render() {
|
||||
var elements = [
|
||||
<DocumentationLink key='DocumentationLink' />,
|
||||
<DiagnosticSnapshot key='DiagnosticSnapshot' tasks={this.props.tasks} task={this.props.tasks.findTask({name: 'dump'})} />,
|
||||
<CapacityAudit key='CapacityAudit' />
|
||||
];
|
||||
if (_.contains(app.version.get('feature_groups'), 'mirantis')) {
|
||||
elements.unshift(
|
||||
<RegistrationInfo key='RegistrationInfo' settings={this.props.settings} tracking={this.props.tracking}/>,
|
||||
<StatisticsSettings key='StatisticsSettings' settings={this.props.settings} statistics={this.props.statistics}/>,
|
||||
<SupportContacts key='SupportContacts' />
|
||||
);
|
||||
} else {
|
||||
elements.push(<StatisticsSettings key='StatisticsSettings' settings={this.props.settings} statistics={this.props.statistics}/>);
|
||||
}
|
||||
return (
|
||||
<div className='support-page'>
|
||||
<div className='page-title'>
|
||||
<h1 className='title'>{i18n('support_page.title')}</h1>
|
||||
</div>
|
||||
<div className='content-box'>
|
||||
{_.reduce(elements, (result, element, index) => {
|
||||
if (index) result.push(<hr key={index} />);
|
||||
result.push(element);
|
||||
return result;
|
||||
}, [])}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var SupportPageElement = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<div className='support-box'>
|
||||
<div className={'support-box-cover ' + this.props.className}></div>
|
||||
<div className='support-box-content'>
|
||||
<h3>{this.props.title}</h3>
|
||||
<p>{this.props.text}</p>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
var SupportPageElement = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<div className='support-box'>
|
||||
<div className={'support-box-cover ' + this.props.className}></div>
|
||||
<div className='support-box-content'>
|
||||
<h3>{this.props.title}</h3>
|
||||
<p>{this.props.text}</p>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var DocumentationLink = React.createClass({
|
||||
render() {
|
||||
var ns = 'support_page.' + (_.contains(app.version.get('feature_groups'), 'mirantis') ? 'mirantis' : 'community') + '_';
|
||||
return (
|
||||
<SupportPageElement
|
||||
className='img-documentation-link'
|
||||
title={i18n(ns + 'title')}
|
||||
text={i18n(ns + 'text')}
|
||||
>
|
||||
<p>
|
||||
<a className='btn btn-default documentation-link' href='https://www.mirantis.com/openstack-documentation/' target='_blank'>
|
||||
{i18n('support_page.documentation_link')}
|
||||
</a>
|
||||
</p>
|
||||
</SupportPageElement>
|
||||
);
|
||||
}
|
||||
});
|
||||
var DocumentationLink = React.createClass({
|
||||
render() {
|
||||
var ns = 'support_page.' + (_.contains(app.version.get('feature_groups'), 'mirantis') ? 'mirantis' : 'community') + '_';
|
||||
return (
|
||||
<SupportPageElement
|
||||
className='img-documentation-link'
|
||||
title={i18n(ns + 'title')}
|
||||
text={i18n(ns + 'text')}
|
||||
>
|
||||
<p>
|
||||
<a className='btn btn-default documentation-link' href='https://www.mirantis.com/openstack-documentation/' target='_blank'>
|
||||
{i18n('support_page.documentation_link')}
|
||||
</a>
|
||||
</p>
|
||||
</SupportPageElement>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var RegistrationInfo = React.createClass({
|
||||
mixins: [
|
||||
statisticsMixin,
|
||||
backboneMixin('tracking', 'change invalid')
|
||||
],
|
||||
render() {
|
||||
if (this.state.isConnected)
|
||||
return (
|
||||
<SupportPageElement
|
||||
className='img-register-fuel'
|
||||
title={i18n('support_page.product_registered_title')}
|
||||
text={i18n('support_page.product_registered_content')}
|
||||
>
|
||||
<div className='registeredData enable-selection'>
|
||||
{_.map(['name', 'email', 'company'], function(value) {
|
||||
return <div key={value}><b>{i18n('statistics.setting_labels.' + value)}:</b> {this.props.tracking.get('statistics')[value].value}</div>;
|
||||
}, this)}
|
||||
<div><b>{i18n('support_page.master_node_uuid')}:</b> {this.props.tracking.get('master_node_uid')}</div>
|
||||
</div>
|
||||
<p>
|
||||
<a className='btn btn-default' href='https://software.mirantis.com/account/' target='_blank'>
|
||||
{i18n('support_page.manage_account')}
|
||||
</a>
|
||||
</p>
|
||||
</SupportPageElement>
|
||||
);
|
||||
return (
|
||||
<SupportPageElement
|
||||
className='img-register-fuel'
|
||||
title={i18n('support_page.register_fuel_title')}
|
||||
text={i18n('support_page.register_fuel_content')}
|
||||
>
|
||||
<div className='tracking'>
|
||||
{this.renderRegistrationForm(this.props.tracking, this.state.actionInProgress, this.state.error, this.state.actionInProgress)}
|
||||
<p>
|
||||
<button className='btn btn-default' onClick={this.connectToMirantis} disabled={this.state.actionInProgress} target='_blank'>
|
||||
{i18n('support_page.register_fuel_title')}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</SupportPageElement>
|
||||
);
|
||||
}
|
||||
});
|
||||
var RegistrationInfo = React.createClass({
|
||||
mixins: [
|
||||
statisticsMixin,
|
||||
backboneMixin('tracking', 'change invalid')
|
||||
],
|
||||
render() {
|
||||
if (this.state.isConnected)
|
||||
return (
|
||||
<SupportPageElement
|
||||
className='img-register-fuel'
|
||||
title={i18n('support_page.product_registered_title')}
|
||||
text={i18n('support_page.product_registered_content')}
|
||||
>
|
||||
<div className='registeredData enable-selection'>
|
||||
{_.map(['name', 'email', 'company'], function(value) {
|
||||
return <div key={value}><b>{i18n('statistics.setting_labels.' + value)}:</b> {this.props.tracking.get('statistics')[value].value}</div>;
|
||||
}, this)}
|
||||
<div><b>{i18n('support_page.master_node_uuid')}:</b> {this.props.tracking.get('master_node_uid')}</div>
|
||||
</div>
|
||||
<p>
|
||||
<a className='btn btn-default' href='https://software.mirantis.com/account/' target='_blank'>
|
||||
{i18n('support_page.manage_account')}
|
||||
</a>
|
||||
</p>
|
||||
</SupportPageElement>
|
||||
);
|
||||
return (
|
||||
<SupportPageElement
|
||||
className='img-register-fuel'
|
||||
title={i18n('support_page.register_fuel_title')}
|
||||
text={i18n('support_page.register_fuel_content')}
|
||||
>
|
||||
<div className='tracking'>
|
||||
{this.renderRegistrationForm(this.props.tracking, this.state.actionInProgress, this.state.error, this.state.actionInProgress)}
|
||||
<p>
|
||||
<button className='btn btn-default' onClick={this.connectToMirantis} disabled={this.state.actionInProgress} target='_blank'>
|
||||
{i18n('support_page.register_fuel_title')}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</SupportPageElement>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var StatisticsSettings = React.createClass({
|
||||
mixins: [
|
||||
statisticsMixin,
|
||||
backboneMixin('statistics'),
|
||||
unsavedChangesMixin
|
||||
],
|
||||
hasChanges() {
|
||||
var initialData = this.props.settings.get('statistics'),
|
||||
currentData = this.props.statistics.get('statistics');
|
||||
return _.any(this.props.statsCheckboxes, (field) => {
|
||||
return !_.isEqual(initialData[field].value, currentData[field].value);
|
||||
});
|
||||
},
|
||||
isSavingPossible() {
|
||||
return !this.state.actionInProgress && this.hasChanges();
|
||||
},
|
||||
applyChanges() {
|
||||
return this.isSavingPossible() ? this.prepareStatisticsToSave() : $.Deferred().resolve();
|
||||
},
|
||||
render() {
|
||||
var statistics = this.props.statistics.get('statistics'),
|
||||
sortedSettings = _.chain(_.keys(statistics))
|
||||
.without('metadata')
|
||||
.sortBy((settingName) => statistics[settingName].weight)
|
||||
.value();
|
||||
return (
|
||||
<SupportPageElement
|
||||
className='img-statistics'
|
||||
title={i18n('support_page.send_statistics_title')}
|
||||
>
|
||||
<div className='tracking'>
|
||||
{this.renderIntro()}
|
||||
<div className='statistics-settings'>
|
||||
{_.map(sortedSettings, (name) => this.renderInput(name))}
|
||||
</div>
|
||||
<p>
|
||||
<button
|
||||
className='btn btn-default'
|
||||
disabled={!this.isSavingPossible()}
|
||||
onClick={this.prepareStatisticsToSave}
|
||||
>
|
||||
{i18n('support_page.save_changes')}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</SupportPageElement>
|
||||
);
|
||||
}
|
||||
var StatisticsSettings = React.createClass({
|
||||
mixins: [
|
||||
statisticsMixin,
|
||||
backboneMixin('statistics'),
|
||||
unsavedChangesMixin
|
||||
],
|
||||
hasChanges() {
|
||||
var initialData = this.props.settings.get('statistics'),
|
||||
currentData = this.props.statistics.get('statistics');
|
||||
return _.any(this.props.statsCheckboxes, (field) => {
|
||||
return !_.isEqual(initialData[field].value, currentData[field].value);
|
||||
});
|
||||
},
|
||||
isSavingPossible() {
|
||||
return !this.state.actionInProgress && this.hasChanges();
|
||||
},
|
||||
applyChanges() {
|
||||
return this.isSavingPossible() ? this.prepareStatisticsToSave() : $.Deferred().resolve();
|
||||
},
|
||||
render() {
|
||||
var statistics = this.props.statistics.get('statistics'),
|
||||
sortedSettings = _.chain(_.keys(statistics))
|
||||
.without('metadata')
|
||||
.sortBy((settingName) => statistics[settingName].weight)
|
||||
.value();
|
||||
return (
|
||||
<SupportPageElement
|
||||
className='img-statistics'
|
||||
title={i18n('support_page.send_statistics_title')}
|
||||
>
|
||||
<div className='tracking'>
|
||||
{this.renderIntro()}
|
||||
<div className='statistics-settings'>
|
||||
{_.map(sortedSettings, (name) => this.renderInput(name))}
|
||||
</div>
|
||||
<p>
|
||||
<button
|
||||
className='btn btn-default'
|
||||
disabled={!this.isSavingPossible()}
|
||||
onClick={this.prepareStatisticsToSave}
|
||||
>
|
||||
{i18n('support_page.save_changes')}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</SupportPageElement>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var SupportContacts = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<SupportPageElement
|
||||
className='img-contact-support'
|
||||
title={i18n('support_page.contact_support')}
|
||||
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>
|
||||
<a className='btn btn-default' href='http://support.mirantis.com/requests/new' target='_blank'>
|
||||
{i18n('support_page.contact_support')}
|
||||
</a>
|
||||
</p>
|
||||
</SupportPageElement>
|
||||
);
|
||||
}
|
||||
var SupportContacts = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<SupportPageElement
|
||||
className='img-contact-support'
|
||||
title={i18n('support_page.contact_support')}
|
||||
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>
|
||||
<a className='btn btn-default' href='http://support.mirantis.com/requests/new' target='_blank'>
|
||||
{i18n('support_page.contact_support')}
|
||||
</a>
|
||||
</p>
|
||||
</SupportPageElement>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var DiagnosticSnapshot = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin('task'),
|
||||
pollingMixin(2)
|
||||
],
|
||||
getInitialState() {
|
||||
return {generating: this.isDumpTaskActive()};
|
||||
},
|
||||
shouldDataBeFetched() {
|
||||
return this.isDumpTaskActive();
|
||||
},
|
||||
fetchData() {
|
||||
return this.props.task.fetch().done(() => {
|
||||
if (!this.isDumpTaskActive()) this.setState({generating: false});
|
||||
});
|
||||
},
|
||||
isDumpTaskActive() {
|
||||
return this.props.task && this.props.task.match({active: true});
|
||||
},
|
||||
downloadLogs() {
|
||||
this.setState({generating: true});
|
||||
(new models.LogsPackage()).save({}, {method: 'PUT'}).always(_.bind(this.props.tasks.fetch, this.props.tasks));
|
||||
},
|
||||
componentDidUpdate() {
|
||||
this.startPolling();
|
||||
},
|
||||
render() {
|
||||
var task = this.props.task,
|
||||
generating = this.state.generating;
|
||||
return (
|
||||
<SupportPageElement
|
||||
className='img-download-logs'
|
||||
title={i18n('support_page.download_diagnostic_snapshot_text')}
|
||||
text={i18n('support_page.log_text')}
|
||||
>
|
||||
<p className='snapshot'>
|
||||
<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')}
|
||||
</button>
|
||||
{' '}
|
||||
{!generating && task &&
|
||||
<span className={task.get('status')}>
|
||||
{task.match({status: 'ready'}) &&
|
||||
<a href={task.get('message')} target='_blank'>
|
||||
<i className='icon-install'></i>
|
||||
<span>{i18n('support_page.diagnostic_snapshot')}</span>
|
||||
</a>
|
||||
}
|
||||
{task.match({status: 'error'}) && task.get('message')}
|
||||
</span>
|
||||
}
|
||||
</p>
|
||||
</SupportPageElement>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var DiagnosticSnapshot = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin('task'),
|
||||
pollingMixin(2)
|
||||
],
|
||||
getInitialState() {
|
||||
return {generating: this.isDumpTaskActive()};
|
||||
},
|
||||
shouldDataBeFetched() {
|
||||
return this.isDumpTaskActive();
|
||||
},
|
||||
fetchData() {
|
||||
return this.props.task.fetch().done(() => {
|
||||
if (!this.isDumpTaskActive()) this.setState({generating: false});
|
||||
});
|
||||
},
|
||||
isDumpTaskActive() {
|
||||
return this.props.task && this.props.task.match({active: true});
|
||||
},
|
||||
downloadLogs() {
|
||||
this.setState({generating: true});
|
||||
(new models.LogsPackage()).save({}, {method: 'PUT'}).always(_.bind(this.props.tasks.fetch, this.props.tasks));
|
||||
},
|
||||
componentDidUpdate() {
|
||||
this.startPolling();
|
||||
},
|
||||
render() {
|
||||
var task = this.props.task,
|
||||
generating = this.state.generating;
|
||||
return (
|
||||
<SupportPageElement
|
||||
className='img-download-logs'
|
||||
title={i18n('support_page.download_diagnostic_snapshot_text')}
|
||||
text={i18n('support_page.log_text')}
|
||||
>
|
||||
<p className='snapshot'>
|
||||
<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')}
|
||||
</button>
|
||||
{' '}
|
||||
{!generating && task &&
|
||||
<span className={task.get('status')}>
|
||||
{task.match({status: 'ready'}) &&
|
||||
<a href={task.get('message')} target='_blank'>
|
||||
<i className='icon-install'></i>
|
||||
<span>{i18n('support_page.diagnostic_snapshot')}</span>
|
||||
</a>
|
||||
}
|
||||
{task.match({status: 'error'}) && task.get('message')}
|
||||
</span>
|
||||
}
|
||||
</p>
|
||||
</SupportPageElement>
|
||||
);
|
||||
}
|
||||
});
|
||||
var CapacityAudit = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<SupportPageElement
|
||||
className='img-audit-logs'
|
||||
title={i18n('support_page.capacity_audit')}
|
||||
text={i18n('support_page.capacity_audit_text')}
|
||||
>
|
||||
<p>
|
||||
<a className='btn btn-default capacity-audit' href='#capacity'>
|
||||
{i18n('support_page.view_capacity_audit')}
|
||||
</a>
|
||||
</p>
|
||||
</SupportPageElement>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var CapacityAudit = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<SupportPageElement
|
||||
className='img-audit-logs'
|
||||
title={i18n('support_page.capacity_audit')}
|
||||
text={i18n('support_page.capacity_audit_text')}
|
||||
>
|
||||
<p>
|
||||
<a className='btn btn-default capacity-audit' href='#capacity'>
|
||||
{i18n('support_page.view_capacity_audit')}
|
||||
</a>
|
||||
</p>
|
||||
</SupportPageElement>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default SupportPage;
|
||||
export default SupportPage;
|
||||
|
|
|
@ -20,125 +20,125 @@ import models from 'models';
|
|||
import {backboneMixin} from 'component_mixins';
|
||||
import statisticsMixin from 'views/statistics_mixin';
|
||||
|
||||
var WelcomePage = React.createClass({
|
||||
mixins: [
|
||||
statisticsMixin,
|
||||
backboneMixin('tracking', 'change invalid')
|
||||
],
|
||||
statics: {
|
||||
title: i18n('welcome_page.title'),
|
||||
hiddenLayout: true,
|
||||
fetchData() {
|
||||
return app.fuelSettings.fetch().then(() => {
|
||||
return {
|
||||
settings: app.fuelSettings,
|
||||
tracking: new models.FuelSettings(_.cloneDeep(app.fuelSettings.attributes))
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
onStartButtonClick() {
|
||||
this.clearRegistrationForm();
|
||||
var statistics = this.props.tracking.get('statistics'),
|
||||
currentAttributes = _.cloneDeep(this.props.settings.attributes);
|
||||
statistics.user_choice_saved.value = true;
|
||||
// locked state is similar to actionInProgress but
|
||||
// we want the page isn't unlocked after successful saving
|
||||
this.setState({locked: true});
|
||||
this.props.settings.set(this.props.tracking.attributes);
|
||||
this.saveSettings(currentAttributes)
|
||||
.done(() => {
|
||||
app.navigate('', {trigger: true});
|
||||
})
|
||||
.fail(() => {
|
||||
statistics.user_choice_saved.value = false;
|
||||
this.setState({locked: false});
|
||||
});
|
||||
},
|
||||
render() {
|
||||
var ns = 'welcome_page.',
|
||||
featureGroups = app.version.get('feature_groups'),
|
||||
isMirantisIso = _.contains(featureGroups, 'mirantis'),
|
||||
statsCollectorLink = 'https://stats.fuel-infra.org/',
|
||||
privacyPolicyLink = 'https://www.mirantis.com/company/privacy-policy/',
|
||||
username = this.props.settings.get('statistics').name.value;
|
||||
var disabled = this.state.actionInProgress || this.state.locked,
|
||||
buttonProps = {
|
||||
disabled: disabled,
|
||||
onClick: this.onStartButtonClick,
|
||||
className: 'btn btn-lg btn-block btn-success'
|
||||
};
|
||||
return (
|
||||
<div className='welcome-page tracking'>
|
||||
<div className='col-md-8 col-md-offset-2 col-xs-10 col-xs-offset-1'>
|
||||
<h1 className='text-center'>{this.getText(ns + 'title')}</h1>
|
||||
{isMirantisIso ?
|
||||
<div>
|
||||
{!_.contains(featureGroups, 'techpreview') &&
|
||||
<div className='register-trial'>
|
||||
{this.state.isConnected ?
|
||||
<div className='happy-cloud'>
|
||||
<div className='cloud-smile' />
|
||||
<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>
|
||||
</div>
|
||||
:
|
||||
<div>
|
||||
<p className='register_installation'>{i18n(ns + 'register.register_installation')}</p>
|
||||
{this.renderRegistrationForm(this.props.tracking, disabled, this.state.error, this.state.actionInProgress && !this.state.locked)}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{this.renderInput('send_anonymous_statistic', 'welcome-checkbox-box', disabled)}
|
||||
{this.renderIntro()}
|
||||
{this.renderInput('send_user_info', 'welcome-checkbox-box', disabled)}
|
||||
<div>
|
||||
<div className='notice'>{i18n(ns + 'privacy_policy')}</div>
|
||||
<div><a href={privacyPolicyLink} target='_blank'>{i18n(ns + 'privacy_policy_link')}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div>
|
||||
{this.renderIntro()}
|
||||
{this.renderInput('send_anonymous_statistic', 'welcome-checkbox-box')}
|
||||
<div>
|
||||
<div>{i18n(ns + 'statistics_collector')}</div>
|
||||
<div><a href={statsCollectorLink} target='_blank'>{statsCollectorLink}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className='welcome-button-box row'>
|
||||
{this.state.isConnected || !isMirantisIso ?
|
||||
<div className='col-xs-6 col-xs-offset-3'>
|
||||
<button autoFocus {...buttonProps}>
|
||||
{i18n(ns + 'start_fuel')}
|
||||
</button>
|
||||
</div>
|
||||
:
|
||||
<div className='col-xs-10 col-xs-offset-1'>
|
||||
<div className='row'>
|
||||
<div className='col-xs-6'>
|
||||
<button {...buttonProps} className='btn btn-lg btn-block btn-default'>
|
||||
{i18n(ns + 'connect_later')}
|
||||
</button>
|
||||
</div>
|
||||
<div className='col-xs-6'>
|
||||
<button autoFocus {...buttonProps} onClick={this.connectToMirantis}>
|
||||
{i18n(ns + 'connect_now')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{isMirantisIso && <div className='welcome-text-box'>{i18n(ns + 'change_settings')}</div>}
|
||||
<div className='welcome-text-box'>{this.getText(ns + 'thanks')}</div>
|
||||
var WelcomePage = React.createClass({
|
||||
mixins: [
|
||||
statisticsMixin,
|
||||
backboneMixin('tracking', 'change invalid')
|
||||
],
|
||||
statics: {
|
||||
title: i18n('welcome_page.title'),
|
||||
hiddenLayout: true,
|
||||
fetchData() {
|
||||
return app.fuelSettings.fetch().then(() => {
|
||||
return {
|
||||
settings: app.fuelSettings,
|
||||
tracking: new models.FuelSettings(_.cloneDeep(app.fuelSettings.attributes))
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
onStartButtonClick() {
|
||||
this.clearRegistrationForm();
|
||||
var statistics = this.props.tracking.get('statistics'),
|
||||
currentAttributes = _.cloneDeep(this.props.settings.attributes);
|
||||
statistics.user_choice_saved.value = true;
|
||||
// locked state is similar to actionInProgress but
|
||||
// we want the page isn't unlocked after successful saving
|
||||
this.setState({locked: true});
|
||||
this.props.settings.set(this.props.tracking.attributes);
|
||||
this.saveSettings(currentAttributes)
|
||||
.done(() => {
|
||||
app.navigate('', {trigger: true});
|
||||
})
|
||||
.fail(() => {
|
||||
statistics.user_choice_saved.value = false;
|
||||
this.setState({locked: false});
|
||||
});
|
||||
},
|
||||
render() {
|
||||
var ns = 'welcome_page.',
|
||||
featureGroups = app.version.get('feature_groups'),
|
||||
isMirantisIso = _.contains(featureGroups, 'mirantis'),
|
||||
statsCollectorLink = 'https://stats.fuel-infra.org/',
|
||||
privacyPolicyLink = 'https://www.mirantis.com/company/privacy-policy/',
|
||||
username = this.props.settings.get('statistics').name.value;
|
||||
var disabled = this.state.actionInProgress || this.state.locked,
|
||||
buttonProps = {
|
||||
disabled: disabled,
|
||||
onClick: this.onStartButtonClick,
|
||||
className: 'btn btn-lg btn-block btn-success'
|
||||
};
|
||||
return (
|
||||
<div className='welcome-page tracking'>
|
||||
<div className='col-md-8 col-md-offset-2 col-xs-10 col-xs-offset-1'>
|
||||
<h1 className='text-center'>{this.getText(ns + 'title')}</h1>
|
||||
{isMirantisIso ?
|
||||
<div>
|
||||
{!_.contains(featureGroups, 'techpreview') &&
|
||||
<div className='register-trial'>
|
||||
{this.state.isConnected ?
|
||||
<div className='happy-cloud'>
|
||||
<div className='cloud-smile' />
|
||||
<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>
|
||||
</div>
|
||||
:
|
||||
<div>
|
||||
<p className='register_installation'>{i18n(ns + 'register.register_installation')}</p>
|
||||
{this.renderRegistrationForm(this.props.tracking, disabled, this.state.error, this.state.actionInProgress && !this.state.locked)}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
{this.renderInput('send_anonymous_statistic', 'welcome-checkbox-box', disabled)}
|
||||
{this.renderIntro()}
|
||||
{this.renderInput('send_user_info', 'welcome-checkbox-box', disabled)}
|
||||
<div>
|
||||
<div className='notice'>{i18n(ns + 'privacy_policy')}</div>
|
||||
<div><a href={privacyPolicyLink} target='_blank'>{i18n(ns + 'privacy_policy_link')}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div>
|
||||
{this.renderIntro()}
|
||||
{this.renderInput('send_anonymous_statistic', 'welcome-checkbox-box')}
|
||||
<div>
|
||||
<div>{i18n(ns + 'statistics_collector')}</div>
|
||||
<div><a href={statsCollectorLink} target='_blank'>{statsCollectorLink}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className='welcome-button-box row'>
|
||||
{this.state.isConnected || !isMirantisIso ?
|
||||
<div className='col-xs-6 col-xs-offset-3'>
|
||||
<button autoFocus {...buttonProps}>
|
||||
{i18n(ns + 'start_fuel')}
|
||||
</button>
|
||||
</div>
|
||||
:
|
||||
<div className='col-xs-10 col-xs-offset-1'>
|
||||
<div className='row'>
|
||||
<div className='col-xs-6'>
|
||||
<button {...buttonProps} className='btn btn-lg btn-block btn-default'>
|
||||
{i18n(ns + 'connect_later')}
|
||||
</button>
|
||||
</div>
|
||||
<div className='col-xs-6'>
|
||||
<button autoFocus {...buttonProps} onClick={this.connectToMirantis}>
|
||||
{i18n(ns + 'connect_now')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{isMirantisIso && <div className='welcome-text-box'>{i18n(ns + 'change_settings')}</div>}
|
||||
<div className='welcome-text-box'>{this.getText(ns + 'thanks')}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default WelcomePage;
|
||||
export default WelcomePage;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue