[CORD-1117] XOS GUI Various fix

Change-Id: I4237a5e23509e9173c958d76aa929a70583ba1e6
diff --git a/src/app/core/index.ts b/src/app/core/index.ts
index 0cc1e12..64d295c 100644
--- a/src/app/core/index.ts
+++ b/src/app/core/index.ts
@@ -24,6 +24,7 @@
 import {PaginationFilter} from './pagination/pagination.filter';
 import {XosDebouncer} from './services/helpers/debounce.helper';
 import {ArrayToListFilter} from './table/array-to-list.filter';
+import {xosLoader} from './loader/loader';
 
 export const xosCore = 'xosCore';
 
@@ -48,6 +49,7 @@
   .component('xosFooter', xosFooter)
   .component('xosNav', xosNav)
   .component('xosLogin', xosLogin)
+  .component('xosLoader', xosLoader)
   .component('xosPagination', xosPagination)
   .component('xosTable', xosTable)
   .component('xosForm', xosForm)
diff --git a/src/app/core/loader/loader.spec.ts b/src/app/core/loader/loader.spec.ts
new file mode 100644
index 0000000..1bcff9c
--- /dev/null
+++ b/src/app/core/loader/loader.spec.ts
@@ -0,0 +1,115 @@
+import * as angular from 'angular';
+import 'angular-mocks';
+import {xosLoader} from './loader';
+
+let loaded = true;
+
+const MockConfig = {
+  lastVisitedUrl: '/test'
+};
+
+const MockDiscover = {
+  areModelsLoaded: () => loaded,
+  discover: null
+};
+
+const MockOnboarder = {
+  onboard: null
+};
+
+describe('The XosLoader component', () => {
+  beforeEach(() => {
+    angular
+      .module('loader', [])
+      .value('XosConfig', MockConfig)
+      .value('XosModelDiscoverer', MockDiscover)
+      .value('XosOnboarder', MockOnboarder)
+      .component('xosLoader', xosLoader);
+    angular.mock.module('loader');
+  });
+
+  let scope, element, isolatedScope, rootScope, compile, timeout, location;
+  const compileElement = () => {
+
+    if (!scope) {
+      scope = rootScope.$new();
+    }
+
+    element = angular.element('<xos-loader></xos-loader>');
+    compile(element)(scope);
+    scope.$digest();
+    isolatedScope = element.isolateScope().vm;
+  };
+
+  beforeEach(inject(function ($q: ng.IQService, $compile: ng.ICompileService, $rootScope: ng.IScope, $timeout: ng.ITimeoutService, $location: ng.ILocationService) {
+    compile = $compile;
+    rootScope = $rootScope;
+    timeout = $timeout;
+    location = $location;
+    spyOn(location, 'path');
+
+    MockDiscover.discover = jasmine.createSpy('discover')
+      .and.callFake(() => {
+        const d = $q.defer();
+        d.resolve(true);
+        return d.promise;
+      });
+
+    MockOnboarder.onboard = jasmine.createSpy('onboard')
+      .and.callFake(() => {
+        const d = $q.defer();
+        d.resolve();
+        return d.promise;
+      });
+  }));
+
+  describe('when models are already loaded', () => {
+
+    beforeEach(() => {
+      compileElement();
+      spyOn(isolatedScope, 'moveOnTo');
+      isolatedScope.run();
+      timeout.flush();
+    });
+
+    it('should redirect to the last visited page', (done) => {
+      window.setTimeout(() => {
+        expect(isolatedScope.moveOnTo).toHaveBeenCalledWith('/test');
+        expect(location.path).toHaveBeenCalledWith('/test');
+        done();
+      }, 600);
+    });
+  });
+
+  describe('when the last visited page is "loader"', () => {
+
+    beforeEach(() => {
+      MockConfig.lastVisitedUrl = '/loader';
+      compileElement();
+      spyOn(isolatedScope, 'moveOnTo');
+      isolatedScope.run();
+    });
+
+    it('should redirect to the "dashboard" page', (done) => {
+      window.setTimeout(() => {
+        expect(isolatedScope.moveOnTo).toHaveBeenCalledWith('/loader');
+        expect(location.path).toHaveBeenCalledWith('/dashboard');
+        done();
+      }, 600);
+    });
+  });
+
+  describe('when models are not loaded', () => {
+
+    beforeEach(() => {
+      loaded = false;
+      compileElement();
+      spyOn(isolatedScope, 'moveOnTo');
+    });
+
+    it('should call XosModelDiscoverer.discover', () => {
+      expect(MockDiscover.discover).toHaveBeenCalled();
+    });
+  });
+
+});
diff --git a/src/app/core/loader/loader.ts b/src/app/core/loader/loader.ts
new file mode 100644
index 0000000..07a9875
--- /dev/null
+++ b/src/app/core/loader/loader.ts
@@ -0,0 +1,83 @@
+import {IXosModelDiscovererService} from '../../datasources/helpers/model-discoverer.service';
+import {IXosOnboarder} from '../../extender/services/onboard.service';
+class LoaderCtrl {
+  static $inject = [
+    '$log',
+    '$rootScope',
+    '$location',
+    '$timeout',
+    'XosConfig',
+    'XosModelDiscoverer',
+    `XosOnboarder`
+  ];
+
+  public aaaaa = 'ciao';
+
+  constructor (
+    private $log: ng.ILogService,
+    private $rootScope: ng.IScope,
+    private $location: ng.ILocationService,
+    private $timeout: ng.ITimeoutService,
+    private XosConfig: any,
+    private XosModelDiscoverer: IXosModelDiscovererService,
+    private XosOnboarder: IXosOnboarder
+  ) {
+
+    this.run();
+  }
+
+  public run() {
+    if (this.XosModelDiscoverer.areModelsLoaded()) {
+      this.moveOnTo(this.XosConfig.lastVisitedUrl);
+    }
+    else {
+      this.XosModelDiscoverer.discover()
+      // NOTE loading XOS Models
+        .then((res) => {
+          if (res) {
+            this.$log.info('[XosLoader] All models loaded');
+          }
+          else {
+            this.$log.info('[XosLoader] Failed to load some models, moving on.');
+          }
+          return this.XosOnboarder.onboard();
+        })
+        // NOTE loading GUI Extensions
+        .then(() => {
+          this.moveOnTo(this.XosConfig.lastVisitedUrl);
+        })
+        .finally(() => {
+          // NOTE it is in a timeout as the searchService is loaded after that
+          // we navigate to another page
+          this.$timeout(() => {
+            this.$rootScope.$emit('xos.core.modelSetup');
+          }, 500);
+        });
+    }
+  }
+
+  public moveOnTo(url: string) {
+    this.$log.info(`[XosLoader] Redirecting to: ${url}`);
+    switch (url) {
+      case '':
+      case '/':
+      case '/loader':
+      case '/login':
+        this.$location.path('/dashboard');
+        break;
+      default:
+        this.$timeout(() => {
+          this.$location.path(url);
+        }, 500);
+        break;
+    }
+  }
+}
+
+export const xosLoader: angular.IComponentOptions = {
+  template: `
+    <div class="loader"></div>
+  `,
+  controllerAs: 'vm',
+  controller: LoaderCtrl
+};
diff --git a/src/app/core/login/login.ts b/src/app/core/login/login.ts
index 558fe37..8aa40d5 100644
--- a/src/app/core/login/login.ts
+++ b/src/app/core/login/login.ts
@@ -2,10 +2,15 @@
 import './login.scss';
 
 import {IXosStyleConfig} from '../../../index';
