CORD-772 Extending the GUI with external apps

Change-Id: Ie13d438716054260e03ff54ac752d9f072fb9d76
diff --git a/conf/browsersync-dist.conf.js b/conf/browsersync-dist.conf.js
index fa45845..cbefe1d 100644
--- a/conf/browsersync-dist.conf.js
+++ b/conf/browsersync-dist.conf.js
@@ -5,7 +5,10 @@
     server: {
       baseDir: [
         conf.paths.dist
-      ]
+      ],
+      routes: {
+        "/spa": "./dist"
+      }
     },
     open: false
   };
diff --git a/gulp_tasks/webpack.js b/gulp_tasks/webpack.js
index ec8e8b1..9671b23 100644
--- a/gulp_tasks/webpack.js
+++ b/gulp_tasks/webpack.js
@@ -20,6 +20,11 @@
   webpackWrapper(false, webpackDistConf, done);
 });
 
+gulp.task('webpack:dist:watch', done => {
+  process.env.NODE_ENV = 'production';
+  webpackWrapper(true, webpackDistConf, done);
+});
+
 function webpackWrapper(watch, conf, done) {
   const webpackBundler = webpack(conf);
 
diff --git a/gulpfile.js b/gulpfile.js
index 541d6e4..08918dc 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -15,6 +15,7 @@
 gulp.task('test:auto', gulp.series('karma:auto-run'));
 gulp.task('serve', gulp.series('webpack:watch', 'watch', 'browsersync'));
 gulp.task('serve:dist', gulp.series('default', 'browsersync:dist'));
+gulp.task('serve:dist:watch', gulp.series('clean', 'other', 'webpack:dist:watch', 'browsersync:dist'));
 gulp.task('default', gulp.series('clean', 'build'));
 gulp.task('watch', watch);
 
diff --git a/package.json b/package.json
index 28ebefb..3a92d89 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
     "bootstrap": "^3.3.7",
     "jquery": "^3.1.1",
     "lodash": "^4.17.2",
+    "oclazyload": "^1.0.9",
     "pluralize": "^3.1.0",
     "rxjs": "^5.0.1",
     "socket.io-client": "^1.7.2"
@@ -85,6 +86,7 @@
     "start": "gulp serve",
     "typings": "typings install",
     "serve:dist": "gulp serve:dist",
+    "serve:dist:watch": "gulp serve:dist:watch",
     "pretest": "npm run lint",
     "test": "gulp test",
     "test:auto": "gulp test:auto",
diff --git a/src/app/core/services/helpers/component-injector.helpers.ts b/src/app/core/services/helpers/component-injector.helpers.ts
index 35c73a0..e70e257 100644
--- a/src/app/core/services/helpers/component-injector.helpers.ts
+++ b/src/app/core/services/helpers/component-injector.helpers.ts
@@ -46,6 +46,9 @@
     });
   }
 
