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) {
|
||||
var pkg = grunt.file.readJSON('package.json');
|
||||
var staticDir = grunt.option('static-dir') || '/tmp/static_compressed';
|
||||
var staticBuildPreparationDir = staticDir + '/_prepare_build';
|
||||
var staticBuildDir = staticDir + '/_build';
|
||||
|
||||
grunt.initConfig({
|
||||
pkg: pkg,
|
||||
requirejs: {
|
||||
compile: {
|
||||
options: {
|
||||
baseUrl: '.',
|
||||
appDir: 'static',
|
||||
dir: staticDir,
|
||||
appDir: staticBuildPreparationDir + '/static',
|
||||
dir: staticBuildDir,
|
||||
mainConfigFile: 'static/js/main.js',
|
||||
waitSeconds: 60,
|
||||
optimize: 'uglify2',
|
||||
@ -33,7 +36,8 @@ module.exports = function(grunt) {
|
||||
},
|
||||
map: {
|
||||
'*': {
|
||||
'css': 'require-css'
|
||||
'css': 'require-css',
|
||||
'JSXTransformer': 'empty:'
|
||||
}
|
||||
},
|
||||
modules: [
|
||||
@ -68,7 +72,7 @@ module.exports = function(grunt) {
|
||||
less: {
|
||||
all: {
|
||||
src: 'static/css/styles.less',
|
||||
dest: 'static/css/styles.css',
|
||||
dest: staticBuildPreparationDir + '/static/css/styles.css',
|
||||
}
|
||||
},
|
||||
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: {
|
||||
trim: {
|
||||
expand: true,
|
||||
cwd: staticDir,
|
||||
cwd: staticBuildDir,
|
||||
src: [
|
||||
'**/*.js',
|
||||
'!js/main.js',
|
||||
'!js/libs/bower/requirejs/js/require.js',
|
||||
'**/*.css',
|
||||
'**/*.less',
|
||||
'!css/styles.css',
|
||||
'templates',
|
||||
'i18n'
|
||||
]
|
||||
},
|
||||
jsx: {
|
||||
expand: true,
|
||||
cwd: staticBuildPreparationDir,
|
||||
src: ['**/*.jsx']
|
||||
},
|
||||
prepare_build: {
|
||||
src: [staticDir]
|
||||
},
|
||||
finalize_build: {
|
||||
src: [staticBuildDir, staticBuildPreparationDir]
|
||||
},
|
||||
options: {
|
||||
force: true
|
||||
}
|
||||
@ -110,7 +187,7 @@ module.exports = function(grunt) {
|
||||
cleanempty: {
|
||||
trim: {
|
||||
expand: true,
|
||||
cwd: staticDir,
|
||||
cwd: staticBuildDir,
|
||||
src: ['**']
|
||||
},
|
||||
options: {
|
||||
@ -121,7 +198,7 @@ module.exports = function(grunt) {
|
||||
replace: {
|
||||
sha: {
|
||||
src: 'static/index.html',
|
||||
dest: staticDir + '/',
|
||||
dest: staticBuildDir + '/',
|
||||
replacements: [{
|
||||
from: '__COMMIT_SHA__',
|
||||
to: function() {
|
||||
@ -150,8 +227,22 @@ module.exports = function(grunt) {
|
||||
.filter(function(npmTaskName) { return npmTaskName.indexOf('grunt-') === 0; })
|
||||
.forEach(grunt.loadNpmTasks.bind(grunt));
|
||||
|
||||
grunt.registerTask('trimstatic', ['clean', 'cleanempty']);
|
||||
grunt.registerTask('build', ['bower', 'less', 'requirejs', 'trimstatic', 'revision', 'replace']);
|
||||
grunt.registerTask('build', [
|
||||
'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.task.loadTasks('grunt');
|
||||
};
|
||||
|
@ -2,6 +2,8 @@
|
||||
"name": "fuel-web",
|
||||
"dependencies": {
|
||||
"jquery": "1.9.1",
|
||||
"react": "0.11.1",
|
||||
"react.backbone": "0.4.0",
|
||||
"requirejs": "2.1.9",
|
||||
"requirejs-text": "2.0.10",
|
||||
"require-css": "0.1.0",
|
||||
@ -17,6 +19,9 @@
|
||||
"jquery": {
|
||||
"js": "jquery.js"
|
||||
},
|
||||
"react": {
|
||||
"js": ["JSXTransformer.js", "react-with-addons.js"]
|
||||
},
|
||||
"requirejs": {
|
||||
"js": "require.js"
|
||||
},
|
||||
|
944
nailgun/npm-shrinkwrap.json
generated
944
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-cleanempty": "~0.2.0",
|
||||
"grunt-contrib-clean": "~0.5.0",
|
||||
"grunt-contrib-copy": "~0.5.0",
|
||||
"grunt-contrib-less": "~0.8.2",
|
||||
"grunt-contrib-requirejs": "~0.4.1",
|
||||
"grunt-debug-task": "~0.1.3",
|
||||
"grunt-git-revision": "~0.0.1",
|
||||
"grunt-jison": "~1.2.1",
|
||||
"grunt-jslint": "~1.1.1",
|
||||
"grunt-react": "~0.9.0",
|
||||
"grunt-text-replace": "~0.3.12",
|
||||
"jison": "~0.4.13",
|
||||
"jslint": "~0.2.5",
|
||||
|
@ -16,6 +16,8 @@
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<div class="container">
|
||||
<div id="navbar" class="container"></div>
|
||||
<div id="breadcrumbs" class="container"></div>
|
||||
<div id="content">
|
||||
<div class="loading"></div>
|
||||
</div>
|
||||
@ -23,5 +25,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="footer"></div>
|
||||
<div id="modal-container"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -15,6 +15,9 @@
|
||||
**/
|
||||
define(
|
||||
[
|
||||
'react',
|
||||
'utils',
|
||||
'jsx!views/layout',
|
||||
'coccyx',
|
||||
'js/coccyx_mixins',
|
||||
'models',
|
||||
@ -23,13 +26,13 @@ define(
|
||||
'views/login_page',
|
||||
'views/cluster_page',
|
||||
'views/cluster_page_tabs/nodes_tab',
|
||||
'views/clusters_page',
|
||||
'jsx!views/clusters_page',
|
||||
'views/releases_page',
|
||||
'views/notifications_page',
|
||||
'views/support_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';
|
||||
|
||||
var AppRouter = Backbone.Router.extend({
|
||||
@ -71,7 +74,7 @@ function(Coccyx, coccyxMixins, models, KeystoneClient, commonViews, LoginPage, C
|
||||
});
|
||||
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')});
|
||||
|
||||
var originalSync = Backbone.sync;
|
||||
@ -126,49 +129,48 @@ function(Coccyx, coccyxMixins, models, KeystoneClient, commonViews, LoginPage, C
|
||||
return originalSync.call(this, method, model, options);
|
||||
};
|
||||
|
||||
this.renderLayout();
|
||||
|
||||
if (version.get('auth_required')) {
|
||||
_.extend(keystoneClient, this.user.pick('username', 'password'));
|
||||
keystoneClient.authenticate()
|
||||
return keystoneClient.authenticate()
|
||||
.done(function() {
|
||||
app.user.set({authenticated: true});
|
||||
})
|
||||
.always(function() {
|
||||
Backbone.history.start();
|
||||
})
|
||||
.fail(function() {
|
||||
app.navigate('#login', {trigger: true});
|
||||
});
|
||||
} else {
|
||||
Backbone.history.start();
|
||||
}
|
||||
return $.Deferred().resolve();
|
||||
}, this)).always(_.bind(function() {
|
||||
this.renderLayout();
|
||||
Backbone.history.start();
|
||||
if (version.get('auth_required') && !this.user.get('authenticated')) {
|
||||
app.navigate('#login', {trigger: true});
|
||||
}
|
||||
}, this));
|
||||
},
|
||||
renderLayout: function() {
|
||||
this.content = $('#content');
|
||||
this.navbar = new commonViews.Navbar({elements: [
|
||||
{label: 'environments', url: '#clusters'},
|
||||
{label: 'releases', url:'#releases'},
|
||||
{label: 'support', url:'#support'}
|
||||
]});
|
||||
this.content.before(this.navbar.render().el);
|
||||
this.breadcrumbs = new commonViews.Breadcrumbs();
|
||||
this.content.before(this.breadcrumbs.render().el);
|
||||
this.footer = new commonViews.Footer();
|
||||
$('#footer').html(this.footer.render().el);
|
||||
this.navbar = React.renderComponent(new layoutComponents.Navbar({
|
||||
elements: [
|
||||
{label: 'environments', url: '#clusters'},
|
||||
{label: 'releases', url:'#releases'},
|
||||
{label: 'support', url:'#support'}
|
||||
],
|
||||
user: this.user,
|
||||
version: this.version,
|
||||
statistics: new models.NodesStatistics(),
|
||||
notifications: new models.Notifications()
|
||||
}), $('#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');
|
||||
},
|
||||
setPage: function(NewPage, options) {
|
||||
if (this.page) {
|
||||
this.page.tearDown();
|
||||
utils.universalUnmount(this.page);
|
||||
}
|
||||
this.page = new NewPage(options);
|
||||
this.page.updateNavbar();
|
||||
this.page.updateBreadcrumbs();
|
||||
this.page.updateTitle();
|
||||
this.content.html(this.page.render().el);
|
||||
|
||||
this.page = utils.universalMount(new NewPage(options), this.content);
|
||||
this.navbar.setActive(_.result(this.page, 'navbarActiveElement'));
|
||||
this.breadcrumbs.setPath(_.result(this.page, 'breadcrumbsPath'));
|
||||
var newTitle = _.result(this.page, 'title');
|
||||
document.title = $.t('common.title') + (newTitle ? ' - ' + newTitle : '');
|
||||
},
|
||||
// routes
|
||||
login: function() {
|
||||
@ -259,7 +261,7 @@ function(Coccyx, coccyxMixins, models, KeystoneClient, commonViews, LoginPage, C
|
||||
cluster.get('nodes').deferred = nodes.deferred;
|
||||
cluster.set('tasks', new models.Tasks(tasks.where({cluster: cluster.id})));
|
||||
}, this);
|
||||
this.setPage(ClustersPage, {collection: clusters});
|
||||
this.setPage(ClustersPage, {clusters: clusters});
|
||||
}, this));
|
||||
},
|
||||
listReleases: function() {
|
||||
@ -276,7 +278,7 @@ function(Coccyx, coccyxMixins, models, KeystoneClient, commonViews, LoginPage, C
|
||||
}, this));
|
||||
},
|
||||
showNotifications: function() {
|
||||
this.setPage(NotificationsPage, {notifications: app.navbar.notifications});
|
||||
this.setPage(NotificationsPage, {notifications: app.navbar.props.notifications});
|
||||
},
|
||||
showSupportPage: function() {
|
||||
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',
|
||||
lodash: 'js/libs/bower/lodash/js/lodash',
|
||||
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',
|
||||
coccyx: 'js/libs/custom/coccyx',
|
||||
cocktail: 'js/libs/bower/cocktail/Cocktail',
|
||||
@ -43,7 +47,8 @@ requirejs.config({
|
||||
models: 'js/models',
|
||||
collections: 'js/collections',
|
||||
views: 'js/views',
|
||||
view_mixins: 'js/view_mixins'
|
||||
view_mixins: 'js/view_mixins',
|
||||
component_mixins: 'js/component_mixins'
|
||||
},
|
||||
shim: {
|
||||
underscore: {
|
||||
@ -92,6 +97,9 @@ requirejs.config({
|
||||
'jquery-autoNumeric': {
|
||||
deps: ['jquery']
|
||||
}
|
||||
},
|
||||
jsx: {
|
||||
fileExtension: '.jsx'
|
||||
}
|
||||
});
|
||||
|
||||
@ -102,6 +110,8 @@ require([
|
||||
'stickit',
|
||||
'deepModel',
|
||||
'coccyx',
|
||||
'react',
|
||||
'react.backbone',
|
||||
'cocktail',
|
||||
'i18next',
|
||||
'bootstrap',
|
||||
@ -110,6 +120,10 @@ require([
|
||||
'jquery-ui',
|
||||
'jquery-autoNumeric',
|
||||
'styles',
|
||||
'text',
|
||||
//>>excludeStart("compressed", pragmas.compressed);
|
||||
'jsx',
|
||||
//>>excludeEnd("compressed");
|
||||
'app'
|
||||
], function() {
|
||||
'use strict';
|
||||
|
@ -13,7 +13,7 @@
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
**/
|
||||
define(['require', 'expression_parser'], function(require, ExpressionParser) {
|
||||
define(['require', 'expression_parser', 'react'], function(require, ExpressionParser, React) {
|
||||
'use strict';
|
||||
|
||||
var utils = {
|
||||
@ -101,9 +101,32 @@ define(['require', 'expression_parser'], function(require, ExpressionParser) {
|
||||
}
|
||||
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) {
|
||||
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();
|
||||
parentView.registerSubView(dialog);
|
||||
dialog.render(_.extend({error: true}, options));
|
||||
|
@ -18,7 +18,7 @@ define(
|
||||
'utils',
|
||||
'models',
|
||||
'views/common',
|
||||
'views/dialogs',
|
||||
'jsx!views/dialogs',
|
||||
'views/cluster_page_tabs/nodes_tab',
|
||||
'views/cluster_page_tabs/network_tab',
|
||||
'views/cluster_page_tabs/settings_tab',
|
||||
@ -180,18 +180,10 @@ function(utils, models, commonViews, dialogViews, NodesTab, NetworkTab, Settings
|
||||
activeTab: this.activeTab
|
||||
})).i18n();
|
||||
var options = {model: this.model, page: this};
|
||||
this.clusterInfo = new ClusterInfo(options);
|
||||
this.registerSubView(this.clusterInfo);
|
||||
this.$('.cluster-info').html(this.clusterInfo.render().el);
|
||||
this.clusterCustomizationMessage = new ClusterCustomizationMessage(options);
|
||||
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);
|
||||
this.clusterInfo = utils.universalMount(new ClusterInfo(options), this.$('.cluster-info'), this);
|
||||
this.clusterCustomizationMessage = utils.universalMount(new ClusterCustomizationMessage(options), this.$('.customization-message'), this);
|
||||
this.deploymentResult = utils.universalMount(new DeploymentResult(options), this.$('.deployment-result'), this);
|
||||
this.deploymentControl = utils.universalMount(new DeploymentControl(options), this.$('.deployment-control'), this);
|
||||
|
||||
var tabs = {
|
||||
'nodes': NodesTab,
|
||||
@ -202,9 +194,11 @@ function(utils, models, commonViews, dialogViews, NodesTab, NetworkTab, Settings
|
||||
'healthcheck': HealthCheckTab
|
||||
};
|
||||
if (_.has(tabs, this.activeTab)) {
|
||||
this.tab = new tabs[this.activeTab]({model: this.model, tabOptions: this.tabOptions, page: this});
|
||||
this.$('#tab-' + this.activeTab).html(this.tab.render().el);
|
||||
this.registerSubView(this.tab);
|
||||
this.tab = utils.universalMount(
|
||||
new tabs[this.activeTab]({model: this.model, tabOptions: this.tabOptions, page: this}),
|
||||
this.$('#tab-' + this.activeTab),
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
return this;
|
||||
|
@ -18,7 +18,7 @@ define(
|
||||
'utils',
|
||||
'models',
|
||||
'views/common',
|
||||
'views/dialogs',
|
||||
'jsx!views/dialogs',
|
||||
'text!templates/cluster/actions_tab.html',
|
||||
'text!templates/cluster/actions_rename.html',
|
||||
'text!templates/cluster/actions_reset.html',
|
||||
|
@ -19,7 +19,7 @@ define(
|
||||
'models',
|
||||
'view_mixins',
|
||||
'views/common',
|
||||
'views/dialogs',
|
||||
'jsx!views/dialogs',
|
||||
'text!templates/cluster/healthcheck_tab.html',
|
||||
'text!templates/cluster/healthcheck_credentials.html',
|
||||
'text!templates/cluster/healthcheck_testset.html',
|
||||
|
@ -18,7 +18,7 @@ define(
|
||||
'utils',
|
||||
'models',
|
||||
'views/common',
|
||||
'views/dialogs',
|
||||
'jsx!views/dialogs',
|
||||
'text!templates/cluster/network_tab.html',
|
||||
'text!templates/cluster/network.html',
|
||||
'text!templates/cluster/range_field.html',
|
||||
|
@ -17,7 +17,7 @@ define(
|
||||
[
|
||||
'utils',
|
||||
'models',
|
||||
'views/dialogs',
|
||||
'jsx!views/dialogs',
|
||||
'views/cluster_page_tabs/nodes_tab_screens/screen',
|
||||
'text!templates/cluster/nodes_management_panel.html',
|
||||
'text!templates/cluster/assign_roles_panel.html',
|
||||
|
@ -19,7 +19,7 @@ define(
|
||||
'models',
|
||||
'view_mixins',
|
||||
'views/common',
|
||||
'views/dialogs',
|
||||
'jsx!views/dialogs',
|
||||
'text!templates/cluster/settings_tab.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(
|
||||
[
|
||||
'utils',
|
||||
'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'
|
||||
'models'
|
||||
],
|
||||
function(utils, models, dialogViews, navbarTemplate, nodesStatsTemplate, notificationsTemplate, notificationsPopoverTemplate, breadcrumbsTemplate, footerTemplate) {
|
||||
function(utils, models) {
|
||||
'use strict';
|
||||
|
||||
var views = {};
|
||||
@ -33,18 +26,7 @@ function(utils, models, dialogViews, navbarTemplate, nodesStatsTemplate, notific
|
||||
views.Page = Backbone.View.extend({
|
||||
navbarActiveElement: null,
|
||||
breadcrumbsPath: 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;
|
||||
}
|
||||
title: null
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
|
@ -16,9 +16,11 @@
|
||||
define(
|
||||
[
|
||||
'require',
|
||||
'react',
|
||||
'utils',
|
||||
'models',
|
||||
'view_mixins',
|
||||
'jsx!component_mixins',
|
||||
'text!templates/dialogs/base_dialog.html',
|
||||
'text!templates/dialogs/discard_changes.html',
|
||||
'text!templates/dialogs/display_changes.html',
|
||||
@ -28,12 +30,13 @@ define(
|
||||
'text!templates/dialogs/update_environment.html',
|
||||
'text!templates/dialogs/show_node.html',
|
||||
'text!templates/dialogs/dismiss_settings.html',
|
||||
'text!templates/dialogs/delete_nodes.html',
|
||||
'text!templates/dialogs/change_password.html'
|
||||
'text!templates/dialogs/delete_nodes.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';
|
||||
|
||||
var cx = React.addons.classSet;
|
||||
|
||||
var views = {};
|
||||
|
||||
views.Dialog = Backbone.View.extend({
|
||||
@ -408,47 +411,93 @@ function(require, utils, models, viewMixins, baseDialogTemplate, discardChangesD
|
||||
}
|
||||
});
|
||||
|
||||
views.ChangePasswordDialog = views.Dialog.extend({
|
||||
template: _.template(changePasswordTemplate),
|
||||
mixins: [viewMixins.toggleablePassword],
|
||||
events: {
|
||||
'click .btn-change-password': 'changePassword',
|
||||
'keyup input': 'onPasswordChange',
|
||||
'keydown': 'onKeydown'
|
||||
views.ChangePasswordDialog = React.createClass({
|
||||
mixins: [componentMixins.dialogMixin, React.addons.LinkedStateMixin],
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: $.t('dialog.change_password.title')
|
||||
};
|
||||
},
|
||||
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() {
|
||||
var currentPassword = this.$('[name=current_password]').val(),
|
||||
newPassword = this.$('[name=new_password]').val(),
|
||||
confirmedPassword= this.$('[name=confirm_new_password]').val();
|
||||
if (currentPassword && (newPassword == confirmedPassword)) {
|
||||
app.keystoneClient.changePassword(currentPassword, newPassword)
|
||||
if (this.isPasswordChangeAvailable()) {
|
||||
this.setState({locked: true});
|
||||
app.keystoneClient.changePassword(this.state.currentPassword, this.state.newPassword)
|
||||
.done(_.bind(function() {
|
||||
app.user.set({password: app.keystoneClient.password});
|
||||
this.$el.modal('hide');
|
||||
this.close();
|
||||
}, this))
|
||||
.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));
|
||||
}
|
||||
},
|
||||
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() {
|
||||
this.$('[autofocus]:first').focus();
|
||||
}, this));
|
||||
app.footer.$el.hide();
|
||||
app.breadcrumbs.$el.hide();
|
||||
app.navbar.$el.hide();
|
||||
$(app.footer.getDOMNode()).hide();
|
||||
$(app.breadcrumbs.getDOMNode()).hide();
|
||||
$(app.navbar.getDOMNode()).hide();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ define(
|
||||
'utils',
|
||||
'models',
|
||||
'views/common',
|
||||
'views/dialogs',
|
||||
'jsx!views/dialogs',
|
||||
'text!templates/notifications/list.html'
|
||||
],
|
||||
function(utils, models, commonViews, dialogViews, notificationsListTemplate) {
|
||||
|
@ -17,7 +17,7 @@ define(
|
||||
[
|
||||
'utils',
|
||||
'views/common',
|
||||
'views/dialogs',
|
||||
'jsx!views/dialogs',
|
||||
'text!templates/release/list.html',
|
||||
'text!templates/release/release.html'
|
||||
],
|
||||
|
@ -19,7 +19,7 @@ define(
|
||||
'utils',
|
||||
'models',
|
||||
'view_mixins',
|
||||
'views/dialogs',
|
||||
'jsx!views/dialogs',
|
||||
'text!templates/dialogs/create_cluster_wizard.html',
|
||||
'text!templates/dialogs/create_cluster_wizard/name_and_release.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.on('page.initialized', function(msg) {
|
||||
this.loadJsFile('bind-polyfill');
|
||||
});
|
||||
|
||||
casper.loadPage = function(page) {
|
||||
//FIXME: hack to prevent ajax requests interruption (phantomjs issue)
|
||||
this.wait(1000);
|
||||
|
Loading…
Reference in New Issue
Block a user