-import {IXosModelDiscovererService} from '../../datasources/helpers/model-discoverer.service';
 
 class LoginCtrl {
-  static $inject = ['$log', 'AuthService', '$state', 'XosModelDiscoverer', 'StyleConfig'];
+  static $inject = [
+    '$log',
+    'AuthService',
+    '$state',
+    'StyleConfig'
+  ];
+
   public loginStyle: any;
   public img: string;
   public showErrorMsg: boolean = false;
@@ -14,7 +19,6 @@
     private $log: ng.ILogService,
     private authService: AuthService,
     private $state: angular.ui.IStateService,
-    private XosModelDiscoverer: IXosModelDiscovererService,
     private StyleConfig: IXosStyleConfig
   ) {
 
@@ -35,15 +39,10 @@
       password: password
     })
       .then(res => {
-        this.showErrorMsg = false;
-        // after login set up models
-        return this.XosModelDiscoverer.discover();
-      })
-      .then(() => {
-        this.$state.go('xos.dashboard');
+        this.$state.go('loader');
       })
       .catch(e => {
-        this.$log.error(`[XosLogin] Error during login.`);
+        this.$log.error(`[XosLogin] Error during login.`, e);
         this.errorMsg = `Something went wrong, please try again.`;
         this.showErrorMsg = true;
       });
