Merge "[CORD-1934] Fixing links in table after first page"
diff --git a/conf/app/app.config.production.js b/conf/app/app.config.production.js
index 780a767..e47ea73 100644
--- a/conf/app/app.config.production.js
+++ b/conf/app/app.config.production.js
@@ -19,5 +19,5 @@
 angular.module('app')
   .constant('AppConfig', {
     apiEndpoint: '/xosapi/v1',
-    websocketClient: 'http://192.168.46.100'
+    websocketClient: '/'
   });
diff --git a/conf/proxy.js b/conf/proxy.js
index 71aca89..e383c27 100644
--- a/conf/proxy.js
+++ b/conf/proxy.js
@@ -20,6 +20,8 @@
 
 const target = process.env.PROXY || '192.168.46.100';
 
+console.info(`Proxying request to: ${target}`);
+
 const proxy = httpProxy.createProxyServer({
   target: `http://${target}`
 });
diff --git a/src/app/core/services/helpers/config.helpers.spec.ts b/src/app/core/services/helpers/config.helpers.spec.ts
index 7718a44..7120a6b 100644
--- a/src/app/core/services/helpers/config.helpers.spec.ts
+++ b/src/app/core/services/helpers/config.helpers.spec.ts
@@ -25,6 +25,7 @@
 import {IXosTableCfg} from '../../table/table';
 import {IXosFormInput, IXosFormCfg} from '../../form/form';
 import {BehaviorSubject} from 'rxjs';
+import {XosFormHelpers} from '../../form/form-helpers';
 
 let service: IXosConfigHelpersService;
 
@@ -95,6 +96,7 @@
       .value('XosModelStore', {
 
       })
