Merge "updated documentation for 4.0"
diff --git a/src/app/core/debug/debug-model.spec.ts b/src/app/core/debug/debug-model.spec.ts
index 46b6344..b081de0 100644
--- a/src/app/core/debug/debug-model.spec.ts
+++ b/src/app/core/debug/debug-model.spec.ts
@@ -91,7 +91,7 @@
 
       dateFields.forEach(f => {
         const date = isolatedScope.parseField(f, model[f]);
-        expect(date).toEqual('Thu Aug 17 2017 15:45:20 GMT-0700 (PDT)');
+        expect(date).toEqual(new Date(model[f] * 1000).toString());
       });
     });
 
diff --git a/src/app/core/debug/debug-summary.html b/src/app/core/debug/debug-summary.html
index 9807ac9..7e185da 100644
--- a/src/app/core/debug/debug-summary.html
+++ b/src/app/core/debug/debug-summary.html
@@ -41,5 +41,12 @@
                 <i class="fa fa-remove text-danger" ng-hide="vm.debugStatus.modelsTab"></i>
             </td>
         </tr>
+        <tr>
+            <td>Notifications</td>
+            <td class="text-right">
+                <i class="fa fa-check text-success" ng-show="vm.debugStatus.notifications"></i>
+                <i class="fa fa-remove text-danger" ng-hide="vm.debugStatus.notifications"></i>
+            </td>
+        </tr>
     </tbody>
 </table>
\ No newline at end of file
diff --git a/src/app/core/debug/debug.service.spec.ts b/src/app/core/debug/debug.service.spec.ts
index a26d8cc..99f9484 100644
--- a/src/app/core/debug/debug.service.spec.ts
+++ b/src/app/core/debug/debug.service.spec.ts
@@ -53,6 +53,13 @@
     expect(service.status.events).toBeTruthy();
   });
 
+  it('should read the notification status from localStorage', () => {
+    spyOn(window.localStorage, 'getItem')
+      .and.returnValue(null);
+    service = new XosDebugService($log, $scope, XosKeyboardShortcut);
+    expect(service.status.notifications).toBeTruthy();
+  });
+
   it('should disable the global debug status', () => {
     spyOn(window.localStorage, 'getItem')
       .and.returnValue('true');
diff --git a/src/app/core/debug/debug.service.ts b/src/app/core/debug/debug.service.ts
index f23fa06..aee1064 100644
--- a/src/app/core/debug/debug.service.ts
+++ b/src/app/core/debug/debug.service.ts
@@ -20,12 +20,13 @@
   global: boolean;
   events: boolean;
   modelsTab: boolean;
+  notifications: boolean;
 }
 
 export interface IXosDebugService {
   status: IXosDebugStatus;
   setupShortcuts(): void;
-  toggleDebug(type: 'global' | 'events' | 'modelsTab'): void;
+  toggleDebug(type: 'global' | 'events' | 'modelsTab' | 'notifications'): void;
 }
 
 export class XosDebugService implements IXosDebugService {
@@ -35,7 +36,8 @@
   public status: IXosDebugStatus = {
     global: false,
     events: false,
-    modelsTab: false
+    modelsTab: false,
+    notifications: true
   };
 
   constructor (
@@ -51,6 +53,9 @@
 
     const debugModelsTab = window.localStorage.getItem('debug-modelsTab');
     this.status.modelsTab = (debugModelsTab === 'true');
+
+    const notifications = window.localStorage.getItem('debug-notifications');
+    this.status.notifications = (notifications !== null ? notifications === 'true' : true);
   }
 
   public setupShortcuts(): void {
@@ -65,9 +70,15 @@
       cb: () => this.toggleDebug('events'),
       description: 'Toggle debug messages for WS events in browser console'
     }, 'global');
+
+    this.XosKeyboardShortcut.registerKeyBinding({
+        key: 'S',
+        cb: () => this.toggleDebug('notifications'),
+        description: 'Toggle notifications'
+    }, 'global');
   }
 