diff --git a/src/app/core/services/keyboard-shortcut.spec.ts b/src/app/core/services/keyboard-shortcut.spec.ts
index b573656..deb04b7 100644
--- a/src/app/core/services/keyboard-shortcut.spec.ts
+++ b/src/app/core/services/keyboard-shortcut.spec.ts
@@ -7,6 +7,7 @@
 let $log: ng.ILogService;
 let $transitions: any;
 let XosSidePanel: IXosSidePanelService;
+let logSpy: any;
 
 const baseGlobalModifiers: IXosKeyboardShortcutBinding[] = [
   {
@@ -55,6 +56,7 @@
       $log = _$log_;
       $transitions = _$transitions_;
       XosSidePanel = _XosSidePanel_;
+      logSpy = spyOn($log, 'warn');
     });
 
     service = new XosKeyboardShortcut($log, $transitions, XosSidePanel);
@@ -167,13 +169,11 @@
     });
 
     it('should not add binding that has an already registered key', () => {
-      function errorFunctionWrapper() {
-        service['registerKeyBinding']({
-          key: 'A',
-          cb: 'cb'
-        }, 'global');
-      }
-      expect(errorFunctionWrapper).toThrow(new Error('[XosKeyboardShortcut] A shortcut for key "a" has already been registered'));
+      service['registerKeyBinding']({
+        key: 'A',
+        cb: 'cb'
+      }, 'global');
+      expect(logSpy).toHaveBeenCalledWith('[XosKeyboardShortcut] A shortcut for key "a" has already been registered');
     });
   });
 });
diff --git a/src/app/core/services/keyboard-shortcut.ts b/src/app/core/services/keyboard-shortcut.ts
index e814943..35af07e 100644
--- a/src/app/core/services/keyboard-shortcut.ts
+++ b/src/app/core/services/keyboard-shortcut.ts
@@ -120,7 +120,8 @@
 
     binding.key = binding.key.toLowerCase();
     if (_.find(this.keyMapping.global, {key: binding.key}) || _.find(this.keyMapping.view, {key: binding.key})) {
-      throw new Error(`[XosKeyboardShortcut] A shortcut for key "${binding.key}" has already been registered`);
+      this.$log.warn(`[XosKeyboardShortcut] A shortcut for key "${binding.key}" has already been registered`);
+      return;
     }
 
     this.$log.debug(`[XosKeyboardShortcut] Registering binding for key: ${binding.key}`);
diff --git a/src/app/datasources/helpers/model-discoverer.service.ts b/src/app/datasources/helpers/model-discoverer.service.ts
index 1078853..ba9cb13 100644
--- a/src/app/datasources/helpers/model-discoverer.service.ts
+++ b/src/app/datasources/helpers/model-discoverer.service.ts
@@ -25,6 +25,7 @@
   discover(): ng.IPromise<boolean>;
   get(modelName: string): IXosModel;
   getApiUrlFromModel(model: IXosModel): string;