+      .service('XosFormHelpers', XosFormHelpers)
       .value('$state', {
         get: () => {
           return [
@@ -283,9 +285,9 @@
   });
 
   describe('the private methods', () => {
-    let modelStoreMock, toastr, auth, stateMock;
+    let modelStoreMock, q, toastr, auth, stateMock, XosFormHelpersMock;
 
-    beforeEach(angular.mock.inject((_toastr_, AuthService) => {
+    beforeEach(angular.mock.inject(($q, _toastr_, AuthService, XosFormHelpers) => {
       modelStoreMock = {
         query: () => {
           const subject = new BehaviorSubject([
@@ -297,9 +299,11 @@
       };
       toastr = _toastr_;
       auth = AuthService;
+      XosFormHelpersMock = XosFormHelpers;
       stateMock = {
         get: ''
       };
+      q = $q;
     }));
 
     const field: IXosModelDefsField = {
@@ -316,7 +320,7 @@
         test: 2
       };
       it('should add the formatted data to the column definition', () => {
-        service = new ConfigHelpers(stateMock, toastr, modelStoreMock);
+        service = new ConfigHelpers(q, stateMock, toastr, modelStoreMock, XosFormHelpersMock);
         service['populateRelated'](item, item.test, field);
         expect(item['test-formatted']).toBe('second');
       });
@@ -332,7 +336,7 @@
       };
 
       it('should add the available choice to the select', () => {
-        service = new ConfigHelpers(stateMock, toastr, modelStoreMock);
+        service = new ConfigHelpers(q, stateMock, toastr, modelStoreMock, XosFormHelpersMock);
         service['populateSelectField'](field, input);
         expect(input.options).toEqual([
           {id: 1, label: 'test'},
diff --git a/src/app/core/services/helpers/config.helpers.ts b/src/app/core/services/helpers/config.helpers.ts
index 2241fd1..16958de 100644
--- a/src/app/core/services/helpers/config.helpers.ts
+++ b/src/app/core/services/helpers/config.helpers.ts
@@ -23,6 +23,7 @@
 import {IXosFormCfg, IXosFormInput, IXosFormInputValidator, IXosFormInputOptions} from '../../form/form';
 import {IXosModelStoreService} from '../../../datasources/stores/model.store';
 import {IXosState} from '../runtime-states';
+import {IXosFormHelpersService} from '../../form/form-helpers';
 
 export interface IXosModelDefsFieldValidators {
   name: string;
@@ -61,9 +62,12 @@
 
 export class ConfigHelpers implements IXosConfigHelpersService {
   static $inject = [
+    '$q',
     '$state',
     'toastr',
-    'XosModelStore'];
+    'XosModelStore',
+    'XosFormHelpers'
+  ];
 
   public excluded_fields = [
     'created',
@@ -96,9 +100,11 @@
   ]);
 
   constructor(
+    private $q: ng.IQService,
     private $state: ng.ui.IStateService,
     private toastr: ng.toastr.IToastrService,
-    private XosModelStore: IXosModelStoreService
+    private XosModelStore: IXosModelStoreService,
+    private XosFormHelpers: IXosFormHelpersService
   ) {
     pluralize.addIrregularRule('xos', 'xoses');
     pluralize.addPluralRule(/slice$/i, 'slices');
@@ -299,7 +305,7 @@
     };
 
     formCfg.actions[0].cb = (item, form: angular.IFormController) => {
-
+      const d = this.$q.defer();
       if (!form.$valid) {
         formCfg.feedback = {
           show: true,
@@ -318,9 +324,15 @@
 
       // remove field added by xosTable
       _.forEach(Object.keys(item), prop => {
-        if (prop.indexOf('-formatted') > -1) {
+        // FIXME what _ptr fields comes from??
+        if (prop.indexOf('-formatted') > -1 || prop.indexOf('_ptr') > -1) {
           delete item[prop];
         }
+
+        // convert dates back to UnixTime
+        if (this.XosFormHelpers._getFieldFormat(item[prop]) === 'date') {
+          item[prop] = new Date(item[prop]).getTime() / 1000;
+        }
       });
 
       const itemName = (angular.isUndefined(itemCopy.name)) ? model.name : itemCopy.name;
@@ -334,6 +346,7 @@
             closeBtn: true
           };
           this.toastr.success(`${itemName} successfully saved`);
+          d.resolve(res);
         })
         .catch(err => {
           formCfg.feedback = {
@@ -343,7 +356,10 @@
             closeBtn: true
           };
           this.toastr.error(err.specific_error || '', `Error while saving ${itemName}: ${err.error}`);
+          d.reject(err);
         });
+
+      return d.promise;
     };
 
     return formCfg;
diff --git a/src/app/datasources/helpers/model-discoverer.service.ts b/src/app/datasources/helpers/model-discoverer.service.ts
index c6dcb7e..0addc05 100644
--- a/src/app/datasources/helpers/model-discoverer.service.ts
+++ b/src/app/datasources/helpers/model-discoverer.service.ts
@@ -27,6 +27,7 @@
 import {IXosRuntimeStatesService, IXosState} from '../../core/services/runtime-states';
 import {IXosModelStoreService} from '../stores/model.store';
 import {IXosAuthService} from '../rest/auth.rest';
+import {IXosModeldefsCache} from './modeldefs.service';
 
 export interface IXosModel {
   name: string; // the model name
@@ -44,7 +45,6 @@
 // Service
 export interface IXosModelDiscovererService {
   discover(): ng.IPromise<boolean>;
-  get(modelName: string): IXosModel;
   getApiUrlFromModel(model: IXosModel): string;
   areModelsLoaded(): boolean;
 }
@@ -59,9 +59,10 @@
     'XosNavigationService',
     'XosModelStore',
     'ngProgressFactory',
-    'AuthService'
+    'AuthService',
+    'XosModeldefsCache'
   ];
-  private xosModels: IXosModel[] = []; // list of augmented model definitions;
+
   private xosServices: string[] = []; // list of loaded services
   private progressBar;
   private modelsLoaded: boolean = false;
@@ -75,7 +76,8 @@
     private XosNavigationService: IXosNavigationService,
     private XosModelStore: IXosModelStoreService,
     private ngProgressFactory: any, // check for type defs
-    private AuthService: IXosAuthService
+    private AuthService: IXosAuthService,
+    private XosModeldefsCache: IXosModeldefsCache
   ) {
     this.progressBar = this.ngProgressFactory.createInstance();
     this.progressBar.setColor('#f6a821');
@@ -85,16 +87,12 @@
     return this.modelsLoaded;
   }
 
-  public get(modelName: string): IXosModel|null {
-    return _.find(this.xosModels, m => m.name === modelName);
-  }
-
   public getApiUrlFromModel(model: IXosModel): string {
     if (model.app === 'core') {
       return `/core/${this.ConfigHelpers.pluralize(model.name.toLowerCase())}`;
     }
     else {
-      const serviceName = this.serviceNameFromAppName(model.app);
+      const serviceName = this.XosModeldefsCache.serviceNameFromAppName(model.app);
       return `/${serviceName}/${this.ConfigHelpers.pluralize(model.name.toLowerCase())}`;
     }
   }
@@ -104,7 +102,7 @@
     this.progressBar.start();
     this.XosModelDefs.get()
       .then((modelsDef: IXosModeldef[]) => {
-
+        // TODO store modeldefs and add a method to retrieve the model definition from the name
         const pArray = [];
         _.forEach(modelsDef, (model: IXosModeldef) => {
           this.$log.debug(`[XosModelDiscovererService] Loading: ${model.name}`);
@@ -155,12 +153,8 @@
     return d.promise;
   }
 
-  private serviceNameFromAppName(appName: string): string {
-    return appName.replace('services.', '');
-  }
-
   private stateNameFromModel(model: IXosModel): string {
-    return `xos.${this.serviceNameFromAppName(model.app)}.${model.name.toLowerCase()}`;
+    return `xos.${this.XosModeldefsCache.serviceNameFromAppName(model.app)}.${model.name.toLowerCase()}`;
   }
 
   private getParentStateFromModel(model: IXosModel): string {
@@ -177,7 +171,7 @@
 
   // add a service state and navigation item if it is not already there
   private addService(model: IXosModel): string {
-    const serviceName: string = this.serviceNameFromAppName(model.app);
+    const serviceName: string = this.XosModeldefsCache.serviceNameFromAppName(model.app);
     if (!_.find(this.xosServices, n => n === serviceName)) {
       const serviceState = {
         url: serviceName,
@@ -222,7 +216,7 @@
       );
 
       // extend model
-      model.clientUrl = `${this.serviceNameFromAppName(model.app)}${clientUrl}`;
+      model.clientUrl = `${this.XosModeldefsCache.serviceNameFromAppName(model.app)}${clientUrl}`;
 
       d.resolve(model);
     } catch (e) {
@@ -300,9 +294,7 @@
 
     const d = this.$q.defer();
 
-    if (!_.find(this.xosModels, m => m.name === model.name)) {
-      this.xosModels.push(model);
-    }
+    this.XosModeldefsCache.cache(model);
 
     d.resolve(model);
 
diff --git a/src/app/datasources/helpers/model.discoverer.service.spec.ts b/src/app/datasources/helpers/model.discoverer.service.spec.ts
index 7a9e54f..8011442 100644
--- a/src/app/datasources/helpers/model.discoverer.service.spec.ts
+++ b/src/app/datasources/helpers/model.discoverer.service.spec.ts
@@ -1,4 +1,3 @@
-
 /*
  * Copyright 2017-present Open Networking Foundation
 
@@ -22,6 +21,7 @@
 import {XosModelDiscovererService, IXosModelDiscovererService} from './model-discoverer.service';
 import {IXosModeldef} from '../rest/modeldefs.rest';
 import {BehaviorSubject} from 'rxjs';
+import {XosModeldefsCache} from './modeldefs.service';
 
 const stubModels: IXosModeldef[] = [
   {
@@ -101,7 +101,8 @@
       .value('XosModelStore', MockXosModelStore)
       .value('ngProgressFactory', MockngProgressFactory)
       .value('XosNavigationService', MockXosNavigationService)
-      .value('AuthService', {});
+      .value('AuthService', {})
+      .service('XosModeldefsCache', XosModeldefsCache);
 
     angular.mock.module('test');
   });
@@ -152,21 +153,6 @@
     expect(service.getApiUrlFromModel(model)).toBe('/test/tenants');
   });
 
-  it('should retrieve a model definition from local cache', () => {
-    const model = {
-      name: 'Node',
-      app: 'core'
-    };
-    service['xosModels'] = [
-      model
-    ];
-    expect(service.get('Node')).toEqual(model);
-  });
-
-  it('should get the service name from the app name', () => {
-    expect(service['serviceNameFromAppName']('services.vsg')).toBe('vsg');
-  });
-
   it('should get the state name from the model', () => {
     expect(service['stateNameFromModel']({name: 'Tenant', app: 'services.vsg'})).toBe('xos.vsg.tenant');
   });
@@ -276,12 +262,6 @@
     );
   });
 
-  it('should store the model in memory', () => {
-    service['storeModel']({name: 'Tenant'});
-    expect(service['xosModels'][0]).toEqual({name: 'Tenant'});
-    expect(service['xosModels'].length).toEqual(1);
-  });
-
   describe('when discovering models', () => {
     beforeEach(() => {
       spyOn(service, 'cacheModelEntries').and.callThrough();
@@ -296,7 +276,7 @@
       service.discover()
         .then((res) => {
           expect(MockProgressBar.start).toHaveBeenCalled();
-          expect(MockXosModelDefs.get).toHaveBeenCalled();
+          expect(MockXosModelDefs.get).toHaveBeenCalled(); // FIXME replace correct spy
           expect(service['cacheModelEntries'].calls.count()).toBe(2);
           expect(service['addState'].calls.count()).toBe(2);
           expect(service['addNavItem'].calls.count()).toBe(2);
diff --git a/src/app/datasources/helpers/modeldefs.service.spec.ts b/src/app/datasources/helpers/modeldefs.service.spec.ts
new file mode 100644
index 0000000..9d5660d
--- /dev/null
+++ b/src/app/datasources/helpers/modeldefs.service.spec.ts
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as angular from 'angular';
+import 'angular-mocks';
+import 'angular-ui-router';
+import {XosModeldefsCache, IXosModeldefsCache} from './modeldefs.service';
+import {IXosModel} from './model-discoverer.service';
+
+describe('The XosModeldefsCache service', () => {
+  let service;
+
+  beforeEach(() => {
+    angular
+      .module('test', [])
+      .service('XosModeldefsCache', XosModeldefsCache);
+
+    angular.mock.module('test');
+  });
+
+  beforeEach(angular.mock.inject((XosModeldefsCache: IXosModeldefsCache) => {
+    service = XosModeldefsCache;
+  }));
+
+  beforeEach(() => {
+    service['xosModelDefs'] = [];
+  });
+
+  describe('the cache method', () => {
+
+    const modelDef: IXosModel = {
+      fields: [
+        {name: 'id', type: 'number'},
+        {name: 'foo', type: 'string'}
+      ],
+      relations: [],
+      name: 'Node',
+      app: 'core',
+      description: '',
+      verbose_name: ''
+    };
+    it('should add a modelDefs', () => {
+      service.cache(modelDef);
+      expect(service['xosModelDefs'].length).toBe(1);
+    });
+
+    it('should not add a modelDefs twice', () => {
+      service.cache(modelDef);
+      service.cache(modelDef);
+      expect(service['xosModelDefs'].length).toBe(1);
+    });
+  });
+
+  it('should get the service name from the app name', () => {
+    expect(service.serviceNameFromAppName('services.vsg')).toBe('vsg');
+  });
+
+  describe('the get method', () => {
+    it('should retrieve a model definition from local cache', () => {
+      const model = {
+        name: 'Node',
+        app: 'core'
+      };
+      service['xosModelDefs'] = [
+        model
+      ];
+      expect(service.get('Node')).toEqual(model);
+    });
+  });
+});
diff --git a/src/app/datasources/helpers/modeldefs.service.ts b/src/app/datasources/helpers/modeldefs.service.ts
new file mode 100644
index 0000000..152b5e8
--- /dev/null
+++ b/src/app/datasources/helpers/modeldefs.service.ts
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as _ from 'lodash';
+import * as pluralize from 'pluralize';
+import {IXosModel} from './model-discoverer.service';
+
+export interface IXosModeldefsCache {
+  cache(modelDef: IXosModel): void;
+  get(modelName: string): IXosModel;
+  getApiUrlFromModel(model: IXosModel): string;
+  serviceNameFromAppName(appName: string): string;
+}
+
+export class XosModeldefsCache implements IXosModeldefsCache {
+
+  private xosModelDefs: IXosModel[] = [];
+
+  public cache(modelDef: IXosModel): void {
+    if (!_.find(this.xosModelDefs, m => m.name === modelDef.name)) {
+      this.xosModelDefs.push(modelDef);
+    }
+  }
+
+  public get(modelName: string): IXosModel|null {
+    return _.find(this.xosModelDefs, m => m.name === modelName);
+  }
+
+  public serviceNameFromAppName(appName: string): string {
+    return appName.replace('services.', '');
+  }
+
+  public getApiUrlFromModel(model: IXosModel): string {
+    // FIXME move pluralize from config to a separate service
+    if (model.app === 'core') {
+      return `/core/${pluralize(model.name.toLowerCase())}`;
+    }
+    else {
+      const serviceName = this.serviceNameFromAppName(model.app);
+      return `/${serviceName}/${pluralize(model.name.toLowerCase())}`;
+    }
+  }
+}
diff --git a/src/app/datasources/helpers/store.helpers.spec.ts b/src/app/datasources/helpers/store.helpers.spec.ts
index 5d0a584..f3b2321 100644
--- a/src/app/datasources/helpers/store.helpers.spec.ts
+++ b/src/app/datasources/helpers/store.helpers.spec.ts
@@ -25,11 +25,13 @@
 import {IWSEvent} from '../websocket/global';
 import {ConfigHelpers} from '../../core/services/helpers/config.helpers';
 import {AuthService} from '../rest/auth.rest';
+import {IXosModeldefsCache} from './modeldefs.service';
 
 let service: IStoreHelpersService;
 let subject: BehaviorSubject<any>;
 let resource: ng.resource.IResourceClass<any>;
 let $resource: ng.resource.IResourceService;
+let xosModelCache: IXosModeldefsCache;
 
 describe('The StoreHelpers service', () => {
 
@@ -40,6 +42,10 @@
       .service('ModelRest', ModelRest) // NOTE evaluate mock
       .service('StoreHelpers', StoreHelpers)
       .service('AuthService', AuthService)
+      .value('XosModeldefsCache', {
+        get: jasmine.createSpy('XosModeldefsCache.get'),
+        getApiUrlFromModel: jasmine.createSpy('XosModeldefsCache.getApiUrlFromModel')
+      })
       .value('AppConfig', {});
 
     angular.mock.module('test');
@@ -47,10 +53,12 @@
 
   beforeEach(angular.mock.inject((
     StoreHelpers: IStoreHelpersService,
+    XosModeldefsCache: IXosModeldefsCache,
     _$resource_: ng.resource.IResourceService
   ) => {
     $resource = _$resource_;
     resource = $resource('/test');
+    xosModelCache = XosModeldefsCache;
     service = StoreHelpers;
   }));
 
@@ -58,13 +66,38 @@
     expect(service.updateCollection).toBeDefined();
   });
 
+  describe('the updateCollection method', () => {
 
-  it('should convert a core model name in an URL', () => {
-    expect(service.urlFromCoreModel('Slice')).toBe('/core/slices');
-    expect(service.urlFromCoreModel('Xos')).toBe('/core/xoses');
-    expect(service.urlFromCoreModel('SiteRole')).toBe('/core/siteroles');
-    expect(service.urlFromCoreModel('SliceRole')).toBe('/core/sliceroles');
-    expect(service.urlFromCoreModel('SlicePrivilege')).toBe('/core/sliceprivileges');
+    beforeEach(() => {
+      subject = new BehaviorSubject([
+        new resource({id: 1, name: 'test'})
+      ]);
+
+    });
+
+    it('should get the correct url for a core model', () => {
+      const mockModelDef = {
+        name: 'Node',
+        app: 'core'
+      };
+
+      xosModelCache.get['and'].returnValue(mockModelDef);
+
+      const event: IWSEvent = {
+        model: 'TestModel',
+        msg: {
+          object: {
+            id: 1,
+            name: 'test'
+          },
+          changed_fields: ['deleted']
+        }
+      };
+
+      service.updateCollection(event, subject);
+      expect(xosModelCache.get).toHaveBeenCalledWith('TestModel');
+      expect(xosModelCache.getApiUrlFromModel).toHaveBeenCalledWith(mockModelDef);
+    });
   });
 
   describe('when updating a collection', () => {
diff --git a/src/app/datasources/helpers/store.helpers.ts b/src/app/datasources/helpers/store.helpers.ts
index 83c3b9a..0df37c7 100644
--- a/src/app/datasources/helpers/store.helpers.ts
+++ b/src/app/datasources/helpers/store.helpers.ts
@@ -18,27 +18,26 @@
 
 import {BehaviorSubject} from 'rxjs';
 import * as _ from 'lodash';
-import * as pluralize from 'pluralize';
 import {IWSEvent} from '../websocket/global';
 import {IXosResourceService} from '../rest/model.rest';
+import {IXosModeldefsCache} from './modeldefs.service';
 
 export interface IStoreHelpersService {
-  urlFromCoreModel(name: string): string;
   updateCollection(event: IWSEvent, subject: BehaviorSubject<any>): BehaviorSubject<any>;
 }
 
 export class StoreHelpers implements IStoreHelpersService {
-  static $inject = ['ModelRest'];
+  static $inject = [
+    'ModelRest',
+    'XosModeldefsCache'
+  ];
 
   constructor (
-    private modelRest: IXosResourceService
+    private modelRest: IXosResourceService,
+    private XosModeldefsCache: IXosModeldefsCache
   ) {
   }
 
-  public urlFromCoreModel(name: string): string {
-    return `/core/${pluralize(name.toLowerCase())}`;
-  }
-
   public updateCollection(event: IWSEvent, subject: BehaviorSubject<any>): BehaviorSubject<any> {
     const collection: any[] = subject.value;
     const index: number = _.findIndex(collection, (i) => {
@@ -49,7 +48,8 @@
     const isDeleted: boolean = _.includes(event.msg.changed_fields, 'deleted');
 
     // generate a resource for the model
-    const endpoint = this.urlFromCoreModel(event.model);
+    const modelDef = this.XosModeldefsCache.get(event.model); // get the model definition
+    const endpoint = this.XosModeldefsCache.getApiUrlFromModel(modelDef);
     const resource = this.modelRest.getResource(endpoint);
     const model = new resource(event.msg.object);
 
diff --git a/src/app/datasources/index.ts b/src/app/datasources/index.ts
index e5adcab..d5869b5 100644
--- a/src/app/datasources/index.ts
+++ b/src/app/datasources/index.ts
@@ -26,6 +26,7 @@
 import {xosCore} from '../core/index';
 import {SearchService} from './helpers/search.service';
 import {XosModelDiscovererService} from './helpers/model-discoverer.service';
+import {XosModeldefsCache} from './helpers/modeldefs.service';
 
 export const xosDataSources = 'xosDataSources';
 
@@ -34,6 +35,7 @@
   .service('ModelRest', ModelRest)
   .service('AuthService', AuthService)
   .service('WebSocket', WebSocketEvent)
+  .service('XosModeldefsCache', XosModeldefsCache)
   .service('StoreHelpers', StoreHelpers)
   .service('SynchronizerStore', SynchronizerStore)
   .service('XosModelStore', XosModelStore)
diff --git a/src/app/datasources/stores/model.store.spec.ts b/src/app/datasources/stores/model.store.spec.ts
index 4e94495..09dc3f9 100644
--- a/src/app/datasources/stores/model.store.spec.ts
+++ b/src/app/datasources/stores/model.store.spec.ts
@@ -27,11 +27,13 @@
 import {ConfigHelpers} from '../../core/services/helpers/config.helpers';
 import {AuthService} from '../rest/auth.rest';
 import {XosDebouncer} from '../../core/services/helpers/debounce.helper';
+import {IXosModeldefsCache} from '../helpers/modeldefs.service';
 
 let service: IXosModelStoreService;
 let httpBackend: ng.IHttpBackendService;
 let $scope;
 let WebSocket;
+let XosModeldefsCache;
 
 class MockWs {
   private _list;
@@ -69,6 +71,10 @@
       .service('ConfigHelpers', ConfigHelpers) // TODO mock
       .service('AuthService', AuthService)
       .constant('AppConfig', MockAppCfg)
+      .value('XosModeldefsCache', {
+        get: jasmine.createSpy('XosModeldefsCache.get').and.returnValue({}),
+        getApiUrlFromModel: jasmine.createSpy('XosModeldefsCache.getApiUrlFromModel')
+      })
       .service('XosDebouncer', XosDebouncer);
 
     angular.mock.module('ModelStore');
@@ -78,12 +84,14 @@
     XosModelStore: IXosModelStoreService,
     $httpBackend: ng.IHttpBackendService,
     _$rootScope_: ng.IRootScopeService,
-    _WebSocket_: any
+    _WebSocket_: any,
+    _XosModeldefsCache_: IXosModeldefsCache
   ) => {
     service = XosModelStore;
     httpBackend = $httpBackend;
     $scope = _$rootScope_;
     WebSocket = _WebSocket_;
+    XosModeldefsCache = _XosModeldefsCache_;
 
     // ModelRest will call the backend
     httpBackend.whenGET(`${MockAppCfg.apiEndpoint}/core/samples`)
@@ -96,6 +104,7 @@
     });
 
     it('the first event should be the resource response', (done) => {
+      XosModeldefsCache.getApiUrlFromModel.and.returnValue(`/core/samples`);
       let event = 0;
       service.query('sample')
         .subscribe(collection => {
@@ -114,6 +123,7 @@
   describe('the GET method', () => {
     it('should return an observable containing a single model', (done) => {
       let event = 0;
+      XosModeldefsCache.getApiUrlFromModel.and.returnValue(`/core/samples`);
       service.get('sample', queryData[1].id)
         .subscribe((model) => {
           event++;
@@ -177,6 +187,8 @@
     it('should create different Subject', (done) => {
       let fevent = 0;
       let sevent = 0;
+      XosModeldefsCache.get.and.callFake(v => v);
+      XosModeldefsCache.getApiUrlFromModel.and.callFake(v => `/core/${v}s`);
       service.query('first')
         .subscribe(first => {
           fevent++;
diff --git a/src/app/datasources/stores/model.store.ts b/src/app/datasources/stores/model.store.ts
index 7735ff0..10c4d2e 100644
--- a/src/app/datasources/stores/model.store.ts
+++ b/src/app/datasources/stores/model.store.ts
@@ -23,6 +23,7 @@
 import {IXosResourceService} from '../rest/model.rest';
 import {IStoreHelpersService} from '../helpers/store.helpers';
 import {IXosDebouncer} from '../../core/services/helpers/debounce.helper';
+import {IXosModeldefsCache} from '../helpers/modeldefs.service';
 
 export interface  IXosModelStoreService {
   query(model: string, apiUrl?: string): Observable<any>;
@@ -31,7 +32,14 @@
 }
 
 export class XosModelStore implements IXosModelStoreService {
-  static $inject = ['$log', 'WebSocket', 'StoreHelpers', 'ModelRest', 'XosDebouncer'];
+  static $inject = [
+    '$log',
+    'WebSocket',
+    'StoreHelpers',
+    'ModelRest',
+    'XosDebouncer',
+    'XosModeldefsCache'
+  ];
   private _collections: any; // NOTE contains a map of {model: BehaviourSubject}
   private efficientNext: any; // NOTE debounce next
   constructor(
@@ -39,7 +47,8 @@
     private webSocket: IWSEventService,
     private storeHelpers: IStoreHelpersService,
     private ModelRest: IXosResourceService,
-    private XosDebouncer: IXosDebouncer
+    private XosDebouncer: IXosDebouncer,
+    private XosModeldefsCache: IXosModeldefsCache
   ) {
     this._collections = {};
     this.efficientNext = this.XosDebouncer.debounce(this.next, 500, this, false);
@@ -59,6 +68,7 @@
       this.efficientNext(this._collections[modelName]);
     }
 
+    // NOTE do we need to subscriber every time we query?
     this.webSocket.list()
       .filter((e: IWSEvent) => e.model === modelName)
       .subscribe(
@@ -128,8 +138,7 @@
   private loadInitialData(model: string, apiUrl?: string) {
     // TODO provide always the apiUrl together with the query() params
     if (!angular.isDefined(apiUrl)) {
-      // NOTE check what is the correct pattern to pluralize this
-      apiUrl = this.storeHelpers.urlFromCoreModel(model);
+      apiUrl = this.XosModeldefsCache.getApiUrlFromModel(this.XosModeldefsCache.get(model));
     }
     this.ModelRest.getResource(apiUrl).query().$promise
       .then(
diff --git a/src/app/datasources/websocket/global.ts b/src/app/datasources/websocket/global.ts
index 32f6f4a..e86282a 100644
--- a/src/app/datasources/websocket/global.ts
+++ b/src/app/datasources/websocket/global.ts
@@ -57,7 +57,6 @@
 
         if (data.msg.changed_fields.length === 0 || _.intersection(data.msg.changed_fields, ignoredFields).length === data.msg.changed_fields.length) {
           // NOTE means that the only updated fields does not change anything in the UI, so don't send events around
-          this.$log.debug(`[WebSocket] Ignoring Event for: ${data.model} [${data.msg.pk}]`);
           return;
         }
 
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 3b51a02..7d21131 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
@@ -24,11 +24,11 @@
 import {XosServiceGraphConfig as config} from '../../graph.config';
 import {IXosDebouncer} from '../../../core/services/helpers/debounce.helper';
 import {IXosServiceGraph, IXosServiceGraphLink, IXosServiceGraphNode} from '../../interfaces';
-import {IXosModelDiscovererService} from '../../../datasources/helpers/model-discoverer.service';
 import {IXosSidePanelService} from '../../../core/side-panel/side-panel.service';
 import {IXosGraphHelpers} from '../../services/d3-helpers/graph.helpers';
 import {IXosServiceGraphExtender, IXosServiceGraphReducer} from '../../services/graph.extender';
 import {IXosServiceInstanceGraphStore} from '../../services/service-instance.graph.store';
+import {IXosModeldefsCache} from '../../../datasources/helpers/modeldefs.service';
 
 class XosFineGrainedTenancyGraphCtrl {
   static $inject = [
@@ -59,7 +59,7 @@
     private $log: ng.ILogService,
     private XosServiceInstanceGraphStore: IXosServiceInstanceGraphStore,
     private XosDebouncer: IXosDebouncer,
-    private XosModelDiscoverer: IXosModelDiscovererService,
+    private XosModeldefsCache: IXosModeldefsCache,
     private XosSidePanel: IXosSidePanelService,
     private XosGraphHelpers: IXosGraphHelpers,
     private XosServiceGraphExtender: IXosServiceGraphExtender
@@ -352,7 +352,7 @@
         }
         selectedModel = n.id;
         const modelName = n.model['class_names'].split(',')[0];
-        const formConfig = this.XosModelDiscoverer.get(modelName).formCfg;
+        const formConfig = this.XosModeldefsCache.get(modelName).formCfg;
         const model = angular.copy(n.model);
         delete model.d3Id;
         this.XosSidePanel.injectComponent('xosForm', {config: formConfig, ngModel: model});
diff --git a/src/app/views/crud/crud.relations.service.spec.ts b/src/app/views/crud/crud.relations.service.spec.ts
index 771c9a8..768c039 100644
--- a/src/app/views/crud/crud.relations.service.spec.ts
+++ b/src/app/views/crud/crud.relations.service.spec.ts
@@ -22,13 +22,14 @@
 } from './crud.relations.service';
 import {BehaviorSubject} from 'rxjs';
 import {ConfigHelpers} from '../../core/services/helpers/config.helpers';
+import {XosFormHelpers} from '../../core/form/form-helpers';
 
 const XosModelStoreMock = {
   get: null,
   query: null
 };
 
-const XosModelDiscovererMock = {
+const XosModeldefsCacheMock = {
   get: null
 };
 
@@ -40,8 +41,9 @@
       .module('test', ['ui.router', 'toastr'])
       .service('XosCrudRelation', XosCrudRelationService)
       .value('XosModelStore', XosModelStoreMock)
-      .value('XosModelDiscoverer', XosModelDiscovererMock)
-      .service('ConfigHelpers', ConfigHelpers);
+      .value('XosModeldefsCache', XosModeldefsCacheMock)
+      .service('ConfigHelpers', ConfigHelpers)
+      .service('XosFormHelpers', XosFormHelpers);
 
     angular.mock.module('test');
   });
@@ -133,7 +135,7 @@
         subject.next(resModel);
         return subject.asObservable();
       });
-      spyOn(XosModelDiscovererMock, 'get').and.returnValue({formCfg: resFormCfg});
+      spyOn(XosModeldefsCacheMock, 'get').and.returnValue({formCfg: resFormCfg});
 
       service.getModel(relation, '5')
         .then((res: IXosCrudRelationFormTabData) => {
@@ -164,7 +166,7 @@
         const subject = new BehaviorSubject(resModels);
         return subject.asObservable();
       });
-      spyOn(XosModelDiscovererMock, 'get').and.returnValue({tableCfg: resTableCfg});
+      spyOn(XosModeldefsCacheMock, 'get').and.returnValue({tableCfg: resTableCfg});
 
       service.getModels(relation, 5)
         .then((res: IXosCrudRelationTableTabData) => {
@@ -196,7 +198,7 @@
         const subject = new BehaviorSubject(resModels);
         return subject.asObservable();
       });
-      spyOn(XosModelDiscovererMock, 'get').and.returnValue({tableCfg: resTableCfg});
+      spyOn(XosModeldefsCacheMock, 'get').and.returnValue({tableCfg: resTableCfg});
 
       service.getModels(relation, 5)
         .then((res: IXosCrudRelationTableTabData) => {
diff --git a/src/app/views/crud/crud.relations.service.ts b/src/app/views/crud/crud.relations.service.ts
index 092dda8..9dd3656 100644
--- a/src/app/views/crud/crud.relations.service.ts
+++ b/src/app/views/crud/crud.relations.service.ts
@@ -18,12 +18,12 @@
 
 import {IXosModelRelation} from './crud';
 import {IXosModelStoreService} from '../../datasources/stores/model.store';
-import {IXosModelDiscovererService} from '../../datasources/helpers/model-discoverer.service';
 import * as _ from 'lodash';
 import {IXosFormCfg} from '../../core/form/form';
 import {IXosTableCfg} from '../../core/table/table';
 import {IXosConfigHelpersService} from '../../core/services/helpers/config.helpers';
 import {Subscription} from 'rxjs';
+import {IXosModeldefsCache} from '../../datasources/helpers/modeldefs.service';
 
 interface IXosCrudRelationBaseTabData {
   model: any;
@@ -51,16 +51,16 @@
     '$log',
     '$q',
     'XosModelStore',
-    'XosModelDiscoverer',
-    'ConfigHelpers'
+    'ConfigHelpers',
+    'XosModeldefsCache'
   ];
 
   constructor (
     private $log: ng.ILogService,
     private $q: ng.IQService,
     private XosModelStore: IXosModelStoreService,
-    private XosModelDiscovererService: IXosModelDiscovererService,
-    private ConfigHelpers: IXosConfigHelpersService
+    private ConfigHelpers: IXosConfigHelpersService,
+    private XosModeldefsCache: IXosModeldefsCache
   ) {}
 
   public getModel (r: IXosModelRelation, id: string | number): Promise<IXosCrudRelationFormTabData> {
@@ -72,7 +72,7 @@
 
           const data: IXosCrudRelationFormTabData = {
             model: item,
-            formConfig: this.XosModelDiscovererService.get(r.model).formCfg,
+            formConfig: this.XosModeldefsCache.get(r.model).formCfg,
             class: angular ? 'full' : 'empty'
           };
 
@@ -96,7 +96,7 @@
           match[`${r.on_field.toLowerCase()}_id`] = source_id;
           const filtered = _.filter(items, match);
           // removing search bar from table
-          const tableCfg = this.XosModelDiscovererService.get(r.model).tableCfg;
+          const tableCfg = this.XosModeldefsCache.get(r.model).tableCfg;
           tableCfg.filter = null;
 
           const data: IXosCrudRelationTableTabData = {
diff --git a/src/app/views/crud/crud.ts b/src/app/views/crud/crud.ts
index b4434c8..0fe9407 100644
--- a/src/app/views/crud/crud.ts
+++ b/src/app/views/crud/crud.ts
@@ -28,6 +28,7 @@
 import {IXosDebugService, IXosDebugStatus} from '../../core/debug/debug.service';
 import {IXosKeyboardShortcutService} from '../../core/services/keyboard-shortcut';
 import {Subscription} from 'rxjs';
+import {IXosModeldefsCache} from '../../datasources/helpers/modeldefs.service';
 
 export interface IXosModelRelation {
   model: string;
@@ -48,7 +49,8 @@
     'XosModelDiscoverer',
     'XosCrudRelation',
     'XosDebug',
-    'XosKeyboardShortcut'
+    'XosKeyboardShortcut',
+    'XosModeldefsCache'
   ];
 
   // bindings
@@ -88,12 +90,13 @@
     private XosModelDiscovererService: IXosModelDiscovererService,
     private XosCrudRelation: IXosCrudRelationService,
     private XosDebug: IXosDebugService,
-    private XosKeyboardShortcut: IXosKeyboardShortcutService
+    private XosKeyboardShortcut: IXosKeyboardShortcutService,
+    private XosModeldefsCache: IXosModeldefsCache
   ) {
     this.$log.info('[XosCrud] Setup', $state.current.data);
 
     this.data = this.$state.current.data;
-    this.modelDef = this.XosModelDiscovererService.get(this.data.model);
+    this.modelDef = this.XosModeldefsCache.get(this.data.model);
     this.modelName = this.modelDef.verbose_name ? this.modelDef.verbose_name : this.modelDef.name;
     this.pluralTitle = this.ConfigHelpers.pluralize(this.modelName);
     this.singularTitle = this.ConfigHelpers.pluralize(this.modelName, 1);
@@ -106,6 +109,18 @@
     this.tableCfg = this.modelDef.tableCfg;
     this.formCfg = this.modelDef.formCfg;
 
+    // attach a redirect to the $save method
+    const originalSave = this.formCfg.actions[0].cb;
+    this.formCfg.actions[0].cb = (item, form: angular.IFormController) => {
+      originalSave(item, form)
+        .then(res => {
+          this.$state.go(this.$state.current, {id: res.id});
+        })
+        .catch(err => {
+          this.$log.error(`[XosCrud] Error while saving:`, item, err);
+        });
+    };
+
     this.debugTab = this.XosDebug.status.modelsTab;
     this.$scope.$on('xos.debug.status', (e, status: IXosDebugStatus) => {
       this.debugTab = status.modelsTab;
@@ -119,7 +134,7 @@
       // if it is the create page
       if ($stateParams['id'] === 'add') {
         // generate a resource for an empty model
-        const endpoint = this.XosModelDiscovererService.getApiUrlFromModel(this.XosModelDiscovererService.get(this.data.model));
+        const endpoint = this.XosModelDiscovererService.getApiUrlFromModel(this.XosModeldefsCache.get(this.data.model));
         const resource = this.ModelRest.getResource(endpoint);
         this.model = new resource({});
       }