React.js integration

Related to blueprint backbone-to-react

Change-Id: Idd801b607fad77c130d528b7fa9c90a5cb19edcd
This commit is contained in:
Vitaly Kramskikh 2014-07-16 20:15:58 +04:00
parent ac829e679b
commit fb848a3193
35 changed files with 1577 additions and 1010 deletions

View File

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

View File

@ -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
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'
],

View File

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

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

View File

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

View File

@ -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
static/js/views/layout.jsx Normal file
View 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;
});

View File

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

View File

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

View File

@ -17,7 +17,7 @@ define(
[
'utils',
'views/common',
'views/dialogs',
'jsx!views/dialogs',
'text!templates/release/list.html',
'text!templates/release/release.html'
],

View File

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

View File

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

View File

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

View File

@ -1,2 +0,0 @@
<h3 class="page-title" data-i18n="clusters_page.title"></h3>
<div class="cluster-list"></div>

View File

@ -1,8 +0,0 @@
<div class="alert alert-info alert-dismissable register-trial">
<button type="button" class="close" aria-hidden="true">&times;</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>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +0,0 @@
<% if (authenticated) { %>
<i class="icon-comment"></i>
<% if (notifications.length) { %>
<span class="badge badge-warning"><%= notifications.length %></span>
<% } %>
<% } %>

View File

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