+  // FIXME
+  // component are correctly injected but not persisted,
+  // if I change route they go away
   public injectComponent(target: string | JQuery, componentName: string, attributes?: any, transclude?: string, clean?: boolean) {
     let targetEl;
     if (angular.isString(target)) {
diff --git a/src/app/datasources/stores/synchronizer.store.ts b/src/app/datasources/stores/synchronizer.store.ts
index 33a0c39..aa25cc0 100644
--- a/src/app/datasources/stores/synchronizer.store.ts
+++ b/src/app/datasources/stores/synchronizer.store.ts
@@ -15,6 +15,9 @@
   ) {
     this.webSocket.list()
       .filter((e: IWSEvent) => {
+        if (!e.msg || !e.msg.changed_fields) {
+          return false;
+        }
         return e.msg.changed_fields.indexOf('backend_status') > -1;
       })
       .subscribe(
diff --git a/src/app/extender/index.ts b/src/app/extender/index.ts
new file mode 100644
index 0000000..622491a
--- /dev/null
+++ b/src/app/extender/index.ts
@@ -0,0 +1,21 @@
+import {xosDataSources} from '../datasources/index';
+export const xosExtender = 'xosExtender';
+
+import 'angular-ui-bootstrap';
+import 'angular-animate';
+import 'angular-toastr';
+import 'oclazyload';
+import {XosOnboarder, IXosOnboarder} from './services/onboard.service';
+
+
+(function () {
+  angular.module(xosExtender, [
+    'oc.lazyLoad',
+    xosDataSources
+  ])
+    .service('XosOnboarder', XosOnboarder)
+    .run(function ($log: ng.ILogService, XosOnboarder: IXosOnboarder) {
+      $log.info('[XosOnboarder] Setup');
+    });
+})();
+
diff --git a/src/app/extender/services/onboard.service.spec.ts b/src/app/extender/services/onboard.service.spec.ts
new file mode 100644
index 0000000..f9373c9
--- /dev/null
+++ b/src/app/extender/services/onboard.service.spec.ts
@@ -0,0 +1,69 @@
+import * as angular from 'angular';
+import 'angular-mocks';
+import 'angular-resource';
+import {Subject} from 'rxjs';
+import {XosOnboarder, IXosOnboarder} from './onboard.service';
+import {IWSEventService} from '../../datasources/websocket/global';
+
+let service, $ocLazyLoad;
+
+const subject = new Subject();
+
+const MockWs: IWSEventService = {
+  list() {
+    return subject.asObservable();
+  }
+};
+
+const MockPromise = {
+  then: (cb) => {
+    cb('done');
+    return MockPromise;
+  },
+  catch: (cb) => {
+    cb('err');
+    return MockPromise;
+  }
+};
+
+const MockLoad = {
+  load: () => {
+    return MockPromise;
+  }
+};
+
+describe('The XosOnboarder service', () => {
+
+  beforeEach(() => {
+
+    angular
+      .module('XosOnboarder', [])
+      .value('WebSocket', MockWs)
+      .value('$ocLazyLoad', MockLoad)
+      .service('XosOnboarder', XosOnboarder);
+
+    angular.mock.module('XosOnboarder');
+  });
+
+  beforeEach(angular.mock.inject((
+    XosOnboarder: IXosOnboarder,
+    _$ocLazyLoad_: any
+  ) => {
+    $ocLazyLoad = _$ocLazyLoad_;
+    spyOn($ocLazyLoad, 'load').and.callThrough();
+    service = XosOnboarder;
+  }));
+
+  describe('when receive an event', () => {
+    it('should use $ocLazyLoad to add modules to the app', () => {
+      subject.next({
+        msg: {
+          app: 'sample',
+          files: ['vendor.js', 'app.js']
+        }
+      });
+      expect($ocLazyLoad.load).toHaveBeenCalledWith('vendor.js');
+      expect($ocLazyLoad.load).toHaveBeenCalledWith('app.js');
+    });
+  });
+});
diff --git a/src/app/extender/services/onboard.service.ts b/src/app/extender/services/onboard.service.ts
new file mode 100644
index 0000000..70d830c
--- /dev/null
+++ b/src/app/extender/services/onboard.service.ts
@@ -0,0 +1,56 @@
+import {IWSEventService} from '../../datasources/websocket/global';
+
+export interface IXosOnboarder {
+
+}
+
+export class XosOnboarder implements IXosOnboarder {
+  static $inject = ['$timeout', '$log', '$q', 'WebSocket', '$ocLazyLoad'];
+
+  constructor(
+    private $timeout: ng.ITimeoutService,
+    private $log: ng.ILogService,
+    private $q: ng.IQService,
+    private webSocket: IWSEventService,
+    private $ocLazyLoad: any // TODO add definition
+  ) {
+    this.$log.info('[XosOnboarder] Setup');
+    this.webSocket.list()
+      .filter((e) => {
+        this.$log.log(e);
+        // TODO define event format
+        return e.msg['files'].length > 0;
+      })
+      .subscribe(
+        (event) => {
+          this.loadFile(event.msg['files'])
+            .then((res) => {
+              this.$log.info(`[XosOnboarder] All files loaded for app: ${event.msg['app']}`);
+            });
+        }
+      );
+  }
+
+  // NOTE files needs to be loaded in order, so async loop!
+  private loadFile(files: string[], d?: ng.IDeferred<any>): ng.IPromise<string[]> {
+    if (!angular.isDefined(d)) {
+      d = this.$q.defer();
+    }
+    const file = files.shift();
+    this.$log.info(`[XosOnboarder] Loading file: ${file}`);
+    this.$ocLazyLoad.load(file)
+      .then((res) => {
+        this.$log.info(`[XosOnboarder] Loaded file: `, file);
+        if (files.length > 0) {
+          return this.loadFile(files, d);
+        }
+        return d.resolve(file);
+      })
+      .catch((err) => {
+        this.$log.error(`[XosOnboarder] Failed to load file: `, err);
+        d.reject(err);
+      });
+
+    return d.promise;
+  }
+}
diff --git a/src/decorators.ts b/src/decorators.ts
index 3b2ab4e..1ff981b 100644
--- a/src/decorators.ts
+++ b/src/decorators.ts
@@ -7,7 +7,7 @@
     let logFn = $delegate.log;
     let infoFn = $delegate.info;
     let warnFn = $delegate.warn;
-    let errorFn = $delegate.error;
+    // let errorFn = $delegate.error;
     let debugFn = $delegate.debug;
 
     // create the replacement function
@@ -31,7 +31,7 @@
     $delegate.info = replacement(infoFn);
     $delegate.log = replacement(logFn);
     $delegate.warn = replacement(warnFn);
-    $delegate.error = replacement(errorFn); // note this will prevent errors to be printed
+    // $delegate.error = replacement(errorFn); // note this will prevent errors to be printed
     $delegate.debug = replacement(debugFn);
 
     return $delegate;
diff --git a/src/index.ts b/src/index.ts
index a4b2714..bf2a1cc 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -26,6 +26,7 @@
 import {IXosModelSetupService} from './app/core/services/helpers/model-setup.helpers';
 import {IXosNavigationRoute} from './app/core/services/navigation';
 import XosLogDecorator from './decorators';
+import {xosExtender} from './app/extender/index';
 
 export interface IXosState extends angular.ui.IState {
   data: IXosCrudData;
@@ -50,6 +51,7 @@
     xosCore,
     xosDataSources,
     xosViews,
+    xosExtender,
     'ui.router',
     'ngResource',
     xosTemplate // template module