React.js integration
Related to blueprint backbone-to-react Change-Id: Idd801b607fad77c130d528b7fa9c90a5cb19edcd
This commit is contained in:
parent
82091e0d61
commit
6304df2eb8
@ -16,14 +16,17 @@
|
|||||||
module.exports = function(grunt) {
|
module.exports = function(grunt) {
|
||||||
var pkg = grunt.file.readJSON('package.json');
|
var pkg = grunt.file.readJSON('package.json');
|
||||||
var staticDir = grunt.option('static-dir') || '/tmp/static_compressed';
|
var staticDir = grunt.option('static-dir') || '/tmp/static_compressed';
|
||||||
|
var staticBuildPreparationDir = staticDir + '/_prepare_build';
|
||||||
|
var staticBuildDir = staticDir + '/_build';
|
||||||
|
|
||||||
grunt.initConfig({
|
grunt.initConfig({
|
||||||
pkg: pkg,
|
pkg: pkg,
|
||||||
requirejs: {
|
requirejs: {
|
||||||
compile: {
|
compile: {
|
||||||
options: {
|
options: {
|
||||||
baseUrl: '.',
|
baseUrl: '.',
|
||||||
appDir: 'static',
|
appDir: staticBuildPreparationDir + '/static',
|
||||||
dir: staticDir,
|
dir: staticBuildDir,
|
||||||
mainConfigFile: 'static/js/main.js',
|
mainConfigFile: 'static/js/main.js',
|
||||||
waitSeconds: 60,
|
waitSeconds: 60,
|
||||||
optimize: 'uglify2',
|
optimize: 'uglify2',
|
||||||
@ -33,7 +36,8 @@ module.exports = function(grunt) {
|
|||||||
},
|
},
|
||||||
map: {
|
map: {
|
||||||
'*': {
|
'*': {
|
||||||
'css': 'require-css'
|
'css': 'require-css',
|
||||||
|
'JSXTransformer': 'empty:'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modules: [
|
modules: [
|
||||||
@ -68,7 +72,7 @@ module.exports = function(grunt) {
|
|||||||
less: {
|
less: {
|
||||||
all: {
|
all: {
|
||||||
src: 'static/css/styles.less',
|
src: 'static/css/styles.less',
|
||||||
dest: 'static/css/styles.css',
|
dest: staticBuildPreparationDir + '/static/css/styles.css',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bower: {
|
bower: {
|
||||||
@ -88,21 +92,94 @@ module.exports = function(grunt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
react: {
|
||||||
|
compile: {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
expand: true,
|
||||||
|
src: [staticBuildPreparationDir + '/static/**/*.jsx'],
|
||||||
|
ext: '.js'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
copy: {
|
||||||
|
prepare_build: {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
expand: true,
|
||||||
|
src: [
|
||||||
|
'static/**',
|
||||||
|
'!**/*.less',
|
||||||
|
'!**/*.js',
|
||||||
|
'!**/*.jsx',
|
||||||
|
'!**/*.jison'
|
||||||
|
],
|
||||||
|
dest: staticBuildPreparationDir + '/'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
preprocess_js: {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
expand: true,
|
||||||
|
src: [
|
||||||
|
'static/**/*.js',
|
||||||
|
'static/**/*.jsx',
|
||||||
|
'!**/JSXTransformer.js'
|
||||||
|
],
|
||||||
|
dest: staticBuildPreparationDir + '/'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
process: function (content, path) {
|
||||||
|
content = content.replace(/jsx!/g, '');
|
||||||
|
if (/\.jsx$/.test(path)) {
|
||||||
|
content = '/** @jsx React.DOM */\n' + content;
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
finalize_build: {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
expand: true,
|
||||||
|
cwd: staticBuildDir,
|
||||||
|
src: ['**'],
|
||||||
|
dest: staticDir
|
||||||
|
}
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
force: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
clean: {
|
clean: {
|
||||||
trim: {
|
trim: {
|
||||||
expand: true,
|
expand: true,
|
||||||
cwd: staticDir,
|
cwd: staticBuildDir,
|
||||||
src: [
|
src: [
|
||||||
'**/*.js',
|
'**/*.js',
|
||||||
'!js/main.js',
|
'!js/main.js',
|
||||||
'!js/libs/bower/requirejs/js/require.js',
|
'!js/libs/bower/requirejs/js/require.js',
|
||||||
'**/*.css',
|
'**/*.css',
|
||||||
'**/*.less',
|
|
||||||
'!css/styles.css',
|
'!css/styles.css',
|
||||||
'templates',
|
'templates',
|
||||||
'i18n'
|
'i18n'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
jsx: {
|
||||||
|
expand: true,
|
||||||
|
cwd: staticBuildPreparationDir,
|
||||||
|
src: ['**/*.jsx']
|
||||||
|
},
|
||||||
|
prepare_build: {
|
||||||
|
src: [staticDir]
|
||||||
|
},
|
||||||
|
finalize_build: {
|
||||||
|
src: [staticBuildDir, staticBuildPreparationDir]
|
||||||
|
},
|
||||||
options: {
|
options: {
|
||||||
force: true
|
force: true
|
||||||
}
|
}
|
||||||
@ -110,7 +187,7 @@ module.exports = function(grunt) {
|
|||||||
cleanempty: {
|
cleanempty: {
|
||||||
trim: {
|
trim: {
|
||||||
expand: true,
|
expand: true,
|
||||||
cwd: staticDir,
|
cwd: staticBuildDir,
|
||||||
src: ['**']
|
src: ['**']
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
@ -121,7 +198,7 @@ module.exports = function(grunt) {
|
|||||||
replace: {
|
replace: {
|
||||||
sha: {
|
sha: {
|
||||||
src: 'static/index.html',
|
src: 'static/index.html',
|
||||||
dest: staticDir + '/',
|
dest: staticBuildDir + '/',
|
||||||
replacements: [{
|
replacements: [{
|
||||||
from: '__COMMIT_SHA__',
|
from: '__COMMIT_SHA__',
|
||||||
to: function() {
|
to: function() {
|
||||||
@ -150,8 +227,22 @@ module.exports = function(grunt) {
|
|||||||
.filter(function(npmTaskName) { return npmTaskName.indexOf('grunt-') === 0; })
|
.filter(function(npmTaskName) { return npmTaskName.indexOf('grunt-') === 0; })
|
||||||
.forEach(grunt.loadNpmTasks.bind(grunt));
|
.forEach(grunt.loadNpmTasks.bind(grunt));
|
||||||
|
|
||||||
grunt.registerTask('trimstatic', ['clean', 'cleanempty']);
|
grunt.registerTask('build', [
|
||||||
grunt.registerTask('build', ['bower', 'less', 'requirejs', 'trimstatic', 'revision', 'replace']);
|
'bower',
|
||||||
|
'clean:prepare_build',
|
||||||
|
'copy:prepare_build',
|
||||||
|
'copy:preprocess_js',
|
||||||
|
'less',
|
||||||
|
'react',
|
||||||
|
'clean:jsx',
|
||||||
|
'requirejs',
|
||||||
|
'clean:trim',
|
||||||
|
'cleanempty:trim',
|
||||||
|
'revision',
|
||||||
|
'replace',
|
||||||
|
'copy:finalize_build',
|
||||||
|
'clean:finalize_build'
|
||||||
|
]);
|
||||||
grunt.registerTask('default', ['build']);
|
grunt.registerTask('default', ['build']);
|
||||||
grunt.task.loadTasks('grunt');
|
grunt.task.loadTasks('grunt');
|
||||||
};
|
};
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
"name": "fuel-web",
|
"name": "fuel-web",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jquery": "1.9.1",
|
"jquery": "1.9.1",
|
||||||
|
"react": "0.11.1",
|
||||||
|
"react.backbone": "0.4.0",
|
||||||
"requirejs": "2.1.9",
|
"requirejs": "2.1.9",
|
||||||
"requirejs-text": "2.0.10",
|
"requirejs-text": "2.0.10",
|
||||||
"require-css": "0.1.0",
|
"require-css": "0.1.0",
|
||||||
@ -17,6 +19,9 @@
|
|||||||
"jquery": {
|
"jquery": {
|
||||||
"js": "jquery.js"
|
"js": "jquery.js"
|
||||||
},
|
},
|
||||||
|
"react": {
|
||||||
|
"js": ["JSXTransformer.js", "react-with-addons.js"]
|
||||||
|
},
|
||||||
"requirejs": {
|
"requirejs": {
|
||||||
"js": "require.js"
|
"js": "require.js"
|
||||||
},
|
},
|
||||||
|
960
nailgun/npm-shrinkwrap.json
generated
960
nailgun/npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,12 +10,14 @@
|
|||||||
"grunt-bower-task": "~0.4.0",
|
"grunt-bower-task": "~0.4.0",
|
||||||
"grunt-cleanempty": "~0.2.0",
|
"grunt-cleanempty": "~0.2.0",
|
||||||
"grunt-contrib-clean": "~0.5.0",
|
"grunt-contrib-clean": "~0.5.0",
|
||||||
|
"grunt-contrib-copy": "~0.5.0",
|
||||||
"grunt-contrib-less": "~0.8.2",
|
"grunt-contrib-less": "~0.8.2",
|
||||||
"grunt-contrib-requirejs": "~0.4.1",
|
"grunt-contrib-requirejs": "~0.4.1",
|
||||||
"grunt-debug-task": "~0.1.3",
|
"grunt-debug-task": "~0.1.3",
|
||||||
"grunt-git-revision": "~0.0.1",
|
"grunt-git-revision": "~0.0.1",
|
||||||
"grunt-jison": "~1.2.1",
|
"grunt-jison": "~1.2.1",
|
||||||
"grunt-jslint": "~1.1.1",
|
"grunt-jslint": "~1.1.1",
|
||||||
|
"grunt-react": "~0.9.0",
|
||||||
"grunt-text-replace": "~0.3.12",
|
"grunt-text-replace": "~0.3.12",
|
||||||
"jison": "~0.4.13",
|
"jison": "~0.4.13",
|
||||||
"jslint": "~0.2.5",
|
"jslint": "~0.2.5",
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="wrap">
|
<div id="wrap">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<div id="navbar" class="container"></div>
|
||||||
|
<div id="breadcrumbs" class="container"></div>
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<div class="loading"></div>
|
<div class="loading"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -23,5 +25,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="footer"></div>
|
<div id="footer"></div>
|
||||||
|
<div id="modal-container"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -15,6 +15,9 @@
|
|||||||
**/
|
**/
|
||||||
define(
|
define(
|
||||||
[
|
[
|
||||||
|
'react',
|
||||||
|
'utils',
|
||||||
|
'jsx!views/layout',
|
||||||
'coccyx',
|
'coccyx',
|
||||||
'js/coccyx_mixins',
|
'js/coccyx_mixins',
|
||||||
'models',
|
'models',
|
||||||
@ -23,13 +26,13 @@ define(
|
|||||||
'views/login_page',
|
'views/login_page',
|
||||||
'views/cluster_page',
|
'views/cluster_page',
|
||||||
'views/cluster_page_tabs/nodes_tab',
|
'views/cluster_page_tabs/nodes_tab',
|
||||||
'views/clusters_page',
|
'jsx!views/clusters_page',
|
||||||
'views/releases_page',
|
'views/releases_page',
|
||||||
'views/notifications_page',
|
'views/notifications_page',
|
||||||
'views/support_page',
|
'views/support_page',
|
||||||
'views/capacity_page'
|
'views/capacity_page'
|
||||||
],
|
],
|
||||||
function(Coccyx, coccyxMixins, models, KeystoneClient, commonViews, LoginPage, ClusterPage, NodesTab, ClustersPage, ReleasesPage, NotificationsPage, SupportPage, CapacityPage) {
|
function(React, utils, layoutComponents, Coccyx, coccyxMixins, models, KeystoneClient, commonViews, LoginPage, ClusterPage, NodesTab, ClustersPage, ReleasesPage, NotificationsPage, SupportPage, CapacityPage) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var AppRouter = Backbone.Router.extend({
|
var AppRouter = Backbone.Router.extend({
|
||||||
@ -71,7 +74,7 @@ function(Coccyx, coccyxMixins, models, KeystoneClient, commonViews, LoginPage, C
|
|||||||
});
|
});
|
||||||
var version = this.version = new models.FuelVersion();
|
var version = this.version = new models.FuelVersion();
|
||||||
|
|
||||||
version.fetch().done(_.bind(function() {
|
version.fetch().then(_.bind(function() {
|
||||||
this.user = new models.User({authenticated: !version.get('auth_required')});
|
this.user = new models.User({authenticated: !version.get('auth_required')});
|
||||||
|
|
||||||
var originalSync = Backbone.sync;
|
var originalSync = Backbone.sync;
|
||||||
@ -126,49 +129,48 @@ function(Coccyx, coccyxMixins, models, KeystoneClient, commonViews, LoginPage, C
|
|||||||
return originalSync.call(this, method, model, options);
|
return originalSync.call(this, method, model, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.renderLayout();
|
|
||||||
|
|
||||||
if (version.get('auth_required')) {
|
if (version.get('auth_required')) {
|
||||||
_.extend(keystoneClient, this.user.pick('username', 'password'));
|
_.extend(keystoneClient, this.user.pick('username', 'password'));
|
||||||
keystoneClient.authenticate()
|
return keystoneClient.authenticate()
|
||||||
.done(function() {
|
.done(function() {
|
||||||
app.user.set({authenticated: true});
|
app.user.set({authenticated: true});
|
||||||
})
|
|
||||||
.always(function() {
|
|
||||||
Backbone.history.start();
|
|
||||||
})
|
|
||||||
.fail(function() {
|
|
||||||
app.navigate('#login', {trigger: true});
|
|
||||||
});
|
});
|
||||||
} else {
|
}
|
||||||
|
return $.Deferred().resolve();
|
||||||
|
}, this)).always(_.bind(function() {
|
||||||
|
this.renderLayout();
|
||||||
Backbone.history.start();
|
Backbone.history.start();
|
||||||
|
if (version.get('auth_required') && !this.user.get('authenticated')) {
|
||||||
|
app.navigate('#login', {trigger: true});
|
||||||
}
|
}
|
||||||
}, this));
|
}, this));
|
||||||
},
|
},
|
||||||
renderLayout: function() {
|
renderLayout: function() {
|
||||||
this.content = $('#content');
|
this.content = $('#content');
|
||||||
this.navbar = new commonViews.Navbar({elements: [
|
this.navbar = React.renderComponent(new layoutComponents.Navbar({
|
||||||
|
elements: [
|
||||||
{label: 'environments', url: '#clusters'},
|
{label: 'environments', url: '#clusters'},
|
||||||
{label: 'releases', url:'#releases'},
|
{label: 'releases', url:'#releases'},
|
||||||
{label: 'support', url:'#support'}
|
{label: 'support', url:'#support'}
|
||||||
]});
|
],
|
||||||
this.content.before(this.navbar.render().el);
|
user: this.user,
|
||||||
this.breadcrumbs = new commonViews.Breadcrumbs();
|
version: this.version,
|
||||||
this.content.before(this.breadcrumbs.render().el);
|
statistics: new models.NodesStatistics(),
|
||||||
this.footer = new commonViews.Footer();
|
notifications: new models.Notifications()
|
||||||
$('#footer').html(this.footer.render().el);
|
}), $('#navbar')[0]);
|
||||||
|
this.breadcrumbs = React.renderComponent(new layoutComponents.Breadcrumbs(), $('#breadcrumbs')[0]);
|
||||||
|
this.footer = React.renderComponent(new layoutComponents.Footer({version: this.version}), $('#footer')[0]);
|
||||||
this.content.find('.loading').addClass('layout-loaded');
|
this.content.find('.loading').addClass('layout-loaded');
|
||||||
},
|
},
|
||||||
setPage: function(NewPage, options) {
|
setPage: function(NewPage, options) {
|
||||||
if (this.page) {
|
if (this.page) {
|
||||||
this.page.tearDown();
|
utils.universalUnmount(this.page);
|
||||||
}
|
}
|
||||||
this.page = new NewPage(options);
|
this.page = utils.universalMount(new NewPage(options), this.content);
|
||||||
this.page.updateNavbar();
|
this.navbar.setActive(_.result(this.page, 'navbarActiveElement'));
|
||||||
this.page.updateBreadcrumbs();
|
this.breadcrumbs.setPath(_.result(this.page, 'breadcrumbsPath'));
|
||||||
this.page.updateTitle();
|
var newTitle = _.result(this.page, 'title');
|
||||||
this.content.html(this.page.render().el);
|
document.title = $.t('common.title') + (newTitle ? ' - ' + newTitle : '');
|
||||||
|
|
||||||
},
|
},
|
||||||
// routes
|
// routes
|
||||||
login: function() {
|
login: function() {
|
||||||
@ -259,7 +261,7 @@ function(Coccyx, coccyxMixins, models, KeystoneClient, commonViews, LoginPage, C
|
|||||||
cluster.get('nodes').deferred = nodes.deferred;
|
cluster.get('nodes').deferred = nodes.deferred;
|
||||||
cluster.set('tasks', new models.Tasks(tasks.where({cluster: cluster.id})));
|
cluster.set('tasks', new models.Tasks(tasks.where({cluster: cluster.id})));
|
||||||
}, this);
|
}, this);
|
||||||
this.setPage(ClustersPage, {collection: clusters});
|
this.setPage(ClustersPage, {clusters: clusters});
|
||||||
}, this));
|
}, this));
|
||||||
},
|
},
|
||||||
listReleases: function() {
|
listReleases: function() {
|
||||||
@ -276,7 +278,7 @@ function(Coccyx, coccyxMixins, models, KeystoneClient, commonViews, LoginPage, C
|
|||||||
}, this));
|
}, this));
|
||||||
},
|
},
|
||||||
showNotifications: function() {
|
showNotifications: function() {
|
||||||
this.setPage(NotificationsPage, {notifications: app.navbar.notifications});
|
this.setPage(NotificationsPage, {notifications: app.navbar.props.notifications});
|
||||||
},
|
},
|
||||||
showSupportPage: function() {
|
showSupportPage: function() {
|
||||||
this.setPage(SupportPage);
|
this.setPage(SupportPage);
|
||||||
|
85
nailgun/static/js/component_mixins.jsx
Normal file
85
nailgun/static/js/component_mixins.jsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014 Mirantis, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
* not use this file except in compliance with the License. You may obtain
|
||||||
|
* a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
**/
|
||||||
|
define(['jquery', 'underscore', 'react'], function($, _, React) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
return {
|
||||||
|
pollingMixin: function(updateInterval) {
|
||||||
|
updateInterval = updateInterval * 1000;
|
||||||
|
return {
|
||||||
|
scheduleDataFetch: function() {
|
||||||
|
var shouldDataBeFetched = !_.isFunction(this.shouldDataBeFetched) || this.shouldDataBeFetched();
|
||||||
|
if (this.isMounted() && !this.activeTimeout && shouldDataBeFetched) {
|
||||||
|
this.activeTimeout = $.timeout(updateInterval).done(_.bind(this.startPolling, this));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startPolling: function() {
|
||||||
|
var shouldDataBeFetched = !_.isFunction(this.shouldDataBeFetched) || this.shouldDataBeFetched();
|
||||||
|
if (shouldDataBeFetched) {
|
||||||
|
this.stopPolling();
|
||||||
|
this.fetchData().always(_.bind(this.scheduleDataFetch, this));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stopPolling: function() {
|
||||||
|
if (this.activeTimeout) {
|
||||||
|
this.activeTimeout.clear();
|
||||||
|
}
|
||||||
|
delete this.activeTimeout;
|
||||||
|
},
|
||||||
|
componentDidMount: function() {
|
||||||
|
this.startPolling();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
dialogMixin: {
|
||||||
|
componentDidMount: function() {
|
||||||
|
var $el = $(this.getDOMNode());
|
||||||
|
var modalOptions = _.clone(this.props.modalOptions) || {};
|
||||||
|
_.defaults(modalOptions, {background: true, keyboard: true});
|
||||||
|
$el.modal(modalOptions);
|
||||||
|
$el.on('hidden', this.handleHidden);
|
||||||
|
$el.on('shown', function() {
|
||||||
|
$el.find('input:first').focus();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
$(this.getDOMNode()).off('shown hidden');
|
||||||
|
},
|
||||||
|
handleHidden: function() {
|
||||||
|
React.unmountComponentAtNode(this.getDOMNode().parentNode);
|
||||||
|
},
|
||||||
|
close: function() {
|
||||||
|
$(this.getDOMNode()).modal('hide');
|
||||||
|
},
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div className="modal fade" tabIndex="-1">
|
||||||
|
<div className="modal-header">
|
||||||
|
<button type="button" className="close" onClick={this.close}>×</button>
|
||||||
|
<h3>{this.props.title}</h3>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body">
|
||||||
|
{this.renderBody()}
|
||||||
|
</div>
|
||||||
|
<div className="modal-footer">
|
||||||
|
{this.renderFooter ? this.renderFooter() : <button className="btn" onClick={this.close}>{$.t('common.close_button')}</button>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
68
nailgun/static/js/libs/custom/jsx.js
Normal file
68
nailgun/static/js/libs/custom/jsx.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* @license The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2014 Felipe O. Carvalho
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
define(['JSXTransformer', 'text'], function (JSXTransformer, text) {
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var buildMap = {};
|
||||||
|
|
||||||
|
var jsx = {
|
||||||
|
version: '0.2.1',
|
||||||
|
|
||||||
|
load: function (name, req, onLoadNative, config) {
|
||||||
|
var fileExtension = config.jsx && config.jsx.fileExtension || '.js';
|
||||||
|
|
||||||
|
var onLoad = function(content) {
|
||||||
|
try {
|
||||||
|
if (-1 === content.indexOf('@jsx React.DOM')) {
|
||||||
|
content = "/** @jsx React.DOM */" + content;
|
||||||
|
}
|
||||||
|
content = JSXTransformer.transform(content).code;
|
||||||
|
} catch (err) {
|
||||||
|
onLoadNative.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.isBuild) {
|
||||||
|
buildMap[name] = content;
|
||||||
|
} else {
|
||||||
|
content += "\n//# sourceURL=" + location.protocol + "//" + location.hostname +
|
||||||
|
config.baseUrl + name + fileExtension;
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoadNative.fromText(content);
|
||||||
|
};
|
||||||
|
|
||||||
|
text.load(name + fileExtension, req, onLoad, config);
|
||||||
|
},
|
||||||
|
|
||||||
|
write: function (pluginName, moduleName, write) {
|
||||||
|
if (buildMap.hasOwnProperty(moduleName)) {
|
||||||
|
var content = buildMap[moduleName];
|
||||||
|
write.asModule(moduleName, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return jsx;
|
||||||
|
});
|
@ -28,6 +28,10 @@ requirejs.config({
|
|||||||
keystone_client: 'js/keystone_client',
|
keystone_client: 'js/keystone_client',
|
||||||
lodash: 'js/libs/bower/lodash/js/lodash',
|
lodash: 'js/libs/bower/lodash/js/lodash',
|
||||||
backbone: 'js/libs/custom/backbone',
|
backbone: 'js/libs/custom/backbone',
|
||||||
|
react: 'js/libs/bower/react/js/react-with-addons',
|
||||||
|
JSXTransformer: 'js/libs/bower/react/js/JSXTransformer',
|
||||||
|
jsx: 'js/libs/custom/jsx',
|
||||||
|
'react.backbone': 'js/libs/bower/react.backbone/react.backbone',
|
||||||
stickit: 'js/libs/bower/backbone.stickit/js/backbone.stickit',
|
stickit: 'js/libs/bower/backbone.stickit/js/backbone.stickit',
|
||||||
coccyx: 'js/libs/custom/coccyx',
|
coccyx: 'js/libs/custom/coccyx',
|
||||||
cocktail: 'js/libs/bower/cocktail/Cocktail',
|
cocktail: 'js/libs/bower/cocktail/Cocktail',
|
||||||
@ -43,7 +47,8 @@ requirejs.config({
|
|||||||
models: 'js/models',
|
models: 'js/models',
|
||||||
collections: 'js/collections',
|
collections: 'js/collections',
|
||||||
views: 'js/views',
|
views: 'js/views',
|
||||||
view_mixins: 'js/view_mixins'
|
view_mixins: 'js/view_mixins',
|
||||||
|
component_mixins: 'js/component_mixins'
|
||||||
},
|
},
|
||||||
shim: {
|
shim: {
|
||||||
underscore: {
|
underscore: {
|
||||||
@ -92,6 +97,9 @@ requirejs.config({
|
|||||||
'jquery-autoNumeric': {
|
'jquery-autoNumeric': {
|
||||||
deps: ['jquery']
|
deps: ['jquery']
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
jsx: {
|
||||||
|
fileExtension: '.jsx'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -102,6 +110,8 @@ require([
|
|||||||
'stickit',
|
'stickit',
|
||||||
'deepModel',
|
'deepModel',
|
||||||
'coccyx',
|
'coccyx',
|
||||||
|
'react',
|
||||||
|
'react.backbone',
|
||||||
'cocktail',
|
'cocktail',
|
||||||
'i18next',
|
'i18next',
|
||||||
'bootstrap',
|
'bootstrap',
|
||||||
@ -110,6 +120,10 @@ require([
|
|||||||
'jquery-ui',
|
'jquery-ui',
|
||||||
'jquery-autoNumeric',
|
'jquery-autoNumeric',
|
||||||
'styles',
|
'styles',
|
||||||
|
'text',
|
||||||
|
//>>excludeStart("compressed", pragmas.compressed);
|
||||||
|
'jsx',
|
||||||
|
//>>excludeEnd("compressed");
|
||||||
'app'
|
'app'
|
||||||
], function() {
|
], function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
* License for the specific language governing permissions and limitations
|
* License for the specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
**/
|
**/
|
||||||
define(['require', 'expression_parser'], function(require, ExpressionParser) {
|
define(['require', 'expression_parser', 'react'], function(require, ExpressionParser, React) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var utils = {
|
var utils = {
|
||||||
@ -101,9 +101,32 @@ define(['require', 'expression_parser'], function(require, ExpressionParser) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
universalMount: function(view, el, parentView) {
|
||||||
|
if (view instanceof Backbone.View) {
|
||||||
|
view.render();
|
||||||
|
if (el) {
|
||||||
|
$(el).html(view.el);
|
||||||
|
}
|
||||||
|
if (parentView) {
|
||||||
|
parentView.registerSubView(view);
|
||||||
|
}
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
return React.renderComponent(view, $(el)[0]);
|
||||||
|
},
|
||||||
|
universalUnmount: function(view) {
|
||||||
|
if (view instanceof Backbone.View) {
|
||||||
|
view.tearDown();
|
||||||
|
} else {
|
||||||
|
React.unmountComponentAtNode(view.getDOMNode().parentNode);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showDialog: function(dialog) {
|
||||||
|
return React.renderComponent(dialog, $('#modal-container')[0]);
|
||||||
|
},
|
||||||
showErrorDialog: function(options, parentView) {
|
showErrorDialog: function(options, parentView) {
|
||||||
parentView = parentView || app.page;
|
parentView = parentView || app.page;
|
||||||
var dialogViews = require('views/dialogs'); // avoid circular dependencies
|
var dialogViews = require('jsx!views/dialogs'); // avoid circular dependencies
|
||||||
var dialog = new dialogViews.Dialog();
|
var dialog = new dialogViews.Dialog();
|
||||||
parentView.registerSubView(dialog);
|
parentView.registerSubView(dialog);
|
||||||
dialog.render(_.extend({error: true}, options));
|
dialog.render(_.extend({error: true}, options));
|
||||||
|
@ -18,7 +18,7 @@ define(
|
|||||||
'utils',
|
'utils',
|
||||||
'models',
|
'models',
|
||||||
'views/common',
|
'views/common',
|
||||||
'views/dialogs',
|
'jsx!views/dialogs',
|
||||||
'views/cluster_page_tabs/nodes_tab',
|
'views/cluster_page_tabs/nodes_tab',
|
||||||
'views/cluster_page_tabs/network_tab',
|
'views/cluster_page_tabs/network_tab',
|
||||||
'views/cluster_page_tabs/settings_tab',
|
'views/cluster_page_tabs/settings_tab',
|
||||||
@ -180,18 +180,10 @@ function(utils, models, commonViews, dialogViews, NodesTab, NetworkTab, Settings
|
|||||||
activeTab: this.activeTab
|
activeTab: this.activeTab
|
||||||
})).i18n();
|
})).i18n();
|
||||||
var options = {model: this.model, page: this};
|
var options = {model: this.model, page: this};
|
||||||
this.clusterInfo = new ClusterInfo(options);
|
this.clusterInfo = utils.universalMount(new ClusterInfo(options), this.$('.cluster-info'), this);
|
||||||
this.registerSubView(this.clusterInfo);
|
this.clusterCustomizationMessage = utils.universalMount(new ClusterCustomizationMessage(options), this.$('.customization-message'), this);
|
||||||
this.$('.cluster-info').html(this.clusterInfo.render().el);
|
this.deploymentResult = utils.universalMount(new DeploymentResult(options), this.$('.deployment-result'), this);
|
||||||
this.clusterCustomizationMessage = new ClusterCustomizationMessage(options);
|
this.deploymentControl = utils.universalMount(new DeploymentControl(options), this.$('.deployment-control'), this);
|
||||||
this.registerSubView(this.clusterCustomizationMessage);
|
|
||||||
this.$('.customization-message').html(this.clusterCustomizationMessage.render().el);
|
|
||||||
this.deploymentResult = new DeploymentResult(options);
|
|
||||||
this.registerSubView(this.deploymentResult);
|
|
||||||
this.$('.deployment-result').html(this.deploymentResult.render().el);
|
|
||||||
this.deploymentControl = new DeploymentControl(options);
|
|
||||||
this.registerSubView(this.deploymentControl);
|
|
||||||
this.$('.deployment-control').html(this.deploymentControl.render().el);
|
|
||||||
|
|
||||||
var tabs = {
|
var tabs = {
|
||||||
'nodes': NodesTab,
|
'nodes': NodesTab,
|
||||||
@ -202,9 +194,11 @@ function(utils, models, commonViews, dialogViews, NodesTab, NetworkTab, Settings
|
|||||||
'healthcheck': HealthCheckTab
|
'healthcheck': HealthCheckTab
|
||||||
};
|
};
|
||||||
if (_.has(tabs, this.activeTab)) {
|
if (_.has(tabs, this.activeTab)) {
|
||||||
this.tab = new tabs[this.activeTab]({model: this.model, tabOptions: this.tabOptions, page: this});
|
this.tab = utils.universalMount(
|
||||||
this.$('#tab-' + this.activeTab).html(this.tab.render().el);
|
new tabs[this.activeTab]({model: this.model, tabOptions: this.tabOptions, page: this}),
|
||||||
this.registerSubView(this.tab);
|
this.$('#tab-' + this.activeTab),
|
||||||
|
this
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
|
@ -18,7 +18,7 @@ define(
|
|||||||
'utils',
|
'utils',
|
||||||
'models',
|
'models',
|
||||||
'views/common',
|
'views/common',
|
||||||
'views/dialogs',
|
'jsx!views/dialogs',
|
||||||
'text!templates/cluster/actions_tab.html',
|
'text!templates/cluster/actions_tab.html',
|
||||||
'text!templates/cluster/actions_rename.html',
|
'text!templates/cluster/actions_rename.html',
|
||||||
'text!templates/cluster/actions_reset.html',
|
'text!templates/cluster/actions_reset.html',
|
||||||
|
@ -19,7 +19,7 @@ define(
|
|||||||
'models',
|
'models',
|
||||||
'view_mixins',
|
'view_mixins',
|
||||||
'views/common',
|
'views/common',
|
||||||
'views/dialogs',
|
'jsx!views/dialogs',
|
||||||
'text!templates/cluster/healthcheck_tab.html',
|
'text!templates/cluster/healthcheck_tab.html',
|
||||||
'text!templates/cluster/healthcheck_credentials.html',
|
'text!templates/cluster/healthcheck_credentials.html',
|
||||||
'text!templates/cluster/healthcheck_testset.html',
|
'text!templates/cluster/healthcheck_testset.html',
|
||||||
|
@ -18,7 +18,7 @@ define(
|
|||||||
'utils',
|
'utils',
|
||||||
'models',
|
'models',
|
||||||
'views/common',
|
'views/common',
|
||||||
'views/dialogs',
|
'jsx!views/dialogs',
|
||||||
'text!templates/cluster/network_tab.html',
|
'text!templates/cluster/network_tab.html',
|
||||||
'text!templates/cluster/network.html',
|
'text!templates/cluster/network.html',
|
||||||
'text!templates/cluster/range_field.html',
|
'text!templates/cluster/range_field.html',
|
||||||
|
@ -17,7 +17,7 @@ define(
|
|||||||
[
|
[
|
||||||
'utils',
|
'utils',
|
||||||
'models',
|
'models',
|
||||||
'views/dialogs',
|
'jsx!views/dialogs',
|
||||||
'views/cluster_page_tabs/nodes_tab_screens/screen',
|
'views/cluster_page_tabs/nodes_tab_screens/screen',
|
||||||
'text!templates/cluster/nodes_management_panel.html',
|
'text!templates/cluster/nodes_management_panel.html',
|
||||||
'text!templates/cluster/assign_roles_panel.html',
|
'text!templates/cluster/assign_roles_panel.html',
|
||||||
|
@ -19,7 +19,7 @@ define(
|
|||||||
'models',
|
'models',
|
||||||
'view_mixins',
|
'view_mixins',
|
||||||
'views/common',
|
'views/common',
|
||||||
'views/dialogs',
|
'jsx!views/dialogs',
|
||||||
'text!templates/cluster/settings_tab.html',
|
'text!templates/cluster/settings_tab.html',
|
||||||
'text!templates/cluster/settings_group.html'
|
'text!templates/cluster/settings_group.html'
|
||||||
],
|
],
|
||||||
|
@ -1,182 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2013 Mirantis, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
* not use this file except in compliance with the License. You may obtain
|
|
||||||
* a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
**/
|
|
||||||
define(
|
|
||||||
[
|
|
||||||
'models',
|
|
||||||
'utils',
|
|
||||||
'views/common',
|
|
||||||
'views/dialogs',
|
|
||||||
'views/wizard',
|
|
||||||
'text!templates/clusters/page.html',
|
|
||||||
'text!templates/clusters/cluster.html',
|
|
||||||
'text!templates/clusters/new.html',
|
|
||||||
'text!templates/clusters/register_trial.html'
|
|
||||||
],
|
|
||||||
function(models, utils, commonViews, dialogViews, wizard, clustersPageTemplate, clusterTemplate, newClusterTemplate, registerTrialTemplate) {
|
|
||||||
'use strict';
|
|
||||||
var ClustersPage, ClusterList, Cluster, RegisterTrial;
|
|
||||||
|
|
||||||
ClustersPage = commonViews.Page.extend({
|
|
||||||
navbarActiveElement: 'clusters',
|
|
||||||
breadcrumbsPath: [['home', '#'], 'environments'],
|
|
||||||
title: function() {
|
|
||||||
return $.t('clusters_page.title');
|
|
||||||
},
|
|
||||||
template: _.template(clustersPageTemplate),
|
|
||||||
render: function() {
|
|
||||||
this.$el.html(this.template({clusters: this.collection})).i18n();
|
|
||||||
var clustersView = new ClusterList({collection: this.collection});
|
|
||||||
this.registerSubView(clustersView);
|
|
||||||
this.$('.cluster-list').html(clustersView.render().el);
|
|
||||||
if (_.contains(app.version.get('feature_groups'), 'mirantis') && !localStorage.trialRemoved) {
|
|
||||||
var registerTrialView = new RegisterTrial();
|
|
||||||
this.registerSubView(registerTrialView);
|
|
||||||
this.$('.page-title').before(registerTrialView.render().el);
|
|
||||||
}
|
|
||||||
app.footer.$el.toggle(app.user.get('authenticated'));
|
|
||||||
app.breadcrumbs.$el.toggle(app.user.get('authenticated'));
|
|
||||||
app.navbar.$el.toggle(app.user.get('authenticated'));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ClusterList = Backbone.View.extend({
|
|
||||||
className: 'roles-block-row',
|
|
||||||
newClusterTemplate: _.template(newClusterTemplate),
|
|
||||||
events: {
|
|
||||||
'click .create-cluster': 'createCluster'
|
|
||||||
},
|
|
||||||
createCluster: function() {
|
|
||||||
app.page.registerSubView(new wizard.CreateClusterWizard({collection: this.collection})).render();
|
|
||||||
},
|
|
||||||
initialize: function() {
|
|
||||||
this.collection.on('sync add', this.render, this);
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
this.tearDownRegisteredSubViews();
|
|
||||||
this.$el.html('');
|
|
||||||
this.collection.each(_.bind(function(cluster) {
|
|
||||||
var clusterView = new Cluster({model: cluster});
|
|
||||||
this.registerSubView(clusterView);
|
|
||||||
this.$el.append(clusterView.render().el);
|
|
||||||
}, this));
|
|
||||||
this.$el.append(this.newClusterTemplate());
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Cluster = Backbone.View.extend({
|
|
||||||
tagName: 'a',
|
|
||||||
className: 'span3 clusterbox',
|
|
||||||
template: _.template(clusterTemplate),
|
|
||||||
templateHelpers: _.pick(utils, 'showDiskSize', 'showMemorySize'),
|
|
||||||
updateInterval: 3000,
|
|
||||||
scheduleUpdate: function() {
|
|
||||||
if (this.model.task('cluster_deletion', ['running', 'ready']) || this.model.tasks({group: 'deployment', status: 'running'}).length) {
|
|
||||||
this.registerDeferred($.timeout(this.updateInterval).done(_.bind(this.update, this)));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
update: function() {
|
|
||||||
var deletionTask = this.model.task('cluster_deletion');
|
|
||||||
var deploymentTask = this.model.task({group: 'deployment', status: 'running'});
|
|
||||||
var request;
|
|
||||||
if (deletionTask) {
|
|
||||||
request = deletionTask.fetch();
|
|
||||||
request.done(_.bind(this.scheduleUpdate, this));
|
|
||||||
request.fail(_.bind(function(response) {
|
|
||||||
if (response.status == 404) {
|
|
||||||
this.model.collection.remove(this.model);
|
|
||||||
this.remove();
|
|
||||||
app.navbar.refresh();
|
|
||||||
}
|
|
||||||
}, this));
|
|
||||||
this.registerDeferred(request);
|
|
||||||
} else if (deploymentTask) {
|
|
||||||
request = deploymentTask.fetch();
|
|
||||||
request.done(_.bind(function() {
|
|
||||||
if (deploymentTask.get('status') == 'running') {
|
|
||||||
this.updateProgress();
|
|
||||||
this.scheduleUpdate();
|
|
||||||
} else {
|
|
||||||
this.model.fetch();
|
|
||||||
app.navbar.refresh();
|
|
||||||
}
|
|
||||||
}, this));
|
|
||||||
this.registerDeferred(request);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateProgress: function() {
|
|
||||||
var task = this.model.task({group: 'deployment', status: 'running'});
|
|
||||||
if (task) {
|
|
||||||
var progress = task.get('progress') || 0;
|
|
||||||
this.$('.bar').css('width', (progress > 3 ? progress : 3) + '%');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
initialize: function() {
|
|
||||||
this.model.on('change', this.render, this);
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
this.$el.html(this.template(_.extend({
|
|
||||||
cluster: this.model,
|
|
||||||
deploymentTask: this.model.task({group: 'deployment', status: 'running'})
|
|
||||||
}, this.templateHelpers))).i18n();
|
|
||||||
this.updateProgress();
|
|
||||||
if (this.model.task('cluster_deletion', ['running', 'ready'])) {
|
|
||||||
this.$el.addClass('disabled-cluster');
|
|
||||||
this.update();
|
|
||||||
} else {
|
|
||||||
this.$el.attr('href', '#cluster/' + this.model.id + '/nodes');
|
|
||||||
if (this.model.task({group: 'deployment', status: 'running'})) {
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
RegisterTrial = Backbone.View.extend({
|
|
||||||
template: _.template(registerTrialTemplate),
|
|
||||||
events: {
|
|
||||||
'click .register-trial .close': 'closeTrialWarning'
|
|
||||||
},
|
|
||||||
bindings: {
|
|
||||||
'.registration-link': {
|
|
||||||
attributes: [{
|
|
||||||
name: 'href',
|
|
||||||
observe: 'key',
|
|
||||||
onGet: function(value) {
|
|
||||||
return !_.isUndefined(value) ? 'http://fuel.mirantis.com/create-subscriber/?key=' + value : '/';
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
initialize: function() {
|
|
||||||
this.fuelKey = new models.FuelKey();
|
|
||||||
this.fuelKey.fetch();
|
|
||||||
app.version.on('sync', this.render, this);
|
|
||||||
},
|
|
||||||
closeTrialWarning: function() {
|
|
||||||
localStorage.setItem('trialRemoved', 'true');
|
|
||||||
this.remove();
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
this.$el.html(this.template()).i18n();
|
|
||||||
this.stickit(this.fuelKey);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return ClustersPage;
|
|
||||||
});
|
|
196
nailgun/static/js/views/clusters_page.jsx
Normal file
196
nailgun/static/js/views/clusters_page.jsx
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Mirantis, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
* not use this file except in compliance with the License. You may obtain
|
||||||
|
* a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
**/
|
||||||
|
define(
|
||||||
|
[
|
||||||
|
'react',
|
||||||
|
'models',
|
||||||
|
'utils',
|
||||||
|
'jsx!component_mixins',
|
||||||
|
'jsx!views/dialogs',
|
||||||
|
'views/wizard'
|
||||||
|
],
|
||||||
|
function(React, models, utils, componentMixins, dialogViews, wizard) {
|
||||||
|
'use strict';
|
||||||
|
var ClustersPage, ClusterList, Cluster, RegisterTrial;
|
||||||
|
|
||||||
|
ClustersPage = React.createClass({
|
||||||
|
navbarActiveElement: 'clusters',
|
||||||
|
breadcrumbsPath: [['home', '#'], 'environments'],
|
||||||
|
title: function() {
|
||||||
|
return $.t('clusters_page.title');
|
||||||
|
},
|
||||||
|
componentDidMount: function() {
|
||||||
|
$(app.footer.getDOMNode()).toggle(app.user.get('authenticated'));
|
||||||
|
$(app.breadcrumbs.getDOMNode()).toggle(app.user.get('authenticated'));
|
||||||
|
$(app.navbar.getDOMNode()).toggle(app.user.get('authenticated'));
|
||||||
|
},
|
||||||
|
getInitialState: function() {
|
||||||
|
return {fuelKey: new models.FuelKey()};
|
||||||
|
},
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<RegisterTrial fuelKey={this.state.fuelKey} />
|
||||||
|
<h3 className="page-title">{$.t('clusters_page.title')}</h3>
|
||||||
|
<ClusterList clusters={this.props.clusters} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ClusterList = React.createClass({
|
||||||
|
mixins: [React.BackboneMixin('clusters')],
|
||||||
|
createCluster: function() {
|
||||||
|
(new wizard.CreateClusterWizard({collection: this.props.clusters})).render();
|
||||||
|
},
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div className="cluster-list">
|
||||||
|
<div className="roles-block-row">
|
||||||
|
{this.props.clusters.map(function(cluster) {
|
||||||
|
return <Cluster key={cluster.id} cluster={cluster} />;
|
||||||
|
}, this)}
|
||||||
|
<div key="add" className="span3 clusterbox create-cluster" onClick={this.createCluster}>
|
||||||
|
<div className="add-icon"><i className="icon-create"></i></div>
|
||||||
|
<div className="create-cluster-text">{$.t('clusters_page.create_cluster_text')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Cluster = React.createClass({
|
||||||
|
mixins: [
|
||||||
|
React.BackboneMixin('cluster'),
|
||||||
|
React.BackboneMixin({modelOrCollection: function(props) {
|
||||||
|
return props.cluster.get('nodes');
|
||||||
|
}}),
|
||||||
|
React.BackboneMixin({modelOrCollection: function(props) {
|
||||||
|
return props.cluster.get('tasks');
|
||||||
|
}}),
|
||||||
|
React.BackboneMixin({modelOrCollection: function(props) {
|
||||||
|
return props.cluster.task({group: 'deployment', status: 'running'});
|
||||||
|
}}),
|
||||||
|
componentMixins.pollingMixin(3)
|
||||||
|
],
|
||||||
|
shouldDataBeFetched: function() {
|
||||||
|
return this.props.cluster.task('cluster_deletion', ['running', 'ready']) || this.props.cluster.task({group: 'deployment', status: 'running'});
|
||||||
|
},
|
||||||
|
fetchData: function() {
|
||||||
|
var request, requests = [];
|
||||||
|
var deletionTask = this.props.cluster.task('cluster_deletion');
|
||||||
|
if (deletionTask) {
|
||||||
|
request = deletionTask.fetch();
|
||||||
|
request.fail(_.bind(function(response) {
|
||||||
|
if (response.status == 404) {
|
||||||
|
this.props.cluster.collection.remove(this.props.cluster);
|
||||||
|
app.navbar.refresh();
|
||||||
|
}
|
||||||
|
}, this));
|
||||||
|
requests.push(request);
|
||||||
|
}
|
||||||
|
var deploymentTask = this.props.cluster.task({group: 'deployment', status: 'running'});
|
||||||
|
if (deploymentTask) {
|
||||||
|
request = deploymentTask.fetch();
|
||||||
|
request.done(_.bind(function() {
|
||||||
|
if (deploymentTask.get('status') != 'running') {
|
||||||
|
this.props.cluster.fetch();
|
||||||
|
app.navbar.refresh();
|
||||||
|
}
|
||||||
|
}, this));
|
||||||
|
requests.push(request);
|
||||||
|
}
|
||||||
|
return $.when.apply($, requests);
|
||||||
|
},
|
||||||
|
render: function() {
|
||||||
|
var cluster = this.props.cluster;
|
||||||
|
var nodes = cluster.get('nodes');
|
||||||
|
var deletionTask = cluster.task('cluster_deletion', ['running', 'ready']);
|
||||||
|
var deploymentTask = cluster.task({group: 'deployment', status: 'running'});
|
||||||
|
return (
|
||||||
|
<a className={'span3 clusterbox ' + (deletionTask ? 'disabled-cluster' : '')} href={!deletionTask ? '#cluster/' + cluster.id + '/nodes' : 'javascript:void 0'}>
|
||||||
|
<div className="cluster-name">{cluster.get('name')}</div>
|
||||||
|
<div className="cluster-hardware">
|
||||||
|
{(!nodes.deferred || nodes.deferred.state() == 'resolved') &&
|
||||||
|
<div className="row-fluid">
|
||||||
|
<div key="nodes-title" className="span6">{$.t('clusters_page.cluster_hardware_nodes')}</div>
|
||||||
|
<div key="nodes-value" className="span4">{nodes.length}</div>
|
||||||
|
{!!nodes.length && [
|
||||||
|
<div key="cpu-title" className="span6">{$.t('clusters_page.cluster_hardware_cpu')}</div>,
|
||||||
|
<div key="cpu-value" className="span4">{nodes.resources('cores')}</div>,
|
||||||
|
<div key="hdd-title" className="span6">{$.t('clusters_page.cluster_hardware_hdd')}</div>,
|
||||||
|
<div key="hdd-value" className="span4">{nodes.resources('hdd') ? utils.showDiskSize(nodes.resources('hdd')) : '?GB'}</div>,
|
||||||
|
<div key="ram-title" className="span6">{$.t('clusters_page.cluster_hardware_ram')}</div>,
|
||||||
|
<div key="ram-value" className="span4">{nodes.resources('ram') ? utils.showMemorySize(nodes.resources('ram')) : '?GB'}</div>
|
||||||
|
]}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="cluster-status">
|
||||||
|
{deploymentTask ?
|
||||||
|
<div className={'cluster-status-progress ' + deploymentTask.get('name')}>
|
||||||
|
<div className={'progress progress-' + (_.contains(['stop_deployment', 'reset_environment'], deploymentTask.get('name')) ? 'warning' : 'success') + ' progress-striped active'}>
|
||||||
|
<div className="bar" style={{width: (deploymentTask.get('progress') > 3 ? deploymentTask.get('progress') : 3) + '%'}}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
$.t('cluster.status.' + cluster.get('status'), {defaultValue: cluster.get('status')})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
RegisterTrial = React.createClass({
|
||||||
|
mixins: [React.BackboneMixin('fuelKey')],
|
||||||
|
shouldShowMessage: function() {
|
||||||
|
return _.contains(app.version.get('feature_groups'), 'mirantis') && !localStorage.trialRemoved;
|
||||||
|
},
|
||||||
|
closeTrialWarning: function() {
|
||||||
|
localStorage.setItem('trialRemoved', 'true');
|
||||||
|
this.forceUpdate();
|
||||||
|
},
|
||||||
|
componentWillMount: function() {
|
||||||
|
if (this.shouldShowMessage()) {
|
||||||
|
this.props.fuelKey.fetch();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render: function() {
|
||||||
|
if (this.shouldShowMessage()) {
|
||||||
|
var key = this.props.fuelKey.get('key');
|
||||||
|
return (
|
||||||
|
<div className="alert alert-info alert-dismissable register-trial">
|
||||||
|
<button type="button" className="close" onClick={this.closeTrialWarning}>×</button>
|
||||||
|
<p>
|
||||||
|
<i className="icon-mirantis"></i>
|
||||||
|
{$.t('clusters_page.register_trial_message.part1')}<br />
|
||||||
|
{$.t('clusters_page.register_trial_message.part2')}
|
||||||
|
<a target="_blank" className="registration-link" href={!_.isUndefined(key) ? 'http://fuel.mirantis.com/create-subscriber/?key=' + key : '/'}>
|
||||||
|
{$.t('clusters_page.register_trial_message.part3')}
|
||||||
|
</a>
|
||||||
|
{$.t('clusters_page.register_trial_message.part4')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ClustersPage;
|
||||||
|
});
|
@ -16,16 +16,9 @@
|
|||||||
define(
|
define(
|
||||||
[
|
[
|
||||||
'utils',
|
'utils',
|
||||||
'models',
|
'models'
|
||||||
'views/dialogs',
|
|
||||||
'text!templates/common/navbar.html',
|
|
||||||
'text!templates/common/nodes_stats.html',
|
|
||||||
'text!templates/common/notifications.html',
|
|
||||||
'text!templates/common/notifications_popover.html',
|
|
||||||
'text!templates/common/breadcrumb.html',
|
|
||||||
'text!templates/common/footer.html'
|
|
||||||
],
|
],
|
||||||
function(utils, models, dialogViews, navbarTemplate, nodesStatsTemplate, notificationsTemplate, notificationsPopoverTemplate, breadcrumbsTemplate, footerTemplate) {
|
function(utils, models) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var views = {};
|
var views = {};
|
||||||
@ -33,18 +26,7 @@ function(utils, models, dialogViews, navbarTemplate, nodesStatsTemplate, notific
|
|||||||
views.Page = Backbone.View.extend({
|
views.Page = Backbone.View.extend({
|
||||||
navbarActiveElement: null,
|
navbarActiveElement: null,
|
||||||
breadcrumbsPath: null,
|
breadcrumbsPath: null,
|
||||||
title: null,
|
title: null
|
||||||
updateNavbar: function() {
|
|
||||||
app.navbar.setActive(_.result(this, 'navbarActiveElement'));
|
|
||||||
},
|
|
||||||
updateBreadcrumbs: function() {
|
|
||||||
app.breadcrumbs.setPath(_.result(this, 'breadcrumbsPath'));
|
|
||||||
},
|
|
||||||
updateTitle: function() {
|
|
||||||
var defaultTitle = $.t('common.title');
|
|
||||||
var title = _.result(this, 'title');
|
|
||||||
document.title = title ? defaultTitle + ' - ' + title : defaultTitle;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
views.Tab = Backbone.View.extend({
|
views.Tab = Backbone.View.extend({
|
||||||
@ -53,256 +35,5 @@ function(utils, models, dialogViews, navbarTemplate, nodesStatsTemplate, notific
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
views.Navbar = Backbone.View.extend({
|
|
||||||
className: 'container',
|
|
||||||
template: _.template(navbarTemplate),
|
|
||||||
updateInterval: 20000,
|
|
||||||
notificationsDisplayCount: 5,
|
|
||||||
events: {
|
|
||||||
'click .change-password': 'showChangePasswordDialog'
|
|
||||||
},
|
|
||||||
showChangePasswordDialog: function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.registerSubView(new dialogViews.ChangePasswordDialog()).render();
|
|
||||||
},
|
|
||||||
setActive: function(url) {
|
|
||||||
this.elements.each(function(element) {
|
|
||||||
element.set({active: element.get('url') == '#' + url});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
scheduleUpdate: function() {
|
|
||||||
this.registerDeferred($.timeout(this.updateInterval).done(_.bind(this.update, this)));
|
|
||||||
},
|
|
||||||
update: function() {
|
|
||||||
this.refresh().always(_.bind(this.scheduleUpdate, this));
|
|
||||||
},
|
|
||||||
refresh: function() {
|
|
||||||
if (app.user.get('authenticated')) {
|
|
||||||
return $.when(this.statistics.fetch(), this.notifications.fetch({limit: this.notificationsDisplayCount}));
|
|
||||||
}
|
|
||||||
return $.Deferred().reject();
|
|
||||||
},
|
|
||||||
initialize: function(options) {
|
|
||||||
this.elements = new Backbone.Collection(options.elements);
|
|
||||||
this.elements.invoke('set', {active: false});
|
|
||||||
this.elements.on('change:active', this.render, this);
|
|
||||||
app.user.on('change:authenticated', function(model, value) {
|
|
||||||
if (value) {
|
|
||||||
this.refresh();
|
|
||||||
} else {
|
|
||||||
this.statistics.clear();
|
|
||||||
this.notifications.reset();
|
|
||||||
}
|
|
||||||
this.render();
|
|
||||||
}, this);
|
|
||||||
this.statistics = new models.NodesStatistics();
|
|
||||||
this.notifications = new models.Notifications();
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
this.tearDownRegisteredSubViews();
|
|
||||||
this.$el.html(this.template({
|
|
||||||
elements: this.elements,
|
|
||||||
user: app.user,
|
|
||||||
version: app.version
|
|
||||||
}));
|
|
||||||
this.stats = new views.NodesStats({statistics: this.statistics, navbar: this});
|
|
||||||
this.registerSubView(this.stats);
|
|
||||||
this.$('.nodes-summary-container').html(this.stats.render().el);
|
|
||||||
this.notificationsButton = new views.Notifications({collection: this.notifications, navbar: this});
|
|
||||||
this.registerSubView(this.notificationsButton);
|
|
||||||
this.$('.notifications').html(this.notificationsButton.render().el);
|
|
||||||
this.popover = new views.NotificationsPopover({collection: this.notifications, navbar: this});
|
|
||||||
this.registerSubView(this.popover);
|
|
||||||
this.$('.notification-wrapper').html(this.popover.render().el);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
views.NodesStats = Backbone.View.extend({
|
|
||||||
template: _.template(nodesStatsTemplate),
|
|
||||||
bindings: {
|
|
||||||
'.total-nodes-count': {
|
|
||||||
observe: 'total',
|
|
||||||
onGet: 'returnValueOrNonBreakingSpace'
|
|
||||||
},
|
|
||||||
'.total-nodes-title': {
|
|
||||||
observe: 'total',
|
|
||||||
onGet: 'formatTitle',
|
|
||||||
updateMethod: 'html'
|
|
||||||
},
|
|
||||||
'.unallocated-nodes-count': {
|
|
||||||
observe: 'unallocated',
|
|
||||||
onGet: 'returnValueOrNonBreakingSpace'
|
|
||||||
},
|
|
||||||
'.unallocated-nodes-title': {
|
|
||||||
observe: 'unallocated',
|
|
||||||
onGet: 'formatTitle',
|
|
||||||
updateMethod: 'html'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
returnValueOrNonBreakingSpace: function(value) {
|
|
||||||
return !_.isUndefined(value) ? value : '\u00A0';
|
|
||||||
},
|
|
||||||
formatTitle: function(value, options) {
|
|
||||||
return !_.isUndefined(value) ? utils.linebreaks(_.escape($.t('navbar.stats.' + options.observe, {count: value}))) : '';
|
|
||||||
},
|
|
||||||
initialize: function(options) {
|
|
||||||
_.defaults(this, options);
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
this.$el.html(this.template({stats: this.statistics}));
|
|
||||||
this.stickit(this.statistics);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
views.Notifications = Backbone.View.extend({
|
|
||||||
template: _.template(notificationsTemplate),
|
|
||||||
events: {
|
|
||||||
'click .icon-comment': 'togglePopover',
|
|
||||||
'click .badge': 'togglePopover'
|
|
||||||
},
|
|
||||||
togglePopover: function(e) {
|
|
||||||
this.navbar.popover.toggle();
|
|
||||||
},
|
|
||||||
initialize: function(options) {
|
|
||||||
_.defaults(this, options);
|
|
||||||
this.collection.on('sync reset', this.render, this);
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
this.$el.html(this.template({
|
|
||||||
notifications: this.collection.where({status: 'unread'}),
|
|
||||||
authenticated: app.user.get('authenticated')
|
|
||||||
}));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
views.NotificationsPopover = Backbone.View.extend({
|
|
||||||
template: _.template(notificationsPopoverTemplate),
|
|
||||||
templateHelpers: _.pick(utils, 'urlify'),
|
|
||||||
visible: false,
|
|
||||||
events: {
|
|
||||||
'click .discover[data-node]' : 'showNodeInfo'
|
|
||||||
},
|
|
||||||
showNodeInfo: function(e) {
|
|
||||||
this.toggle();
|
|
||||||
var node = new models.Node({id: $(e.currentTarget).data('node')});
|
|
||||||
node.deferred = node.fetch();
|
|
||||||
var dialog = new dialogViews.ShowNodeInfoDialog({node: node});
|
|
||||||
this.registerSubView(dialog);
|
|
||||||
dialog.render();
|
|
||||||
},
|
|
||||||
toggle: function() {
|
|
||||||
this.visible = !this.visible;
|
|
||||||
this.render();
|
|
||||||
},
|
|
||||||
hide: function(e) {
|
|
||||||
if (this.visible && (!e || (!$(e.target).closest(this.navbar.notificationsButton.el).length && !$(e.target).closest(this.el).length))) {
|
|
||||||
this.visible = false;
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
markAsRead: function() {
|
|
||||||
var notificationsToMark = new models.Notifications(this.collection.where({status : 'unread'}));
|
|
||||||
if (notificationsToMark.length) {
|
|
||||||
notificationsToMark.toJSON = function() {
|
|
||||||
return notificationsToMark.map(function(notification) {
|
|
||||||
notification.set({status: 'read'}, {silent: true});
|
|
||||||
return _.pick(notification.attributes, 'id', 'status');
|
|
||||||
}, this);
|
|
||||||
};
|
|
||||||
Backbone.sync('update', notificationsToMark).done(_.bind(function() {
|
|
||||||
this.collection.trigger('sync');
|
|
||||||
}, this));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeTearDown: function() {
|
|
||||||
this.unbindEvents();
|
|
||||||
},
|
|
||||||
initialize: function(options) {
|
|
||||||
_.defaults(this, options);
|
|
||||||
this.collection.bind('add', this.render, this);
|
|
||||||
this.eventNamespace = 'click.click-notifications';
|
|
||||||
},
|
|
||||||
bindEvents: function() {
|
|
||||||
$('html').on(this.eventNamespace, _.bind(this.hide, this));
|
|
||||||
Backbone.history.on('route', this.hide, this);
|
|
||||||
},
|
|
||||||
unbindEvents: function() {
|
|
||||||
$('html').off(this.eventNamespace);
|
|
||||||
Backbone.history.off('route', this.hide, this);
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
if (this.visible) {
|
|
||||||
this.$el.html(this.template(_.extend({
|
|
||||||
notifications: this.collection,
|
|
||||||
displayCount: this.navbar.notificationsDisplayCount,
|
|
||||||
showMore: (Backbone.history.getHash() != 'notifications') && this.collection.length
|
|
||||||
}, this.templateHelpers))).i18n();
|
|
||||||
this.markAsRead();
|
|
||||||
this.bindEvents();
|
|
||||||
} else {
|
|
||||||
this.$el.html('');
|
|
||||||
this.unbindEvents();
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
views.Breadcrumbs = Backbone.View.extend({
|
|
||||||
className: 'container',
|
|
||||||
template: _.template(breadcrumbsTemplate),
|
|
||||||
path: [],
|
|
||||||
setPath: function(path) {
|
|
||||||
this.path = path;
|
|
||||||
this.render();
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
this.$el.html(this.template({path: this.path}));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
views.Footer = Backbone.View.extend({
|
|
||||||
template: _.template(footerTemplate),
|
|
||||||
events: {
|
|
||||||
'click .footer-lang li a': 'setLocale'
|
|
||||||
},
|
|
||||||
setLocale: function(e) {
|
|
||||||
var newLocale = _.find(this.locales, {locale: $(e.currentTarget).data('locale')});
|
|
||||||
$.i18n.setLng(newLocale.locale, {});
|
|
||||||
window.location.reload();
|
|
||||||
},
|
|
||||||
getAvailableLocales: function() {
|
|
||||||
return _.map(_.keys($.i18n.options.resStore).sort(), function(locale) {
|
|
||||||
return {locale: locale, name: $.t('language', {lng: locale})};
|
|
||||||
}, this);
|
|
||||||
},
|
|
||||||
getCurrentLocale: function() {
|
|
||||||
return _.find(this.locales, {locale: $.i18n.lng()});
|
|
||||||
},
|
|
||||||
setDefaultLocale: function() {
|
|
||||||
var currentLocale = this.getCurrentLocale();
|
|
||||||
if (!currentLocale) {
|
|
||||||
$.i18n.setLng(this.locales[0].locale, {});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
initialize: function(options) {
|
|
||||||
this.locales = this.getAvailableLocales();
|
|
||||||
this.setDefaultLocale();
|
|
||||||
app.version.on('sync', this.render, this);
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
this.$el.html(this.template({
|
|
||||||
version: app.version,
|
|
||||||
locales: this.locales,
|
|
||||||
currentLocale: this.getCurrentLocale()
|
|
||||||
})).i18n();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return views;
|
return views;
|
||||||
});
|
});
|
||||||
|
@ -16,9 +16,11 @@
|
|||||||
define(
|
define(
|
||||||
[
|
[
|
||||||
'require',
|
'require',
|
||||||
|
'react',
|
||||||
'utils',
|
'utils',
|
||||||
'models',
|
'models',
|
||||||
'view_mixins',
|
'view_mixins',
|
||||||
|
'jsx!component_mixins',
|
||||||
'text!templates/dialogs/base_dialog.html',
|
'text!templates/dialogs/base_dialog.html',
|
||||||
'text!templates/dialogs/discard_changes.html',
|
'text!templates/dialogs/discard_changes.html',
|
||||||
'text!templates/dialogs/display_changes.html',
|
'text!templates/dialogs/display_changes.html',
|
||||||
@ -28,12 +30,13 @@ define(
|
|||||||
'text!templates/dialogs/update_environment.html',
|
'text!templates/dialogs/update_environment.html',
|
||||||
'text!templates/dialogs/show_node.html',
|
'text!templates/dialogs/show_node.html',
|
||||||
'text!templates/dialogs/dismiss_settings.html',
|
'text!templates/dialogs/dismiss_settings.html',
|
||||||
'text!templates/dialogs/delete_nodes.html',
|
'text!templates/dialogs/delete_nodes.html'
|
||||||
'text!templates/dialogs/change_password.html'
|
|
||||||
],
|
],
|
||||||
function(require, utils, models, viewMixins, baseDialogTemplate, discardChangesDialogTemplate, displayChangesDialogTemplate, removeClusterDialogTemplate, stopDeploymentDialogTemplate, resetEnvironmentDialogTemplate, updateEnvironmentDialogTemplate, showNodeInfoTemplate, discardSettingsChangesTemplate, deleteNodesTemplate, changePasswordTemplate) {
|
function(require, React, utils, models, viewMixins, componentMixins, baseDialogTemplate, discardChangesDialogTemplate, displayChangesDialogTemplate, removeClusterDialogTemplate, stopDeploymentDialogTemplate, resetEnvironmentDialogTemplate, updateEnvironmentDialogTemplate, showNodeInfoTemplate, discardSettingsChangesTemplate, deleteNodesTemplate) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
var cx = React.addons.classSet;
|
||||||
|
|
||||||
var views = {};
|
var views = {};
|
||||||
|
|
||||||
views.Dialog = Backbone.View.extend({
|
views.Dialog = Backbone.View.extend({
|
||||||
@ -408,47 +411,93 @@ function(require, utils, models, viewMixins, baseDialogTemplate, discardChangesD
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
views.ChangePasswordDialog = views.Dialog.extend({
|
views.ChangePasswordDialog = React.createClass({
|
||||||
template: _.template(changePasswordTemplate),
|
mixins: [componentMixins.dialogMixin, React.addons.LinkedStateMixin],
|
||||||
mixins: [viewMixins.toggleablePassword],
|
getDefaultProps: function() {
|
||||||
events: {
|
return {
|
||||||
'click .btn-change-password': 'changePassword',
|
title: $.t('dialog.change_password.title')
|
||||||
'keyup input': 'onPasswordChange',
|
};
|
||||||
'keydown': 'onKeydown'
|
},
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
currentPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
validationError: false,
|
||||||
|
locked: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
renderBody: function() {
|
||||||
|
var ns = 'dialog.change_password.';
|
||||||
|
return (
|
||||||
|
<form className="change-password-form">
|
||||||
|
<div className="parameter-box clearfix">
|
||||||
|
<div className="parameter-name">{$.t(ns + 'current_password')}</div>
|
||||||
|
<div className="parameter-control input-append">
|
||||||
|
<input ref="currentPassword"
|
||||||
|
onChange={this.handleChange.bind(this, 'currentPassword', true)}
|
||||||
|
onKeyDown={this.handleKeyDown}
|
||||||
|
className={cx({'input-append': true, error: this.state.validationError})}
|
||||||
|
disabled={this.state.locked}
|
||||||
|
type="password"
|
||||||
|
maxLength="50" />
|
||||||
|
<span className="add-on"><i className="icon-eye"/></span>
|
||||||
|
</div>
|
||||||
|
<div className="parameter-description validation-error">
|
||||||
|
{this.state.validationError && $.t('dialog.change_password.wrong_current_password')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="parameter-box clearfix">
|
||||||
|
<div className="parameter-name">{$.t(ns + 'new_password')}</div>
|
||||||
|
<div className="parameter-control input-append">
|
||||||
|
<input ref="newPassword"
|
||||||
|
onChange={this.handleChange.bind(this, 'newPassword', false)}
|
||||||
|
onKeyDown={this.handleKeyDown}
|
||||||
|
className="input-append"
|
||||||
|
disabled={this.state.locked}
|
||||||
|
type="password"
|
||||||
|
maxLength="50" />
|
||||||
|
<span className="add-on"><i className="icon-eye"/></span>
|
||||||
|
</div>
|
||||||
|
<div className="parameter-description validation-error"></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
renderFooter: function() {
|
||||||
|
return [
|
||||||
|
<button key="cancel" className="btn" onClick={this.close} disabled={this.state.locked}>{$.t('common.cancel_button')}</button>,
|
||||||
|
<button key="apply" className="btn btn-success" onClick={this.changePassword} disabled={this.state.locked || !this.isPasswordChangeAvailable()}>{$.t('common.apply_button')}</button>
|
||||||
|
];
|
||||||
|
},
|
||||||
|
isPasswordChangeAvailable: function() {
|
||||||
|
return !!(this.state.currentPassword && this.state.newPassword);
|
||||||
|
},
|
||||||
|
handleKeyDown: function(e) {
|
||||||
|
if (e.key == 'Enter') {
|
||||||
|
this.changePassword();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleChange: function(name, clearError, e) {
|
||||||
|
var newState = {};
|
||||||
|
newState[name] = e.target.value;
|
||||||
|
if (clearError) {
|
||||||
|
newState.validationError = false;
|
||||||
|
}
|
||||||
|
this.setState(newState);
|
||||||
},
|
},
|
||||||
changePassword: function() {
|
changePassword: function() {
|
||||||
var currentPassword = this.$('[name=current_password]').val(),
|
if (this.isPasswordChangeAvailable()) {
|
||||||
newPassword = this.$('[name=new_password]').val(),
|
this.setState({locked: true});
|
||||||
confirmedPassword= this.$('[name=confirm_new_password]').val();
|
app.keystoneClient.changePassword(this.state.currentPassword, this.state.newPassword)
|
||||||
if (currentPassword && (newPassword == confirmedPassword)) {
|
|
||||||
app.keystoneClient.changePassword(currentPassword, newPassword)
|
|
||||||
.done(_.bind(function() {
|
.done(_.bind(function() {
|
||||||
app.user.set({password: app.keystoneClient.password});
|
app.user.set({password: app.keystoneClient.password});
|
||||||
this.$el.modal('hide');
|
this.close();
|
||||||
}, this))
|
}, this))
|
||||||
.fail(_.bind(function() {
|
.fail(_.bind(function() {
|
||||||
this.$('[name=current_password]').focus().addClass('error').parent().siblings('.validation-error').show();
|
this.setState({validationError: true, locked: false});
|
||||||
|
$(this.refs.currentPassword.getDOMNode()).focus();
|
||||||
}, this));
|
}, this));
|
||||||
}
|
}
|
||||||
},
|
|
||||||
onPasswordChange: function(e) {
|
|
||||||
this.$(e.currentTarget).removeClass('error').parent().siblings('.validation-error').hide();
|
|
||||||
var newPassword = this.$('[name=new_password]'),
|
|
||||||
confirmedPassword = this.$('[name=confirm_new_password]');
|
|
||||||
confirmedPassword.removeClass('error').parent().siblings('.validation-error').hide();
|
|
||||||
if (newPassword.val() != confirmedPassword.val()) {
|
|
||||||
confirmedPassword.addClass('error').parent().siblings('.validation-error').show();
|
|
||||||
}
|
|
||||||
var disabled = (!_.all(_.invoke(_.map(this.$('input'), $), 'val')) || this.$('.validation-error').is(':visible'));
|
|
||||||
_.defer(_.bind(function() {
|
|
||||||
this.$('.btn-change-password').attr('disabled', disabled);
|
|
||||||
}, this));
|
|
||||||
},
|
|
||||||
onKeydown: function(e) {
|
|
||||||
if (e.which == 13) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.changePassword();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
299
nailgun/static/js/views/layout.jsx
Normal file
299
nailgun/static/js/views/layout.jsx
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014 Mirantis, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
* not use this file except in compliance with the License. You may obtain
|
||||||
|
* a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
**/
|
||||||
|
define(
|
||||||
|
[
|
||||||
|
'react',
|
||||||
|
'utils',
|
||||||
|
'models',
|
||||||
|
'jsx!component_mixins',
|
||||||
|
'jsx!views/dialogs'
|
||||||
|
],
|
||||||
|
function(React, utils, models, componentMixins, dialogs) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var components = {};
|
||||||
|
|
||||||
|
var cx = React.addons.classSet;
|
||||||
|
|
||||||
|
components.Navbar = React.createClass({
|
||||||
|
mixins: [
|
||||||
|
React.BackboneMixin('user'),
|
||||||
|
React.BackboneMixin('version'),
|
||||||
|
componentMixins.pollingMixin(20)
|
||||||
|
],
|
||||||
|
showChangePasswordDialog: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
utils.showDialog(dialogs.ChangePasswordDialog());
|
||||||
|
},
|
||||||
|
togglePopover: function(visible) {
|
||||||
|
this.setState({popoverVisible: _.isBoolean(visible) ? visible : !this.state.popoverVisible});
|
||||||
|
},
|
||||||
|
setActive: function(url) {
|
||||||
|
this.setState({activeElement: url});
|
||||||
|
},
|
||||||
|
shouldDataBeFetched: function() {
|
||||||
|
return this.props.user.get('authenticated');
|
||||||
|
},
|
||||||
|
fetchData: function() {
|
||||||
|
return $.when(this.props.statistics.fetch(), this.props.notifications.fetch({limit: this.props.notificationsDisplayCount}));
|
||||||
|
},
|
||||||
|
refresh: function() {
|
||||||
|
if (this.shouldDataBeFetched()) {
|
||||||
|
return this.fetchData();
|
||||||
|
}
|
||||||
|
return $.Deferred().reject();
|
||||||
|
},
|
||||||
|
componentDidMount: function() {
|
||||||
|
this.props.user.on('change:authenticated', function(model, value) {
|
||||||
|
if (value) {
|
||||||
|
this.refresh();
|
||||||
|
} else {
|
||||||
|
this.props.statistics.clear();
|
||||||
|
this.props.notifications.reset();
|
||||||
|
}
|
||||||
|
}, this);
|
||||||
|
},
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {notificationsDisplayCount: 5};
|
||||||
|
},
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
activeElement: null,
|
||||||
|
popoverVisible: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="user-info-box">
|
||||||
|
{this.props.version.get('auth_required') && this.props.user.get('authenticated') &&
|
||||||
|
<div>
|
||||||
|
<i className="icon-user"></i>
|
||||||
|
{this.props.user.get('username')}
|
||||||
|
<a className="change-password" onClick={this.showChangePasswordDialog}>{$.t('common.change_password')}</a>
|
||||||
|
<a href="#logout">{$.t('common.logout')}</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="navigation-bar">
|
||||||
|
<div className="navigation-bar-box">
|
||||||
|
<ul className="navigation-bar-ul">
|
||||||
|
<li className="product-logo">
|
||||||
|
<a href="#"><div className="logo"></div></a>
|
||||||
|
</li>
|
||||||
|
{_.map(this.props.elements, function(element) {
|
||||||
|
return <li key={element.label}>
|
||||||
|
<a className={cx({active: this.state.activeElement == element.url.slice(1)})} href={element.url}>{$.t('navbar.' + element.label, {defaultValue: element.label})}</a>
|
||||||
|
</li>;
|
||||||
|
}, this)}
|
||||||
|
<li className="space"></li>
|
||||||
|
<Notifications ref="notifications"
|
||||||
|
notifications={this.props.notifications}
|
||||||
|
togglePopover={this.togglePopover}
|
||||||
|
/>
|
||||||
|
<NodeStats statistics={this.props.statistics} />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="notification-wrapper">
|
||||||
|
{this.state.popoverVisible &&
|
||||||
|
<NotificationsPopover ref="popover"
|
||||||
|
notifications={this.props.notifications}
|
||||||
|
displayCount={this.props.notificationsDisplayCount}
|
||||||
|
togglePopover={this.togglePopover}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var NodeStats = React.createClass({
|
||||||
|
mixins: [React.BackboneMixin('statistics')],
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<li className="navigation-bar-icon nodes-summary-container">
|
||||||
|
<div className="statistic">
|
||||||
|
{_.map(['total', 'unallocated'], function(prop) {
|
||||||
|
var value = this.props.statistics.get(prop);
|
||||||
|
return _.isUndefined(value) ? '' : [
|
||||||
|
<div className="stat-count">{value}</div>,
|
||||||
|
<div className="stat-title" dangerouslySetInnerHTML={{__html: utils.linebreaks(_.escape($.t('navbar.stats.' + prop, {count: value})))}}></div>
|
||||||
|
];
|
||||||
|
}, this)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var Notifications = React.createClass({
|
||||||
|
mixins: [React.BackboneMixin('notifications')],
|
||||||
|
render: function() {
|
||||||
|
var unreadNotifications = this.props.notifications.where({status: 'unread'});
|
||||||
|
return (
|
||||||
|
<li className="navigation-bar-icon notifications" onClick={this.props.togglePopover}>
|
||||||
|
<i className="icon-comment"></i>
|
||||||
|
{unreadNotifications.length && <span className="badge badge-warning">{unreadNotifications.length}</span>}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var NotificationsPopover = React.createClass({
|
||||||
|
mixins: [React.BackboneMixin('notifications')],
|
||||||
|
showNodeInfo: function(id) {
|
||||||
|
var node = new models.Node({id: id});
|
||||||
|
node.deferred = node.fetch();
|
||||||
|
(new dialogViews.ShowNodeInfoDialog({node: node})).render();
|
||||||
|
},
|
||||||
|
toggle: function(visible) {
|
||||||
|
this.props.togglePopover(visible);
|
||||||
|
},
|
||||||
|
handleBodyClick: function(e) {
|
||||||
|
if (_.all([this.el, this._owner.refs.notifications.el], function(el) {
|
||||||
|
return !$(e.target).closest(el).length;
|
||||||
|
})) {
|
||||||
|
_.defer(_.bind(this.toggle, this, false));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
markAsRead: function() {
|
||||||
|
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(function(notification) {
|
||||||
|
notification.set({status: 'read'});
|
||||||
|
return _.pick(notification.attributes, 'id', 'status');
|
||||||
|
}, this);
|
||||||
|
};
|
||||||
|
Backbone.sync('update', notificationsToMark);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
componentDidMount: function() {
|
||||||
|
this.markAsRead();
|
||||||
|
this.eventNamespace = 'click.click-notifications';
|
||||||
|
$('html').on(this.eventNamespace, _.bind(this.handleBodyClick, this));
|
||||||
|
Backbone.history.on('route', this.toggle, this);
|
||||||
|
},
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
$('html').off(this.eventNamespace);
|
||||||
|
Backbone.history.off('route', this.toggle, this);
|
||||||
|
},
|
||||||
|
getInitialState: function() {
|
||||||
|
return {unreadNotificationsIds: []};
|
||||||
|
},
|
||||||
|
render: function() {
|
||||||
|
var showMore = (Backbone.history.getHash() != 'notifications') && this.props.notifications.length;
|
||||||
|
var notifications = this.props.notifications.last(this.props.displayCount).reverse();
|
||||||
|
return (
|
||||||
|
<div className="message-list-placeholder">
|
||||||
|
<ul className="message-list-popover">
|
||||||
|
{this.props.notifications.length ? (
|
||||||
|
_.map(notifications, function(notification, index, collection) {
|
||||||
|
var unread = notification.get('status') == 'unread' || _.contains(this.state.unreadNotificationsIds, notification.id);
|
||||||
|
return [
|
||||||
|
<li key={'notification' + notification.id} className={cx({'enable-selection': true, 'new': unread}) + ' ' + notification.get('topic')} onClick={notification.get('node_id') && _.bind(this.showNodeInfo, this, notification.get('node_id'))}>
|
||||||
|
<i className={{error: 'icon-attention', warning: 'icon-attention', discover: 'icon-bell'}[notification.get('topic')] || 'icon-info-circled'}></i>
|
||||||
|
<span dangerouslySetInnerHTML={{__html: utils.urlify(notification.escape('message'))}}></span>
|
||||||
|
</li>,
|
||||||
|
(showMore || index < (collection.length - 1)) && <li key={'divider' + notification.id} className="divider"></li>
|
||||||
|
];
|
||||||
|
}, this)
|
||||||
|
) : <li key="no_notifications">{$.t('notifications_popover.no_notifications_text')}</li>}
|
||||||
|
</ul>
|
||||||
|
{showMore && <div className="show-more-notifications"><a href="#notifications">{$.t('notifications_popover.view_all_button')}</a></div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
components.Footer = React.createClass({
|
||||||
|
mixins: [React.BackboneMixin('version')],
|
||||||
|
render: function () {
|
||||||
|
return (
|
||||||
|
<div className="footer-box">
|
||||||
|
{_.contains(this.props.version.get('feature_groups'), 'mirantis') &&
|
||||||
|
<div>
|
||||||
|
<a href="http://www.mirantis.com" target="_blank" className="footer-logo"></a>
|
||||||
|
<div className="footer-copyright pull-left" data-i18n="common.copyright"></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{this.props.version.get('release') &&
|
||||||
|
<div className="footer-version pull-right">Version: {this.props.version.get('release')}</div>
|
||||||
|
}
|
||||||
|
<div className="footer-lang pull-right">
|
||||||
|
<div className="dropdown dropup">
|
||||||
|
<button className="dropdown-toggle current-locale btn btn-link" data-toggle="dropdown">{this.getCurrentLocale().name}</button>
|
||||||
|
<ul className="dropdown-menu locales">
|
||||||
|
{_.map(this.props.locales, function(locale) {
|
||||||
|
return <li key={locale.name} onClick={_.bind(this.setLocale, this, locale)}>
|
||||||
|
<a>{locale.name}</a>
|
||||||
|
</li>;
|
||||||
|
}, this)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
setLocale: function(newLocale) {
|
||||||
|
$.i18n.setLng(newLocale.locale, {});
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
getAvailableLocales: function() {
|
||||||
|
return _.map(_.keys($.i18n.options.resStore).sort(), function(locale) {
|
||||||
|
return {locale: locale, name: $.t('language', {lng: locale})};
|
||||||
|
}, this);
|
||||||
|
},
|
||||||
|
getCurrentLocale: function() {
|
||||||
|
return _.find(this.props.locales, {locale: $.i18n.lng()});
|
||||||
|
},
|
||||||
|
setDefaultLocale: function() {
|
||||||
|
if (!this.getCurrentLocale()) {
|
||||||
|
$.i18n.setLng(this.props.locales[0].locale, {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {locales: this.prototype.getAvailableLocales()};
|
||||||
|
},
|
||||||
|
componentWillMount: function(options) {
|
||||||
|
this.setDefaultLocale();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
components.Breadcrumbs = React.createClass({
|
||||||
|
setPath: function(path) {
|
||||||
|
this.setProps({path: path});
|
||||||
|
},
|
||||||
|
render: function() {
|
||||||
|
return <ul className="breadcrumb">
|
||||||
|
{_.map(this.props.path, function(breadcrumb, index) {
|
||||||
|
if (_.isArray(breadcrumb)) {
|
||||||
|
if (breadcrumb[2]) {
|
||||||
|
return <li key={index} className="active">{breadcrumb[0]}</li>;
|
||||||
|
}
|
||||||
|
return <li key={index}><a href={breadcrumb[1]}>{$.t('breadcrumbs.' + breadcrumb[0], {defaultValue: breadcrumb[0]})}</a><span className="divider">/</span></li>;
|
||||||
|
}
|
||||||
|
return <li key={index} className="active">{$.t('breadcrumbs.' + breadcrumb, {defaultValue: breadcrumb})}</li>;
|
||||||
|
})}
|
||||||
|
</ul>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return components;
|
||||||
|
});
|
@ -68,9 +68,9 @@ function(commonViews, loginPageTemplate) {
|
|||||||
_.defer(_.bind(function() {
|
_.defer(_.bind(function() {
|
||||||
this.$('[autofocus]:first').focus();
|
this.$('[autofocus]:first').focus();
|
||||||
}, this));
|
}, this));
|
||||||
app.footer.$el.hide();
|
$(app.footer.getDOMNode()).hide();
|
||||||
app.breadcrumbs.$el.hide();
|
$(app.breadcrumbs.getDOMNode()).hide();
|
||||||
app.navbar.$el.hide();
|
$(app.navbar.getDOMNode()).hide();
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ define(
|
|||||||
'utils',
|
'utils',
|
||||||
'models',
|
'models',
|
||||||
'views/common',
|
'views/common',
|
||||||
'views/dialogs',
|
'jsx!views/dialogs',
|
||||||
'text!templates/notifications/list.html'
|
'text!templates/notifications/list.html'
|
||||||
],
|
],
|
||||||
function(utils, models, commonViews, dialogViews, notificationsListTemplate) {
|
function(utils, models, commonViews, dialogViews, notificationsListTemplate) {
|
||||||
|
@ -17,7 +17,7 @@ define(
|
|||||||
[
|
[
|
||||||
'utils',
|
'utils',
|
||||||
'views/common',
|
'views/common',
|
||||||
'views/dialogs',
|
'jsx!views/dialogs',
|
||||||
'text!templates/release/list.html',
|
'text!templates/release/list.html',
|
||||||
'text!templates/release/release.html'
|
'text!templates/release/release.html'
|
||||||
],
|
],
|
||||||
|
@ -19,7 +19,7 @@ define(
|
|||||||
'utils',
|
'utils',
|
||||||
'models',
|
'models',
|
||||||
'view_mixins',
|
'view_mixins',
|
||||||
'views/dialogs',
|
'jsx!views/dialogs',
|
||||||
'text!templates/dialogs/create_cluster_wizard.html',
|
'text!templates/dialogs/create_cluster_wizard.html',
|
||||||
'text!templates/dialogs/create_cluster_wizard/name_and_release.html',
|
'text!templates/dialogs/create_cluster_wizard/name_and_release.html',
|
||||||
'text!templates/dialogs/create_cluster_wizard/common_wizard_panel.html',
|
'text!templates/dialogs/create_cluster_wizard/common_wizard_panel.html',
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
<div class="cluster-name"><%- cluster.get('name') %></div>
|
|
||||||
<% var nodes = cluster.get('nodes') %>
|
|
||||||
<div class="cluster-hardware">
|
|
||||||
<% if (!nodes.deferred || nodes.deferred.state() == 'resolved') { %>
|
|
||||||
<div class="row-fluid">
|
|
||||||
<div class="span6" data-i18n="clusters_page.cluster_hardware_nodes"></div>
|
|
||||||
<div class="span4"><%= nodes.length %></div>
|
|
||||||
<% if (nodes.length) { %>
|
|
||||||
<div class="span6" data-i18n="clusters_page.cluster_hardware_cpu"></div>
|
|
||||||
<div class="span4"><%= nodes.resources('cores') %></div>
|
|
||||||
<div class="span6" data-i18n="clusters_page.cluster_hardware_hdd"></div>
|
|
||||||
<div class="span4"><%= nodes.resources('hdd') ? showDiskSize(nodes.resources('hdd')) : '?GB' %></div>
|
|
||||||
<div class="span6" data-i18n="clusters_page.cluster_hardware_ram"></div>
|
|
||||||
<div class="span4"><%= nodes.resources('ram') ? showMemorySize(nodes.resources('ram')) : '?GB' %></div>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
<div class="cluster-status">
|
|
||||||
<% if (deploymentTask) { %>
|
|
||||||
<div class="cluster-status-progress <%- deploymentTask.get('name') %>">
|
|
||||||
<div class="progress progress-<%= _.contains(['stop_deployment', 'reset_environment'], deploymentTask.get('name')) ? 'warning' : 'success' %> progress-striped active">
|
|
||||||
<div class="bar"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% } else { %>
|
|
||||||
<%- $.t('cluster.status.' + cluster.get('status'), {defaultValue: cluster.get('status')}) %>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
@ -1,4 +0,0 @@
|
|||||||
<div class="span3 clusterbox create-cluster">
|
|
||||||
<div class="add-icon"><i class="icon-create"></i></div>
|
|
||||||
<div class="create-cluster-text"><%- $.t('clusters_page.create_cluster_text') %></div>
|
|
||||||
</div>
|
|
@ -1,2 +0,0 @@
|
|||||||
<h3 class="page-title" data-i18n="clusters_page.title"></h3>
|
|
||||||
<div class="cluster-list"></div>
|
|
@ -1,8 +0,0 @@
|
|||||||
<div class="alert alert-info alert-dismissable register-trial">
|
|
||||||
<button type="button" class="close" aria-hidden="true">×</button>
|
|
||||||
<p>
|
|
||||||
<i class="icon-mirantis"></i>
|
|
||||||
<%- $.t('clusters_page.register_trial_message.part1') %><br>
|
|
||||||
<%- $.t('clusters_page.register_trial_message.part2') %><a target="_blank" class="registration-link" href=""><%- $.t('clusters_page.register_trial_message.part3') %></a><%- $.t('clusters_page.register_trial_message.part4') %>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
@ -1,15 +0,0 @@
|
|||||||
<ul class="breadcrumb">
|
|
||||||
<% _.each(path, function(part) { %>
|
|
||||||
<% if (_.isArray(part)) { %>
|
|
||||||
<% if (part[2]) { %>
|
|
||||||
<li class="active"><%- part[0] %></li>
|
|
||||||
<% } else { %>
|
|
||||||
<li>
|
|
||||||
<a href="<%- part[1] %>"><%- $.t('breadcrumbs.' + part[0], {defaultValue: part[0]}) %></a><span class="divider">/</span>
|
|
||||||
</li>
|
|
||||||
<% } %>
|
|
||||||
<% } else { %>
|
|
||||||
<li class="active"><%- $.t('breadcrumbs.' + part, {defaultValue: part}) %></li>
|
|
||||||
<% } %>
|
|
||||||
<% }) %>
|
|
||||||
</ul>
|
|
@ -1,21 +0,0 @@
|
|||||||
<div class="footer-box">
|
|
||||||
<% if (_.contains(version.get('feature_groups'), 'mirantis')) { %>
|
|
||||||
<a href="http://www.mirantis.com" target="_blank" class="footer-logo"></a>
|
|
||||||
<div class="footer-copyright pull-left" data-i18n="common.copyright"></div>
|
|
||||||
<% } %>
|
|
||||||
<% if (version.get('release')) { %>
|
|
||||||
<div class="footer-version pull-right">Version: <%- version.get('release') %></div>
|
|
||||||
<% } %>
|
|
||||||
<div class="footer-lang pull-right">
|
|
||||||
<div class="dropdown dropup">
|
|
||||||
<button class="dropdown-toggle current-locale btn btn-link" data-toggle="dropdown"><%- currentLocale.name %></button>
|
|
||||||
<ul class="dropdown-menu locales">
|
|
||||||
<% _.each(locales, function(locale) { %>
|
|
||||||
<li>
|
|
||||||
<a data-locale="<%- locale.locale %>"><%- locale.name %></a>
|
|
||||||
</li>
|
|
||||||
<% }) %>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,24 +0,0 @@
|
|||||||
<div class="user-info-box">
|
|
||||||
<% if (version.get('auth_required') && user.get('authenticated')) { %>
|
|
||||||
<i class="icon-user"></i>
|
|
||||||
<%- user.get('username') %>
|
|
||||||
<a class="change-password"><%- $.t('common.change_password') %></a>
|
|
||||||
<a href="#logout"><%- $.t('common.logout') %></a>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
<div class="navigation-bar">
|
|
||||||
<div class="navigation-bar-box">
|
|
||||||
<ul class="navigation-bar-ul">
|
|
||||||
<li class="product-logo">
|
|
||||||
<a href="#"><div class="logo"></div></a>
|
|
||||||
</li>
|
|
||||||
<% elements.each(function(element) { %>
|
|
||||||
<li><a class="<%= element.get('active') ? 'active' : '' %>" href="<%- element.get('url') %>"><%- $.t('navbar.' + element.get('label'), {defaultValue: element.get('label')}) %></a></li>
|
|
||||||
<% }) %>
|
|
||||||
<li class="space"></li>
|
|
||||||
<li class="navigation-bar-icon notifications"></li>
|
|
||||||
<li class="navigation-bar-icon nodes-summary-container"></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="notification-wrapper"></div>
|
|
@ -1,6 +0,0 @@
|
|||||||
<div class="statistic">
|
|
||||||
<div class="stat-count total-nodes-count"></div>
|
|
||||||
<div class="stat-title total-nodes-title"></div>
|
|
||||||
<div class="stat-count unallocated-nodes-count"></div>
|
|
||||||
<div class="stat-title unallocated-nodes-title"></div>
|
|
||||||
</div>
|
|
@ -1,6 +0,0 @@
|
|||||||
<% if (authenticated) { %>
|
|
||||||
<i class="icon-comment"></i>
|
|
||||||
<% if (notifications.length) { %>
|
|
||||||
<span class="badge badge-warning"><%= notifications.length %></span>
|
|
||||||
<% } %>
|
|
||||||
<% } %>
|
|
@ -1,22 +0,0 @@
|
|||||||
<div class="message-list-placeholder">
|
|
||||||
<ul class="message-list-popover" role="menu">
|
|
||||||
<% if (notifications.length) { %>
|
|
||||||
<% _.each(notifications.last(displayCount).reverse(), function(notification, index, collection) { %>
|
|
||||||
<% var topic = notification.get('topic'), icons = {'error': 'icon-attention', 'warning': 'icon-attention', 'discover': 'icon-bell'} %>
|
|
||||||
<% var nodeId = notification.get('node_id') %>
|
|
||||||
<li class="enable-selection <%= notification.get('status') == 'unread' ? 'new' : '' %> <%= topic %>" <%- nodeId ? 'data-node=' + nodeId : '' %>>
|
|
||||||
<i class="<%= icons[topic] || 'icon-info-circled' %>"></i>
|
|
||||||
<%= urlify(notification.escape('message')) %>
|
|
||||||
</li>
|
|
||||||
<% if (showMore || index < (collection.length - 1)) { %>
|
|
||||||
<li class="divider"></li>
|
|
||||||
<% } %>
|
|
||||||
<% }) %>
|
|
||||||
<% } else { %>
|
|
||||||
<li data-i18n="notifications_popover.no_notifications_text"></li>
|
|
||||||
<% } %>
|
|
||||||
</ul>
|
|
||||||
<% if (showMore) { %>
|
|
||||||
<div class="show-more-notifications"><a href="#notifications" data-i18n="notifications_popover.view_all_button"></a></div>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
35
nailgun/ui_tests/bind-polyfill.js
Normal file
35
nailgun/ui_tests/bind-polyfill.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
(function() {
|
||||||
|
|
||||||
|
var Ap = Array.prototype;
|
||||||
|
var slice = Ap.slice;
|
||||||
|
var Fp = Function.prototype;
|
||||||
|
|
||||||
|
if (!Fp.bind) {
|
||||||
|
// PhantomJS doesn't support Function.prototype.bind natively, so
|
||||||
|
// polyfill it whenever this module is required.
|
||||||
|
Fp.bind = function(context) {
|
||||||
|
var func = this;
|
||||||
|
var args = slice.call(arguments, 1);
|
||||||
|
|
||||||
|
function bound() {
|
||||||
|
var invokedAsConstructor = func.prototype && (this instanceof func);
|
||||||
|
return func.apply(
|
||||||
|
// Ignore the context parameter when invoking the bound function
|
||||||
|
// as a constructor. Note that this includes not only constructor
|
||||||
|
// invocations using the new keyword but also calls to base class
|
||||||
|
// constructors such as BaseClass.call(this, ...) or super(...).
|
||||||
|
!invokedAsConstructor && context || this,
|
||||||
|
args.concat(slice.call(arguments))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The bound function must share the .prototype of the unbound
|
||||||
|
// function so that any object created by one constructor will count
|
||||||
|
// as an instance of both constructors.
|
||||||
|
bound.prototype = func.prototype;
|
||||||
|
|
||||||
|
return bound;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
@ -24,6 +24,10 @@ casper.on('page.error', function(msg) {
|
|||||||
casper.echo(msg, 'ERROR');
|
casper.echo(msg, 'ERROR');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
casper.on('page.initialized', function(msg) {
|
||||||
|
this.loadJsFile('bind-polyfill');
|
||||||
|
});
|
||||||
|
|
||||||
casper.loadPage = function(page) {
|
casper.loadPage = function(page) {
|
||||||
//FIXME: hack to prevent ajax requests interruption (phantomjs issue)
|
//FIXME: hack to prevent ajax requests interruption (phantomjs issue)
|
||||||
this.wait(1000);
|
this.wait(1000);
|
||||||
|
Loading…
Reference in New Issue
Block a user