Dinamically generate views for CORE Models

Change-Id: Ib1d042f366f916c2ba8513ee62014e7256ceb53d
diff --git a/conf/webpack-dist.conf.js b/conf/webpack-dist.conf.js
index 33f0acb..e34afa4 100644
--- a/conf/webpack-dist.conf.js
+++ b/conf/webpack-dist.conf.js
@@ -46,7 +46,8 @@
       template: conf.path.src('index.html')
     }),
     new webpack.optimize.UglifyJsPlugin({
-      compress: {unused: true, dead_code: true, warnings: false} // eslint-disable-line camelcase
+      compress: {unused: true, dead_code: true, warnings: false}, // eslint-disable-line camelcase
+      mangle: false // NOTE mangling was breaking the build
     }),
     new ExtractTextPlugin('index-[contenthash].css'),
     new webpack.optimize.CommonsChunkPlugin({name: 'vendor'})
diff --git a/src/app/core/header/header.ts b/src/app/core/header/header.ts
index 140abff..c91ef5f 100644
--- a/src/app/core/header/header.ts
+++ b/src/app/core/header/header.ts
@@ -1,19 +1,20 @@
 import './header.scss';
 import {StyleConfig} from '../../config/style.config';
-import {IStoreService} from '../../datasources/stores/slices.store';
 import {IWSEvent} from '../../datasources/websocket/global';
