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:
Vitaly Kramskikh 2016-01-15 19:45:18 +03:00
parent 87d0779a55
commit c45545695b
87 changed files with 19681 additions and 19694 deletions

View File

@ -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

View File

@ -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};

View File

@ -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']);

View File

@ -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;

View File

@ -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
});
}
};
}

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;

File diff suppressed because it is too large Load Diff

View File

@ -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};

View File

@ -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;

View File

@ -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>
&nbsp;
({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>
&nbsp;
({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;

View File

@ -15,7 +15,7 @@
**/
define(['./intern'], function(config) {
'use strict';
config.environments = [{browserName: 'chrome'}];
return config;
'use strict';
config.environments = [{browserName: 'chrome'}];
return config;
});

View File

@ -15,7 +15,7 @@
**/
define(['./intern'], function(config) {
'use strict';
config.environments = [{browserName: 'firefox'}];
return config;
'use strict';
config.environments = [{browserName: 'firefox'}];
return config;
});

View File

@ -15,7 +15,7 @@
**/
define(['./intern'], function(config) {
'use strict';
config.environments = [{browserName: 'phantomjs'}];
return config;
'use strict';
config.environments = [{browserName: 'phantomjs'}];
return config;
});

View File

@ -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']
};
});

View File

@ -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
};
});

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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
};
});

View File

@ -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');
}
};
});
});

View File

@ -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);
});
}
};
});
});

View File

@ -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');
}
};
});
});

View File

@ -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();
});
}
};
});
});

View File

@ -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');
});
}
};
});
});

View File

@ -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');
}
};
});
});

View File

@ -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');
}
};
});
});

View File

@ -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');
}
};
});
});

View File

@ -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');
}
};
});
});

View File

@ -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);
}
};
});
});

View File

@ -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');
}
};
});
});

View File

@ -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');
}
}
};
});
});

View File

@ -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();
});
}
};
});
});

View File

@ -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');
});
}
};
});
});

View File

@ -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');
}
};
});
});

View File

@ -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');
}
};
});
});

View File

@ -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');
}
};
});
});

View File

@ -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');
}
};
});
});

View File

@ -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');
}
});
});
});

View File

@ -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');
});
});

View File

@ -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');
});
});
});

View File

@ -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');
});
});

View File

@ -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');
});
});

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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'>&mdash;</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'>&mdash;</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;

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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)}>&times;</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)}> &nbsp; </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)}>&times;</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)}> &nbsp; </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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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>&nbsp;</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>&nbsp;</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>
);
}
});

View File

@ -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

View File

@ -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;

View File

@ -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}>&times;</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}>&times;</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>
);
}
});

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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
});
}
};

View File

@ -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;

View File

@ -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