+  areModelsLoaded(): boolean;
 }
 
 export class XosModelDiscovererService implements IXosModelDiscovererService {
@@ -41,6 +42,7 @@
   private xosModels: IXosModel[] = []; // list of augmented model definitions;
   private xosServices: string[] = []; // list of loaded services
   private progressBar;
+  private modelsLoaded: boolean = false;
 
   constructor (
     private $log: ng.ILogService,
@@ -56,6 +58,10 @@
     this.progressBar.setColor('#f6a821');
   }
 
+  public areModelsLoaded(): boolean {
+    return this.modelsLoaded;
+  }
+
   public get(modelName: string): IXosModel|null {
     return _.find(this.xosModels, m => m.name === modelName);
   }
@@ -119,6 +125,7 @@
           })
           .finally(() => {
             this.progressBar.complete();
+            this.modelsLoaded = true;
           });
       });
     return d.promise;
diff --git a/src/app/datasources/helpers/search.service.ts b/src/app/datasources/helpers/search.service.ts
index fd038a2..2a77ab6 100644
--- a/src/app/datasources/helpers/search.service.ts
+++ b/src/app/datasources/helpers/search.service.ts
@@ -41,6 +41,7 @@
         return list;
       }, []);
       this.states = _.uniqBy(this.states, 'state');
+      this.$log.debug(`[XosSearchService] States: `, this.states);
     });
   }
 
diff --git a/src/app/extender/services/onboard.service.spec.ts b/src/app/extender/services/onboard.service.spec.ts
index d53f3af..02237cc 100644
--- a/src/app/extender/services/onboard.service.spec.ts
+++ b/src/app/extender/services/onboard.service.spec.ts
@@ -61,6 +61,9 @@
     spyOn($ocLazyLoad, 'load').and.callThrough();
     service = XosOnboarder;
     $timeout = _$timeout_;