+import {IStoreService} from '../../datasources/stores/synchronizer.store';
 
 interface INotification extends IWSEvent {
   viewed?: boolean;
 }
 
 class HeaderController {
-  static $inject = ['SynchronizerStore'];
+  static $inject = ['$scope', 'SynchronizerStore'];
   public title: string;
   public notifications: INotification[] = [];
   public newNotifications: INotification[] = [];
 
   constructor(
+    private $scope: angular.IScope,
     private syncStore: IStoreService
   ) {
     this.title = StyleConfig.projectName;
@@ -21,8 +22,10 @@
     this.syncStore.query()
       .subscribe(
         (event: IWSEvent) => {
-          this.notifications.unshift(event);
-          this.newNotifications = this.getNewNotifications(this.notifications);
+          $scope.$evalAsync(() => {
+            this.notifications.unshift(event);
+            this.newNotifications = this.getNewNotifications(this.notifications);
+          });
         }
       );
   }
diff --git a/src/app/core/index.ts b/src/app/core/index.ts
index ee73f99..270541b 100644
--- a/src/app/core/index.ts
+++ b/src/app/core/index.ts
@@ -4,12 +4,16 @@
 import routesConfig from './routes';
 import {xosLogin} from './login/login';
 import {xosTable} from './table/table';
+import {RuntimeStates} from './services/runtime-states';
+import {NavigationService} from './services/navigation';
 
 export const xosCore = 'xosCore';
 
 angular
   .module('xosCore', ['ui.router'])
   .config(routesConfig)
+  .provider('RuntimeStates', RuntimeStates)
+  .service('NavigationService', NavigationService)
   .component('xosHeader', xosHeader)
   .component('xosFooter', xosFooter)
   .component('xosNav', xosNav)
diff --git a/src/app/core/nav/nav.html b/src/app/core/nav/nav.html
index 6256369..2bcdadd 100644
--- a/src/app/core/nav/nav.html
+++ b/src/app/core/nav/nav.html
@@ -1,7 +1,8 @@
 <div class="nav">
   <ul>
-    <li ng-repeat="route in vm.routes" ui-sref-active="active">
-      <a ui-sref="{{route.state}}">{{route.label}}</a>
+    <li ng-repeat="route in vm.routes" ui-sref-active="active" ng-class="vm.isRouteActive(route)">
+      <a ng-if="route.state" ui-sref="{{route.state}}">{{route.label}}</a>
+      <a ng-if="route.url" href="#/{{route.url}}">{{route.label}}</a>
     </li>
   </ul>
 </div>
diff --git a/src/app/core/nav/nav.scss b/src/app/core/nav/nav.scss
index 8591c15..5c2c85c 100644
--- a/src/app/core/nav/nav.scss
+++ b/src/app/core/nav/nav.scss
@@ -2,8 +2,9 @@
   display: flex;
   flex: 1;
   flex-direction: column;
-  flex-basis: 10%;
+  flex-basis: 15%;
   background: darken(grey, 10);
+  overflow-y: scroll;
 
   ul {
     list-style: none;
diff --git a/src/app/core/nav/nav.ts b/src/app/core/nav/nav.ts
index 82d9d64..d83f859 100644
--- a/src/app/core/nav/nav.ts
+++ b/src/app/core/nav/nav.ts
@@ -1,32 +1,19 @@
 import './nav.scss';
-
-export interface INavItem {
-  label: string;
-  state: string;
-}
+import {IXosNavigationService, IXosNavigationRoute} from '../services/navigation';
 
 class NavCtrl {
-  public routes: INavItem[];
+  static $inject = ['$state', 'NavigationService'];
+  public routes: IXosNavigationRoute[];
 
-  constructor() {
-    this.routes = [
-      {
-        label: 'Home',
-        state: 'xos.dashboard'
-      },
-      {
-        label: 'Instances',
-        state: 'xos.instances'
-      },
-      {
-        label: 'Slices',
-        state: 'xos.slices'
-      },
-      {
-        label: 'Nodes',
-        state: 'xos.nodes'
-      }
-    ];
+  constructor(
+    private $state: angular.ui.IStateService,
+    private navigationService: IXosNavigationService
+  ) {
+    this.routes = this.navigationService.query();
+  }
+
+  isRouteActive(route: IXosNavigationRoute) {
+    return this.$state.current.url === route.url ? 'active' : '';
   }
 }
 
diff --git a/src/app/core/services/navigation.ts b/src/app/core/services/navigation.ts
new file mode 100644
index 0000000..8cb3b66
--- /dev/null
+++ b/src/app/core/services/navigation.ts
@@ -0,0 +1,31 @@
+export interface IXosNavigationRoute {
+  label: string;
+  state?: string;
+  url?: string;
+}
+
+export interface IXosNavigationService {
+  query(): IXosNavigationRoute[];
+  add(route: IXosNavigationRoute): void;
+}
+
+export class NavigationService {
+  private routes: IXosNavigationRoute[];
+
+  constructor() {
+    this.routes = [
+      {
+        label: 'Home',
+        state: 'xos.dashboard'
+      }
+    ];
+  }
+
+  query() {
+    return this.routes;
+  }
+
+  add(route: IXosNavigationRoute) {
+    this.routes.push(route);
+  }
+}
diff --git a/src/app/core/services/runtime-states.ts b/src/app/core/services/runtime-states.ts
new file mode 100644
index 0000000..401075a
--- /dev/null
+++ b/src/app/core/services/runtime-states.ts
@@ -0,0 +1,15 @@
+import {IXosState} from '../../../index';
+export interface IRuntimeStatesService {
+  addState(name: string, state: angular.ui.IState): void;
+}
+
+export function RuntimeStates($stateProvider: angular.ui.IStateProvider): angular.IServiceProvider {
+  this.$get = function($state: angular.ui.IStateService) { // for example
+    return {
+      addState: function(name: string, state: IXosState) {
+        $stateProvider.state(name, state);
+      }
+    };
+  };
+  return this;
+}
diff --git a/src/app/core/table/table.ts b/src/app/core/table/table.ts
index a837600..1d40092 100644
--- a/src/app/core/table/table.ts
+++ b/src/app/core/table/table.ts
@@ -11,7 +11,7 @@
 
 export interface IXosTableCfg {
   columns: any[];
-  order: IXosTableCgfOrder; // | boolean;
+  order?: IXosTableCgfOrder; // | boolean;
 }
 
 class TableCtrl {
diff --git a/src/app/datasources/index.ts b/src/app/datasources/index.ts
index 535937b..936920e 100644
--- a/src/app/datasources/index.ts
+++ b/src/app/datasources/index.ts
@@ -1,17 +1,18 @@
 import {CoreRest} from './rest/core.rest';
-import {SlicesRest} from './rest/slices.rest';
+import {ModelRest} from './rest/model.rest';
 import {AuthService} from './rest/auth.rest';
 import {WebSocketEvent} from './websocket/global';
-import {SliceStore} from './stores/slices.store';
+import {ModelStore} from './stores/model.store';
 import {StoreHelpers} from './helpers/store.helpers';
 import {SynchronizerStore} from './stores/synchronizer.store';
+import {ModeldefsService} from './rest/modeldefs.rest';
 
-export const xosRest = 'xosDataSources';
+export const xosDataSources = 'xosDataSources';
 
 angular
   .module('xosDataSources', ['ngCookies'])
   .service('CoreRest', CoreRest)
-  .service('SlicesRest', SlicesRest)
+  .service('ModelRest', ModelRest)
   .service('AuthService', AuthService)
   .service('WebSocket', WebSocketEvent);
 
@@ -19,4 +20,5 @@
   .module('xosDataSources')
   .service('StoreHelpers', StoreHelpers)
   .service('SynchronizerStore', SynchronizerStore)
-  .service('SlicesStore', SliceStore);
+  .service('ModelStore', ModelStore)
+  .service('ModelDefs', ModeldefsService);
diff --git a/src/app/datasources/rest/model.rest.ts b/src/app/datasources/rest/model.rest.ts
new file mode 100644
index 0000000..45431d4
--- /dev/null
+++ b/src/app/datasources/rest/model.rest.ts
@@ -0,0 +1,21 @@
+import {AppConfig} from '../../config/app.config';
+
+export interface IXosResourceService {
+  getResource(url: string): ng.resource.IResourceClass<any>;
+}
+
+export class ModelRest implements IXosResourceService {
+  static $inject = ['$resource'];
+  private resource: angular.resource.IResourceClass<any>;
+
+  /** @ngInject */
+  constructor(
+    private $resource: ng.resource.IResourceService
+  ) {
+
+  }
+
+  public getResource(url: string): ng.resource.IResourceClass<ng.resource.IResource<any>> {
+    return this.resource = this.$resource(`${AppConfig.apiEndpoint}${url}`);
+  }
+}
diff --git a/src/app/datasources/rest/modeldefs.rest.ts b/src/app/datasources/rest/modeldefs.rest.ts
new file mode 100644
index 0000000..d05aa6d
--- /dev/null
+++ b/src/app/datasources/rest/modeldefs.rest.ts
@@ -0,0 +1,36 @@
+import {AppConfig} from '../../config/app.config';
+
+interface IModeldefField {
+  name: string;
+  type: string;
+}
+
+export interface IModeldef {
+  fields: IModeldefField[];
+  relations: string[];
+  name: string;
+}
+
+export interface IModeldefsService {
+  get(): Promise<IModeldef[]>;
+}
+
+export class ModeldefsService {
+  constructor(
+    private $http: angular.IHttpService,
+    private $q: angular.IQService,
+  ) {
+  }
+
+  public get(): Promise<any> {
+    const d = this.$q.defer();
+    this.$http.get(`${AppConfig.apiEndpoint}/utility/modeldefs/`)
+      .then((res) => {
+        d.resolve(res.data);
+      })
+      .catch(e => {
+        d.reject(e);
+      });
+    return d.promise;
+  }
+}
diff --git a/src/app/datasources/rest/slices.rest.ts b/src/app/datasources/rest/slices.rest.ts
deleted file mode 100644
index a5b7e5c..0000000
--- a/src/app/datasources/rest/slices.rest.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import {AppConfig} from '../../config/app.config';
-
-export interface IXosResourceService {
-  getResource(): ng.resource.IResourceClass<any>;
-}
-
-export class SlicesRest implements IXosResourceService {
-  static $inject = ['$resource'];
-  private resource: angular.resource.IResourceClass<any>;
-
-  /** @ngInject */
-  constructor(
-    private $resource: ng.resource.IResourceService
-  ) {
-    this.resource = this.$resource(`${AppConfig.apiEndpoint}/core/slices/`);
-  }
-
-  public getResource(): ng.resource.IResourceClass<ng.resource.IResource<any>> {
-    return this.resource;
-  }
-}
diff --git a/src/app/datasources/stores/model.store.ts b/src/app/datasources/stores/model.store.ts
new file mode 100644
index 0000000..f31d571
--- /dev/null
+++ b/src/app/datasources/stores/model.store.ts
@@ -0,0 +1,44 @@
+/// <reference path="../../../../typings/index.d.ts"/>
+
+import {BehaviorSubject, Observable} from 'rxjs/Rx';
+import {IWSEvent, IWSEventService} from '../websocket/global';
+import {IXosResourceService} from '../rest/model.rest';
+import {IStoreHelpersService} from '../helpers/store.helpers';
+
+export interface  IModelStoreService {
+  query(model: string): Observable<any>;
+}
+
+export class ModelStore {
+  static $inject = ['WebSocket', 'StoreHelpers', 'ModelRest'];
+  private _slices: BehaviorSubject<any[]> = new BehaviorSubject([]);
+  constructor(
+    private webSocket: IWSEventService,
+    private storeHelpers: IStoreHelpersService,
+    private sliceService: IXosResourceService
+  ) {
+  }
+
+  query(model: string) {
+    this.loadInitialData(model);
+    this.webSocket.list()
+      .filter((e: IWSEvent) => e.model === model)
+      .subscribe(
+        (event: IWSEvent) => {
+          this.storeHelpers.updateCollection(event, this._slices);
+        }
+      );
+    return this._slices.asObservable();
+  }
+
+  private loadInitialData(model: string) {
+    const endpoint = `/core/${model.toLowerCase()}s/`;
+    this.sliceService.getResource(endpoint).query().$promise
+      .then(
+        res => {
+          this._slices.next(res);
+        },
+        err => console.log(`Error retrieving ${model}`, err)
+      );
+  }
+}
diff --git a/src/app/datasources/stores/slices.store.ts b/src/app/datasources/stores/slices.store.ts
deleted file mode 100644
index 9c3389f..0000000
--- a/src/app/datasources/stores/slices.store.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/// <reference path="../../../../typings/index.d.ts"/>
-
-import {BehaviorSubject, Observable} from 'rxjs/Rx';
-import {IWSEvent, IWSEventService} from '../websocket/global';
-import {IXosResourceService} from '../rest/slices.rest';
-import {IStoreHelpersService} from '../helpers/store.helpers';
-
-export interface  IStoreService {
-  query(): Observable<any>;
-}
-
-export class SliceStore {
-  static $inject = ['WebSocket', 'StoreHelpers', 'SlicesRest'];
-  private _slices: BehaviorSubject<any[]> = new BehaviorSubject([]);
-  constructor(
-    private webSocket: IWSEventService,
-    private storeHelpers: IStoreHelpersService,
-    private sliceService: IXosResourceService
-  ) {
-    this.loadInitialData();
-    this.webSocket.list()
-      .filter((e: IWSEvent) => e.model === 'Slice')
-      .subscribe(
-        (event: IWSEvent) => {
-          this.storeHelpers.updateCollection(event, this._slices);
-        }
-      );
-  }
-
-  query() {
-    return this._slices.asObservable();
-  }
-
-  private loadInitialData() {
-    this.sliceService.getResource().query().$promise
-      .then(
-        res => {
-          this._slices.next(res);
-        },
-        err => console.log('Error retrieving Slices', err)
-      );
-  }
-}
diff --git a/src/app/datasources/stores/synchronizer.store.ts b/src/app/datasources/stores/synchronizer.store.ts
index 598d23c..33a0c39 100644
--- a/src/app/datasources/stores/synchronizer.store.ts
+++ b/src/app/datasources/stores/synchronizer.store.ts
@@ -1,8 +1,12 @@
 /// <reference path="../../../../typings/index.d.ts"/>
 
-import {Subject} from 'rxjs/Rx';
+import {Subject, Observable} from 'rxjs/Rx';
 import {IWSEvent, IWSEventService} from '../websocket/global';
 
+export interface  IStoreService {
+  query(): Observable<any>;
+}
+
 export class SynchronizerStore {
   static $inject = ['WebSocket'];
   private _notifications: Subject<IWSEvent> = new Subject();
diff --git a/src/app/views/crud/crud.ts b/src/app/views/crud/crud.ts
index 853669c..4663b59 100644
--- a/src/app/views/crud/crud.ts
+++ b/src/app/views/crud/crud.ts
@@ -1,34 +1,28 @@
 import {IXosTableCfg} from '../../core/table/table';
-import {IStoreService} from '../../datasources/stores/slices.store';
+import {IModelStoreService} from '../../datasources/stores/model.store';
 export interface IXosCrudData {
-  title: string;
-  store: string;
+  model: string;
   xosTableCfg: IXosTableCfg;
 }
 
 class CrudController {
-  // TODO dynamically inject store
-  static $inject = ['$state', '$injector', '$scope'];
+  static $inject = ['$state', '$scope', 'ModelStore'];
 
   public data: IXosCrudData;
   public tableCfg: IXosTableCfg;
   public title: string;
-  public storeName: string;
-  public store: IStoreService;
   public tableData: any[];
 
   constructor(
     private $state: angular.ui.IStateService,
-    private $injector: angular.Injectable<any>,
-    private $scope: angular.IScope
+    private $scope: angular.IScope,
+    private store: IModelStoreService
   ) {
     this.data = this.$state.current.data;
     this.tableCfg = this.data.xosTableCfg;
-    this.title = this.data.title;
-    this.storeName = this.data.store;
-    this.store = this.$injector.get(this.storeName);
+    this.title = this.data.model;
 
-    this.store.query()
+    this.store.query(this.data.model)
       .subscribe(
         (event) => {
           // NOTE Observable mess with $digest cycles, we need to schedule the expression later
diff --git a/src/index.scss b/src/index.scss
index 8813c49..84c8ea2 100644
--- a/src/index.scss
+++ b/src/index.scss
@@ -20,7 +20,7 @@
 .main-container {
   display: flex;
   flex-direction: column;
-  min-height: 100%;
+  height: 100%;
 }
 .main {
   flex: 1;
@@ -31,10 +31,11 @@
     display: flex;
     flex: 1;
     flex-direction: column;
-    flex-basis: 90%;
+    flex-basis: 85%;
     background: darken(grey, 25);
     padding: 20px;
     color: #eee;
+    overflow-y: scroll;
   }
 }
 
diff --git a/src/index.ts b/src/index.ts
index c654e52..bdb09ab 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -11,15 +11,83 @@
 
 import './index.scss';
 import {xosCore} from './app/core/index';
-import {xosRest} from './app/datasources/index';
+import {xosDataSources} from './app/datasources/index';
 import {xosViews} from './app/views/index';
-import {interceptorConfig, userStatusInterceptor, CredentialsInterceptor} from './interceptors';
+import {
+  interceptorConfig, userStatusInterceptor, CredentialsInterceptor,
+  NoHyperlinksInterceptor
+} from './interceptors';
+import {IRuntimeStatesService} from './app/core/services/runtime-states';
+import {IModeldefsService, IModeldef} from './app/datasources/rest/modeldefs.rest';
+import {IXosCrudData} from './app/views/crud/crud';
+import * as _ from 'lodash';
+import {IXosNavigationService} from './app/core/services/navigation';
+
+export interface IXosState extends angular.ui.IState {
+  data: IXosCrudData;
+};
+
+const modeldefToTableCfg = (fields: {name: string, type: string}[]): any[] => {
+  const excluded_fields = [
+    'created',
+    'updated',
+    'enacted',
+    'policed',
+    'backend_register',
+    'deleted',
+    'write_protect',
+    'lazy_blocked',
+    'no_sync',
+    'no_policy',
+    'omf_friendly',
+    'enabled'
+  ];
+  const cfg =  _.map(fields, (f) => {
+    if (excluded_fields.indexOf(f.name) > -1) {
+      return;
+    }
+    return {
+      label: `${f.name}`,
+      prop: f.name
+    };
+  })
+    .filter(v => angular.isDefined(v));
+
+  return cfg;
+};
 
 angular
-  .module('app', [xosCore, xosRest, xosViews, 'ui.router', 'ngResource'])
+  .module('app', [xosCore, xosDataSources, xosViews, 'ui.router', 'ngResource'])
   .config(routesConfig)
   .config(interceptorConfig)
   .factory('UserStatusInterceptor', userStatusInterceptor)
   .factory('CredentialsInterceptor', CredentialsInterceptor)
-  .component('xos', main);
+  .factory('NoHyperlinksInterceptor', NoHyperlinksInterceptor)
+  .component('xos', main)
+  .run((ModelDefs: IModeldefsService, RuntimeStates: IRuntimeStatesService, NavigationService: IXosNavigationService) => {
+    // Dinamically add a state
+    RuntimeStates.addState('test', {
+      parent: 'xos',
+      url: 'test',
+      template: 'Test State'
+    });
 
+    ModelDefs.get()
+      .then((models: IModeldef[]) => {
+        _.forEach(models, (m: IModeldef) => {
+          const state: IXosState = {
+            parent: 'xos',
+            url: `${m.name.toLowerCase()}s`,
+            component: 'xosCrud',
+            data: {
+              model: m.name,
+              xosTableCfg: {
+                columns: modeldefToTableCfg(m.fields)
+              }
+            }
+          };
+          RuntimeStates.addState(`${m.name.toLowerCase()}s`, state);
+          NavigationService.add({label: `${m.name}s`, url: `${m.name.toLowerCase()}s`});
+        });
+      });
+  });
diff --git a/src/interceptors.ts b/src/interceptors.ts
index 0a1cd08..4db7b9c 100644
--- a/src/interceptors.ts
+++ b/src/interceptors.ts
@@ -5,6 +5,7 @@
 export function interceptorConfig($httpProvider: angular.IHttpProvider, $resourceProvider: angular.resource.IResourceServiceProvider) {
   $httpProvider.interceptors.push('UserStatusInterceptor');
   $httpProvider.interceptors.push('CredentialsInterceptor');
+  $httpProvider.interceptors.push('NoHyperlinksInterceptor');
   $resourceProvider.defaults.stripTrailingSlashes = false;
 }
 
@@ -39,3 +40,14 @@
     }
   };
 }
+
+export function NoHyperlinksInterceptor() {
+  return {
+    request: (req) => {
+      if (req.url.indexOf('.html') === -1) {
+        req.url += '?no_hyperlinks=1';
+      }
+      return req;
+    }
+  };
+}
diff --git a/src/routes.ts b/src/routes.ts
index d5a0800..35723c5 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -18,32 +18,6 @@
       parent: 'xos',
       template: '<h1>Dashboard</h1>'
     })
-    .state('xos.instances', {
-      url: 'instances',
-      parent: 'xos',
-      template: '<h1>Instances</h1>'
-    })
-    .state('xos.slices', {
-      url: 'slices',
-      parent: 'xos',
-      component: `xosCrud`,
-      data: {
-        title: 'Slices',
-        store: 'SlicesStore',
-        xosTableCfg: {
-          columns: [
-            {
-              label: 'Name',
-              prop: 'name'
-            },
-            {
-              label: 'Default Isolation',
-              prop: 'default_isolation'
-            }
-          ]
-        }
-      }
-    })
     .state('xos.nodes', {
       url: 'nodes',
       parent: 'xos',
diff --git a/tslint.json b/tslint.json
index bec150e..32eddba 100644
--- a/tslint.json
+++ b/tslint.json
@@ -12,7 +12,7 @@
     "curly": true,
     "eofline": true,
     "forin": true,
-    "indent": [true, 2],
+    "indent": [true, "spaces"],
     "interface-name": true,
     "jsdoc-format": true,
     "label-position": true,