-  public toggleDebug(type: 'global' | 'events' | 'modelsTab'): void {
+  public toggleDebug(type: 'global' | 'events' | 'modelsTab' | 'notifications'): void {
     if (window.localStorage.getItem(`debug-${type}`) === 'true') {
       this.$log.info(`[XosDebug] Disabling ${type} debug`);
       window.localStorage.setItem(`debug-${type}`, 'false');
diff --git a/src/app/core/header/header.spec.ts b/src/app/core/header/header.spec.ts
index eb236b1..a4a7d90 100644
--- a/src/app/core/header/header.spec.ts
+++ b/src/app/core/header/header.spec.ts
@@ -24,6 +24,7 @@
 import 'angular-mocks';
 import {xosHeader, INotification} from './header';
 import {Subject} from 'rxjs';
+import {IXosDebugService} from '../debug/debug.service';
 
 let element, scope: angular.IRootScopeService, compile: ng.ICompileService, isolatedScope;
 const events = new Subject();
@@ -79,6 +80,17 @@
   registerKeyBinding: jasmine.createSpy('registerKeyBinding')
 };
 
+const MockXosDebug: IXosDebugService = {
+  status: {
+    global: false,
+    events: false,
+    modelsTab: false,
+    notifications: true
+  },
+  setupShortcuts: jasmine.createSpy('debug.createShortcuts'),
+  toggleDebug: jasmine.createSpy('debug.toggleDebug')
+};
+
 describe('header component', () => {
   beforeEach(() => {
     angular
@@ -96,7 +108,8 @@
       .value('StyleConfig', {
         logo: 'cord-logo.png',
       })
-      .value('SearchService', {});
+      .value('SearchService', {})
+      .value('XosDebug', MockXosDebug);
 
     angular.mock.module('xosHeader');
   });
@@ -146,7 +159,16 @@
     });
   });
 
