[CORD-1338] Inline navigation for related models

Change-Id: I58ff4a4675d1ce1140fe162f1f8360f2dc9a6527
diff --git a/src/app/core/index.ts b/src/app/core/index.ts
index 64d295c..33db0ae 100644
--- a/src/app/core/index.ts
+++ b/src/app/core/index.ts
@@ -32,7 +32,8 @@
   .module('xosCore', [
     'ui.router',
     'toastr',
-    'ui.bootstrap.typeahead'
+    'ui.bootstrap.typeahead',
+    'ui.bootstrap.tabs'
   ])
   .config(routesConfig)
   .provider('XosRuntimeStates', XosRuntimeStates)
diff --git a/src/app/core/services/helpers/config.helpers.ts b/src/app/core/services/helpers/config.helpers.ts
index ed3f0c0..5dbd74e 100644
--- a/src/app/core/services/helpers/config.helpers.ts
+++ b/src/app/core/services/helpers/config.helpers.ts
@@ -80,9 +80,10 @@
     pluralize.addPluralRule(/slice$/i, 'slices');
     pluralize.addSingularRule(/slice$/i, 'slice');
     pluralize.addPluralRule(/library$/i, 'librarys');
-    pluralize.addPluralRule(/imagedeployments/i, 'imagedeploymentses');
-    pluralize.addPluralRule(/controllerimages/i, 'controllerimageses');
-    pluralize.addPluralRule(/servicedependency/i, 'servicedependencies');
+    pluralize.addPluralRule(/imagedeployments/i, 'imagedeploymentss');
+    pluralize.addPluralRule(/controllerimages/i, 'controllerimagess');
+    pluralize.addPluralRule(/servicedependency/i, 'servicedependencys');
+    pluralize.addPluralRule(/servicemonitoringagentinfo/i, 'servicemonitoringagentinfoes');
   }
 
   public pluralize(string: string, quantity?: number, count?: boolean): string {
diff --git a/src/app/core/services/runtime-states.ts b/src/app/core/services/runtime-states.ts
index 1f495ba..17b3052 100644
--- a/src/app/core/services/runtime-states.ts
+++ b/src/app/core/services/runtime-states.ts
@@ -1,6 +1,8 @@
+import {IXosModelRelation} from '../../views/crud/crud';
 export interface IXosState extends angular.ui.IState {
   data: {
-    model: string
+    model: string,
+    relations?: IXosModelRelation[]
   };
 };
 
diff --git a/src/app/core/table/table.html b/src/app/core/table/table.html
index 6621e5b..8583fd9 100644
--- a/src/app/core/table/table.html
+++ b/src/app/core/table/table.html
@@ -12,7 +12,7 @@
         <table ng-class="vm.classes">
         <thead>
         <tr>
-            <th ng-repeat="col in vm.columns">
+            <th ng-repeat="col in vm.config.columns">
                 {{col.label}}
                 <div ng-if="vm.config.order">
                     <a href="" ng-click="vm.orderBy = col.prop; vm.reverse = false">
@@ -28,7 +28,7 @@
         </thead>
         <tbody ng-if="vm.config.filter == 'field'">
             <tr>
-                <td ng-repeat="col in vm.columns">
+                <td ng-repeat="col in vm.config.columns">
                     <input
                             ng-if="col.type !== 'boolean' && col.type !== 'array' && col.type !== 'object' && col.type !== 'custom'"
                             class="form-control"
@@ -49,7 +49,7 @@
         </tbody>
         <tbody>
         <tr ng-repeat="item in vm.data | filter:vm.query | orderBy:vm.orderBy:vm.reverse | pagination:vm.currentPage * vm.config.pagination.pageSize | limitTo: (vm.config.pagination.pageSize || vm.data.length) track by $index">
-            <td ng-repeat="col in vm.columns" xos-link-wrapper>
+            <td ng-repeat="col in vm.config.columns" xos-link-wrapper>
                 <span ng-if="!col.type || col.type === 'text'">{{item[col.prop]}}</span>
                 <span ng-if="col.type === 'boolean'">
                     <i class="fa"
diff --git a/src/app/core/table/table.spec.ts b/src/app/core/table/table.spec.ts
index c7856f3..c2d8f2f 100644
--- a/src/app/core/table/table.spec.ts
+++ b/src/app/core/table/table.spec.ts
@@ -93,7 +93,7 @@
     it('should contain 2 columns', function() {
       const th = element[0].getElementsByTagName('th');
       expect(th.length).toEqual(2);
-      expect(isolatedScope.columns.length).toEqual(2);
+      expect(isolatedScope.config.columns.length).toEqual(2);
     });
 
     it('should contain 3 rows', function() {
@@ -476,7 +476,7 @@
       it('should have 3 columns', () => {
         const th = element[0].getElementsByTagName('th');
         expect(th.length).toEqual(3);
-        expect(isolatedScope.columns.length).toEqual(2);
+        expect(isolatedScope.config.columns.length).toEqual(2);
       });
 
       it('when clicking on action should invoke callback', () => {
diff --git a/src/app/core/table/table.ts b/src/app/core/table/table.ts
index 5fb884c..23960e1 100644
--- a/src/app/core/table/table.ts
+++ b/src/app/core/table/table.ts
@@ -127,7 +127,7 @@
       this.currentPage = 0;
     }
 
-    this.columns = this.config.columns;
+    // this.columns = this.config.columns;
 
   }
 
diff --git a/src/app/datasources/helpers/model-discoverer.service.ts b/src/app/datasources/helpers/model-discoverer.service.ts
index 1ef2904..37ecc93 100644
--- a/src/app/datasources/helpers/model-discoverer.service.ts
+++ b/src/app/datasources/helpers/model-discoverer.service.ts
@@ -186,11 +186,15 @@
         id: null
       },
       data: {
-        model: model.name
+        model: model.name,
       },
       component: 'xosCrud',
     };
 
+    if (angular.isDefined(model.relations)) {
+      state.data.relations = model.relations;
+    }
+
     try {
       this.XosRuntimeStates.addState(
         this.stateNameFromModel(model),
diff --git a/src/app/datasources/helpers/model.discoverer.service.spec.ts b/src/app/datasources/helpers/model.discoverer.service.spec.ts
index d5a3232..b72b311 100644
--- a/src/app/datasources/helpers/model.discoverer.service.spec.ts
+++ b/src/app/datasources/helpers/model.discoverer.service.spec.ts
@@ -190,6 +190,30 @@
     scope.$apply();
   });
 
+  it('should add a state with relations in the system', (done) => {
+    MockXosRuntimeStates.addState.calls.reset();
+    service['addState']({name: 'Tenant', app: 'services.vsg', relations: [{model: 'Something', type: 'manytoone'}]})
+      .then((model) => {
+        expect(MockXosRuntimeStates.addState).toHaveBeenCalledWith('xos.vsg.tenant', {
+          parent: 'xos.vsg',
+          url: '/tenants/:id?',
+          params: {
+            id: null
+          },
+          data: {
+            model: 'Tenant',
+            relations: [
+              {model: 'Something', type: 'manytoone'}
+            ]
+          },
+          component: 'xosCrud',
+        });
+        expect(model.clientUrl).toBe('vsg/tenants/:id?');
+        done();
+      });
+    scope.$apply();
+  });
+
   it('should add an item to navigation', () => {
     service['addNavItem']({name: 'Tenant', app: 'services.vsg'});
     expect(MockXosNavigationService.add).toHaveBeenCalledWith({
diff --git a/src/app/datasources/rest/modeldefs.rest.ts b/src/app/datasources/rest/modeldefs.rest.ts
index fdb4b99..0af00ff 100644
--- a/src/app/datasources/rest/modeldefs.rest.ts
+++ b/src/app/datasources/rest/modeldefs.rest.ts
@@ -16,6 +16,7 @@
 export interface IXosModelDefsRelation {
   model: string; // model name
   type: string; // relation type
+  on_field: string; // the field that is containing the relation
 }
 
 export interface IXosModeldef {
diff --git a/src/app/datasources/stores/model.store.spec.ts b/src/app/datasources/stores/model.store.spec.ts
index b114de4..846ef84 100644
--- a/src/app/datasources/stores/model.store.spec.ts
+++ b/src/app/datasources/stores/model.store.spec.ts
@@ -31,7 +31,7 @@
 
 const queryData = [
   {id: 1, name: 'foo'},
-  {id: 1, name: 'bar'}
+  {id: 2, name: 'bar'}
 ];
 
 const MockAppCfg = {
@@ -72,23 +72,42 @@
       .respond(queryData);
   }));
 
-  it('should return an Observable', () => {
-    expect(typeof service.query('test').subscribe).toBe('function');
+  describe('the QUERY method', () => {
+    it('should return an Observable', () => {
+      expect(typeof service.query('test').subscribe).toBe('function');
+    });
+
+    it('the first event should be the resource response', (done) => {
+      let event = 0;
+      service.query('sample')
+        .subscribe(collection => {
+          event++;
+          if (event === 2) {
+            expect(collection[0].id).toEqual(queryData[0].id);
+            expect(collection[1].id).toEqual(queryData[1].id);
+            done();
+          }
+        });
+      $scope.$apply();
+      httpBackend.flush();
+    });
   });
 
-  it('the first event should be the resource response', (done) => {
-    let event = 0;
-    service.query('sample')
-      .subscribe(collection => {
-        event++;
-        if (event === 2) {
-          expect(collection[0].id).toEqual(queryData[0].id);
-          expect(collection[1].id).toEqual(queryData[1].id);
-          done();
-        }
-      });
-    $scope.$apply();
-    httpBackend.flush();
+  describe('the GET method', () => {
+    it('should return an observable containing a single model', (done) => {
+      let event = 0;
+      service.get('sample', queryData[1].id)
+        .subscribe((model) => {
+          event++;
+          if (event === 2) {
+            expect(model.id).toEqual(queryData[1].id);
+            expect(model.name).toEqual(queryData[1].name);
+            done();
+          }
+        });
+      httpBackend.flush();
+      $scope.$apply();
+    });
   });
 
   describe('when a web-socket event is received for that model', () => {
diff --git a/src/app/datasources/stores/model.store.ts b/src/app/datasources/stores/model.store.ts
index 950d441..10da16e 100644
--- a/src/app/datasources/stores/model.store.ts
+++ b/src/app/datasources/stores/model.store.ts
@@ -8,6 +8,7 @@
 
 export interface  IXosModelStoreService {
   query(model: string, apiUrl?: string): Observable<any>;
+  get(model: string, id: string | number): Observable<any>;
   search(modelName: string): any[];
 }
 
@@ -26,7 +27,7 @@
     this.efficientNext = this.XosDebouncer.debounce(this.next, 500, this, false);
   }
 
-  public query(modelName: string, apiUrl: string): Observable<any> {
+  public query(modelName: string, apiUrl?: string): Observable<any> {
     // if there isn't already an observable for that item
     // create a new one and .next() is called by this.loadInitialData once data are received
     if (!this._collections[modelName]) {
@@ -80,8 +81,31 @@
     }
   }
 
-  public get(model: string, id: number) {
-    // TODO implement a get method
+  public get(modelName: string, modelId: string | number): Observable<any> {
+    const subject = new BehaviorSubject([]);
+
+    const _findModel = (subject) => {
+      this._collections[modelName]
+        .subscribe((res) => {
+          const model = _.find(res, {id: modelId});
+          if (model) {
+            subject.next(model);
+          }
+        });
+    };
+
+    if (!this._collections[modelName]) {
+      // cache the models in that collection
+      this.query(modelName)
+        .subscribe((res) => {
+          _findModel(subject);
+        });
+    }
+    else {
+      _findModel(subject);
+    }
+
+    return subject.asObservable();
   }
 
   private next(subject: BehaviorSubject<any>): void {
@@ -89,7 +113,7 @@
   }
 
   private loadInitialData(model: string, apiUrl?: string) {
-    // TODO provide always the apiUrl togheter with the query() params
+    // 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);
diff --git a/src/app/views/crud/crud.html b/src/app/views/crud/crud.html
index adb0500..b162cc5 100644
--- a/src/app/views/crud/crud.html
+++ b/src/app/views/crud/crud.html
@@ -19,29 +19,26 @@
         <hr>
     </div>
 </div>
-<div class="row" ng-show="vm.related.length > 0 && vm.model.id">
-    <div class="view-header">
-        <div class="col-lg-4">
-            <h4>Related Items: </h4>
-        </div>
-        <div class="col-lg-8 text-right">
-            <div class="btn-group">
-                <a ng-if="vm.list" ng-repeat="r in vm.related" href="#/core/{{r.model.toLowerCase()}}s/" class="btn btn-default">
-                    {{r.model}}
-                </a>
-                <a ng-if="!vm.list && vm.getRelatedItem(r, vm.model)" ng-repeat="r in vm.related" href="#/core/{{r.model.toLowerCase()}}s/{{vm.getRelatedItem(r, vm.model)}}" class="btn btn-default">
-                    {{r.model}}
-                </a>
-            </div>
-        </div>
-        <hr>
-    </div>
-</div>
 <div ng-if="vm.list">
     <xos-table config="vm.tableCfg" data="vm.tableData"></xos-table>
 </div>
 
 <div ng-if="!vm.list">
-    <!--<pre>{{vm.model | json}}</pre>-->
-    <xos-form ng-model="vm.model" config="vm.formCfg"></xos-form>
+    <uib-tabset active="active">
+        <uib-tab heading="{{vm.data.model}}">
+            <div class="panel-body">
+                <xos-form ng-model="vm.model" config="vm.formCfg"></xos-form>
+            </div>
+        </uib-tab>
+        <uib-tab ng-if="vm.getRelatedItemId(r, vm.model)" ng-repeat="r in vm.related.manytoone" heading="{{r.model}} {{vm.getHumanReadableOnField(r)}}">
+            <div class="panel-body">
+                <xos-form ng-model="vm.relatedModels.manytoone[r.model][r.on_field].model" config="vm.relatedModels.manytoone[r.model][r.on_field].formConfig"></xos-form>
+            </div>
+        </uib-tab>
+        <uib-tab classes="{{vm.relatedModels.onetomany[r.model][r.on_field].class}}" ng-if="vm.relatedModels.onetomany[r.model]" ng-repeat="r in vm.related.onetomany" heading="{{r.model}} {{vm.getHumanReadableOnField(r)}}">
+            <div class="panel-body">
+                <xos-table config="vm.relatedModels.onetomany[r.model][r.on_field].tableConfig" data="vm.relatedModels.onetomany[r.model][r.on_field].model"></xos-table>
+            </div>
+        </uib-tab>
+    </uib-tabset>
 </div>
diff --git a/src/app/views/crud/crud.relations.service.spec.ts b/src/app/views/crud/crud.relations.service.spec.ts
new file mode 100644
index 0000000..8c0f2f9
--- /dev/null
+++ b/src/app/views/crud/crud.relations.service.spec.ts
@@ -0,0 +1,196 @@
+import {
+  IXosCrudRelationService, XosCrudRelationService, IXosCrudRelationFormTabData,
+  IXosCrudRelationTableTabData
+} from './crud.relations.service';
+import {BehaviorSubject} from 'rxjs';
+import {ConfigHelpers} from '../../core/services/helpers/config.helpers';
+
+const XosModelStoreMock = {
+  get: null,
+  query: null
+};
+
+const XosModelDiscovererMock = {
+  get: null
+};
+
+let service, scope;
+
+describe('The XosCrudRelation service', () => {
+  beforeEach(() => {
+    angular
+      .module('test', ['ui.router', 'toastr'])
+      .service('XosCrudRelation', XosCrudRelationService)
+      .value('XosModelStore', XosModelStoreMock)
+      .value('XosModelDiscoverer', XosModelDiscovererMock)
+      .service('ConfigHelpers', ConfigHelpers);
+
+    angular.mock.module('test');
+  });
+
+  beforeEach(angular.mock.inject((XosCrudRelation: IXosCrudRelationService, $rootScope: ng.IScope) => {
+    service = XosCrudRelation;
+    scope = $rootScope;
+  }));
+
+  it('should have the correct methods', () => {
+    expect(service.getModel).toBeDefined();
+    expect(service.getModels).toBeDefined();
+    expect(service.existsRelatedItem).toBeDefined();
+  });
+
+  describe('the existsRelatedItem method', () => {
+    it('should return true if the we have a reference to the related model', () => {
+      const relation = {
+        model: 'Test',
+        type: 'manytoone',
+        on_field: 'test'
+      };
+      const item = {test_id: 5};
+
+      const res = service.existsRelatedItem(relation, item);
+      expect(res).toBeTruthy();
+    });
+    it('should return false if the we don\'t have a reference to the related model', () => {
+      const relation = {
+        model: 'Test',
+        type: 'manytoone',
+        on_field: 'test'
+      };
+      const item = {foo: 5};
+
+      const res = service.existsRelatedItem(relation, item);
+      expect(res).toBeFalsy();
+    });
+  });
+
+  describe('the getHumanReadableOnField method', () => {
+    it('should return a human readable version of the on_field param', () => {
+      const relation = {
+        model: 'Test',
+        type: 'onetomany',
+        on_field: 'relate_to_test'
+      };
+
+      const res = service.getHumanReadableOnField(relation, 'Instance');
+      expect(res).toEqual('[Relate to test]');
+    });
+
+    it('should return am empty string if the on_field param equal the model param', () => {
+      const relation = {
+        model: 'Test',
+        type: 'onetomany',
+        on_field: 'test'
+      };
+
+      const res = service.getHumanReadableOnField(relation, 'Instance');
+      expect(res).toEqual('');
+    });
+
+    it('should return am empty string if the type on_field equal the base model', () => {
+      const relation = {
+        model: 'Test',
+        type: 'manytoone',
+        on_field: 'instance'
+      };
+
+      const res = service.getHumanReadableOnField(relation, 'Instance');
+      expect(res).toEqual('');
+    });
+  });
+
+  describe('the getModel method', () => {
+    it('should return the tab config for a single object', (done) => {
+      const relation = {
+        model: 'Test',
+        type: 'manytoone',
+        on_field: 'test'
+      };
+
+      const resModel = {foo: 'bar'};
+      const resFormCfg = {form: 'config'};
+
+      spyOn(XosModelStoreMock, 'get').and.callFake(() => {
+        const subject = new BehaviorSubject({});
+        subject.next(resModel);
+        return subject.asObservable();
+      });
+      spyOn(XosModelDiscovererMock, 'get').and.returnValue({formCfg: resFormCfg});
+
+      service.getModel(relation, '5')
+        .then((res: IXosCrudRelationFormTabData) => {
+          expect(res.model).toEqual(resModel);
+          expect(res.class).toEqual('full');
+          expect(res.formConfig).toEqual(resFormCfg);
+          done();
+        });
+      scope.$apply();
+    });
+  });
+
+  describe('the getModels method', () => {
+    it('should return one related model', (done) => {
+      const relation = {
+        model: 'Test',
+        type: 'onetomany',
+        on_field: 'test'
+      };
+
+      const resModels = [
+        {test_id: 5},
+        {test_id: 25}
+      ];
+      const resTableCfg = {table: 'config'};
+
+      spyOn(XosModelStoreMock, 'query').and.callFake(() => {
+        const subject = new BehaviorSubject(resModels);
+        return subject.asObservable();
+      });
+      spyOn(XosModelDiscovererMock, 'get').and.returnValue({tableCfg: resTableCfg});
+
+      service.getModels(relation, 5)
+        .then((res: IXosCrudRelationTableTabData) => {
+          expect(res.model.length).toEqual(1);
+          expect(res.class).toEqual('full');
+          expect(res.tableConfig).toEqual({
+            table: 'config',
+            filter: null
+          });
+          done();
+        });
+      scope.$apply();
+    });
+
+    it('should not return related models', (done) => {
+      const relation = {
+        model: 'Test',
+        type: 'onetomany',
+        on_field: 'test'
+      };
+
+      const resModels = [
+        {test_id: 15},
+        {test_id: 25}
+      ];
+      const resTableCfg = {table: 'config'};
+
+      spyOn(XosModelStoreMock, 'query').and.callFake(() => {
+        const subject = new BehaviorSubject(resModels);
+        return subject.asObservable();
+      });
+      spyOn(XosModelDiscovererMock, 'get').and.returnValue({tableCfg: resTableCfg});
+
+      service.getModels(relation, 5)
+        .then((res: IXosCrudRelationTableTabData) => {
+          expect(res.model.length).toEqual(0);
+          expect(res.class).toEqual('empty');
+          expect(res.tableConfig).toEqual({
+            table: 'config',
+            filter: null
+          });
+          done();
+        });
+      scope.$apply();
+    });
+  });
+});
diff --git a/src/app/views/crud/crud.relations.service.ts b/src/app/views/crud/crud.relations.service.ts
new file mode 100644
index 0000000..292c6f5
--- /dev/null
+++ b/src/app/views/crud/crud.relations.service.ts
@@ -0,0 +1,109 @@
+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';
+
+interface IXosCrudRelationBaseTabData {
+  model: any;
+  class?: 'full' | 'empty';
+}
+
+export interface IXosCrudRelationFormTabData extends IXosCrudRelationBaseTabData {
+  formConfig: IXosFormCfg;
+}
+
+export interface IXosCrudRelationTableTabData extends IXosCrudRelationBaseTabData {
+  tableConfig: IXosTableCfg;
+}
+
+export interface IXosCrudRelationService {
+  getModel(r: IXosModelRelation, id: string | number): Promise<IXosCrudRelationFormTabData>;
+  getModels(r: IXosModelRelation, source_id: string | number): Promise<IXosCrudRelationTableTabData>;
+  existsRelatedItem(r: IXosModelRelation, item: any): boolean;
+  getHumanReadableOnField(r: IXosModelRelation, baseModel: string): string;
+}
+
+export class XosCrudRelationService implements IXosCrudRelationService {
+
+  static $inject = [
+    '$log',
+    '$q',
+    'XosModelStore',
+    'XosModelDiscoverer',
+    'ConfigHelpers'
+  ];
+
+  constructor (
+    private $log: ng.ILogService,
+    private $q: ng.IQService,
+    private XosModelStore: IXosModelStoreService,
+    private XosModelDiscovererService: IXosModelDiscovererService,
+    private ConfigHelpers: IXosConfigHelpersService
+  ) {}
+
+  public getModel (r: IXosModelRelation, id: string | number): Promise<IXosCrudRelationFormTabData> {
+    const d = this.$q.defer();
+    this.XosModelStore.get(r.model, id)
+      .subscribe(
+        item => {
+          this.$log.debug(`[XosCrud] Loaded manytoone relation with ${r.model} on ${r.on_field}`, item);
+
+          const data: IXosCrudRelationFormTabData = {
+            model: item,
+            formConfig: this.XosModelDiscovererService.get(r.model).formCfg,
+            class: angular ? 'full' : 'empty'
+          };
+
+          d.resolve(data);
+        },
+        err => d.reject
+      );
+    return d.promise;
+  };
+
+  public getModels(r: IXosModelRelation, source_id: string | number): Promise<IXosCrudRelationTableTabData> {
+    const d = this.$q.defer();
+
+    this.XosModelStore.query(r.model)
+      .subscribe(
+        items => {
+          this.$log.debug(`[XosCrud] Loaded onetomany relation with ${r.model} on ${r.on_field}`, items);
+          // building the filter parameters
+          const match = {};
+          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;
+          tableCfg.filter = null;
+
+          const data: IXosCrudRelationTableTabData = {
+            model: filtered,
+            tableConfig: tableCfg,
+            class: filtered.length > 0 ? 'full' : 'empty'
+          };
+
+          d.resolve(data);
+        },
+        err => d.reject
+      );
+
+    return d.promise;
+  }
+
+  public existsRelatedItem(r: IXosModelRelation, item: any): boolean {
+    return item && angular.isDefined(item[`${r.on_field.toLowerCase()}_id`]);
+  }
+
+  public getHumanReadableOnField(r: IXosModelRelation, baseModel: string): string {
+    if (r.on_field.toLowerCase() === baseModel.toLowerCase()) {
+      return '';
+    }
+    if (r.model.toLowerCase() === r.on_field.toLowerCase()) {
+      return '';
+    }
+    return `[${this.ConfigHelpers.toLabel(r.on_field, false)}]`;
+  }
+}
diff --git a/src/app/views/crud/crud.scss b/src/app/views/crud/crud.scss
new file mode 100644
index 0000000..ac6fd81
--- /dev/null
+++ b/src/app/views/crud/crud.scss
@@ -0,0 +1,7 @@
+@import './../../style/vars.scss';
+li.uib-tab {
+  &.empty > a {
+    color: $color-placeholder;
+    font-style: italic;
+  }
+}
\ No newline at end of file
diff --git a/src/app/views/crud/crud.ts b/src/app/views/crud/crud.ts
index 90d3342..fc422f3 100644
--- a/src/app/views/crud/crud.ts
+++ b/src/app/views/crud/crud.ts
@@ -2,35 +2,34 @@
 import {IXosModelStoreService} from '../../datasources/stores/model.store';
 import {IXosConfigHelpersService} from '../../core/services/helpers/config.helpers';
 import * as _ from 'lodash';
-import {IXosFormCfg} from '../../core/form/form';
 import {IXosResourceService} from '../../datasources/rest/model.rest';
 import {IStoreHelpersService} from '../../datasources/helpers/store.helpers';
 import {IXosModelDiscovererService} from '../../datasources/helpers/model-discoverer.service';
-
-export interface IXosCrudData {
-  model: string;
-  related: IXosModelRelation[];
-  xosTableCfg: IXosTableCfg;
-  xosFormCfg: IXosFormCfg;
-}
+import './crud.scss';
+import {IXosCrudRelationService} from './crud.relations.service';
 
 export interface IXosModelRelation {
   model: string;
   type: string;
+  on_field: string;
 }
 
 class CrudController {
   static $inject = [
     '$scope',
+    '$log',
     '$state',
     '$stateParams',
     'XosModelStore',
     'ConfigHelpers',
     'ModelRest',
     'StoreHelpers',
-    'XosModelDiscoverer'
+    'XosModelDiscoverer',
+    'XosCrudRelation'
   ];
 
+  // bindings
+
   public data: {model: string};
   public tableCfg: IXosTableCfg;
   public formCfg: any;
@@ -39,18 +38,28 @@
   public title: string;
   public tableData: any[];
   public model: any;
-  public related: string[];
+  public related: {manytoone: IXosModelRelation[], onetomany: IXosModelRelation[]} = {
+    manytoone: [],
+    onetomany: []
+  };
+  public relatedModels: {manytoone: any, onetomany: any} = {
+    manytoone: {},
+    onetomany: {}
+  };
 
   constructor(
     private $scope: angular.IScope,
+    private $log: angular.ILogService,
     private $state: angular.ui.IStateService,
     private $stateParams: ng.ui.IStateParamsService,
     private store: IXosModelStoreService,
     private ConfigHelpers: IXosConfigHelpersService,
     private ModelRest: IXosResourceService,
     private StoreHelpers: IStoreHelpersService,
-    private XosModelDiscovererService: IXosModelDiscovererService
+    private XosModelDiscovererService: IXosModelDiscovererService,
+    private XosCrudRelation: IXosCrudRelationService
   ) {
+
     this.data = this.$state.current.data;
     this.model = this.XosModelDiscovererService.get(this.data.model);
     this.title = this.ConfigHelpers.pluralize(this.data.model);
@@ -60,8 +69,7 @@
     // TODO get the proper URL from model discoverer
     this.baseUrl = '#/' + this.model.clientUrl.replace(':id?', '');
 
-
-    this.related = $state.current.data.related;
+    this.$log.debug('[XosCrud]', $state.current.data);
 
     this.tableCfg = this.model.tableCfg;
     this.formCfg = this.model.formCfg;
@@ -76,7 +84,10 @@
 
             // if it is a detail page for an existing model
             if ($stateParams['id'] && $stateParams['id'] !== 'add') {
+              this.related.onetomany = _.filter($state.current.data.relations, {type: 'onetomany'});
+              this.related.manytoone = _.filter($state.current.data.relations, {type: 'manytoone'});
               this.model = _.find(this.tableData, {id: parseInt($stateParams['id'], 10)});
+              this.getRelatedModels(this.related, this.model);
             }
           });
         }
@@ -96,11 +107,59 @@
     }
   }
 
-  public getRelatedItem(relation: IXosModelRelation, item: any): number {
-    if (item && angular.isDefined(item[`${relation.model.toLowerCase()}_id`])) {
-      return item[`${relation.model.toLowerCase()}_id`];
-    }
-    return 0;
+
+  public getRelatedItemId(relation: IXosModelRelation, item: any): boolean {
+    return this.XosCrudRelation.existsRelatedItem(relation, item);
+  }
+
+  public getHumanReadableOnField(r: IXosModelRelation) {
+    return this.XosCrudRelation.getHumanReadableOnField(r, this.data.model);
+  }
+
+  public getRelatedModels(relations: {manytoone: IXosModelRelation[], onetomany: IXosModelRelation[]}, item: any) {
+    this.$log.info(`[XosCrud] Managing relation for ${this.data.model}:`, relations);
+
+    // loading many to one relations (you'll get a model)
+    _.forEach(relations.manytoone, (r: IXosModelRelation) => {
+      if (!item || !item[`${r.on_field.toLowerCase()}_id`]) {
+        return;
+      }
+
+      this.$log.debug(`[XosCrud] Loading manytoone relation with ${r.model} on ${r.on_field}`);
+
+      if (!angular.isDefined(this.relatedModels.manytoone[r.model])) {
+        this.relatedModels.manytoone[r.model] = {};
+      }
+
+      this.XosCrudRelation.getModel(r, item[`${r.on_field.toLowerCase()}_id`])
+        .then(res => {
+          this.relatedModels.manytoone[r.model][r.on_field] = res;
+        })
+        .catch(err => {
+          this.$log.error(`[XosCrud] Error loading manytoone relation with ${r.model} on ${r.on_field}`, err);
+        });
+    });
+
+    // loading onetomany relations (you'll get a list of models)
+    _.forEach(relations.onetomany, (r: IXosModelRelation) => {
+      if (!item) {
+        return;
+      }
+
+      this.$log.debug(`[XosCrud] Loading onetomany relation with ${r.model} on ${r.on_field}`);
+
+      if (!angular.isDefined(this.relatedModels.onetomany[r.model])) {
+        this.relatedModels.onetomany[r.model] = {};
+      }
+
+      this.XosCrudRelation.getModels(r, item.id)
+        .then(res => {
+          this.relatedModels.onetomany[r.model][r.on_field] = res;
+        })
+        .catch(err => {
+          this.$log.error(`[XosCrud] Error loading onetomany relation with ${r.model} on ${r.on_field}`, err);
+        });
+    });
   }
 }
 
diff --git a/src/app/views/index.ts b/src/app/views/index.ts
index ee87ef9..b296f9b 100644
--- a/src/app/views/index.ts
+++ b/src/app/views/index.ts
@@ -1,10 +1,12 @@
 import {xosCore} from '../core/index';
 import {xosCrud} from './crud/crud';
 import {xosDashboard} from './dashboard/dashboard';
+import {XosCrudRelationService} from './crud/crud.relations.service';
 
 export const xosViews = 'xosViews';
 
 angular
   .module('xosViews', [xosCore])
+  .service('XosCrudRelation', XosCrudRelationService)
   .component('xosCrud', xosCrud)
   .component('xosDashboard', xosDashboard);