Merge branch 'master' of github.com:open-cloud/xos
diff --git a/views/ngXosLib/gulpfile.js b/views/ngXosLib/gulpfile.js
index e514d7a..e030ad9 100644
--- a/views/ngXosLib/gulpfile.js
+++ b/views/ngXosLib/gulpfile.js
@@ -2,6 +2,7 @@
var gulp = require('gulp');
var wrench = require('wrench');
+var path = require('path');
var options = {
ngXosVendor: '../../xos/core/xoslib/static/js/vendor/', //save here the minfied vendor file, this is automatically loaded in the django page
@@ -11,9 +12,9 @@
docs: './docs'
};
-wrench.readdirSyncRecursive('./gulp')
+wrench.readdirSyncRecursive(path.join(__dirname, './gulp'))
.map(function(file) {
- require('./gulp/' + file)(options);
+ require(path.join(__dirname, './gulp/' + file))(options);
});
gulp.task('default', function () {
diff --git a/views/ngXosLib/xosHelpers/spec/notification.test.js b/views/ngXosLib/xosHelpers/spec/notification.test.js
new file mode 100644
index 0000000..cbc1e56
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/spec/notification.test.js
@@ -0,0 +1,63 @@
+(function () {
+ 'use strict';
+
+ describe('The xos.helper module', function(){
+ describe('The xosNotification service', () => {
+
+ let service, scope;
+
+ const options = {icon: 'icon', body: 'message'};
+
+ let notificationMock = {
+ requestPermission: () => {
+ return {
+ then: cb => cb('granted')
+ }
+ },
+ permission: 'granted'
+ }
+
+
+ // load the application module
+ beforeEach(module('xos.helpers', ($provide) => {
+ $provide.value('Notification', notificationMock);
+ }));
+
+ // inject the cartService
+ beforeEach(inject(function (_xosNotification_, $rootScope) {
+ // The injector unwraps the underscores (_) from around the parameter names when matching
+ service = _xosNotification_;
+ scope = $rootScope;
+ spyOn(service, 'sendNotification');
+ spyOn(service, 'checkPermission').and.callThrough();
+ spyOn(notificationMock, 'requestPermission').and.callThrough();
+ }));
+
+ it('should exist', () => {
+ expect(service).toBeDefined();
+ });
+
+ describe('when permission are granted', () => {
+ it('should send the notification', () => {
+ service.notify('Test', options);
+ expect(service.sendNotification).toHaveBeenCalledWith('Test', options);
+ });
+ });
+
+ describe('when permission are not granted', () => {
+ beforeEach(() => {
+ notificationMock.permission = false;
+ });
+
+ it('should request permission', () => {
+ service.notify('Test', options);
+ expect(service.checkPermission).toHaveBeenCalled();
+ scope.$apply(); // this resolve the promise
+ expect(service.sendNotification).toHaveBeenCalledWith('Test', options);
+ });
+ });
+
+ });
+ });
+
+})();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/spec/services/helpers/user-prefs.test.js b/views/ngXosLib/xosHelpers/spec/services/helpers/user-prefs.test.js
new file mode 100644
index 0000000..4c76484
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/spec/services/helpers/user-prefs.test.js
@@ -0,0 +1,113 @@
+/**
+ * © OpenCORD
+ *
+ * Created by teone on 5/25/16.
+ */
+
+(function () {
+ 'use strict';
+ let service;
+ let cookies = {
+ xosUserPrefs: JSON.stringify({test: true})
+ };
+
+ const cookieMock = {
+ get: (name) => {
+ return cookies[name]
+ },
+ put: (name, value) => {
+ cookies[name] = value
+ }
+ };
+
+ describe('The xos.helper module', function(){
+
+ describe('The XosUserPrefs service', () => {
+
+ // load the application module
+ beforeEach(module('xos.helpers', ($provide) => {
+ $provide.value('$cookies', cookieMock);
+ }));
+
+ // inject the cartService
+ beforeEach(inject(function (_XosUserPrefs_) {
+ // The injector unwraps the underscores (_) from around the parameter names when matching
+ service = _XosUserPrefs_;
+ spyOn(cookieMock, 'put').and.callThrough();
+ }));
+
+ it('should exists and have methods', () => {
+ expect(service).toBeDefined();
+ expect(service.getAll).toBeDefined();
+ expect(service.setAll).toBeDefined();
+ expect(service.getSynchronizerNotificationStatus).toBeDefined();
+ expect(service.setSynchronizerNotificationStatus).toBeDefined();
+ });
+
+ describe('the getAll method', () => {
+ it('should return all the stored prefs', () => {
+ let prefs = service.getAll();
+ expect(prefs).toEqual(JSON.parse(cookies.xosUserPrefs));
+ });
+ });
+
+ describe('the setAll method', () => {
+ it('should override all preferences', () => {
+ service.setAll({test: true, updated: true});
+ expect(JSON.parse(cookies.xosUserPrefs)).toEqual({test: true, updated: true});
+ });
+ });
+
+ describe('the synchronizers status', () => {
+ let syncNotification;
+ beforeEach(() => {
+ syncNotification = {
+ synchronizers: {
+ notification: {
+ first: true,
+ second: false
+ }
+ }
+ }
+ cookies.xosUserPrefs = JSON.stringify(syncNotification);
+ });
+
+ describe('the getSynchronizerNotificationStatus method', () => {
+ it('should return notification status for all synchronizers', () => {
+ expect(service.getSynchronizerNotificationStatus()).toEqual(syncNotification.synchronizers.notification);
+ });
+
+ it('should return notification status for a single synchronizers', () => {
+ expect(service.getSynchronizerNotificationStatus('first')).toEqual(syncNotification.synchronizers.notification.first);
+ expect(service.getSynchronizerNotificationStatus('second')).toEqual(syncNotification.synchronizers.notification.second);
+ });
+ });
+
+ describe('the setSynchronizerNotificationStatus', () => {
+
+ it('should throw an error if called without synchronizer name', () => {
+ function wrapper (){
+ service.setSynchronizerNotificationStatus();
+ }
+ expect(wrapper).toThrowError('[XosUserPrefs] When updating a synchronizer is mandatory to provide a name.');
+ });
+
+ it('should update a synchronizer notification status', () => {
+ service.setSynchronizerNotificationStatus('second', true);
+ expect(service.getSynchronizerNotificationStatus('second')).toEqual(true);
+ expect(service.getSynchronizerNotificationStatus('first')).toEqual(true);
+
+ // should persist the change
+ expect(cookieMock.put).toHaveBeenCalledWith('xosUserPrefs', '{"synchronizers":{"notification":{"first":true,"second":true}}}');
+ });
+
+ it('should handle empty cookies', () => {
+ cookies.xosUserPrefs = '';
+ service.setSynchronizerNotificationStatus('second', true);
+ expect(service.getSynchronizerNotificationStatus('second')).toEqual(true);
+ });
+ });
+ });
+ });
+ });
+})();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/services/helpers/user-prefs.service.js b/views/ngXosLib/xosHelpers/src/services/helpers/user-prefs.service.js
new file mode 100644
index 0000000..27edf7f
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/services/helpers/user-prefs.service.js
@@ -0,0 +1,95 @@
+(function () {
+
+ angular.module('xos.helpers')
+
+ /**
+ * @ngdoc service
+ * @name xos.helpers.XosUserPrefs
+ * @description
+ * This service is used to store the user preferences in cookies, so that they survive to page changes.
+ * The structure of the user preference is:
+ * ```
+ * {
+ * synchronizers: {
+ * notification: {
+ * 'volt': boolean,
+ * 'openstack': boolean,
+ * ...
+ * }
+ * }
+ * }
+ * ```
+ **/
+
+ .service('XosUserPrefs', function($cookies){
+
+ let userPrefs = $cookies.get('xosUserPrefs') ? JSON.parse($cookies.get('xosUserPrefs')) : {};
+
+ /**
+ * @ngdoc method
+ * @name xos.helpers.XosUserPrefs#getAll
+ * @methodOf xos.helpers.XosUserPrefs
+ * @description
+ * Return all the user preferences stored in cookies
+ * @returns {object} The user preferences
+ **/
+ this.getAll = () => {
+ userPrefs = $cookies.get('xosUserPrefs') ? JSON.parse($cookies.get('xosUserPrefs')) : {};
+ return userPrefs;
+ };
+
+ /**
+ * @ngdoc method
+ * @name xos.helpers.XosUserPrefs#setAll
+ * @methodOf xos.helpers.XosUserPrefs
+ * @description
+ * Override all user preferences
+ * @param {object} prefs The user preferences
+ **/
+ this.setAll = (prefs) => {
+ $cookies.put('xosUserPrefs', JSON.stringify(prefs));
+ };
+
+ /**
+ * @ngdoc method
+ * @name xos.helpers.XosUserPrefs#getSynchronizerNotificationStatus
+ * @methodOf xos.helpers.XosUserPrefs
+ * @description
+ * Return the synchronizer notification status, if name is not provided return the status for all synchronizers
+ * @param {string=} prefs The synchronizer name
+ * @returns {object | string} The synchronizer status
+ **/
+ this.getSynchronizerNotificationStatus = (name = false) => {
+ if(name){
+ return this.getAll().synchronizers.notification[name];
+ }
+ return this.getAll().synchronizers.notification;
+ };
+
+ /**
+ * @ngdoc method
+ * @name xos.helpers.XosUserPrefs#setSynchronizerNotificationStatus
+ * @methodOf xos.helpers.XosUserPrefs
+ * @description
+ * Update the notification status for a single synchronizer
+ * @param {string} name The synchronizer name
+ * @param {boolean} value The notification status (true means that it has been sent)
+ **/
+ this.setSynchronizerNotificationStatus = (name = false, value) => {
+ if(!name){
+ throw new Error('[XosUserPrefs] When updating a synchronizer is mandatory to provide a name.')
+ }
+
+ let cookies = this.getAll();
+
+ if(!cookies.synchronizers){
+ cookies.synchronizers = {
+ notification: {}
+ }
+ }
+
+ cookies.synchronizers.notification[name] = value;
+ this.setAll(cookies);
+ }
+ });
+})();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/services/notification.service.js b/views/ngXosLib/xosHelpers/src/services/notification.service.js
new file mode 100644
index 0000000..e190451
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/services/notification.service.js
@@ -0,0 +1,61 @@
+(function() {
+ 'use strict';
+
+ angular
+ .module('xos.helpers')
+ .factory('Notification', function(){
+ return window.Notification;
+ })
+ /**
+ * @ngdoc service
+ * @name xos.helpers.xosNotification
+ * @description This factory define a set of helper function to trigger desktop notification
+ **/
+ .service('xosNotification', function($q, $log, Notification) {
+
+ this.checkPermission = () => {
+ const deferred = $q.defer();
+ Notification.requestPermission()
+ .then(permission => {
+ if (permission === 'granted') {
+ deferred.resolve(permission);
+ }
+ else {
+ deferred.reject(permission);
+ }
+ });
+ return deferred.promise;
+ };
+
+ this.sendNotification = (title, options) => {
+ const notification = new Notification(title, options);
+ notification.onerror = function(err){
+ $log.error(err);
+ };
+ };
+
+ /**
+ * @ngdoc method
+ * @name xos.helpers.xosNotification#notify
+ * @methodOf xos.helpers.xosNotification
+ * @description
+ * This method will check for user permission and if granted will send a browser notification.
+ * @param {string} title The notification title
+ * @param {object} options The notification options: `{icon: 'url', body: 'Notification body'}`
+ **/
+
+ this.notify = (title, options) => {
+ if (!('Notification' in window)) {
+ $log.info('This browser does not support desktop notification');
+ }
+ else if (Notification.permission !== 'granted') {
+ this.checkPermission()
+ .then(() => this.sendNotification(title, options));
+ }
+ else if (Notification.permission === 'granted') {
+ this.sendNotification(title, options);
+ }
+ }
+
+ })
+})();
diff --git a/views/ngXosViews/synchronizerNotifier/.bowerrc b/views/ngXosViews/synchronizerNotifier/.bowerrc
new file mode 100644
index 0000000..e491038
--- /dev/null
+++ b/views/ngXosViews/synchronizerNotifier/.bowerrc
@@ -0,0 +1,3 @@
+{
+ "directory": "src/vendor/"
+}
\ No newline at end of file
diff --git a/views/ngXosViews/synchronizerNotifier/.eslintrc b/views/ngXosViews/synchronizerNotifier/.eslintrc
new file mode 100644
index 0000000..c852748
--- /dev/null
+++ b/views/ngXosViews/synchronizerNotifier/.eslintrc
@@ -0,0 +1,42 @@
+{
+ "ecmaFeatures": {
+ "blockBindings": true,
+ "forOf": true,
+ "destructuring": true,
+ "arrowFunctions": true,
+ "templateStrings": true
+ },
+ "env": {
+ "browser": true,
+ "node": true,
+ "es6": true
+ },
+ "plugins": [
+ //"angular"
+ ],
+ "rules": {
+ "quotes": [2, "single"],
+ "camelcase": [1, {"properties": "always"}],
+ "no-underscore-dangle": 1,
+ "eqeqeq": [2, "smart"],
+ "no-alert": 1,
+ "key-spacing": [1, { "beforeColon": false, "afterColon": true }],
+ "indent": [2, 2],
+ "no-irregular-whitespace": 1,
+ "eol-last": 0,
+ "max-nested-callbacks": [2, 4],
+ "comma-spacing": [1, {"before": false, "after": true}],
+ "no-trailing-spaces": [1, { skipBlankLines: true }],
+ "no-unused-vars": [1, {"vars": "all", "args": "after-used"}],
+ "new-cap": 0,
+
+ //"angular/ng_module_name": [2, '/^xos\.*[a-z]*$/'],
+ //"angular/ng_controller_name": [2, '/^[a-z].*Ctrl$/'],
+ //"angular/ng_service_name": [2, '/^[A-Z].*Service$/'],
+ //"angular/ng_directive_name": [2, '/^[a-z]+[[A-Z].*]*$/'],
+ //"angular/ng_di": [0, "function or array"]
+ },
+ "globals" :{
+ "angular": true
+ }
+}
\ No newline at end of file
diff --git a/views/ngXosViews/synchronizerNotifier/.gitignore b/views/ngXosViews/synchronizerNotifier/.gitignore
new file mode 100644
index 0000000..567aee4
--- /dev/null
+++ b/views/ngXosViews/synchronizerNotifier/.gitignore
@@ -0,0 +1,6 @@
+dist/
+src/vendor
+.tmp
+node_modules
+npm-debug.log
+dist/
\ No newline at end of file
diff --git a/views/ngXosViews/synchronizerNotifier/bower.json b/views/ngXosViews/synchronizerNotifier/bower.json
new file mode 100644
index 0000000..8b0eb12
--- /dev/null
+++ b/views/ngXosViews/synchronizerNotifier/bower.json
@@ -0,0 +1,32 @@
+{
+ "name": "xos-synchronizerNotifier",
+ "version": "0.0.0",
+ "authors": [
+ "Matteo Scandolo <matteo.scandolo@gmail.com>"
+ ],
+ "description": "The synchronizerNotifier view",
+ "license": "MIT",
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "bower_components",
+ "static/js/vendor/",
+ "test",
+ "tests"
+ ],
+ "dependencies": {},
+ "devDependencies": {
+ "jquery": "2.1.4",
+ "angular-mocks": "1.4.7",
+ "angular": "1.4.7",
+ "angular-ui-router": "0.2.15",
+ "angular-cookies": "1.4.7",
+ "angular-animate": "1.4.7",
+ "angular-resource": "1.4.7",
+ "lodash": "~4.11.1",
+ "bootstrap-css": "3.3.6",
+ "angular-chart.js": "~0.10.2",
+ "d3": "~3.5.17",
+ "angular-recursion": "~1.0.5"
+ }
+}
diff --git a/views/ngXosViews/synchronizerNotifier/gulp/build.js b/views/ngXosViews/synchronizerNotifier/gulp/build.js
new file mode 100644
index 0000000..47cb69c
--- /dev/null
+++ b/views/ngXosViews/synchronizerNotifier/gulp/build.js
@@ -0,0 +1,164 @@
+'use strict';
+
+// BUILD
+//
+// The only purpose of this gulpfile is to build a XOS view and copy the correct files into
+// .html => dashboards
+// .js (minified and concat) => static/js
+//
+// The template are parsed and added to js with angular $templateCache
+
+var gulp = require('gulp');
+var ngAnnotate = require('gulp-ng-annotate');
+var uglify = require('gulp-uglify');
+var templateCache = require('gulp-angular-templatecache');
+var runSequence = require('run-sequence');
+var concat = require('gulp-concat-util');
+var del = require('del');
+var wiredep = require('wiredep');
+var angularFilesort = require('gulp-angular-filesort');
+var _ = require('lodash');
+var eslint = require('gulp-eslint');
+var inject = require('gulp-inject');
+var rename = require('gulp-rename');
+var replace = require('gulp-replace');
+var postcss = require('gulp-postcss');
+var autoprefixer = require('autoprefixer');
+var mqpacker = require('css-mqpacker');
+var csswring = require('csswring');
+
+const TEMPLATE_FOOTER = `
+angular.module('xos.synchronizerNotifier')
+.run(['$location', function(a){
+ a.path('/');
+}])
+`
+
+module.exports = function(options){
+
+ // delete previous builded file
+ gulp.task('clean', function(){
+ return del(
+ [
+ options.dashboards + 'xosSynchronizerNotifier.html',
+ options.static + 'css/xosSynchronizerNotifier.css'
+ ],
+ {force: true}
+ );
+ });
+
+ // minify css
+ gulp.task('css', function () {
+ var processors = [
+ autoprefixer({browsers: ['last 1 version']}),
+ mqpacker,
+ csswring
+ ];
+
+ gulp.src([
+ `${options.css}**/*.css`,
+ `!${options.css}dev.css`
+ ])
+ .pipe(postcss(processors))
+ .pipe(gulp.dest(options.tmp + '/css/'));
+ });
+
+ // copy css in correct folder
+ gulp.task('copyCss', ['wait'], function(){
+ return gulp.src([`${options.tmp}/css/*.css`])
+ .pipe(concat('xosSynchronizerNotifier.css'))
+ .pipe(gulp.dest(options.static + 'css/'))
+ });
+
+ // compile and minify scripts
+ gulp.task('scripts', function() {
+ return gulp.src([
+ options.tmp + '**/*.js'
+ ])
+ .pipe(ngAnnotate())
+ .pipe(angularFilesort())
+ .pipe(concat('xosSynchronizerNotifier.js'))
+ .pipe(concat.header('//Autogenerated, do not edit!!!\n'))
+ .pipe(concat.footer(TEMPLATE_FOOTER))
+ .pipe(uglify())
+ .pipe(gulp.dest(options.static + 'js/'));
+ });
+
+ // set templates in cache
+ gulp.task('templates', function(){
+ return gulp.src('./src/templates/*.html')
+ .pipe(templateCache({
+ module: 'xos.synchronizerNotifier',
+ root: 'templates/'
+ }))
+ .pipe(gulp.dest(options.tmp));
+ });
+
+ // copy html index to Django Folder
+ gulp.task('copyHtml', function(){
+ return gulp.src(options.src + 'index.html')
+ // remove dev dependencies from html
+ .pipe(replace(/<!-- bower:css -->(\n^<link.*)*\n<!-- endbower -->/gmi, ''))
+ .pipe(replace(/<!-- bower:js -->(\n^<script.*)*\n<!-- endbower -->/gmi, ''))
+ // injecting minified files
+ .pipe(
+ inject(
+ gulp.src([
+ options.static + 'js/vendor/xosSynchronizerNotifierVendor.js',
+ options.static + 'js/xosSynchronizerNotifier.js',
+ options.static + 'css/xosSynchronizerNotifier.css'
+ ]),
+ {ignorePath: '/../../../xos/core/xoslib'}
+ )
+ )
+ .pipe(rename('xosSynchronizerNotifier.html'))
+ .pipe(gulp.dest(options.dashboards));
+ });
+
+ // minify vendor js files
+ gulp.task('wiredep', function(){
+ var bowerDeps = wiredep().js;
+ if(!bowerDeps){
+ return;
+ }
+
+ // remove angular (it's already loaded)
+ _.remove(bowerDeps, function(dep){
+ return dep.indexOf('angular/angular.js') !== -1;
+ });
+
+ return gulp.src(bowerDeps)
+ .pipe(concat('xosSynchronizerNotifierVendor.js'))
+ .pipe(uglify())
+ .pipe(gulp.dest(options.static + 'js/vendor/'));
+ });
+
+ gulp.task('lint', function () {
+ return gulp.src(['src/js/**/*.js'])
+ .pipe(eslint())
+ .pipe(eslint.format())
+ .pipe(eslint.failAfterError());
+ });
+
+ gulp.task('wait', function (cb) {
+ // setTimeout could be any async task
+ setTimeout(function () {
+ cb();
+ }, 1000);
+ });
+
+ gulp.task('build', function() {
+ runSequence(
+ 'clean',
+ 'sass',
+ 'templates',
+ 'babel',
+ 'scripts',
+ 'wiredep',
+ 'css',
+ 'copyCss',
+ 'copyHtml',
+ 'cleanTmp'
+ );
+ });
+};
\ No newline at end of file
diff --git a/views/ngXosViews/synchronizerNotifier/gulp/server.js b/views/ngXosViews/synchronizerNotifier/gulp/server.js
new file mode 100644
index 0000000..1e40a34
--- /dev/null
+++ b/views/ngXosViews/synchronizerNotifier/gulp/server.js
@@ -0,0 +1,170 @@
+'use strict';
+
+var gulp = require('gulp');
+var browserSync = require('browser-sync').create();
+var inject = require('gulp-inject');
+var runSequence = require('run-sequence');
+var angularFilesort = require('gulp-angular-filesort');
+var babel = require('gulp-babel');
+var wiredep = require('wiredep').stream;
+var httpProxy = require('http-proxy');
+var del = require('del');
+var sass = require('gulp-sass');
+var fs = require('fs');
+var path = require('path');
+
+const environment = process.env.NODE_ENV;
+
+if(!fs.existsSync(path.join(__dirname, `../../../env/${environment || 'default'}.js`))){
+ if(!environment){
+ throw new Error('You should define a default.js config in /views/env folder.');
+ }
+ else{
+ throw new Error(`Since you are loading a custom environment, you should define a ${environment}.js config in /views/env folder.`);
+ }
+}
+
+var conf = require(path.join(__dirname, `../../../env/${environment || 'default'}.js`));
+
+var proxy = httpProxy.createProxyServer({
+ target: conf.host
+});
+
+
+proxy.on('error', function(error, req, res) {
+ res.writeHead(500, {
+ 'Content-Type': 'text/plain'
+ });
+
+ console.error('[Proxy]', error);
+});
+
+module.exports = function(options){
+
+ gulp.task('browser', function() {
+ browserSync.init({
+ startPath: '#/',
+ snippetOptions: {
+ rule: {
+ match: /<!-- browserSync -->/i
+ }
+ },
+ server: {
+ baseDir: options.src,
+ routes: {
+ '/xos/core/xoslib/static/js/vendor': options.helpers,
+ '/xos/core/static': options.static + '../../static/'
+ },
+ middleware: function(req, res, next){
+ if(
+ req.url.indexOf('/api/') !== -1
+ ){
+ if(conf.xoscsrftoken && conf.xossessionid){
+ req.headers.cookie = `xoscsrftoken=${conf.xoscsrftoken}; xossessionid=${conf.xossessionid}`;
+ req.headers['x-csrftoken'] = conf.xoscsrftoken;
+ }
+ proxy.web(req, res);
+ }
+ else{
+ next();
+ }
+ }
+ }
+ });
+
+ gulp.watch(options.src + 'js/**/*.js', ['js-watch']);
+ gulp.watch(options.src + 'vendor/**/*.js', ['bower'], function(){
+ browserSync.reload();
+ });
+ gulp.watch(options.src + '**/*.html', function(){
+ browserSync.reload();
+ });
+ gulp.watch(options.css + '**/*.css', function(){
+ browserSync.reload();
+ });
+ gulp.watch(`${options.sass}/**/*.scss`, ['sass'], function(){
+ browserSync.reload();
+ });
+
+ gulp.watch([
+ options.helpers + 'ngXosHelpers.js',
+ options.static + '../../static/xosNgLib.css'
+ ], function(){
+ browserSync.reload();
+ });
+ });
+
+ // compile sass
+ gulp.task('sass', function () {
+ return gulp.src(`${options.sass}/**/*.scss`)
+ .pipe(sass().on('error', sass.logError))
+ .pipe(gulp.dest(options.css));
+ });
+
+ // transpile js with sourceMaps
+ gulp.task('babel', function(){
+ return gulp.src(options.scripts + '**/*.js')
+ .pipe(babel({sourceMaps: true}))
+ .pipe(gulp.dest(options.tmp));
+ });
+
+ // inject scripts
+ gulp.task('injectScript', ['cleanTmp', 'babel'], function(){
+ return gulp.src(options.src + 'index.html')
+ .pipe(
+ inject(
+ gulp.src([
+ options.tmp + '**/*.js',
+ options.helpers + 'ngXosHelpers.js'
+ ])
+ .pipe(angularFilesort()),
+ {
+ ignorePath: [options.src, '/../../ngXosLib']
+ }
+ )
+ )
+ .pipe(gulp.dest(options.src));
+ });
+
+ // inject CSS
+ gulp.task('injectCss', function(){
+ return gulp.src(options.src + 'index.html')
+ .pipe(
+ inject(
+ gulp.src([
+ options.src + 'css/*.css',
+ options.static + '../../static/xosNgLib.css'
+ ]),
+ {
+ ignorePath: [options.src]
+ }
+ )
+ )
+ .pipe(gulp.dest(options.src));
+ });
+
+ // inject bower dependencies with wiredep
+ gulp.task('bower', function () {
+ return gulp.src(options.src + 'index.html')
+ .pipe(wiredep({devDependencies: true}))
+ .pipe(gulp.dest(options.src));
+ });
+
+ gulp.task('js-watch', ['injectScript'], function(){
+ browserSync.reload();
+ });
+
+ gulp.task('cleanTmp', function(){
+ return del([options.tmp + '**/*']);
+ });
+
+ gulp.task('serve', function() {
+ runSequence(
+ 'sass',
+ 'bower',
+ 'injectScript',
+ 'injectCss',
+ ['browser']
+ );
+ });
+};
diff --git a/views/ngXosViews/synchronizerNotifier/gulpfile.js b/views/ngXosViews/synchronizerNotifier/gulpfile.js
new file mode 100644
index 0000000..08df554
--- /dev/null
+++ b/views/ngXosViews/synchronizerNotifier/gulpfile.js
@@ -0,0 +1,26 @@
+'use strict';
+
+var gulp = require('gulp');
+var wrench = require('wrench');
+
+var options = {
+ src: 'src/',
+ css: 'src/css/',
+ sass: 'src/sass/',
+ scripts: 'src/js/',
+ tmp: 'src/.tmp',
+ dist: 'dist/',
+ api: '../../ngXosLib/api/',
+ helpers: '../../../xos/core/xoslib/static/js/vendor/',
+ static: '../../../xos/core/xoslib/static/', // this is the django static folder
+ dashboards: '../../../xos/core/xoslib/dashboards/' // this is the django html folder
+};
+
+wrench.readdirSyncRecursive('./gulp')
+.map(function(file) {
+ require('./gulp/' + file)(options);
+});
+
+gulp.task('default', function () {
+ gulp.start('build');
+});
diff --git a/views/ngXosViews/synchronizerNotifier/karma.conf.js b/views/ngXosViews/synchronizerNotifier/karma.conf.js
new file mode 100644
index 0000000..4123be9
--- /dev/null
+++ b/views/ngXosViews/synchronizerNotifier/karma.conf.js
@@ -0,0 +1,88 @@
+// Karma configuration
+// Generated on Tue Oct 06 2015 09:27:10 GMT+0000 (UTC)
+
+/* eslint indent: [2,2], quotes: [2, "single"]*/
+
+/*eslint-disable*/
+var wiredep = require('wiredep');
+var path = require('path');
+
+var bowerComponents = wiredep( {devDependencies: true} )[ 'js' ].map(function( file ){
+ return path.relative(process.cwd(), file);
+});
+
+module.exports = function(config) {
+/*eslint-enable*/
+ config.set({
+
+ // base path that will be used to resolve all patterns (eg. files, exclude)
+ basePath: '',
+
+
+ // frameworks to use
+ // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+ frameworks: ['jasmine'],
+
+
+ // list of files / patterns to load in the browser
+ files: bowerComponents.concat([
+ '../../../xos/core/xoslib/static/js/vendor/ngXosVendor.js',
+ '../../../xos/core/xoslib/static/js/vendor/ngXosHelpers.js',
+ 'src/js/**/*.js',
+ 'spec/**/*.mock.js',
+ 'spec/**/*.test.js',
+ 'src/**/*.html'
+ ]),
+
+
+ // list of files to exclude
+ exclude: [
+ ],
+
+
+ // preprocess matching files before serving them to the browser
+ // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+ preprocessors: {
+ 'src/js/**/*.js': ['babel'],
+ 'spec/**/*.test.js': ['babel'],
+ 'src/**/*.html': ['ng-html2js']
+ },
+
+ ngHtml2JsPreprocessor: {
+ stripPrefix: 'src/', //strip the src path from template url (http://stackoverflow.com/questions/22869668/karma-unexpected-request-when-testing-angular-directive-even-with-ng-html2js)
+ moduleName: 'templates' // define the template module name
+ },
+
+ // test results reporter to use
+ // possible values: 'dots', 'progress'
+ // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+ reporters: ['mocha'],
+
+
+ // web server port
+ port: 9876,
+
+
+ // enable / disable colors in the output (reporters and logs)
+ colors: true,
+
+
+ // level of logging
+ // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+ logLevel: config.LOG_INFO,
+
+
+ // enable / disable watching file and executing tests whenever any file changes
+ autoWatch: true,
+
+
+ // start these browsers
+ // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+ browsers: ['PhantomJS'],
+
+
+ // Continuous Integration mode
+ // if true, Karma captures browsers, runs the tests and exits
+ singleRun: false
+ });
+};
diff --git a/views/ngXosViews/synchronizerNotifier/package.json b/views/ngXosViews/synchronizerNotifier/package.json
new file mode 100644
index 0000000..ad76efe
--- /dev/null
+++ b/views/ngXosViews/synchronizerNotifier/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "xos-synchronizerNotifier",
+ "version": "1.0.0",
+ "description": "Angular Application for XOS, created with generator-xos",
+ "scripts": {
+ "prestart": "npm install && bower install",
+ "start": "gulp serve",
+ "prebuild": "npm install && bower install",
+ "build": "gulp",
+ "test": "karma start",
+ "test:ci": "karma start --single-run",
+ "lint": "eslint src/js/"
+ },
+ "keywords": [
+ "XOS",
+ "Angular",
+ "XOSlib"
+ ],
+ "author": "Matteo Scandolo",
+ "license": "MIT",
+ "dependencies": {},
+ "devDependencies": {
+ "autoprefixer": "^6.3.3",
+ "browser-sync": "^2.9.11",
+ "css-mqpacker": "^4.0.0",
+ "csswring": "^4.2.1",
+ "del": "^2.0.2",
+ "easy-mocker": "^1.2.0",
+ "eslint": "^1.8.0",
+ "eslint-plugin-angular": "linkmesrl/eslint-plugin-angular",
+ "gulp": "^3.9.0",
+ "gulp-angular-filesort": "^1.1.1",
+ "gulp-angular-templatecache": "^1.8.0",
+ "gulp-babel": "^5.3.0",
+ "gulp-concat": "^2.6.0",
+ "gulp-concat-util": "^0.5.5",
+ "gulp-eslint": "^1.0.0",
+ "gulp-inject": "^3.0.0",
+ "gulp-minify-html": "^1.0.4",
+ "gulp-ng-annotate": "^1.1.0",
+ "gulp-postcss": "^6.0.1",
+ "gulp-rename": "^1.2.2",
+ "gulp-replace": "^0.5.4",
+ "gulp-sass": "^2.2.0",
+ "gulp-uglify": "^1.4.2",
+ "http-proxy": "^1.12.0",
+ "ink-docstrap": "^0.5.2",
+ "jasmine-core": "~2.3.4",
+ "karma": "^0.13.14",
+ "karma-babel-preprocessor": "~5.2.2",
+ "karma-coverage": "^0.5.3",
+ "karma-jasmine": "~0.3.6",
+ "karma-mocha-reporter": "~1.1.1",
+ "karma-ng-html2js-preprocessor": "^0.2.0",
+ "karma-phantomjs-launcher": "~0.2.1",
+ "lodash": "^3.10.1",
+ "phantomjs": "^1.9.19",
+ "proxy-middleware": "^0.15.0",
+ "run-sequence": "^1.1.4",
+ "wiredep": "^3.0.0-beta",
+ "wrench": "^1.5.8"
+ }
+}
diff --git a/views/ngXosViews/synchronizerNotifier/spec/notification.test.js b/views/ngXosViews/synchronizerNotifier/spec/notification.test.js
new file mode 100644
index 0000000..265b7b9
--- /dev/null
+++ b/views/ngXosViews/synchronizerNotifier/spec/notification.test.js
@@ -0,0 +1,76 @@
+'use strict';
+
+describe('The Synchronizer Notification Panel', () => {
+
+ var scope, element, isolatedScope, XosUserPrefs;
+ const xosNotification = {
+ notify: jasmine.createSpy('notify')
+ };
+
+ const failureEvent = {
+ name: 'test',
+ status: false
+ };
+
+ const successEvent = {
+ name: 'test',
+ status: true
+ };
+
+ beforeEach(module('xos.synchronizerNotifier', ($provide) => {
+ $provide.value('Diag', {
+ start: () => null
+ });
+
+ $provide.value('xosNotification', xosNotification);
+ }));
+ beforeEach(module('templates'));
+
+ beforeEach(inject(function($compile, $rootScope, _XosUserPrefs_){
+
+ XosUserPrefs = _XosUserPrefs_;
+ scope = $rootScope.$new();
+ element = angular.element('<sync-status></sync-status>');
+ $compile(element)(scope);
+ scope.$digest();
+ isolatedScope = element.isolateScope().vm;
+ }));
+
+ describe('when an event is received', () => {
+
+ beforeEach(() => {
+ xosNotification.notify.calls.reset()
+ });
+
+ describe('and notification have not been sent', () => {
+
+ beforeEach(() => {
+ XosUserPrefs.setSynchronizerNotificationStatus('test', false);
+ scope.$emit('diag', failureEvent);
+ });
+
+ it('should trigger notification', () => {
+ expect(xosNotification.notify).toHaveBeenCalled();
+ });
+
+ it('should update status in the scope', () => {
+ expect(isolatedScope.synchronizers.test).toEqual(failureEvent);
+ scope.$emit('diag', successEvent);
+ expect(isolatedScope.synchronizers.test).toEqual(successEvent);
+ });
+ });
+
+ describe('and notification have been sent', () => {
+
+ beforeEach(() => {
+ XosUserPrefs.setSynchronizerNotificationStatus('test', true);
+ scope.$emit('diag', failureEvent);
+ });
+
+ it('should not trigger multiple notification for the same synchronizer', () => {
+ expect(xosNotification.notify).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+});
\ No newline at end of file
diff --git a/views/ngXosViews/synchronizerNotifier/src/css/main.css b/views/ngXosViews/synchronizerNotifier/src/css/main.css
new file mode 100644
index 0000000..88a08e7
--- /dev/null
+++ b/views/ngXosViews/synchronizerNotifier/src/css/main.css
@@ -0,0 +1,14 @@
+#xosSynchronizerNotifier {
+ float: left; }
+ #xosSynchronizerNotifier .alert {
+ margin-bottom: 0px !important; }
+ #xosSynchronizerNotifier .sync-status-container {
+ position: relative;
+ z-index: 200; }
+ #xosSynchronizerNotifier .notification-panel {
+ position: absolute;
+ width: 200px; }
+ #xosSynchronizerNotifier sync-status .badge.success {
+ background-color: #5cb85c; }
+ #xosSynchronizerNotifier sync-status .badge.warning {
+ background-color: #f0ad4e; }
diff --git a/views/ngXosViews/synchronizerNotifier/src/index.html b/views/ngXosViews/synchronizerNotifier/src/index.html
new file mode 100644
index 0000000..cb9b7b5
--- /dev/null
+++ b/views/ngXosViews/synchronizerNotifier/src/index.html
@@ -0,0 +1,35 @@
+<!-- browserSync -->
+<!-- bower:css -->
+<link rel="stylesheet" href="vendor/bootstrap-css/css/bootstrap.min.css" />
+<link rel="stylesheet" href="vendor/angular-chart.js/dist/angular-chart.css" />
+<!-- endbower -->
+<!-- endcss -->
+<!-- inject:css -->
+<link rel="stylesheet" href="/css/main.css">
+<link rel="stylesheet" href="/../../../xos/core/static/xosNgLib.css">
+<!-- endinject -->
+
+<div id="xosSynchronizerNotifier">
+ <sync-status></sync-status>
+</div>
+
+<!-- bower:js -->
+<script src="vendor/jquery/dist/jquery.js"></script>
+<script src="vendor/angular/angular.js"></script>
+<script src="vendor/angular-mocks/angular-mocks.js"></script>
+<script src="vendor/angular-ui-router/release/angular-ui-router.js"></script>
+<script src="vendor/angular-cookies/angular-cookies.js"></script>
+<script src="vendor/angular-animate/angular-animate.js"></script>
+<script src="vendor/angular-resource/angular-resource.js"></script>
+<script src="vendor/lodash/lodash.js"></script>
+<script src="vendor/bootstrap-css/js/bootstrap.min.js"></script>
+<script src="vendor/Chart.js/Chart.js"></script>
+<script src="vendor/angular-chart.js/dist/angular-chart.js"></script>
+<script src="vendor/d3/d3.js"></script>
+<script src="vendor/angular-recursion/angular-recursion.js"></script>
+<!-- endbower -->
+<!-- endjs -->
+<!-- inject:js -->
+<script src="/../../../xos/core/xoslib/static/js/vendor/ngXosHelpers.js"></script>
+<script src="/.tmp/main.js"></script>
+<!-- endinject -->
\ No newline at end of file
diff --git a/views/ngXosViews/synchronizerNotifier/src/js/main.js b/views/ngXosViews/synchronizerNotifier/src/js/main.js
new file mode 100644
index 0000000..e761fad
--- /dev/null
+++ b/views/ngXosViews/synchronizerNotifier/src/js/main.js
@@ -0,0 +1,131 @@
+'use strict';
+
+angular.module('xos.synchronizerNotifier', [
+ 'ngResource',
+ 'ngCookies',
+ 'ui.router',
+ 'xos.helpers'
+])
+.service('Diag', function($rootScope, $http, $q, $interval){
+
+ let isRunning = false;
+
+ this.getDiags = () => {
+ let d = $q.defer();
+ $http.get('/api/core/diags')
+ .then(res => {
+ d.resolve(res.data);
+ })
+ .catch(err => {
+ d.reject(err);
+ });
+
+ return d.promise;
+ };
+
+ this.sendEvents = (diags) => {
+ diags.forEach(d => {
+ let status = JSON.parse(d.backend_register);
+ status.last_run = new Date(status.last_run * 1000);
+ status.last_duration = status.last_duration * 1000;
+ status.last_synchronizer_start = new Date(status.last_synchronizer_start * 1000);
+ status.last_syncrecord_start = status.last_syncrecord_start ? new Date(status.last_syncrecord_start * 1000) : null;
+ $rootScope.$broadcast(`diag`, {
+ name: d.name,
+ updated: d.updated,
+ info: status,
+ status: this.getSyncStatus(status)
+ });
+ });
+ };
+
+ this.start = () => {
+ isRunning = true;
+ this.getDiags()
+ .then(diags => {
+ this.sendEvents(diags);
+ });
+ return isRunning;
+ };
+
+ this.stop = () => {
+ isRunning = false;
+ return isRunning;
+ };
+
+ this.getSyncStatus = (status) => {
+
+ const now = new Date();
+ const gap = 15 * 60 * 1000; /* ms */
+ // const gap = 1 * 60 * 1000; // for demo use 1 minute
+ // if all of this values are older than 15 min,
+ // probably something is wrong
+ if (
+ (now - status.last_synchronizer_start) > gap &&
+ (now - status.last_syncrecord_start) > gap &&
+ (now - status.last_run) > gap
+ ){
+ return false;
+ }
+ else{
+ return true;
+ }
+ }
+
+ $interval(() => {
+ if(isRunning){
+ this.getDiags()
+ .then(diags => {
+ this.sendEvents(diags);
+ });
+ }
+ }, 5 * 60 * 1000);
+})
+.directive('syncStatus', function() {
+ return {
+ restrict: 'E',
+ scope: {},
+ bindToController: true,
+ controllerAs: 'vm',
+ templateUrl: 'templates/sync-status.tpl.html',
+ controller: function($log, $rootScope, Diag, xosNotification, XosUserPrefs){
+ Diag.start();
+ // to debug set this to true,
+ // the panel will be opened by default
+ // this.showNotificationPanel = true;
+ this.synchronizers = {};
+
+ this.showNoSync = true;
+
+ $rootScope.$on('diag', (e, d) => {
+ this.synchronizers[d.name] = d;
+
+ // if errored
+ if(!d.status){
+ // and not already notified
+ if(!XosUserPrefs.getSynchronizerNotificationStatus(d.name)){
+ xosNotification.notify('CORD Synchronizer', {
+ icon: '/static/cord-logo.png',
+ body: `The ${d.name} synchronizer has not performed actions in the last 15 minutes.`
+ });
+ }
+ XosUserPrefs.setSynchronizerNotificationStatus(d.name, true);
+ }
+ else {
+ XosUserPrefs.setSynchronizerNotificationStatus(d.name, false);
+ }
+
+ // hide list if empty
+ this.showNoSync = false;
+ if(Object.keys(this.synchronizers).length === 0){
+ this.showNoSync = true;
+ }
+ });
+
+ }
+ }
+});
+
+angular.element(document).ready(function() {
+ angular.bootstrap('#xosSynchronizerNotifier', ['xos.synchronizerNotifier']);
+});
\ No newline at end of file
diff --git a/views/ngXosViews/synchronizerNotifier/src/sass/main.scss b/views/ngXosViews/synchronizerNotifier/src/sass/main.scss
new file mode 100644
index 0000000..d101d76
--- /dev/null
+++ b/views/ngXosViews/synchronizerNotifier/src/sass/main.scss
@@ -0,0 +1,32 @@
+@import '../../../../style/sass/lib/_variables.scss';
+
+#xosSynchronizerNotifier {
+
+ float: left;
+
+ .alert {
+ margin-bottom: 0px !important;
+ }
+
+ .sync-status-container {
+ position: relative;
+ z-index: 200;
+ }
+
+ .notification-panel {
+ position: absolute;
+ width: 200px;
+ }
+
+ sync-status {
+ .badge {
+ &.success {
+ background-color: $brand-success;
+ }
+
+ &.warning {
+ background-color: $brand-warning;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/views/ngXosViews/synchronizerNotifier/src/templates/sync-status.tpl.html b/views/ngXosViews/synchronizerNotifier/src/templates/sync-status.tpl.html
new file mode 100644
index 0000000..cbd794e
--- /dev/null
+++ b/views/ngXosViews/synchronizerNotifier/src/templates/sync-status.tpl.html
@@ -0,0 +1,21 @@
+<div class="sync-status-container">
+ <div class="btn btn-default" ng-click="vm.showNotificationPanel = !vm.showNotificationPanel">
+ <i class="glyphicon glyphicon-inbox"></i>
+ </div>
+ <div class="notification-panel panel panel-default" ng-show="vm.showNotificationPanel">
+ <ul class="list-group" ng-show="!vm.showNoSync">
+ <li class="list-group-item" ng-repeat="(syncName, syncStatus) in vm.synchronizers">
+ <span class="badge" ng-class="{success: syncStatus.status, warning: !syncStatus.status}">
+ <span ng-show="syncStatus.status"><i class="glyphicon glyphicon-ok"></i></span>
+ <span ng-hide="syncStatus.status"><i class="glyphicon glyphicon-time"></i></span>
+ </span>
+ <b>{{syncName}}</b>
+ <br/>
+ <small><i>{{syncStatus.info.last_run | date:'mediumTime'}}</i></small>
+ </li>
+ </ul>
+ <div class="alert alert-info" ng-show="vm.showNoSync">
+ No syncronizers are running.
+ </div>
+ </div>
+</div>
diff --git a/xos/api/utility/synchronizer.py b/xos/api/utility/synchronizer.py
new file mode 100644
index 0000000..84bae00
--- /dev/null
+++ b/xos/api/utility/synchronizer.py
@@ -0,0 +1,61 @@
+from rest_framework.decorators import api_view
+from rest_framework.response import Response
+from rest_framework.reverse import reverse
+from rest_framework import serializers
+from rest_framework import generics
+from rest_framework.exceptions import APIException
+from core.models import *
+from django.forms import widgets
+from core.xoslib.objects.sliceplus import SlicePlus
+from xos.apibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView, XOSPermissionDenied
+import json
+from core.models import Slice, SlicePrivilege, SliceRole, Instance, Site, Node, User
+from operator import itemgetter, attrgetter
+from api.xosapi_helpers import PlusObjectMixin, PlusModelSerializer
+
+IdField = serializers.ReadOnlyField
+
+
+class SynchronizerSerializer(PlusModelSerializer):
+ id = IdField()
+
+ name = serializers.CharField(required=False)
+
+ class Meta:
+ model = Diag
+ fields = ('id', 'name', 'backend_status', 'backend_register')
+
+
+class SynchronizerList(XOSListCreateAPIView):
+ queryset = Diag.objects.all()
+ serializer_class = SynchronizerSerializer
+
+ method_kind = "list"
+ method_name = "synchronizer"
+
+ def get_queryset(self):
+ name = self.request.query_params.get('name', False)
+
+ if (not self.request.user.is_authenticated()):
+ raise XOSPermissionDenied("You must be authenticated in order to use this API")
+ if(name):
+ return Diag.objects.filter(name=name)
+ return Diag.objects.all()
+
+
+class SynchronizerDetail(XOSRetrieveUpdateDestroyAPIView):
+ queryset = Diag.objects.all()
+ serializer_class = SynchronizerSerializer
+
+ method_kind = "detail"
+ method_name = "synchronizer"
+
+ def get_queryset(self):
+ if (not self.request.user.is_authenticated()):
+ raise XOSPermissionDenied("You must be authenticated in order to use this API")
+
+ print "kwargs"
+ print self.request
+ print self.kwargs['pk']
+
+ return Diag.objects.filter(id=self.kwargs['pk'])
diff --git a/xos/core/admin.py b/xos/core/admin.py
index 6d16fc9..200bb5c 100644
--- a/xos/core/admin.py
+++ b/xos/core/admin.py
@@ -2392,6 +2392,15 @@
def has_add_permission(self, request):
return False
+class DiagAdmin(XOSBaseAdmin):
+ list_display = ("name", "backend_status", "backend_register")
+ list_display_links = ('name',)
+
+ fieldsets = [
+ (None, {'fields': ['name', 'backend_status', 'backend_register'],
+ 'classes':['suit-tab suit-tab-general']}),
+ ]
+
# Now register the new UserAdmin...
admin.site.register(User, UserAdmin)
# ... and, since we're not using Django's builtin permissions,
@@ -2436,3 +2445,4 @@
admin.site.register(TenantRole, TenantRoleAdmin)
admin.site.register(TenantAttribute, TenantAttributeAdmin)
admin.site.register(AddressPool, AddressPoolAdmin)
+ admin.site.register(Diag, DiagAdmin)
diff --git a/xos/core/models/plcorebase.py b/xos/core/models/plcorebase.py
index 6be949d..b84b526 100644
--- a/xos/core/models/plcorebase.py
+++ b/xos/core/models/plcorebase.py
@@ -204,7 +204,7 @@
policed = models.DateTimeField(null=True, blank=True, default=None)
# This is a scratchpad used by the Observer
- backend_register = models.CharField(max_length=140,
+ backend_register = models.CharField(max_length=1024,
default="{}", null=True)
backend_status = models.CharField(max_length=1024,
diff --git a/xos/core/templatetags/core_tags.py b/xos/core/templatetags/core_tags.py
index 6caf034..a17be08 100644
--- a/xos/core/templatetags/core_tags.py
+++ b/xos/core/templatetags/core_tags.py
@@ -2,9 +2,11 @@
# import sys
from core.models import DashboardView
from itertools import chain
+from core.dashboard.views.home import DashboardDynamicView
register = template.Library()
+
@register.inclusion_tag('admin/tags/dashboard_list.html', takes_context=True)
def dashboard_list(context):
request = context['request']
@@ -14,4 +16,10 @@
result_list = list(chain(dashboards, customize))
else:
result_list = []
- return {'dashboards': result_list, 'path': request.path}
\ No newline at end of file
+ return {'dashboards': result_list, 'path': request.path}
+
+
+@register.inclusion_tag('admin/tags/notification.html', takes_context=True)
+def notification(context):
+ template = DashboardDynamicView.readTemplate(DashboardDynamicView(), "xosSynchronizerNotifier")
+ return {'template': template}
diff --git a/xos/core/views/observer.py b/xos/core/views/observer.py
index 77f1d1b..ff3ee28 100644
--- a/xos/core/views/observer.py
+++ b/xos/core/views/observer.py
@@ -9,15 +9,17 @@
try:
observer_name = Config().observer_name
except AttributeError:
- observer_name = ''
+ observer_name = 'openstack'
diag = Diag.objects.filter(name=observer_name).first()
if not diag:
return HttpResponse(json.dumps({"health": ":-X", "time": time.time(), "comp": 0}))
t = time.time()
+
d = json.loads(diag.backend_register)
+
comp = d['last_run'] + d['last_duration']*2 + 300
if comp>t:
d['health'] = ':-)'
diff --git a/xos/core/xoslib/dashboards/xosSynchronizerNotifier.html b/xos/core/xoslib/dashboards/xosSynchronizerNotifier.html
new file mode 100644
index 0000000..ef69861
--- /dev/null
+++ b/xos/core/xoslib/dashboards/xosSynchronizerNotifier.html
@@ -0,0 +1,16 @@
+<!-- browserSync -->
+
+<!-- endcss -->
+<!-- inject:css -->
+<link rel="stylesheet" href="/static/css/xosSynchronizerNotifier.css">
+<!-- endinject -->
+
+<div id="xosSynchronizerNotifier">
+ <sync-status></sync-status>
+</div>
+
+
+<!-- endjs -->
+<!-- inject:js -->
+<script src="/static/js/xosSynchronizerNotifier.js"></script>
+<!-- endinject -->
\ No newline at end of file
diff --git a/xos/core/xoslib/static/css/xosSynchronizerNotifier.css b/xos/core/xoslib/static/css/xosSynchronizerNotifier.css
new file mode 100644
index 0000000..89374fb
--- /dev/null
+++ b/xos/core/xoslib/static/css/xosSynchronizerNotifier.css
@@ -0,0 +1 @@
+#xosSynchronizerNotifier{float:left}#xosSynchronizerNotifier .alert{margin-bottom:0!important}#xosSynchronizerNotifier .sync-status-container{position:relative;z-index:200}#xosSynchronizerNotifier .notification-panel{position:absolute;width:200px}#xosSynchronizerNotifier sync-status .badge.success{background-color:#5cb85c}#xosSynchronizerNotifier sync-status .badge.warning{background-color:#f0ad4e}
\ No newline at end of file
diff --git a/xos/core/xoslib/static/js/vendor/ngXosHelpers.js b/xos/core/xoslib/static/js/vendor/ngXosHelpers.js
index 070ac59..4af4da5 100644
--- a/xos/core/xoslib/static/js/vendor/ngXosHelpers.js
+++ b/xos/core/xoslib/static/js/vendor/ngXosHelpers.js
@@ -1 +1 @@
-"use strict";!function(){angular.module("xos.uiComponents",["chart.js","RecursionHelper"])}();var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol?"symbol":typeof e};!function(){angular.module("xos.uiComponents").directive("xosSmartTable",function(){return{restrict:"E",scope:{config:"="},template:'\n <div class="row" ng-show="vm.data.length > 0">\n <div class="col-xs-12 text-right">\n <a href="" class="btn btn-success" ng-click="vm.createItem()">\n Add\n </a>\n </div>\n </div>\n <div class="row">\n <div class="col-xs-12 table-responsive">\n <xos-table config="vm.tableConfig" data="vm.data"></xos-table>\n </div>\n </div>\n <div class="panel panel-default" ng-show="vm.detailedItem">\n <div class="panel-heading">\n <div class="row">\n <div class="col-xs-11">\n <h3 class="panel-title" ng-show="vm.detailedItem.id">Update {{vm.config.resource}} {{vm.detailedItem.id}}</h3>\n <h3 class="panel-title" ng-show="!vm.detailedItem.id">Create {{vm.config.resource}} item</h3>\n </div>\n <div class="col-xs-1">\n <a href="" ng-click="vm.cleanForm()">\n <i class="glyphicon glyphicon-remove pull-right"></i>\n </a>\n </div>\n </div>\n </div>\n <div class="panel-body">\n <xos-form config="vm.formConfig" ng-model="vm.detailedItem"></xos-form>\n </div>\n </div>\n <xos-alert config="{type: \'success\', closeBtn: true}" show="vm.responseMsg">{{vm.responseMsg}}</xos-alert>\n <xos-alert config="{type: \'danger\', closeBtn: true}" show="vm.responseErr">{{vm.responseErr}}</xos-alert>\n ',bindToController:!0,controllerAs:"vm",controller:["$injector","LabelFormatter","_","XosFormHelpers",function(e,n,o,t){var r=this;this.responseMsg=!1,this.responseErr=!1,this.tableConfig={columns:[],actions:[{label:"delete",icon:"remove",cb:function(e){r.Resource["delete"]({id:e.id}).$promise.then(function(){o.remove(r.data,function(n){return n.id===e.id}),r.responseMsg=r.config.resource+" with id "+e.id+" successfully deleted"})["catch"](function(n){r.responseErr=n.data.detail||"Error while deleting "+r.config.resource+" with id "+e.id})},color:"red"},{label:"details",icon:"search",cb:function(e){r.detailedItem=e}}],classes:"table table-striped table-bordered table-responsive",filter:"field",order:!0,pagination:{pageSize:10}},this.formConfig={exclude:this.config.hiddenFields,fields:{},formName:this.config.resource+"Form",actions:[{label:"Save",icon:"ok",cb:function(e){var n=void 0,o=!0;e.id?(n=e.$update(),o=!1):n=e.$save(),n.then(function(n){o&&r.data.push(angular.copy(n)),delete r.detailedItem,r.responseMsg=r.config.resource+" with id "+e.id+" successfully saved"})["catch"](function(n){r.responseErr=n.data.detail||"Error while saving "+r.config.resource+" with id "+e.id})},"class":"success"}]},this.cleanForm=function(){delete r.detailedItem},this.createItem=function(){r.detailedItem=new r.Resource},this.Resource=e.get(this.config.resource);var i=function(){r.Resource.query().$promise.then(function(e){if(e[0]){var i=e[0],a=Object.keys(i);o.remove(a,function(e){return"id"==e||"validators"==e}),angular.isArray(r.config.hiddenFields)&&(a=o.difference(a,r.config.hiddenFields));var s=a.map(function(e){return n.format(e)});a.forEach(function(e,n){var o={label:s[n],prop:e};"string"!=typeof i[e]&&"undefined"!=typeof i[e]&&(o.type=_typeof(i[e])),r.tableConfig.columns.push(o)}),a.forEach(function(e,o){r.formConfig.fields[e]={label:n.format(s[o]).replace(":",""),type:t._getFieldFormat(i[e])}}),r.data=e}})};i()}]}})}(),function(){angular.module("xos.uiComponents").directive("xosSmartPie",function(){return{restrict:"E",scope:{config:"="},template:'\n <canvas\n class="chart chart-pie {{vm.config.classes}}"\n chart-data="vm.data" chart-labels="vm.labels"\n chart-legend="{{vm.config.legend}}">\n </canvas>\n ',bindToController:!0,controllerAs:"vm",controller:["$injector","$interval","$scope","$timeout","_",function(e,n,o,t,r){var i=this;if(!this.config.resource&&!this.config.data)throw new Error("[xosSmartPie] Please provide a resource or an array of data in the configuration");var a=function(e){return r.groupBy(e,i.config.groupBy)},s=function(e){return r.reduce(Object.keys(e),function(n,o){return n.concat(e[o].length)},[])},l=function(e){return angular.isFunction(i.config.labelFormatter)?i.config.labelFormatter(Object.keys(e)):Object.keys(e)},c=function(e){var n=a(e);i.data=s(n),i.labels=l(n)};this.config.resource?!function(){i.Resource=e.get(i.config.resource);var o=function(){i.Resource.query().$promise.then(function(e){e[0]&&c(e)})};o(),i.config.poll&&n(function(){o()},1e3*i.config.poll)}():o.$watch(function(){return i.config.data},function(e){e&&c(i.config.data)},!0),o.$on("create",function(e,n){console.log("create: "+n.id)}),o.$on("destroy",function(e,n){console.log("destroy: "+n.id)})}]}})}(),function(){angular.module("xos.uiComponents").directive("xosValidation",function(){return{restrict:"E",scope:{field:"=",form:"="},template:'\n <div ng-cloak>\n <xos-alert config="vm.config" show="vm.field.$error.required !== undefined && vm.field.$error.required !== false && (vm.field.$touched || vm.form.$submitted)">\n Field required\n </xos-alert>\n <xos-alert config="vm.config" show="vm.field.$error.email !== undefined && vm.field.$error.email !== false && (vm.field.$touched || vm.form.$submitted)">\n This is not a valid email\n </xos-alert>\n <xos-alert config="vm.config" show="vm.field.$error.minlength !== undefined && vm.field.$error.minlength !== false && (vm.field.$touched || vm.form.$submitted)">\n Too short\n </xos-alert>\n <xos-alert config="vm.config" show="vm.field.$error.maxlength !== undefined && vm.field.$error.maxlength !== false && (vm.field.$touched || vm.form.$submitted)">\n Too long\n </xos-alert>\n <xos-alert config="vm.config" show="vm.field.$error.custom !== undefined && vm.field.$error.custom !== false && (vm.field.$touched || vm.form.$submitted)">\n Field invalid\n </xos-alert>\n </div>\n ',transclude:!0,bindToController:!0,controllerAs:"vm",controller:function(){this.config={type:"danger"}}}})}(),function(){angular.module("xos.uiComponents").directive("xosTable",function(){return{restrict:"E",scope:{data:"=",config:"="},template:'\n <div ng-show="vm.data.length > 0">\n <div class="row" ng-if="vm.config.filter == \'fulltext\'">\n <div class="col-xs-12">\n <input\n class="form-control"\n placeholder="Type to search.."\n type="text"\n ng-model="vm.query"/>\n </div>\n </div>\n <table ng-class="vm.classes" ng-hide="vm.data.length == 0">\n <thead>\n <tr>\n <th ng-repeat="col in vm.columns">\n {{col.label}}\n <span ng-if="vm.config.order">\n <a href="" ng-click="vm.orderBy = col.prop; vm.reverse = false">\n <i class="glyphicon glyphicon-chevron-up"></i>\n </a>\n <a href="" ng-click="vm.orderBy = col.prop; vm.reverse = true">\n <i class="glyphicon glyphicon-chevron-down"></i>\n </a>\n </span>\n </th>\n <th ng-if="vm.config.actions">Actions:</th>\n </tr>\n </thead>\n <tbody ng-if="vm.config.filter == \'field\'">\n <tr>\n <td ng-repeat="col in vm.columns">\n <input\n ng-if="col.type !== \'boolean\'"\n class="form-control"\n placeholder="Type to search by {{col.label}}"\n type="text"\n ng-model="vm.query[col.prop]"/>\n <select\n ng-if="col.type === \'boolean\'"\n class="form-control"\n ng-model="vm.query[col.prop]">\n <option value="">-</option>\n <option value="true">True</option>\n <option value="false">False</option>\n </select>\n </td>\n <td ng-if="vm.config.actions"></td>\n </tr>\n </tbody>\n <tbody>\n <tr ng-repeat="item in vm.data | filter:vm.query | orderBy:vm.orderBy:vm.reverse | pagination:vm.currentPage * vm.config.pagination.pageSize | limitTo: (vm.config.pagination.pageSize || vm.data.length) track by $index">\n <td ng-repeat="col in vm.columns" link-wrapper>\n <span ng-if="!col.type">{{item[col.prop]}}</span>\n <span ng-if="col.type === \'boolean\'">\n <i class="glyphicon"\n ng-class="{\'glyphicon-ok\': item[col.prop], \'glyphicon-remove\': !item[col.prop]}">\n </i>\n </span>\n <span ng-if="col.type === \'date\'">\n {{item[col.prop] | date:\'H:mm MMM d, yyyy\'}}\n </span>\n <span ng-if="col.type === \'array\'">\n {{item[col.prop] | arrayToList}}\n </span>\n <span ng-if="col.type === \'object\'">\n <dl class="dl-horizontal">\n <span ng-repeat="(k,v) in item[col.prop]">\n <dt>{{k}}</dt>\n <dd>{{v}}</dd>\n </span>\n </dl>\n </span>\n <span ng-if="col.type === \'custom\'">\n {{col.formatter(item)}}\n </span>\n <span ng-if="col.type === \'icon\'">\n <i class="glyphicon glyphicon-{{col.formatter(item)}}">\n </i>\n </span>\n </td>\n <td ng-if="vm.config.actions">\n <a href=""\n ng-repeat="action in vm.config.actions"\n ng-click="action.cb(item)"\n title="{{action.label}}">\n <i\n class="glyphicon glyphicon-{{action.icon}}"\n style="color: {{action.color}};"></i>\n </a>\n </td>\n </tr>\n </tbody>\n </table>\n <xos-pagination\n ng-if="vm.config.pagination"\n page-size="vm.config.pagination.pageSize"\n total-elements="vm.data.length"\n change="vm.goToPage">\n </xos-pagination>\n </div>\n <div ng-show="vm.data.length == 0 || !vm.data">\n <xos-alert config="{type: \'info\'}">\n No data to show.\n </xos-alert>\n </div>\n ',bindToController:!0,controllerAs:"vm",controller:["_",function(e){var n=this;if(!this.config)throw new Error('[xosTable] Please provide a configuration via the "config" attribute');if(!this.config.columns)throw new Error("[xosTable] Please provide a columns list in the configuration");this.config.order&&angular.isObject(this.config.order)&&(this.reverse=this.config.order.reverse||!1,this.orderBy=this.config.order.field||"id");var o=e.filter(this.config.columns,{type:"custom"});angular.isArray(o)&&o.length>0&&e.forEach(o,function(e){if(!e.formatter||!angular.isFunction(e.formatter))throw new Error("[xosTable] You have provided a custom field type, a formatter function should provided too.")});var t=e.filter(this.config.columns,{type:"icon"});angular.isArray(t)&&t.length>0&&e.forEach(t,function(e){if(!e.formatter||!angular.isFunction(e.formatter))throw new Error("[xosTable] You have provided an icon field type, a formatter function should provided too.")});var r=e.filter(this.config.columns,function(e){return angular.isDefined(e.link)});angular.isArray(r)&&r.length>0&&e.forEach(r,function(e){if(!angular.isFunction(e.link))throw new Error("[xosTable] The link property should be a function.")}),this.columns=this.config.columns,this.classes=this.config.classes||"table table-striped table-bordered",this.config.actions,this.config.pagination&&(this.currentPage=0,this.goToPage=function(e){n.currentPage=e})}]}}).filter("arrayToList",function(){return function(e){return angular.isArray(e)?e.join(", "):e}}).directive("linkWrapper",function(){return{restrict:"A",transclude:!0,template:'\n <a ng-if="col.link" href="{{col.link(item)}}">\n <div ng-transclude></div>\n </a>\n <div ng-transclude ng-if="!col.link"></div>\n '}})}(),function(){angular.module("xos.uiComponents").directive("xosPagination",function(){return{restrict:"E",scope:{pageSize:"=",totalElements:"=",change:"="},template:'\n <div class="row" ng-if="vm.pageList.length > 1">\n <div class="col-xs-12 text-center">\n <ul class="pagination">\n <li\n ng-click="vm.goToPage(vm.currentPage - 1)"\n ng-class="{disabled: vm.currentPage == 0}">\n <a href="" aria-label="Previous">\n <span aria-hidden="true">«</span>\n </a>\n </li>\n <li ng-repeat="i in vm.pageList" ng-class="{active: i === vm.currentPage}">\n <a href="" ng-click="vm.goToPage(i)">{{i + 1}}</a>\n </li>\n <li\n ng-click="vm.goToPage(vm.currentPage + 1)"\n ng-class="{disabled: vm.currentPage == vm.pages - 1}">\n <a href="" aria-label="Next">\n <span aria-hidden="true">»</span>\n </a>\n </li>\n </ul>\n </div>\n </div>\n ',bindToController:!0,controllerAs:"vm",controller:["$scope",function(e){var n=this;this.currentPage=0,this.goToPage=function(e){0>e||e===n.pages||(n.currentPage=e,n.change(e))},this.createPages=function(e){for(var n=[],o=0;e>o;o++)n.push(o);return n},e.$watch(function(){return n.totalElements},function(){n.totalElements&&(n.pages=Math.ceil(n.totalElements/n.pageSize),n.pageList=n.createPages(n.pages))})}]}}).filter("pagination",function(){return function(e,n){return e&&angular.isArray(e)?(n=parseInt(n,10),e.slice(n)):e}})}(),function(){angular.module("xos.uiComponents").directive("xosForm",function(){return{restrict:"E",scope:{config:"=",ngModel:"="},template:'\n <ng-form name="vm.{{vm.config.formName || \'form\'}}">\n <div class="form-group" ng-repeat="(name, field) in vm.formField">\n <xos-field name="name" field="field" ng-model="vm.ngModel[name]"></xos-field>\n <xos-validation errors="vm[vm.config.formName || \'form\'][name].$error"></xos-validation>\n </div>\n <div class="form-group" ng-if="vm.config.actions">\n <button role="button" href=""\n ng-repeat="action in vm.config.actions"\n ng-click="action.cb(vm.ngModel)"\n class="btn btn-{{action.class}}"\n title="{{action.label}}">\n <i class="glyphicon glyphicon-{{action.icon}}"></i>\n {{action.label}}\n </button>\n </div>\n </ng-form>\n ',bindToController:!0,controllerAs:"vm",controller:["$scope","$log","_","XosFormHelpers",function(e,n,o,t){var r=this;if(!this.config)throw new Error('[xosForm] Please provide a configuration via the "config" attribute');if(!this.config.actions)throw new Error("[xosForm] Please provide an action list in the configuration");this.excludedField=["id","validators","created","updated","deleted","backend_status"],this.config&&this.config.exclude&&(this.excludedField=this.excludedField.concat(this.config.exclude)),this.formField=[],e.$watch(function(){return r.ngModel},function(e){if(r.formField={},e){var n=o.difference(Object.keys(e),r.excludedField),i=t.parseModelField(n);r.formField=t.buildFormStructure(i,r.config.fields,e)}})}]}})}(),function(){angular.module("xos.uiComponents").directive("xosField",["RecursionHelper",function(e){return{restrict:"E",scope:{name:"=",field:"=",ngModel:"="},template:'\n <label ng-if="vm.field.type !== \'object\'">{{vm.field.label}}</label>\n <input\n ng-if="vm.field.type !== \'boolean\' && vm.field.type !== \'object\'"\n type="{{vm.field.type}}"\n name="{{vm.name}}"\n class="form-control"\n ng-model="vm.ngModel"\n ng-minlength="vm.field.validators.minlength || 0"\n ng-maxlength="vm.field.validators.maxlength || 2000"\n ng-required="vm.field.validators.required || false" />\n <span class="boolean-field" ng-if="vm.field.type === \'boolean\'">\n <button\n class="btn btn-success"\n ng-show="vm.ngModel"\n ng-click="vm.ngModel = false">\n <i class="glyphicon glyphicon-ok"></i>\n </button>\n <button\n class="btn btn-danger"\n ng-show="!vm.ngModel"\n ng-click="vm.ngModel = true">\n <i class="glyphicon glyphicon-remove"></i>\n </button>\n </span>\n <div\n class="panel panel-default object-field"\n ng-if="vm.field.type == \'object\' && !vm.isEmptyObject(vm.ngModel)"\n >\n <div class="panel-heading">{{vm.field.label}}</div>\n <div class="panel-body">\n <div ng-repeat="(k, v) in vm.ngModel">\n <xos-field\n name="k"\n field="{label: vm.formatLabel(k), type: vm.getType(v)}"\n ng-model="v">\n </xos-field>\n </div>\n </div>\n </div>\n ',bindToController:!0,controllerAs:"vm",compile:function(n){return e.compile(n)},controller:["$attrs","XosFormHelpers","LabelFormatter",function(e,n,o){if(!this.name)throw new Error("[xosField] Please provide a field name");if(!this.field)throw new Error("[xosField] Please provide a field definition");if(!e.ngModel)throw new Error("[xosField] Please provide an ng-model");this.getType=n._getFieldFormat,this.formatLabel=o.format,this.isEmptyObject=function(e){return 0===Object.keys(e).length}}]}}])}(),function(){angular.module("xos.uiComponents").directive("xosAlert",function(){return{restrict:"E",scope:{config:"=",show:"=?"},template:'\n <div ng-cloak class="alert alert-{{vm.config.type}}" ng-hide="!vm.show">\n <button type="button" class="close" ng-if="vm.config.closeBtn" ng-click="vm.dismiss()">\n <span aria-hidden="true">×</span>\n </button>\n <p ng-transclude></p>\n </div>\n ',transclude:!0,bindToController:!0,controllerAs:"vm",controller:["$timeout",function(e){var n=this;if(!this.config)throw new Error('[xosAlert] Please provide a configuration via the "config" attribute');this.show=this.show!==!1,this.dismiss=function(){n.show=!1},this.config.autoHide&&!function(){var o=e(function(){n.dismiss(),e.cancel(o)},n.config.autoHide)}()}]}})}(),function(){function e(){var e=function(e){return e.split("_").join(" ").trim()},n=function(e){return e.split(/(?=[A-Z])/).map(function(e){return e.toLowerCase()}).join(" ")},o=function(e){return e.slice(0,1).toUpperCase()+e.slice(1)},t=function(t){return t=e(t),t=n(t),t=o(t).replace(/\s\s+/g," ")+":",t.replace("::",":")};return{_formatByUnderscore:e,_formatByUppercase:n,_capitalize:o,format:t}}angular.module("xos.uiComponents").factory("LabelFormatter",e)}();var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol?"symbol":typeof e};!function(){angular.module("xos.uiComponents").service("XosFormHelpers",["_","LabelFormatter",function(e,n){var o=this;this._isEmail=function(e){var n=/(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/;return n.test(e)},this._getFieldFormat=function(n){return angular.isArray(n)?"array":e.isDate(n)||!Number.isNaN(Date.parse(n))&&new Date(n).getTime()>6311808e5?"date":"boolean"==typeof n?"boolean":o._isEmail(n)?"email":"string"==typeof n||null===n?"text":"undefined"==typeof n?"undefined":_typeof(n)},this.buildFormStructure=function(t,r,i){return t=Object.keys(t).length>0?t:r,r=r||{},e.reduce(Object.keys(t),function(e,t){return e[t]={label:r[t]&&r[t].label?r[t].label+":":n.format(t),type:r[t]&&r[t].type?r[t].type:o._getFieldFormat(i[t]),validators:r[t]&&r[t].validators?r[t].validators:{},hint:r[t]&&r[t].hint?r[t].hint:""},"date"===e[t].type&&(i[t]=new Date(i[t])),"number"===e[t].type&&(i[t]=parseInt(i[t],10)),e},{})},this.parseModelField=function(n){return e.reduce(n,function(e,n){return e[n]={},e},{})}}])}(),function(){function e(e,n,o){e.interceptors.push("SetCSRFToken"),n.startSymbol("{$"),n.endSymbol("$}"),o.defaults.stripTrailingSlashes=!1}e.$inject=["$httpProvider","$interpolateProvider","$resourceProvider"],angular.module("bugSnag",[]).factory("$exceptionHandler",function(){return function(e,n){window.Bugsnag?Bugsnag.notifyException(e,{diagnostics:{cause:n}}):console.error(e,n,e.stack)}}),angular.module("xos.helpers",["ngCookies","ngResource","ngAnimate","bugSnag","xos.uiComponents"]).config(e).factory("_",["$window",function(e){return e._}])}(),function(){angular.module("xos.helpers").service("vSG-Collection",["$resource",function(e){return e("/api/service/vsg/")}])}(),function(){angular.module("xos.helpers").service("vOLT-Collection",["$resource",function(e){return e("/api/tenant/cord/volt/:volt_id/",{volt_id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("Login",["$resource",function(e){return e("/api/utility/login/")}]).service("Logout",["$resource",function(e){return e("/api/utility/logout/")}])}(),function(){angular.module("xos.helpers").service("Users",["$resource",function(e){return e("/api/core/users/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("Truckroll",["$resource",function(e){return e("/api/tenant/truckroll/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("Tenants",["$resource",function(e){return e("/api/core/tenants/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("Subscribers",["$resource",function(e){return e("/api/tenant/cord/subscriber/:id/",{id:"@id"},{update:{method:"PUT"},"View-a-Subscriber-Features-Detail":{method:"GET",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/"},"Read-Subscriber-uplink_speed":{method:"GET",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/uplink_speed/"},"Update-Subscriber-uplink_speed":{method:"PUT",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/uplink_speed/"},"Read-Subscriber-downlink_speed":{method:"GET",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/downlink_speed/"},"Update-Subscriber-downlink_speed":{method:"PUT",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/downlink_speed/"},"Read-Subscriber-cdn":{method:"GET",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/cdn/"},"Update-Subscriber-cdn":{method:"PUT",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/cdn/"},"Read-Subscriber-uverse":{method:"GET",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/uverse/"},"Update-Subscriber-uverse":{method:"PUT",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/uverse/"},"Read-Subscriber-status":{method:"GET",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/status/"},"Update-Subscriber-status":{method:"PUT",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/status/"}})}])}(),function(){angular.module("xos.helpers").service("SlicesPlus",["$http","$q",function(e,n){this.query=function(o){var t=n.defer();return e.get("/api/utility/slicesplus/",{params:o}).then(function(e){t.resolve(e.data)})["catch"](function(e){t.reject(e.data)}),{$promise:t.promise}},this.get=function(o,t){var r=n.defer();return e.get("/api/utility/slicesplus/"+o,{params:t}).then(function(e){r.resolve(e.data)})["catch"](function(e){r.reject(e.data)}),{$promise:r.promise}}}])}(),function(){angular.module("xos.helpers").service("Slices",["$resource",function(e){return e("/api/core/slices/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("Sites",["$resource",function(e){return e("/api/core/sites/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("Services",["$resource",function(e){return e("/api/core/services/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("ONOS-Services-Collection",["$resource",function(e){return e("/api/service/onos/")}])}(),function(){angular.module("xos.helpers").service("ONOS-App-Collection",["$resource",function(e){return e("/api/tenant/onos/app/")}])}(),function(){angular.module("xos.helpers").service("Nodes",["$resource",function(e){return e("/api/core/nodes/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("Networks",["$resource",function(e){return e("/api/core/networks/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("Instances",["$resource",function(e){return e("/api/core/instances/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("Flavors",["$resource",function(e){return e("/api/core/flavors/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("Example-Services-Collection",["$resource",function(e){return e("/api/service/exampleservice/")}])}(),function(){angular.module("xos.helpers").service("Deployments",["$resource",function(e){return e("/api/core/deployments/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("GraphService",["$q","Tenants","Services",function(e,n,o){var t=this;this.loadCoarseData=function(){var t=void 0,r=e.defer();return o.query().$promise.then(function(e){return t=e,n.query({kind:"coarse"}).$promise}).then(function(e){r.resolve({tenants:e,services:t})}),r.promise},this.getCoarseGraph=function(){return t.loadCoarseData().then(function(e){console.log(e)}),"ciao"}}])}(),function(){function e(){return{request:function(e){return-1===e.url.indexOf(".html")&&(e.url+="?no_hyperlinks=1"),e}}}angular.module("xos.helpers").factory("NoHyperlinks",e)}(),angular.module("xos.helpers").config(["$provide",function(e){e.decorator("$log",["$delegate",function(e){var n=function(){return window.location.href.indexOf("debug=true")>=0},o=e.log,t=e.info,r=e.warn,i=e.error,a=e.debug,s=function(o){return function(){if(n()){var t=[].slice.call(arguments),r=new Date;t[0]="["+r.getHours()+":"+r.getMinutes()+":"+r.getSeconds()+"] "+t[0],"function"!=typeof e.reset||e.debug.logs instanceof Array||e.reset(),o.apply(null,t)}}};return e.info=s(t),e.log=s(o),e.warn=s(r),e.error=s(i),e.debug=s(a),e}])}]),function(){function e(){var e=function(e){return e.split("_").join(" ").trim()},n=function(e){return e.split(/(?=[A-Z])/).map(function(e){return e.toLowerCase()}).join(" ")},o=function(e){return e.slice(0,1).toUpperCase()+e.slice(1)},t=function(t){return t=e(t),t=n(t),t=o(t).replace(/\s\s+/g," ")+":",t.replace("::",":")};return{_formatByUnderscore:e,_formatByUppercase:n,_capitalize:o,format:t}}angular.module("xos.uiComponents").factory("LabelFormatter",e)}(),function(){function e(e){return{request:function(n){return"GET"!==n.method&&(n.headers["X-CSRFToken"]=e.get("xoscsrftoken")),n}}}e.$inject=["$cookies"],angular.module("xos.helpers").factory("SetCSRFToken",e)}();
\ No newline at end of file
+"use strict";!function(){angular.module("xos.uiComponents",["chart.js","RecursionHelper"])}(),function(){function e(){var e=function(e){return e.split("_").join(" ").trim()},n=function(e){return e.split(/(?=[A-Z])/).map(function(e){return e.toLowerCase()}).join(" ")},o=function(e){return e.slice(0,1).toUpperCase()+e.slice(1)},t=function(t){return t=e(t),t=n(t),t=o(t).replace(/\s\s+/g," ")+":",t.replace("::",":")};return{_formatByUnderscore:e,_formatByUppercase:n,_capitalize:o,format:t}}angular.module("xos.uiComponents").factory("LabelFormatter",e)}();var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol?"symbol":typeof e};!function(){angular.module("xos.uiComponents").service("XosFormHelpers",["_","LabelFormatter",function(e,n){var o=this;this._isEmail=function(e){var n=/(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/;return n.test(e)},this._getFieldFormat=function(n){return angular.isArray(n)?"array":e.isDate(n)||!Number.isNaN(Date.parse(n))&&new Date(n).getTime()>6311808e5?"date":"boolean"==typeof n?"boolean":o._isEmail(n)?"email":"string"==typeof n||null===n?"text":"undefined"==typeof n?"undefined":_typeof(n)},this.buildFormStructure=function(t,i,r){return t=Object.keys(t).length>0?t:i,i=i||{},e.reduce(Object.keys(t),function(e,t){return e[t]={label:i[t]&&i[t].label?i[t].label+":":n.format(t),type:i[t]&&i[t].type?i[t].type:o._getFieldFormat(r[t]),validators:i[t]&&i[t].validators?i[t].validators:{},hint:i[t]&&i[t].hint?i[t].hint:""},"date"===e[t].type&&(r[t]=new Date(r[t])),"number"===e[t].type&&(r[t]=parseInt(r[t],10)),e},{})},this.parseModelField=function(n){return e.reduce(n,function(e,n){return e[n]={},e},{})}}])}(),function(){angular.module("xos.uiComponents").directive("xosValidation",function(){return{restrict:"E",scope:{field:"=",form:"="},template:'\n <div ng-cloak>\n <xos-alert config="vm.config" show="vm.field.$error.required !== undefined && vm.field.$error.required !== false && (vm.field.$touched || vm.form.$submitted)">\n Field required\n </xos-alert>\n <xos-alert config="vm.config" show="vm.field.$error.email !== undefined && vm.field.$error.email !== false && (vm.field.$touched || vm.form.$submitted)">\n This is not a valid email\n </xos-alert>\n <xos-alert config="vm.config" show="vm.field.$error.minlength !== undefined && vm.field.$error.minlength !== false && (vm.field.$touched || vm.form.$submitted)">\n Too short\n </xos-alert>\n <xos-alert config="vm.config" show="vm.field.$error.maxlength !== undefined && vm.field.$error.maxlength !== false && (vm.field.$touched || vm.form.$submitted)">\n Too long\n </xos-alert>\n <xos-alert config="vm.config" show="vm.field.$error.custom !== undefined && vm.field.$error.custom !== false && (vm.field.$touched || vm.form.$submitted)">\n Field invalid\n </xos-alert>\n </div>\n ',transclude:!0,bindToController:!0,controllerAs:"vm",controller:function(){this.config={type:"danger"}}}})}(),function(){angular.module("xos.uiComponents").directive("xosForm",function(){return{restrict:"E",scope:{config:"=",ngModel:"="},template:'\n <ng-form name="vm.{{vm.config.formName || \'form\'}}">\n <div class="form-group" ng-repeat="(name, field) in vm.formField">\n <xos-field name="name" field="field" ng-model="vm.ngModel[name]"></xos-field>\n <xos-validation errors="vm[vm.config.formName || \'form\'][name].$error"></xos-validation>\n </div>\n <div class="form-group" ng-if="vm.config.actions">\n <button role="button" href=""\n ng-repeat="action in vm.config.actions"\n ng-click="action.cb(vm.ngModel)"\n class="btn btn-{{action.class}}"\n title="{{action.label}}">\n <i class="glyphicon glyphicon-{{action.icon}}"></i>\n {{action.label}}\n </button>\n </div>\n </ng-form>\n ',bindToController:!0,controllerAs:"vm",controller:["$scope","$log","_","XosFormHelpers",function(e,n,o,t){var i=this;if(!this.config)throw new Error('[xosForm] Please provide a configuration via the "config" attribute');if(!this.config.actions)throw new Error("[xosForm] Please provide an action list in the configuration");this.excludedField=["id","validators","created","updated","deleted","backend_status"],this.config&&this.config.exclude&&(this.excludedField=this.excludedField.concat(this.config.exclude)),this.formField=[],e.$watch(function(){return i.ngModel},function(e){if(i.formField={},e){var n=o.difference(Object.keys(e),i.excludedField),r=t.parseModelField(n);i.formField=t.buildFormStructure(r,i.config.fields,e)}})}]}})}(),function(){angular.module("xos.uiComponents").directive("xosTable",function(){return{restrict:"E",scope:{data:"=",config:"="},template:'\n <div ng-show="vm.data.length > 0">\n <div class="row" ng-if="vm.config.filter == \'fulltext\'">\n <div class="col-xs-12">\n <input\n class="form-control"\n placeholder="Type to search.."\n type="text"\n ng-model="vm.query"/>\n </div>\n </div>\n <table ng-class="vm.classes" ng-hide="vm.data.length == 0">\n <thead>\n <tr>\n <th ng-repeat="col in vm.columns">\n {{col.label}}\n <span ng-if="vm.config.order">\n <a href="" ng-click="vm.orderBy = col.prop; vm.reverse = false">\n <i class="glyphicon glyphicon-chevron-up"></i>\n </a>\n <a href="" ng-click="vm.orderBy = col.prop; vm.reverse = true">\n <i class="glyphicon glyphicon-chevron-down"></i>\n </a>\n </span>\n </th>\n <th ng-if="vm.config.actions">Actions:</th>\n </tr>\n </thead>\n <tbody ng-if="vm.config.filter == \'field\'">\n <tr>\n <td ng-repeat="col in vm.columns">\n <input\n ng-if="col.type !== \'boolean\'"\n class="form-control"\n placeholder="Type to search by {{col.label}}"\n type="text"\n ng-model="vm.query[col.prop]"/>\n <select\n ng-if="col.type === \'boolean\'"\n class="form-control"\n ng-model="vm.query[col.prop]">\n <option value="">-</option>\n <option value="true">True</option>\n <option value="false">False</option>\n </select>\n </td>\n <td ng-if="vm.config.actions"></td>\n </tr>\n </tbody>\n <tbody>\n <tr ng-repeat="item in vm.data | filter:vm.query | orderBy:vm.orderBy:vm.reverse | pagination:vm.currentPage * vm.config.pagination.pageSize | limitTo: (vm.config.pagination.pageSize || vm.data.length) track by $index">\n <td ng-repeat="col in vm.columns" link-wrapper>\n <span ng-if="!col.type">{{item[col.prop]}}</span>\n <span ng-if="col.type === \'boolean\'">\n <i class="glyphicon"\n ng-class="{\'glyphicon-ok\': item[col.prop], \'glyphicon-remove\': !item[col.prop]}">\n </i>\n </span>\n <span ng-if="col.type === \'date\'">\n {{item[col.prop] | date:\'H:mm MMM d, yyyy\'}}\n </span>\n <span ng-if="col.type === \'array\'">\n {{item[col.prop] | arrayToList}}\n </span>\n <span ng-if="col.type === \'object\'">\n <dl class="dl-horizontal">\n <span ng-repeat="(k,v) in item[col.prop]">\n <dt>{{k}}</dt>\n <dd>{{v}}</dd>\n </span>\n </dl>\n </span>\n <span ng-if="col.type === \'custom\'">\n {{col.formatter(item)}}\n </span>\n <span ng-if="col.type === \'icon\'">\n <i class="glyphicon glyphicon-{{col.formatter(item)}}">\n </i>\n </span>\n </td>\n <td ng-if="vm.config.actions">\n <a href=""\n ng-repeat="action in vm.config.actions"\n ng-click="action.cb(item)"\n title="{{action.label}}">\n <i\n class="glyphicon glyphicon-{{action.icon}}"\n style="color: {{action.color}};"></i>\n </a>\n </td>\n </tr>\n </tbody>\n </table>\n <xos-pagination\n ng-if="vm.config.pagination"\n page-size="vm.config.pagination.pageSize"\n total-elements="vm.data.length"\n change="vm.goToPage">\n </xos-pagination>\n </div>\n <div ng-show="vm.data.length == 0 || !vm.data">\n <xos-alert config="{type: \'info\'}">\n No data to show.\n </xos-alert>\n </div>\n ',bindToController:!0,controllerAs:"vm",controller:["_",function(e){var n=this;if(!this.config)throw new Error('[xosTable] Please provide a configuration via the "config" attribute');if(!this.config.columns)throw new Error("[xosTable] Please provide a columns list in the configuration");this.config.order&&angular.isObject(this.config.order)&&(this.reverse=this.config.order.reverse||!1,this.orderBy=this.config.order.field||"id");var o=e.filter(this.config.columns,{type:"custom"});angular.isArray(o)&&o.length>0&&e.forEach(o,function(e){if(!e.formatter||!angular.isFunction(e.formatter))throw new Error("[xosTable] You have provided a custom field type, a formatter function should provided too.")});var t=e.filter(this.config.columns,{type:"icon"});angular.isArray(t)&&t.length>0&&e.forEach(t,function(e){if(!e.formatter||!angular.isFunction(e.formatter))throw new Error("[xosTable] You have provided an icon field type, a formatter function should provided too.")});var i=e.filter(this.config.columns,function(e){return angular.isDefined(e.link)});angular.isArray(i)&&i.length>0&&e.forEach(i,function(e){if(!angular.isFunction(e.link))throw new Error("[xosTable] The link property should be a function.")}),this.columns=this.config.columns,this.classes=this.config.classes||"table table-striped table-bordered",this.config.actions,this.config.pagination&&(this.currentPage=0,this.goToPage=function(e){n.currentPage=e})}]}}).filter("arrayToList",function(){return function(e){return angular.isArray(e)?e.join(", "):e}}).directive("linkWrapper",function(){return{restrict:"A",transclude:!0,template:'\n <a ng-if="col.link" href="{{col.link(item)}}">\n <div ng-transclude></div>\n </a>\n <div ng-transclude ng-if="!col.link"></div>\n '}})}(),function(){angular.module("xos.uiComponents").directive("xosPagination",function(){return{restrict:"E",scope:{pageSize:"=",totalElements:"=",change:"="},template:'\n <div class="row" ng-if="vm.pageList.length > 1">\n <div class="col-xs-12 text-center">\n <ul class="pagination">\n <li\n ng-click="vm.goToPage(vm.currentPage - 1)"\n ng-class="{disabled: vm.currentPage == 0}">\n <a href="" aria-label="Previous">\n <span aria-hidden="true">«</span>\n </a>\n </li>\n <li ng-repeat="i in vm.pageList" ng-class="{active: i === vm.currentPage}">\n <a href="" ng-click="vm.goToPage(i)">{{i + 1}}</a>\n </li>\n <li\n ng-click="vm.goToPage(vm.currentPage + 1)"\n ng-class="{disabled: vm.currentPage == vm.pages - 1}">\n <a href="" aria-label="Next">\n <span aria-hidden="true">»</span>\n </a>\n </li>\n </ul>\n </div>\n </div>\n ',bindToController:!0,controllerAs:"vm",controller:["$scope",function(e){var n=this;this.currentPage=0,this.goToPage=function(e){0>e||e===n.pages||(n.currentPage=e,n.change(e))},this.createPages=function(e){for(var n=[],o=0;e>o;o++)n.push(o);return n},e.$watch(function(){return n.totalElements},function(){n.totalElements&&(n.pages=Math.ceil(n.totalElements/n.pageSize),n.pageList=n.createPages(n.pages))})}]}}).filter("pagination",function(){return function(e,n){return e&&angular.isArray(e)?(n=parseInt(n,10),e.slice(n)):e}})}(),function(){angular.module("xos.uiComponents").directive("xosAlert",function(){return{restrict:"E",scope:{config:"=",show:"=?"},template:'\n <div ng-cloak class="alert alert-{{vm.config.type}}" ng-hide="!vm.show">\n <button type="button" class="close" ng-if="vm.config.closeBtn" ng-click="vm.dismiss()">\n <span aria-hidden="true">×</span>\n </button>\n <p ng-transclude></p>\n </div>\n ',transclude:!0,bindToController:!0,controllerAs:"vm",controller:["$timeout",function(e){var n=this;if(!this.config)throw new Error('[xosAlert] Please provide a configuration via the "config" attribute');this.show=this.show!==!1,this.dismiss=function(){n.show=!1},this.config.autoHide&&!function(){var o=e(function(){n.dismiss(),e.cancel(o)},n.config.autoHide)}()}]}})}(),function(){angular.module("xos.uiComponents").directive("xosField",["RecursionHelper",function(e){return{restrict:"E",scope:{name:"=",field:"=",ngModel:"="},template:'\n <label ng-if="vm.field.type !== \'object\'">{{vm.field.label}}</label>\n <input\n ng-if="vm.field.type !== \'boolean\' && vm.field.type !== \'object\'"\n type="{{vm.field.type}}"\n name="{{vm.name}}"\n class="form-control"\n ng-model="vm.ngModel"\n ng-minlength="vm.field.validators.minlength || 0"\n ng-maxlength="vm.field.validators.maxlength || 2000"\n ng-required="vm.field.validators.required || false" />\n <span class="boolean-field" ng-if="vm.field.type === \'boolean\'">\n <button\n class="btn btn-success"\n ng-show="vm.ngModel"\n ng-click="vm.ngModel = false">\n <i class="glyphicon glyphicon-ok"></i>\n </button>\n <button\n class="btn btn-danger"\n ng-show="!vm.ngModel"\n ng-click="vm.ngModel = true">\n <i class="glyphicon glyphicon-remove"></i>\n </button>\n </span>\n <div\n class="panel panel-default object-field"\n ng-if="vm.field.type == \'object\' && !vm.isEmptyObject(vm.ngModel)"\n >\n <div class="panel-heading">{{vm.field.label}}</div>\n <div class="panel-body">\n <div ng-repeat="(k, v) in vm.ngModel">\n <xos-field\n name="k"\n field="{label: vm.formatLabel(k), type: vm.getType(v)}"\n ng-model="v">\n </xos-field>\n </div>\n </div>\n </div>\n ',bindToController:!0,controllerAs:"vm",compile:function(n){return e.compile(n)},controller:["$attrs","XosFormHelpers","LabelFormatter",function(e,n,o){if(!this.name)throw new Error("[xosField] Please provide a field name");if(!this.field)throw new Error("[xosField] Please provide a field definition");if(!e.ngModel)throw new Error("[xosField] Please provide an ng-model");this.getType=n._getFieldFormat,this.formatLabel=o.format,this.isEmptyObject=function(e){return 0===Object.keys(e).length}}]}}])}();var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol?"symbol":typeof e};!function(){angular.module("xos.uiComponents").directive("xosSmartTable",function(){return{restrict:"E",scope:{config:"="},template:'\n <div class="row" ng-show="vm.data.length > 0">\n <div class="col-xs-12 text-right">\n <a href="" class="btn btn-success" ng-click="vm.createItem()">\n Add\n </a>\n </div>\n </div>\n <div class="row">\n <div class="col-xs-12 table-responsive">\n <xos-table config="vm.tableConfig" data="vm.data"></xos-table>\n </div>\n </div>\n <div class="panel panel-default" ng-show="vm.detailedItem">\n <div class="panel-heading">\n <div class="row">\n <div class="col-xs-11">\n <h3 class="panel-title" ng-show="vm.detailedItem.id">Update {{vm.config.resource}} {{vm.detailedItem.id}}</h3>\n <h3 class="panel-title" ng-show="!vm.detailedItem.id">Create {{vm.config.resource}} item</h3>\n </div>\n <div class="col-xs-1">\n <a href="" ng-click="vm.cleanForm()">\n <i class="glyphicon glyphicon-remove pull-right"></i>\n </a>\n </div>\n </div>\n </div>\n <div class="panel-body">\n <xos-form config="vm.formConfig" ng-model="vm.detailedItem"></xos-form>\n </div>\n </div>\n <xos-alert config="{type: \'success\', closeBtn: true}" show="vm.responseMsg">{{vm.responseMsg}}</xos-alert>\n <xos-alert config="{type: \'danger\', closeBtn: true}" show="vm.responseErr">{{vm.responseErr}}</xos-alert>\n ',bindToController:!0,controllerAs:"vm",controller:["$injector","LabelFormatter","_","XosFormHelpers",function(e,n,o,t){var i=this;this.responseMsg=!1,this.responseErr=!1,this.tableConfig={columns:[],actions:[{label:"delete",icon:"remove",cb:function(e){i.Resource["delete"]({id:e.id}).$promise.then(function(){o.remove(i.data,function(n){return n.id===e.id}),i.responseMsg=i.config.resource+" with id "+e.id+" successfully deleted"})["catch"](function(n){i.responseErr=n.data.detail||"Error while deleting "+i.config.resource+" with id "+e.id})},color:"red"},{label:"details",icon:"search",cb:function(e){i.detailedItem=e}}],classes:"table table-striped table-bordered table-responsive",filter:"field",order:!0,pagination:{pageSize:10}},this.formConfig={exclude:this.config.hiddenFields,fields:{},formName:this.config.resource+"Form",actions:[{label:"Save",icon:"ok",cb:function(e){var n=void 0,o=!0;e.id?(n=e.$update(),o=!1):n=e.$save(),n.then(function(n){o&&i.data.push(angular.copy(n)),delete i.detailedItem,i.responseMsg=i.config.resource+" with id "+e.id+" successfully saved"})["catch"](function(n){i.responseErr=n.data.detail||"Error while saving "+i.config.resource+" with id "+e.id})},"class":"success"}]},this.cleanForm=function(){delete i.detailedItem},this.createItem=function(){i.detailedItem=new i.Resource},this.Resource=e.get(this.config.resource);var r=function(){i.Resource.query().$promise.then(function(e){if(e[0]){var r=e[0],s=Object.keys(r);o.remove(s,function(e){return"id"==e||"validators"==e}),angular.isArray(i.config.hiddenFields)&&(s=o.difference(s,i.config.hiddenFields));var a=s.map(function(e){return n.format(e)});s.forEach(function(e,n){var o={label:a[n],prop:e};"string"!=typeof r[e]&&"undefined"!=typeof r[e]&&(o.type=_typeof(r[e])),i.tableConfig.columns.push(o)}),s.forEach(function(e,o){i.formConfig.fields[e]={label:n.format(a[o]).replace(":",""),type:t._getFieldFormat(r[e])}}),i.data=e}})};r()}]}})}(),function(){angular.module("xos.uiComponents").directive("xosSmartPie",function(){return{restrict:"E",scope:{config:"="},template:'\n <canvas\n class="chart chart-pie {{vm.config.classes}}"\n chart-data="vm.data" chart-labels="vm.labels"\n chart-legend="{{vm.config.legend}}">\n </canvas>\n ',bindToController:!0,controllerAs:"vm",controller:["$injector","$interval","$scope","$timeout","_",function(e,n,o,t,i){var r=this;if(!this.config.resource&&!this.config.data)throw new Error("[xosSmartPie] Please provide a resource or an array of data in the configuration");var s=function(e){return i.groupBy(e,r.config.groupBy)},a=function(e){return i.reduce(Object.keys(e),function(n,o){return n.concat(e[o].length)},[])},c=function(e){return angular.isFunction(r.config.labelFormatter)?r.config.labelFormatter(Object.keys(e)):Object.keys(e)},l=function(e){var n=s(e);r.data=a(n),r.labels=c(n)};this.config.resource?!function(){r.Resource=e.get(r.config.resource);var o=function(){r.Resource.query().$promise.then(function(e){e[0]&&l(e)})};o(),r.config.poll&&n(function(){o()},1e3*r.config.poll)}():o.$watch(function(){return r.config.data},function(e){e&&l(r.config.data)},!0),o.$on("create",function(e,n){console.log("create: "+n.id)}),o.$on("destroy",function(e,n){console.log("destroy: "+n.id)})}]}})}(),function(){function e(e,n,o){e.interceptors.push("SetCSRFToken"),n.startSymbol("{$"),n.endSymbol("$}"),o.defaults.stripTrailingSlashes=!1}e.$inject=["$httpProvider","$interpolateProvider","$resourceProvider"],angular.module("bugSnag",[]).factory("$exceptionHandler",function(){return function(e,n){window.Bugsnag?Bugsnag.notifyException(e,{diagnostics:{cause:n}}):console.error(e,n,e.stack)}}),angular.module("xos.helpers",["ngCookies","ngResource","ngAnimate","bugSnag","xos.uiComponents"]).config(e).factory("_",["$window",function(e){return e._}])}(),function(){angular.module("xos.helpers").service("vSG-Collection",["$resource",function(e){return e("/api/service/vsg/")}])}(),function(){angular.module("xos.helpers").service("vOLT-Collection",["$resource",function(e){return e("/api/tenant/cord/volt/:volt_id/",{volt_id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("Login",["$resource",function(e){return e("/api/utility/login/")}]).service("Logout",["$resource",function(e){return e("/api/utility/logout/")}])}(),function(){angular.module("xos.helpers").service("Users",["$resource",function(e){return e("/api/core/users/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("Truckroll",["$resource",function(e){return e("/api/tenant/truckroll/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("Tenants",["$resource",function(e){return e("/api/core/tenants/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("Subscribers",["$resource",function(e){return e("/api/tenant/cord/subscriber/:id/",{id:"@id"},{update:{method:"PUT"},"View-a-Subscriber-Features-Detail":{method:"GET",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/"},"Read-Subscriber-uplink_speed":{method:"GET",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/uplink_speed/"},"Update-Subscriber-uplink_speed":{method:"PUT",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/uplink_speed/"},"Read-Subscriber-downlink_speed":{method:"GET",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/downlink_speed/"},"Update-Subscriber-downlink_speed":{method:"PUT",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/downlink_speed/"},"Read-Subscriber-cdn":{method:"GET",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/cdn/"},"Update-Subscriber-cdn":{method:"PUT",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/cdn/"},"Read-Subscriber-uverse":{method:"GET",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/uverse/"},"Update-Subscriber-uverse":{method:"PUT",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/uverse/"},"Read-Subscriber-status":{method:"GET",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/status/"},"Update-Subscriber-status":{method:"PUT",isArray:!1,url:"/api/tenant/cord/subscriber/:id/features/status/"}})}])}(),function(){angular.module("xos.helpers").service("SlicesPlus",["$http","$q",function(e,n){this.query=function(o){var t=n.defer();return e.get("/api/utility/slicesplus/",{params:o}).then(function(e){t.resolve(e.data)})["catch"](function(e){t.reject(e.data)}),{$promise:t.promise}},this.get=function(o,t){var i=n.defer();return e.get("/api/utility/slicesplus/"+o,{params:t}).then(function(e){i.resolve(e.data)})["catch"](function(e){i.reject(e.data)}),{$promise:i.promise}}}])}(),function(){angular.module("xos.helpers").service("Slices",["$resource",function(e){return e("/api/core/slices/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("Sites",["$resource",function(e){return e("/api/core/sites/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("Services",["$resource",function(e){return e("/api/core/services/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("ONOS-Services-Collection",["$resource",function(e){return e("/api/service/onos/")}])}(),function(){angular.module("xos.helpers").service("ONOS-App-Collection",["$resource",function(e){return e("/api/tenant/onos/app/")}])}(),function(){angular.module("xos.helpers").service("Nodes",["$resource",function(e){return e("/api/core/nodes/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("Networks",["$resource",function(e){return e("/api/core/networks/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("Instances",["$resource",function(e){return e("/api/core/instances/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("Flavors",["$resource",function(e){return e("/api/core/flavors/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("Example-Services-Collection",["$resource",function(e){return e("/api/service/exampleservice/")}])}(),function(){angular.module("xos.helpers").service("Deployments",["$resource",function(e){return e("/api/core/deployments/:id/",{id:"@id"},{update:{method:"PUT"}})}])}(),function(){angular.module("xos.helpers").service("XosUserPrefs",["$cookies",function(e){var n=this,o=e.get("xosUserPrefs")?JSON.parse(e.get("xosUserPrefs")):{};this.getAll=function(){return o=e.get("xosUserPrefs")?JSON.parse(e.get("xosUserPrefs")):{}},this.setAll=function(n){e.put("xosUserPrefs",JSON.stringify(n))},this.getSynchronizerNotificationStatus=function(){var e=arguments.length<=0||void 0===arguments[0]?!1:arguments[0];return e?n.getAll().synchronizers.notification[e]:n.getAll().synchronizers.notification},this.setSynchronizerNotificationStatus=function(){var e=arguments.length<=0||void 0===arguments[0]?!1:arguments[0],o=arguments[1];if(!e)throw new Error("[XosUserPrefs] When updating a synchronizer is mandatory to provide a name.");var t=n.getAll();t.synchronizers||(t.synchronizers={notification:{}}),t.synchronizers.notification[e]=o,n.setAll(t)}}])}(),function(){angular.module("xos.helpers").service("GraphService",["$q","Tenants","Services",function(e,n,o){var t=this;this.loadCoarseData=function(){var t=void 0,i=e.defer();return o.query().$promise.then(function(e){return t=e,n.query({kind:"coarse"}).$promise}).then(function(e){i.resolve({tenants:e,services:t})}),i.promise},this.getCoarseGraph=function(){return t.loadCoarseData().then(function(e){console.log(e)}),"ciao"}}])}(),function(){angular.module("xos.helpers").factory("Notification",function(){return window.Notification}).service("xosNotification",["$q","$log","Notification",function(e,n,o){var t=this;this.checkPermission=function(){var n=e.defer();return o.requestPermission().then(function(e){"granted"===e?n.resolve(e):n.reject(e)}),n.promise},this.sendNotification=function(e,t){var i=new o(e,t);i.onerror=function(e){n.error(e)}},this.notify=function(e,i){"Notification"in window?"granted"!==o.permission?t.checkPermission().then(function(){return t.sendNotification(e,i)}):"granted"===o.permission&&t.sendNotification(e,i):n.info("This browser does not support desktop notification")}}])}(),function(){function e(){return{request:function(e){return-1===e.url.indexOf(".html")&&(e.url+="?no_hyperlinks=1"),e}}}angular.module("xos.helpers").factory("NoHyperlinks",e)}(),angular.module("xos.helpers").config(["$provide",function(e){e.decorator("$log",["$delegate",function(e){var n=function(){return window.location.href.indexOf("debug=true")>=0},o=e.log,t=e.info,i=e.warn,r=e.error,s=e.debug,a=function(o){return function(){if(n()){var t=[].slice.call(arguments),i=new Date;t[0]="["+i.getHours()+":"+i.getMinutes()+":"+i.getSeconds()+"] "+t[0],"function"!=typeof e.reset||e.debug.logs instanceof Array||e.reset(),o.apply(null,t)}}};return e.info=a(t),e.log=a(o),e.warn=a(i),e.error=a(r),e.debug=a(s),e}])}]),function(){function e(){var e=function(e){return e.split("_").join(" ").trim()},n=function(e){return e.split(/(?=[A-Z])/).map(function(e){return e.toLowerCase()}).join(" ")},o=function(e){return e.slice(0,1).toUpperCase()+e.slice(1)},t=function(t){return t=e(t),t=n(t),t=o(t).replace(/\s\s+/g," ")+":",t.replace("::",":")};return{_formatByUnderscore:e,_formatByUppercase:n,_capitalize:o,format:t}}angular.module("xos.uiComponents").factory("LabelFormatter",e)}(),function(){function e(e){return{request:function(n){return"GET"!==n.method&&(n.headers["X-CSRFToken"]=e.get("xoscsrftoken")),n}}}e.$inject=["$cookies"],angular.module("xos.helpers").factory("SetCSRFToken",e)}();
\ No newline at end of file
diff --git a/xos/core/xoslib/static/js/xosSynchronizerNotifier.js b/xos/core/xoslib/static/js/xosSynchronizerNotifier.js
new file mode 100644
index 0000000..894f9ea
--- /dev/null
+++ b/xos/core/xoslib/static/js/xosSynchronizerNotifier.js
@@ -0,0 +1 @@
+"use strict";angular.module("xos.synchronizerNotifier",["ngResource","ngCookies","ui.router","xos.helpers"]).service("Diag",["$rootScope","$http","$q","$interval",function(n,t,s,e){var a=this,i=!1;this.getDiags=function(){var n=s.defer();return t.get("/api/core/diags").then(function(t){n.resolve(t.data)})["catch"](function(t){n.reject(t)}),n.promise},this.sendEvents=function(t){t.forEach(function(t){var s=JSON.parse(t.backend_register);s.last_run=new Date(1e3*s.last_run),s.last_duration=1e3*s.last_duration,s.last_synchronizer_start=new Date(1e3*s.last_synchronizer_start),s.last_syncrecord_start=s.last_syncrecord_start?new Date(1e3*s.last_syncrecord_start):null,n.$broadcast("diag",{name:t.name,updated:t.updated,info:s,status:a.getSyncStatus(s)})})},this.start=function(){return i=!0,a.getDiags().then(function(n){a.sendEvents(n)}),i},this.stop=function(){return i=!1},this.getSyncStatus=function(n){var t=new Date,s=9e5;return!(t-n.last_synchronizer_start>s&&t-n.last_syncrecord_start>s&&t-n.last_run>s)},e(function(){i&&a.getDiags().then(function(n){a.sendEvents(n)})},3e5)}]).directive("syncStatus",function(){return{restrict:"E",scope:{},bindToController:!0,controllerAs:"vm",templateUrl:"templates/sync-status.tpl.html",controller:["$log","$rootScope","Diag","xosNotification","XosUserPrefs",function(n,t,s,e,a){var i=this;s.start(),this.synchronizers={},this.showNoSync=!0,t.$on("diag",function(n,t){i.synchronizers[t.name]=t,t.status?a.setSynchronizerNotificationStatus(t.name,!1):(a.getSynchronizerNotificationStatus(t.name)||e.notify("CORD Synchronizer",{icon:"/static/cord-logo.png",body:"The "+t.name+" synchronizer has not performed actions in the last 15 minutes."}),a.setSynchronizerNotificationStatus(t.name,!0)),i.showNoSync=!1,0===Object.keys(i.synchronizers).length&&(i.showNoSync=!0)})}]}}),angular.element(document).ready(function(){angular.bootstrap("#xosSynchronizerNotifier",["xos.synchronizerNotifier"])}),angular.module("xos.synchronizerNotifier").run(["$templateCache",function(n){n.put("templates/sync-status.tpl.html",'<div class="sync-status-container">\n <div class="btn btn-default" ng-click="vm.showNotificationPanel = !vm.showNotificationPanel">\n <i class="glyphicon glyphicon-inbox"></i>\n </div>\n <div class="notification-panel panel panel-default" ng-show="vm.showNotificationPanel">\n <ul class="list-group" ng-show="!vm.showNoSync">\n <li class="list-group-item" ng-repeat="(syncName, syncStatus) in vm.synchronizers">\n <span class="badge" ng-class="{success: syncStatus.status, warning: !syncStatus.status}">\n <span ng-show="syncStatus.status"><i class="glyphicon glyphicon-ok"></i></span>\n <span ng-hide="syncStatus.status"><i class="glyphicon glyphicon-time"></i></span>\n </span>\n <b>{{syncName}}</b>\n <br/>\n <small><i>{{syncStatus.info.last_run | date:\'mediumTime\'}}</i></small>\n </li>\n </ul>\n <div class="alert alert-info" ng-show="vm.showNoSync">\n No syncronizers are running.\n </div>\n </div>\n</div>\n')}]),angular.module("xos.synchronizerNotifier").run(["$location",function(n){n.path("/")}]);
\ No newline at end of file
diff --git a/xos/synchronizers/base/backend.py b/xos/synchronizers/base/backend.py
index d7307fa..af51e73 100644
--- a/xos/synchronizers/base/backend.py
+++ b/xos/synchronizers/base/backend.py
@@ -7,12 +7,16 @@
from xos.logger import Logger, logging
from synchronizers.model_policy import run_policy
from xos.config import Config
+from django.utils import timezone
+from diag import update_diag
logger = Logger(level=logging.INFO)
class Backend:
def run(self):
+ update_diag(sync_start=time.time(), backend_status="0 - Synchronizer Start")
+
# start the openstack observer
observer = XOSObserver()
observer_thread = threading.Thread(target=observer.run,name='synchronizer')
diff --git a/xos/synchronizers/base/diag.py b/xos/synchronizers/base/diag.py
new file mode 100644
index 0000000..87fab70
--- /dev/null
+++ b/xos/synchronizers/base/diag.py
@@ -0,0 +1,40 @@
+import os
+import time
+import sys
+import traceback
+import json
+
+from core.models import Diag
+from xos.config import Config, XOS_DIR
+from xos.logger import Logger, logging, logger
+
+logger = Logger(level=logging.INFO)
+
+def update_diag(loop_end=None, loop_start=None, syncrecord_start=None, sync_start=None, backend_status=None):
+ try:
+ observer_name = Config().observer_name
+ except:
+ observer_name = 'openstack'
+
+ try:
+ diag = Diag.objects.filter(name=observer_name).first()
+ if (not diag):
+ diag = Diag(name=observer_name)
+ br_str = diag.backend_register
+ br = json.loads(br_str)
+ if loop_end:
+ br['last_run'] = loop_end
+ if loop_end and loop_start:
+ br['last_duration'] = loop_end - loop_start
+ if syncrecord_start:
+ br['last_syncrecord_start'] = syncrecord_start
+ if sync_start:
+ br['last_synchronizer_start'] = sync_start
+ if backend_status:
+ diag.backend_status = backend_status
+ diag.backend_register = json.dumps(br)
+ diag.save()
+ except:
+ logger.log_exc("Exception in update_diag")
+ traceback.print_exc()
+
diff --git a/xos/synchronizers/base/event_loop.py b/xos/synchronizers/base/event_loop.py
index 4f7d436..37ec4ca 100644
--- a/xos/synchronizers/base/event_loop.py
+++ b/xos/synchronizers/base/event_loop.py
@@ -28,6 +28,8 @@
from synchronizers.base.error_mapper import *
from synchronizers.openstack.openstacksyncstep import OpenStackSyncStep
from synchronizers.base.steps.sync_object import SyncObject
+from django.utils import timezone
+from diag import update_diag
# Load app models
@@ -521,20 +523,10 @@
loop_end = time.time()
- try:
- observer_name = Config().observer_name
- except:
- observer_name = ''
+ update_diag(loop_end=loop_end, loop_start=loop_start, backend_status="1 - Bottom Of Loop")
- diag = Diag.objects.filter(name=observer_name).first()
- if (diag):
- br_str = diag.backend_register
- br = json.loads(br_str)
- br['last_run'] = loop_end
- br['last_duration'] = loop_end - loop_start
- diag.backend_register = json.dumps(br)
- diag.save()
except Exception, e:
logger.error('Core error. This seems like a misconfiguration or bug: %r. This error will not be relayed to the user!' % e)
logger.log_exc("Exception in observer run loop")
traceback.print_exc()
+ update_diag(backend_status="2 - Exception in Event Loop")
diff --git a/xos/synchronizers/base/syncstep.py b/xos/synchronizers/base/syncstep.py
index d505481..2fa6c38 100644
--- a/xos/synchronizers/base/syncstep.py
+++ b/xos/synchronizers/base/syncstep.py
@@ -10,8 +10,8 @@
from django.db import reset_queries
from synchronizers.base.ansible import *
from generate.dependency_walker import *
+from diag import update_diag
-from time import time
import json
import time
import pdb
@@ -236,9 +236,11 @@
self.sync_record(o)
- if (not run_always):
- o.enacted = new_enacted
+# if (not run_always):
+# o.enacted = new_enacted
+ update_diag(syncrecord_start = time.time(), backend_status="1 - Synced Record")
+ o.enacted = new_enacted
scratchpad = {'next_run':0, 'exponent':0, 'last_success':time.time()}
o.backend_register = json.dumps(scratchpad)
o.backend_status = "1 - OK"
diff --git a/xos/templates/admin/base.html b/xos/templates/admin/base.html
index ba01201..408a1e8 100644
--- a/xos/templates/admin/base.html
+++ b/xos/templates/admin/base.html
@@ -1,4 +1,4 @@
-{% load admin_static %}{% load suit_tags %}{% load url from future %}
+{% load admin_static %}{% load suit_tags core_tags %}{% load url from future %}
<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE|default:"en-us" }}" {% if LANGUAGE_BIDI %}dir="rtl"{% endif %}>
<head>
@@ -148,6 +148,7 @@
{% endblock %}
{% if user.is_active and user.is_staff %}
+ {% notification %}
<div id="user-tools">
{% trans 'Welcome,' %}
<a href="http://{{ request.get_host}}/admin/core/user/{{user.id}}">{{user.email}}</a>
diff --git a/xos/templates/admin/tags/notification.html b/xos/templates/admin/tags/notification.html
new file mode 100644
index 0000000..558b9ff
--- /dev/null
+++ b/xos/templates/admin/tags/notification.html
@@ -0,0 +1 @@
+{{template | safe}}
\ No newline at end of file