-  it('should display a toastr for a new notification', () => {
+  it('should not display a toastr for a new notification (if notifications are disabled)', () => {
+      MockXosDebug.status.notifications = false;
+      sendEvent(infoNotification);
+      scope.$digest();
+
+      expect(MockToastr.info).not.toHaveBeenCalled();
+  });
+
+  it('should display a toastr for a new notification (if notifications are enabled)', () => {
+    MockXosDebug.status.notifications = true;
     sendEvent(infoNotification);
     scope.$digest();
 
diff --git a/src/app/core/header/header.ts b/src/app/core/header/header.ts
index a465f8f..26f6fd0 100644
--- a/src/app/core/header/header.ts
+++ b/src/app/core/header/header.ts
@@ -28,6 +28,7 @@
 import {IXosKeyboardShortcutService} from '../services/keyboard-shortcut';
 import {Subscription} from 'rxjs';
 import {IXosConfigHelpersService} from '../services/helpers/config.helpers';
+import {IXosDebugService} from '../debug/debug.service';
 
 export interface INotification extends IWSEvent {
   viewed?: boolean;
@@ -47,7 +48,8 @@
     'StyleConfig',
     'SearchService',
     'XosKeyboardShortcut',
-    'ConfigHelpers'
+    'ConfigHelpers',
+    'XosDebug'
   ];
   public notifications: INotification[] = [];
   public newNotifications: INotification[] = [];
@@ -73,7 +75,8 @@
     private StyleConfig: IXosStyleConfig,
     private SearchService: IXosSearchService,
     private XosKeyboardShortcut: IXosKeyboardShortcutService,
-    private ConfigHelpers: IXosConfigHelpersService
+    private ConfigHelpers: IXosConfigHelpersService,
+    private XosDebugService: IXosDebugService
   ) {
 
   }
@@ -123,6 +126,12 @@
         (event: IWSEvent) => {
           this.$scope.$evalAsync(() => {
 
+            if (!this.XosDebugService.status.notifications) {
+              // NOTE: notifications can be disabled
+              return;
+            }
+
+
             if (event.model === 'Diag') {
               // NOTE skip notifications for Diag model
               return;
diff --git a/src/app/core/services/navigation.spec.ts b/src/app/core/services/navigation.spec.ts
index bc27772..b9cb84c 100644
--- a/src/app/core/services/navigation.spec.ts
+++ b/src/app/core/services/navigation.spec.ts
@@ -116,7 +116,7 @@
     service.add(testRoute);
     service.add(testRoute);
     expect($log.warn).toHaveBeenCalled();
-    expect($log.warn).toHaveBeenCalledWith(`[XosNavigation] Route with label: ${testRoute.label}, state: ${testRoute.state} and parent: ${testRoute.parent} already exist`);
+    expect($log.warn).toHaveBeenCalledWith(`[XosNavigation] Route with label: ${testRoute.label}, state: ${testRoute.state} and parent: ${testRoute.parent} already exists`);
     expect(service.query()).toEqual(defaultRoutes.concat([testRoute]));
   });
 });
diff --git a/src/app/core/services/navigation.ts b/src/app/core/services/navigation.ts
index e7fa4ed..660c144 100644
--- a/src/app/core/services/navigation.ts
+++ b/src/app/core/services/navigation.ts
@@ -71,24 +71,27 @@
     return this.routes;
   }
 
-  add(route: IXosNavigationRoute) {
+  add(route: IXosNavigationRoute, override: boolean = false) {
     if (angular.isDefined(route.state) && angular.isDefined(route.url)) {
       throw new Error('[XosNavigation] You can\'t provide both state and url');
     }
 
     // NOTE factor this out in a separate method an eventually use recursion since we can nest more routes
+    let preExisting = null;
     const routeExist = _.findIndex(this.routes, (r: IXosNavigationRoute) => {
-      if (r.label === route.label && r.state === route.state && r.parent === route.parent) {
+      if (r.label === route.label && (r.state === route.state || override) && r.parent === route.parent) {
+        preExisting = r;
         return true;
       }
       else if (_.findIndex(r.children, route) > -1) {
+        preExisting = r;
         return true;
       }
       return false;
     }) > -1;
 
-    if (routeExist) {
-      this.$log.warn(`[XosNavigation] Route with label: ${route.label}, state: ${route.state} and parent: ${route.parent} already exist`);
+    if (routeExist && !override) {
+      this.$log.warn(`[XosNavigation] Route with label: ${route.label}, state: ${route.state} and parent: ${route.parent} already exists`);
       return;
     }
 
@@ -97,6 +100,9 @@
       const parentRoute = _.find(this.routes, {state: route.parent});
       if (angular.isDefined(parentRoute)) {
         if (angular.isArray(parentRoute.children)) {
+          if (override) {
+            _.remove(parentRoute.children, r => r === preExisting);
+          }
           parentRoute.children.push(route);
         }
         else {
@@ -104,11 +110,14 @@
         }
       }
       else {
-        this.$log.warn(`[XosNavigation] Parent State (${route.parent}) for state: ${route.state} does not exists`);
+        this.$log.warn(`[XosNavigation] Parent State (${route.parent}) for state: ${route.state} does not exist`);
         return;
       }
     }
     else {
+      if (override) {
+        _.remove(this.routes, r => r === preExisting);
+      }
       this.routes.push(route);
     }
   }
diff --git a/src/app/datasources/helpers/store.helpers.spec.ts b/src/app/datasources/helpers/store.helpers.spec.ts
index f3b2321..2f617bc 100644
--- a/src/app/datasources/helpers/store.helpers.spec.ts
+++ b/src/app/datasources/helpers/store.helpers.spec.ts
@@ -26,6 +26,7 @@
 import {ConfigHelpers} from '../../core/services/helpers/config.helpers';
 import {AuthService} from '../rest/auth.rest';
 import {IXosModeldefsCache} from './modeldefs.service';
+import {XosFormHelpers} from '../../core/form/form-helpers';
 
 let service: IStoreHelpersService;
 let subject: BehaviorSubject<any>;
@@ -42,6 +43,7 @@
       .service('ModelRest', ModelRest) // NOTE evaluate mock
       .service('StoreHelpers', StoreHelpers)
       .service('AuthService', AuthService)
+      .service('XosFormHelpers', XosFormHelpers)
       .value('XosModeldefsCache', {
         get: jasmine.createSpy('XosModeldefsCache.get'),
         getApiUrlFromModel: jasmine.createSpy('XosModeldefsCache.getApiUrlFromModel')
diff --git a/src/app/datasources/rest/model.rest.spec.ts b/src/app/datasources/rest/model.rest.spec.ts
index d545ac2..da4f2f4 100644
--- a/src/app/datasources/rest/model.rest.spec.ts
+++ b/src/app/datasources/rest/model.rest.spec.ts
@@ -22,6 +22,7 @@
 import 'angular-cookies';
 import {IXosResourceService} from './model.rest';
 import {xosDataSources} from '../index';
+import {IXosFormHelpersService} from '../../core/form/form-helpers';
 
 let service: IXosResourceService;
 let resource: ng.resource.IResourceClass<any>;
@@ -34,6 +35,10 @@
   websocketClient: 'http://xos-test:3000'
 };
 
+const MockFormHelpers: IXosFormHelpersService = {
+  _getFieldFormat: () => 'date'
+};
+
 describe('The ModelRest service', () => {
 
   beforeEach(angular.mock.module(xosDataSources));
@@ -41,7 +46,8 @@
   beforeEach(() => {
 
     angular.module(xosDataSources)
-      .constant('AppConfig', MockAppCfg);
+      .constant('AppConfig', MockAppCfg)
+      .value('XosFormHelpers', MockFormHelpers);
 
     angular.mock.module(xosDataSources);
   });
@@ -99,4 +105,31 @@
     $scope.$apply();
     httpBackend.flush();
   });
+
+  describe('when saving a model', () => {
+
+    let item, date;
+    const timestamp = 1509552402000;
+
+    beforeEach(() => {
+      httpBackend.expectPOST(`${MockAppCfg.apiEndpoint}/core/test`)
+        .respond((method, url, req) => {
+          return [200, req];
+        });
+      resource = service.getResource('/core/test');
+      date = new Date(timestamp);
+      item = new resource({date: date.toString()});
+    });
+
+    xit('should convert dates to timestamps', (done) => {
+      item.$save()
+        .then(res => {
+          expect(res.date).toEqual(timestamp);
+          done();
+        });
+      $scope.$apply();
+      httpBackend.flush();
+      done();
+    });
+  });
 });
diff --git a/src/app/datasources/rest/model.rest.ts b/src/app/datasources/rest/model.rest.ts
index 6c72d99..f390616 100644
--- a/src/app/datasources/rest/model.rest.ts
+++ b/src/app/datasources/rest/model.rest.ts
@@ -15,37 +15,48 @@
  * limitations under the License.
  */
 
-
+import * as _ from 'lodash';
 import {IXosAppConfig} from '../../../index';
+import {IXosFormHelpersService} from '../../core/form/form-helpers';
+
 export interface IXosResourceService {
   getResource(url: string): ng.resource.IResourceClass<any>;
 }
 
 export class ModelRest implements IXosResourceService {
-  static $inject = ['$resource', 'AppConfig'];
+  static $inject = ['$resource', 'AppConfig', 'XosFormHelpers'];
 
   /** @ngInject */
   constructor(
     private $resource: ng.resource.IResourceService,
-    private AppConfig: IXosAppConfig
+    private AppConfig: IXosAppConfig,
+    private XosFormHelpers: IXosFormHelpersService
   ) {
 
   }
 
   public getResource(url: string): ng.resource.IResourceClass<ng.resource.IResource<any>> {
+    const self = this;
     const resource: angular.resource.IResourceClass<any> = this.$resource(`${this.AppConfig.apiEndpoint}${url}/:id/`, {id: '@id'}, {
       update: { method: 'PUT' },
       query: {
         method: 'GET',
         isArray: true,
         transformResponse: (res) => {
-          // FIXME chameleon return everything inside "items"
           return res.items ? res.items : res;
         }
       }
     });
 
     resource.prototype.$save = function() {
+
+      // NOTE converting dates back to timestamp
+      _.forEach(Object.keys(this), (k: string) => {
+        if (self.XosFormHelpers._getFieldFormat(this[k]) === 'date') {
+          this[k] = new Date(this[k]).getTime();
+        }
+      });
+
       if (this.id) {
         return this.$update();
       } else {
diff --git a/src/app/datasources/stores/model.store.spec.ts b/src/app/datasources/stores/model.store.spec.ts
index 09dc3f9..ce3e2cd 100644
--- a/src/app/datasources/stores/model.store.spec.ts
+++ b/src/app/datasources/stores/model.store.spec.ts
@@ -28,6 +28,7 @@
 import {AuthService} from '../rest/auth.rest';
 import {XosDebouncer} from '../../core/services/helpers/debounce.helper';
 import {IXosModeldefsCache} from '../helpers/modeldefs.service';
+import {XosFormHelpers} from '../../core/form/form-helpers';
 
 let service: IXosModelStoreService;
 let httpBackend: ng.IHttpBackendService;
@@ -70,6 +71,7 @@
       .service('XosModelStore', XosModelStore)
       .service('ConfigHelpers', ConfigHelpers) // TODO mock
       .service('AuthService', AuthService)
+      .service('XosFormHelpers', XosFormHelpers)
       .constant('AppConfig', MockAppCfg)
       .value('XosModeldefsCache', {
         get: jasmine.createSpy('XosModeldefsCache.get').and.returnValue({}),
diff --git a/src/app/views/dashboard/dashboard.ts b/src/app/views/dashboard/dashboard.ts
index 45bd138..285c2df 100644
--- a/src/app/views/dashboard/dashboard.ts
+++ b/src/app/views/dashboard/dashboard.ts
@@ -18,37 +18,53 @@
 
 import {IXosModelStoreService} from '../../datasources/stores/model.store';
 import {IXosAuthService} from '../../datasources/rest/auth.rest';
+import {Subscription} from 'rxjs/Subscription';
+
+
 class DashboardController {
-  static $inject = ['$scope', '$state', 'XosModelStore', 'AuthService'];
+  static $inject = [
+    '$log',
+    '$scope',
+    '$state',
+    'XosModelStore',
+    'AuthService'
+  ];
 
   public nodes: number;
   public slices: number;
   public instances: number;
 
+  private nodeSubscription: Subscription;
+  private sliceSubscription: Subscription;
+  private instanceSubscription: Subscription;
+
   constructor(
+    private $log: ng.ILogService,
     private $scope: ng.IScope,
     private $state: ng.ui.IStateService,
     private store: IXosModelStoreService,
     private auth: IXosAuthService
   ) {
 
+    this.$log.info(`[XosDashboardView] Setup`);
+
     if (!this.auth.isAuthenticated()) {
       this.$state.go('login');
     }
     else {
-      this.store.query('Node')
+      this.nodeSubscription = this.store.query('Node')
         .subscribe((event) => {
           this.$scope.$evalAsync(() => {
             this.nodes = event.length;
           });
         });
-      this.store.query('Instance')
+      this.instanceSubscription = this.store.query('Instance')
         .subscribe((event) => {
           this.$scope.$evalAsync(() => {
             this.instances = event.length;
           });
         });
-      this.store.query('Slice')
+      this.sliceSubscription = this.store.query('Slice')
         .subscribe((event) => {
           this.$scope.$evalAsync(() => {
             this.slices = event.length;
@@ -59,6 +75,12 @@
       this.slices = 0;
     }
   }
+
+  $onDestroy () {
+    this.nodeSubscription.unsubscribe();
+    this.instanceSubscription.unsubscribe();
+    this.sliceSubscription.unsubscribe();
+  }
 }
 
 export const xosDashboard: angular.IComponentOptions = {
diff --git a/src/index.ts b/src/index.ts
index 2a287d5..68920b0 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -83,11 +83,12 @@
   .component('xos', main)
   .provider('XosConfig', function(){
     // save the last visited state before reload
-    const lastVisitedUrl = window.location.hash.replace('#', '');
+    let lastVisitedUrl = window.location.hash.replace('#', '');
     this.$get = [() => {
-      return {
-        lastVisitedUrl
-      };
+      if (lastVisitedUrl === '/login' || lastVisitedUrl === '/loader') {
+        lastVisitedUrl = '/dashboard';
+      }
+      return {lastVisitedUrl};
     }] ;
     return this;
   })
@@ -129,11 +130,15 @@
     // if the user is authenticated
     $log.info(`[XOS] Is user authenticated? ${AuthService.isAuthenticated()}`);
     if (AuthService.isAuthenticated()) {
+      $log.info(`[XOS] Redirect to "loader"`);
       $state.go('loader');
+      $rootScope.$apply();
     }
     else {
       AuthService.clearUser();
+      $log.info(`[XOS] Redirect to "login"`);
       $state.go('login');
+      $rootScope.$apply();
     }
 
     // register keyboard shortcut