+
+    // start the service
+    service.onboard();
   }));
 
   describe('when receive an event', () => {
diff --git a/src/app/extender/services/onboard.service.ts b/src/app/extender/services/onboard.service.ts
index b6a9abc..206a968 100644
--- a/src/app/extender/services/onboard.service.ts
+++ b/src/app/extender/services/onboard.service.ts
@@ -4,11 +4,18 @@
 import {Observable} from 'rxjs';
 
 export interface IXosOnboarder {
-
+  onboard(): ng.IPromise<boolean>;
 }
 
 export class XosOnboarder implements IXosOnboarder {
-  static $inject = ['$timeout', '$log', '$q', 'WebSocket', '$ocLazyLoad', 'XosModelStore'];
+  static $inject = [
+    '$timeout',
+    '$log',
+    '$q',
+    'WebSocket',
+    '$ocLazyLoad',
+    'XosModelStore'
+  ];
 
   constructor(
     private $timeout: ng.ITimeoutService,
@@ -18,23 +25,37 @@
     private $ocLazyLoad: any, // TODO add definition
     private XosModelStore: IXosModelStoreService
   ) {
+
+  }
+
+  public onboard(): ng.IPromise<boolean> {
+    const d = this.$q.defer();
+
     this.$log.info('[XosOnboarder] Setup');
 
     // Load onboarded app
     const ComponentObservable: Observable<any> = this.XosModelStore.query('XOSGuiExtension');
 
     ComponentObservable.subscribe(
-        (component) => {
-          _.forEach(component, (c) => {
-            this.$log.info(`[XosOnboarder] Loading files for app: ${c.name}`);
-            const files = c.files.split(',').map(s => s.trim());
-            this.loadFile(files)
-              .then((res) => {
-                this.$log.info(`[XosOnboarder] All files loaded for app: ${c.name}`);
-              });
-          });
+      (component) => {
+        if (component.length === 0) {
+          return d.resolve();
         }
-      );
+        _.forEach(component, (c) => {
+          this.$log.info(`[XosOnboarder] Loading files for app: ${c.name}`);
+          const files = c.files.split(',').map(s => s.trim());
+          this.loadFile(files)
+            .then((res) => {
+              this.$log.info(`[XosOnboarder] All files loaded for app: ${c.name}`);
+              d.resolve();
+            })
+            .catch(e => {
+              this.$log.info(`[XosOnboarder] Error while onboarding apps: `, e);
+            });
+        });
+      }
+    );
+    return d.promise;
   }
 
   // NOTE files needs to be loaded in order, so async loop!
@@ -43,6 +64,7 @@
       d = this.$q.defer();
     }
     const file = files.shift();
+
     this.$log.info(`[XosOnboarder] Loading file: ${file}`);
     this.$ocLazyLoad.load(file)
       .then((res) => {
diff --git a/src/app/service-graph/components/coarse/coarse.component.ts b/src/app/service-graph/components/coarse/coarse.component.ts
index d02bb23..c7a7ac5 100644
--- a/src/app/service-graph/components/coarse/coarse.component.ts
+++ b/src/app/service-graph/components/coarse/coarse.component.ts
@@ -80,6 +80,9 @@
   }
 
   private _renderGraph() {
+    if (!angular.isDefined(this.graph) || !angular.isDefined(this.graph.nodes) || !angular.isDefined(this.graph.links)) {
+      return;
+    }
     this.addNodeLinksToForceLayout(this.graph);
     this.renderNodes(this.graph.nodes);
     this.renderLinks(this.graph.links);
@@ -87,8 +90,8 @@
 
   private getSvgDimensions(): {width: number, heigth: number} {
     return {
-      width: $('xos-coarse-tenancy-graph svg').width(),
-      heigth: $('xos-coarse-tenancy-graph svg').height()
+      width: $('xos-coarse-tenancy-graph svg').width() || 0,
+      heigth: $('xos-coarse-tenancy-graph svg').height() || 0
     };
   }
 
diff --git a/src/app/service-graph/components/fine-grained/fine-grained.component.ts b/src/app/service-graph/components/fine-grained/fine-grained.component.ts
index 64955ae..d0aea83 100644
--- a/src/app/service-graph/components/fine-grained/fine-grained.component.ts
+++ b/src/app/service-graph/components/fine-grained/fine-grained.component.ts
@@ -83,6 +83,9 @@
   }
 
   private _renderGraph() {
+    if (!angular.isDefined(this.graph) || !angular.isDefined(this.graph.nodes) || !angular.isDefined(this.graph.links)) {
+      return;
+    }
     this.addNodeLinksToForceLayout(this.graph);
     this.renderNodes(this.graph.nodes);
     this.renderLinks(this.graph.links);
diff --git a/src/index.ts b/src/index.ts
index d2f6c72..042f1bd 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -62,16 +62,15 @@
   .factory('CredentialsInterceptor', CredentialsInterceptor)
   .factory('NoHyperlinksInterceptor', NoHyperlinksInterceptor)
   .component('xos', main)
-  .run(function($log: ng.ILogService, $rootScope: ng.IRootScopeService, $transitions: any, StyleConfig: IXosStyleConfig) {
-    $rootScope['favicon'] = `./app/images/brand/${StyleConfig.favicon}`;
-    $transitions.onSuccess({ to: '**' }, (transtion) => {
-      if (transtion.$to().name === 'login') {
-        $rootScope['class'] = 'blank';
-      }
-      else {
-        $rootScope['class'] = '';
-      }
-    });
+  .provider('XosConfig', function(){
+    // save the last visited state before reload
+    const lastVisitedUrl = window.location.hash.replace('#', '');
+    this.$get = [() => {
+      return {
+        lastVisitedUrl
+      };
+    }] ;
+    return this;
   })
   .run((
     $rootScope: ng.IRootScopeService,
@@ -79,14 +78,27 @@
     $log: ng.ILogService,
     $location: ng.ILocationService,
     $state: ng.ui.IStateService,
+    StyleConfig: IXosStyleConfig,
     XosModelDiscoverer: IXosModelDiscovererService,
     AuthService: IXosAuthService,
     XosKeyboardShortcut: IXosKeyboardShortcutService,
-    toastr: ng.toastr.IToastrService,
-    PageTitle: IXosPageTitleService
+    PageTitle: IXosPageTitleService // NOTE this service is not used, but needs to be loaded somewhere
   ) => {
+    // handle style configs
+    $rootScope['favicon'] = `./app/images/brand/${StyleConfig.favicon}`;
+    if ($state.current.data && $state.current.data.specialClass) {
+      $rootScope['class'] = $state.current.data.specialClass;
+    }
+    $transitions.onSuccess({ to: '**' }, (transtion) => {
+      if ($state.current.data && $state.current.data.specialClass) {
+        $rootScope['class'] = transtion.$to().data.specialClass;
+      }
+      else {
+        $rootScope['class'] = '';
+      }
+    });
 
-    // check the user login
+    // check the user login (on route change)
     $transitions.onSuccess({ to: '**' }, (transtion) => {
       if (!AuthService.isAuthenticated()) {
         AuthService.clearUser();
@@ -94,40 +106,10 @@
       }
     });
 
-    // preserve debug=true query string parameter
-    $transitions.onStart({ to: '**' }, (transtion) => {
-      // save location.search so we can add it back after transition is done
-      this.locationSearch = $location.search();
-    });
-
-    $transitions.onSuccess({ to: '**' }, (transtion) => {
-      // restore all query string parameters back to $location.search
-      if (angular.isDefined(this.locationSearch.debug) && this.locationSearch.debug) {
-        $location.search({debug: 'true'});
-      }
-    });
-
-    // save the last visited state before reload
-    const lastRoute = $location.path();
-    const lastQueryString = $location.search();
-
     // if the user is authenticated
     $log.info(`[XOS] Is user authenticated? ${AuthService.isAuthenticated()}`);
     if (AuthService.isAuthenticated()) {
-      XosModelDiscoverer.discover()
-        .then((res) => {
-          if (res) {
-            $log.info('[XOS] All models loaded');
-          }
-          else {
-            $log.info('[XOS] Failed to load some models, moving on.');
-          }
-          // after setting up dynamic routes, redirect to previous state
-          $location.path(lastRoute).search(lastQueryString);
-        })
-        .finally(() => {
-          $rootScope.$emit('xos.core.modelSetup');
-        });
+      $state.go('loader');
     }
     else {
       AuthService.clearUser();
@@ -139,7 +121,6 @@
 
     XosKeyboardShortcut.registerKeyBinding({
       key: 'D',
-      // modifiers: ['Command'],
       cb: () => {
         if (window.localStorage.getItem('debug') === 'true') {
           $log.info(`[XosKeyboardShortcut] Disabling debug`);
diff --git a/src/routes.ts b/src/routes.ts
index 8685ce7..28a5906 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -5,18 +5,25 @@
 /** @ngInject */
 function routesConfig($stateProvider: angular.ui.IStateProvider, $urlRouterProvider: angular.ui.IUrlRouterProvider, $locationProvider: angular.ILocationProvider) {
   $locationProvider.html5Mode(false).hashPrefix('');
-  $urlRouterProvider.otherwise('/');
+  $urlRouterProvider.otherwise('/loader');
 
   // declare here static endpoints,
   // core related endpoints are dynamically generated
   $stateProvider
+    .state('loader', {
+      url: '/loader',
+      component: 'xosLoader',
+      data: {
+        specialClass: 'blank'
+      }
+    })
     .state('xos', {
       abstract: true,
       url: '/',
       component: 'xos'
     })
     .state('xos.dashboard', {
-      url: '',
+      url: 'dashboard',
       parent: 'xos',
       template: '<xos-dashboard></xos-dashboard>'
     })