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,