[CORD-1043] Fine-grained service graph first draft

Change-Id: I16566b0c38dda64fa920120ce16ea699ca157279
diff --git a/src/app/core/services/helpers/debounce.helper.ts b/src/app/core/services/helpers/debounce.helper.ts
index 0534106..89f62ee 100644
--- a/src/app/core/services/helpers/debounce.helper.ts
+++ b/src/app/core/services/helpers/debounce.helper.ts
@@ -12,7 +12,7 @@
   }
 
   // wait for 'wait' ms without actions to call the function
-  // if 'immediate' call it immediatly then wait for 'wait'
+  // if 'immediate' call it immediately then wait for 'wait'
   // NOTE that we cannot use $timeout service to debounce functions as it trigger infiniteDigest Exception
   public debounce(func: any, wait: number, context: any, immediate?: boolean) {
     let timeout;
diff --git a/src/app/core/services/navigation.spec.ts b/src/app/core/services/navigation.spec.ts
index 82a235e..50af9d9 100644
--- a/src/app/core/services/navigation.spec.ts
+++ b/src/app/core/services/navigation.spec.ts
@@ -49,6 +49,10 @@
       {
         label: 'Core',
         state: 'xos.core'
+      },
+      {
+        label: 'Service Graph',
+        state: 'xos.fine-grained-graph'
       }
     ].concat(mockRoutes);
   }));
diff --git a/src/app/core/services/navigation.ts b/src/app/core/services/navigation.ts
index 9869b5a..e41333c 100644
--- a/src/app/core/services/navigation.ts
+++ b/src/app/core/services/navigation.ts
@@ -36,10 +36,10 @@
         label: 'Core',
         state: 'xos.core'
       },
-      // {
-      //   label: 'Service',
-      //   state: 'xos.services'
-      // },
+      {
+        label: 'Service Graph',
+        state: 'xos.fine-grained-graph'
+      },
     ];
     // adding configuration defined routes
     // this.routes = StyleConfig.routes.concat(defaultRoutes).reverse();
diff --git a/src/app/service-graph/components/coarse/coarse.component.ts b/src/app/service-graph/components/coarse/coarse.component.ts
index 566698c..d610aa6 100644
--- a/src/app/service-graph/components/coarse/coarse.component.ts
+++ b/src/app/service-graph/components/coarse/coarse.component.ts
@@ -23,7 +23,7 @@
   private linkGroup;
   private nodeGroup;
 
-  // debounce functions
+  // debounced functions
   private renderGraph;
 
   constructor (
@@ -40,6 +40,7 @@
     this.CoarseGraphSubscription = this.XosServiceGraphStore.getCoarse()
       .subscribe(
         (res: IXosServiceGraph) => {
+
           // id there are no data, do nothing
           if (!res.nodes || res.nodes.length === 0 || !res.links || res.links.length === 0) {
             return;
@@ -63,7 +64,6 @@
 
   $onDestroy() {
     this.CoarseGraphSubscription.unsubscribe();
-    this.XosServiceGraphStore.dispose();
   }
 
   private _renderGraph() {
diff --git a/src/app/service-graph/components/fine-grained/fine-grained.component.html b/src/app/service-graph/components/fine-grained/fine-grained.component.html
new file mode 100644
index 0000000..10e4887
--- /dev/null
+++ b/src/app/service-graph/components/fine-grained/fine-grained.component.html
@@ -0,0 +1,4 @@
+<h1>Fine Grained Tenancy Graph</h1>
+
+<svg>
+</svg>
diff --git a/src/app/service-graph/components/fine-grained/fine-grained.component.scss b/src/app/service-graph/components/fine-grained/fine-grained.component.scss
new file mode 100644
index 0000000..425b1bf
--- /dev/null
+++ b/src/app/service-graph/components/fine-grained/fine-grained.component.scss
@@ -0,0 +1,55 @@
+@import './../../../style/vars.scss';
+@import '../../../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/variables';
+
+xos-fine-grained-tenancy-graph {
+  display: block;
+  // background: $color-accent;
+
+  svg {
+    height: 800px;
+    width: 100%;
+    background-color: $panel-filled-bg;
+    border-radius: 3px;
+  }
+
+  .node-group {
+
+    .node {
+      cursor: pointer;
+    }
+
+    .node {
+      rect, circle {
+        stroke: $color-accent;
+        fill: $background-color;
+      }
+
+      &.network > circle{
+        stroke: blue;
+      }
+
+      &.tenant > rect{
+        stroke: green;
+      }
+
+      &.subscriber > rect{
+        stroke: red;
+      }
+    }
+
+    .node > text {
+      fill: #fff;
+      font-size: 20px;
+    }
+  }
+
+  .link-group {
+    line {
+      stroke: $color-accent;
+    }
+  }
+  .arrow-marker {
+    stroke: $color-accent;
+    fill: $color-accent;
+  }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..b0f71a6
--- /dev/null
+++ b/src/app/service-graph/components/fine-grained/fine-grained.component.ts
@@ -0,0 +1,270 @@
+import {IXosServiceGraphStore} from '../../services/graph.store';
+import './fine-grained.component.scss';
+import * as d3 from 'd3';
+import * as $ from 'jquery';
+import {Subscription} from 'rxjs';
+import {XosServiceGraphConfig as config} from '../../graph.config';
+import {IXosDebouncer} from '../../../core/services/helpers/debounce.helper';
+import {IXosServiceGraph, IXosServiceGraphLink, IXosServiceGraphNode} from '../../interfaces';
+
+class XosFineGrainedTenancyGraphCtrl {
+  static $inject = [
+    '$log',
+    'XosServiceGraphStore',
+    'XosDebouncer'
+  ];
+
+  public graph: IXosServiceGraph;
+
+  private GraphSubscription: Subscription;
+  private svg;
+  private forceLayout;
+  private linkGroup;
+  private nodeGroup;
+
+  // debounced functions
+  private renderGraph;
+
+  constructor(
+    private $log: ng.ILogService,
+    private XosServiceGraphStore: IXosServiceGraphStore,
+    private XosDebouncer: IXosDebouncer
+  ) {
+    this.handleSvg();
+    this.setupForceLayout();
+    this.renderGraph = this.XosDebouncer.debounce(this._renderGraph, 500, this);
+
+    $(window).on('resize', () => {
+      this.setupForceLayout();
+      this.renderGraph();
+    });
+
+    this.GraphSubscription = this.XosServiceGraphStore.get()
+      .subscribe(
+        (graph) => {
+
+          if (!graph.nodes || graph.nodes.length === 0 || !graph.links || graph.links.length === 0) {
+            return;
+          }
+
+          this.$log.debug(`[XosFineGrainedTenancyGraphCtrl] Coarse Event and render`, graph);
+          this.graph = graph;
+          this.renderGraph();
+        },
+        (err) => {
+          this.$log.error(`[XosFineGrainedTenancyGraphCtrl] Error: `, err);
+        }
+      );
+  }
+
+  $onDestroy() {
+    this.GraphSubscription.unsubscribe();
+  }
+
+  private _renderGraph() {
+    this.addNodeLinksToForceLayout(this.graph);
+    this.renderNodes(this.graph.nodes);
+    this.renderLinks(this.graph.links);
+  }
+
+  private getSvgDimensions(): {width: number, heigth: number} {
+    return {
+      width: $('xos-fine-grained-tenancy-graph svg').width(),
+      heigth: $('xos-fine-grained-tenancy-graph svg').height()
+    };
+  }
+
+  private handleSvg() {
+    this.svg = d3.select('svg');
+
+    this.linkGroup = this.svg.append('g')
+      .attr({
+        class: 'link-group'
+      });
+
+    this.nodeGroup = this.svg.append('g')
+      .attr({
+        class: 'node-group'
+      });
+  }
+
+  private setupForceLayout() {
+
+    const tick = () => {
+      this.nodeGroup.selectAll('g.node')
+        .attr({
+          transform: d => `translate(${d.x}, ${d.y})`
+        });
+
+      this.linkGroup.selectAll('line')
+        .attr({
+          x1: l => l.source.x || 0,
+          y1: l => l.source.y || 0,
+          x2: l => l.target.x || 0,
+          y2: l => l.target.y || 0,
+        });
+    };
+    const svgDim = this.getSvgDimensions();
+    this.forceLayout = d3.layout.force()
+      .size([svgDim.width, svgDim.heigth])
+      .linkDistance(config.force.linkDistance)
+      .charge(config.force.charge)
+      .gravity(config.force.gravity)
+      .on('tick', tick);
+  }
+
+  private addNodeLinksToForceLayout(data: IXosServiceGraph) {
+    this.forceLayout
+      .nodes(data.nodes)
+      .links(data.links)
+      .start();
+  }
+
+  private getSiblingTextBBox(contex: any /* D3 this */) {
+    return d3.select(contex.parentNode).select('text').node().getBBox();
+  }
+
+  private renderServiceNodes(nodes: any) {
+
+    const self = this;
+    nodes.append('rect')
+    .attr({
+      rx: config.node.radius,
+      ry: config.node.radius
+    });
+
+    nodes.append('text')
+      .attr({
+        'text-anchor': 'middle'
+      })
+      .text(n => n.label);
+    // .text(n => `${n.id} - ${n.label}`);
+
+    const existing = nodes.selectAll('rect');
+
+    // resize node > rect as contained text
+    existing.each(function() {
+      const textBBox = self.getSiblingTextBBox(this);
+      const rect = d3.select(this);
+      rect.attr({
+        width: textBBox.width + config.node.padding,
+        height: textBBox.height + config.node.padding,
+        x: textBBox.x - (config.node.padding / 2),
+        y: textBBox.y - (config.node.padding / 2)
+      });
+    });
+  }
+
+  private renderTenantNodes(nodes: any) {
+    nodes.append('rect')
+      .attr({
+        width: 40,
+        height: 40,
+        x: -25,
+        y: -25,
+        transform: `rotate(45)`
+      });
+
+    nodes.append('text')
+      .attr({
+        'text-anchor': 'middle'
+      })
+      .text(n => n.label);
+  }
+
+  private renderNetworkNodes(nodes: any) {
+    const self = this;
+    nodes.append('circle');
+
+    nodes.append('text')
+      .attr({
+        'text-anchor': 'middle'
+      })
+      .text(n => n.label);
+
+    const existing = nodes.selectAll('circle');
+
+    // resize node > rect as contained text
+    existing.each(function() {
+      const textBBox = self.getSiblingTextBBox(this);
+      const rect = d3.select(this);
+      rect.attr({
+        r: (textBBox.width / 2) + config.node.padding,
+        cy: - (textBBox.height / 4)
+      });
+    });
+  }
+
+  private renderSubscriberNodes(nodes: any) {
+    const self = this;
+    nodes.append('rect');
+
+    nodes.append('text')
+      .attr({
+        'text-anchor': 'middle'
+      })
+      .text(n => n.label);
+
+    const existing = nodes.selectAll('rect');
+
+    // resize node > rect as contained text
+    existing.each(function() {
+      const textBBox = self.getSiblingTextBBox(this);
+      const rect = d3.select(this);
+      rect.attr({
+        width: textBBox.width + config.node.padding,
+        height: textBBox.height + config.node.padding,
+        x: textBBox.x - (config.node.padding / 2),
+        y: textBBox.y - (config.node.padding / 2)
+      });
+    });
+  }
+
+  private renderNodes(nodes: IXosServiceGraphNode[]) {
+    const node = this.nodeGroup
+      .selectAll('g.node')
+      .data(nodes, n => n.id);
+
+    const svgDim = this.getSvgDimensions();
+    const hStep = svgDim.width / (nodes.length - 1);
+    const vStep = svgDim.heigth / (nodes.length - 1);
+    const entering = node.enter()
+      .append('g')
+      .attr({
+        class: n => `node ${n.type}`,
+        transform: (n, i) => `translate(${hStep * i}, ${vStep * i})`
+      })
+      .call(this.forceLayout.drag)
+      .on('mousedown', () => {
+        d3.event.stopPropagation();
+      })
+      .on('mouseup', (d) => {
+        d.fixed = true;
+      });
+
+    this.renderServiceNodes(entering.filter('.service'));
+    this.renderTenantNodes(entering.filter('.tenant'));
+    this.renderNetworkNodes(entering.filter('.network'));
+    this.renderSubscriberNodes(entering.filter('.subscriber'));
+  }
+
+  private renderLinks(links: IXosServiceGraphLink[]) {
+    const link = this.linkGroup
+      .selectAll('line')
+      .data(links, l => l.id);
+
+    const entering = link.enter();
+
+    entering.append('line')
+      .attr({
+        class: 'link',
+        'marker-start': 'url(#arrow)'
+      });
+  }
+}
+
+export const XosFineGrainedTenancyGraph: angular.IComponentOptions = {
+  template: require('./fine-grained.component.html'),
+  controllerAs: 'vm',
+  controller: XosFineGrainedTenancyGraphCtrl,
+};
diff --git a/src/app/service-graph/index.ts b/src/app/service-graph/index.ts
index ab9207b..70b255e 100644
--- a/src/app/service-graph/index.ts
+++ b/src/app/service-graph/index.ts
@@ -2,12 +2,21 @@
 import {XosServiceGraphStore} from './services/graph.store';
 import {xosCore} from '../core/index';
 import {XosCoarseTenancyGraph} from './components/coarse/coarse.component';
+import {XosFineGrainedTenancyGraph} from './components/fine-grained/fine-grained.component';
 export const xosServiceGraph = 'xosServiceGraph';
 
 angular
   .module(xosServiceGraph, [xosDataSources, xosCore])
   .service('XosServiceGraphStore', XosServiceGraphStore)
   .component('xosCoarseTenancyGraph', XosCoarseTenancyGraph)
+  .component('xosFineGrainedTenancyGraph', XosFineGrainedTenancyGraph)
+  .config(($stateProvider) => {
+    $stateProvider
+      .state('xos.fine-grained-graph', {
+        url: 'tenancy-graph',
+        component: 'xosFineGrainedTenancyGraph',
+      });
+  })
   .run(($log: ng.ILogService) => {
     $log.info(`[${xosServiceGraph}] Module Setup`);
   });
diff --git a/src/app/service-graph/interfaces.ts b/src/app/service-graph/interfaces.ts
index ab80c79..e08bc78 100644
--- a/src/app/service-graph/interfaces.ts
+++ b/src/app/service-graph/interfaces.ts
@@ -1,5 +1,6 @@
 export interface IXosServiceModel {
   id: number;
+  d3Id?: string;
   backend_status: string;
   kind: string;
   name: string;
@@ -8,6 +9,7 @@
 
 export interface IXosTenantModel {
   id: number;
+  d3Id?: string;
   backend_status: string;
   kind: string;
 
@@ -17,10 +19,11 @@
   // destination
   subscriber_service_id: number;
   subscriber_tenant_id: number;
-  subscriber_user_id: number;
   subscriber_root_id: number;
   subscriber_network_id: number;
 
+  subscriber_user_id: number;
+
   // extra informations
   service_specific_id: string;
   service_specific_attribute: string;
@@ -35,13 +38,18 @@
   tenants: IXosTenantModel[];
 }
 
+export interface IXosFineGrainedGraphData extends IXosCoarseGraphData {
+  subscribers: IXosServiceModel[];
+  networks: IXosTenantModel[];
+}
+
 export interface IXosServiceGraphNodeBadge {
   type: 'info'|'success'|'warning'|'danger';
   text: string;
 }
 
 export interface IXosServiceGraphNode {
-  id: number;
+  id: number | string;
   label: string;
   x?: number;
   y?: number;
@@ -49,10 +57,11 @@
   py?: number;
   badge?: IXosServiceGraphNodeBadge;
   model: IXosServiceModel;
+  type: 'service' | 'tenant' | 'network' | 'subscriber';
 }
 
 export interface IXosServiceGraphLink {
-  id: number;
+  id: number | string;
   source: number;
   target: number;
   model: IXosTenantModel;
diff --git a/src/app/service-graph/services/graph.store.ts b/src/app/service-graph/services/graph.store.ts
index 821da41..0ed0960 100644
--- a/src/app/service-graph/services/graph.store.ts
+++ b/src/app/service-graph/services/graph.store.ts
@@ -3,13 +3,12 @@
 import {IXosModelStoreService} from '../../datasources/stores/model.store';
 import {
   IXosServiceGraph, IXosServiceModel, IXosTenantModel, IXosCoarseGraphData,
-  IXosServiceGraphNode, IXosServiceGraphLink
+  IXosServiceGraphNode, IXosServiceGraphLink, IXosFineGrainedGraphData
 } from '../interfaces';
 import {IXosDebouncer} from '../../core/services/helpers/debounce.helper';
 export interface IXosServiceGraphStore {
   get(): Observable<IXosServiceGraph>;
   getCoarse(): Observable<IXosServiceGraph>;
-  dispose(): void;
 }
 
 export class XosServiceGraphStore implements IXosServiceGraphStore {
@@ -20,18 +19,22 @@
   ];
 
   // graph data store
-  private graphData: BehaviorSubject<IXosCoarseGraphData> = new BehaviorSubject({
+  private graphData: BehaviorSubject<IXosFineGrainedGraphData> = new BehaviorSubject({
     services: [],
-    tenants: []
+    tenants: [],
+    networks: [],
+    subscribers: []
   });
 
-  // reprentations of the graph as D3 requires
+  // representation of the graph as D3 requires
   private d3CoarseGraph = new BehaviorSubject({});
   private d3FineGrainedGraph = new BehaviorSubject({});
 
   // storing locally reference to the data model
   private services;
   private tenants;
+  private subscribers;
+  private networks;
 
   // debounced functions
   private handleData;
@@ -39,6 +42,8 @@
   // datastore
   private ServiceSubscription: Subscription;
   private TenantSubscription: Subscription;
+  private SubscriberSubscription: Subscription;
+  private NetworkSubscription: Subscription;
 
   constructor (
     private $log: ng.ILogService,
@@ -51,15 +56,16 @@
     // we want to have a quiet period of 500ms from the last event before doing anything
     this.handleData = this.XosDebouncer.debounce(this._handleData, 500, this, false);
 
-
     // observe models and populate graphData
+    // TODO get Nodes (model that represent compute nodes in a pod)
+    // TODO get Instances (model that represent deployed VMs)
     this.ServiceSubscription = this.XosModelStore.query('Service', '/core/services')
       .subscribe(
         (res) => {
           this.combineData(res, 'services');
         },
         (err) => {
-          this.$log.error(`[XosServiceGraphStore] graphData Observable: `, err);
+          this.$log.error(`[XosServiceGraphStore] Service Observable: `, err);
         }
       );
 
@@ -69,31 +75,41 @@
           this.combineData(res, 'tenants');
         },
         (err) => {
-          this.$log.error(`[XosServiceGraphStore] graphData Observable: `, err);
+          this.$log.error(`[XosServiceGraphStore] Tenant Observable: `, err);
         }
       );
 
-    // observe graphData and build Coarse or FineGrained graphs (based on who's subscribed)
-    this.graphData
+    this.SubscriberSubscription = this.XosModelStore.query('Subscriber', '/core/subscribers')
       .subscribe(
-        (res: IXosCoarseGraphData) => {
-          if (this.d3CoarseGraph.observers.length > 0) {
-            this.graphDataToCoarseGraph(res);
-          }
-          if (this.d3FineGrainedGraph.observers.length > 0) {
-            // TODO graphDataToFineGrainedGraph
-          }
+        (res) => {
+          this.combineData(res, 'subscribers');
+        },
+        (err) => {
+          this.$log.error(`[XosServiceGraphStore] Subscriber Observable: `, err);
+        }
+      );
+
+    this.NetworkSubscription = this.XosModelStore.query('Network', '/core/networks')
+      .subscribe(
+        (res) => {
+          this.combineData(res, 'networks');
         },
         (err) => {
           this.$log.error(`[XosServiceGraphStore] graphData Observable: `, err);
         }
       );
-  }
 
-  public dispose() {
-    // cancel subscriptions from observables
-    this.ServiceSubscription.unsubscribe();
-    this.TenantSubscription.unsubscribe();
+    // observe graphData and build Coarse and FineGrained graphs
+    this.graphData
+      .subscribe(
+        (res: IXosFineGrainedGraphData) => {
+          this.graphDataToCoarseGraph(res);
+          this.graphDataToFineGrainedGraph(res);
+        },
+        (err) => {
+          this.$log.error(`[XosServiceGraphStore] graphData Observable: `, err);
+        }
+      );
   }
 
   public get() {
@@ -104,7 +120,7 @@
     return this.d3CoarseGraph.asObservable();
   }
 
-  private combineData(data: any, type: 'services'|'tenants') {
+  private combineData(data: any, type: 'services'|'tenants'|'subscribers'|'networks') {
     switch (type) {
       case 'services':
         this.services = data;
@@ -112,6 +128,12 @@
       case 'tenants':
         this.tenants = data;
         break;
+      case 'subscribers':
+        this.subscribers = data;
+        break;
+      case 'networks':
+        this.networks = data;
+        break;
     }
     this.handleData(this.services, this.tenants);
   }
@@ -119,23 +141,71 @@
   private _handleData(services: IXosServiceModel[], tenants: IXosTenantModel[]) {
     this.graphData.next({
       services: this.services,
-      tenants: this.tenants
+      tenants: this.tenants,
+      subscribers: this.subscribers,
+      networks: this.networks
     });
   }
 
-  private getCoarseNodeIndexById(id: number, nodes: IXosServiceModel[]) {
+  private getNodeIndexById(id: number | string, nodes: IXosServiceModel[]) {
     return _.findIndex(nodes, {id: id});
   }
 
+  private d3Id(type: string, id: number) {
+    return `${type.toLowerCase()}~${id}`;
+  }
+
+  private getTargetId(tenant: IXosTenantModel) {
+
+    let targetId;
+    if (tenant.subscriber_service_id) {
+      targetId = this.d3Id('service', tenant.subscriber_service_id);
+    }
+    else if (tenant.subscriber_tenant_id) {
+      targetId = this.d3Id('tenant', tenant.subscriber_tenant_id);
+    }
+    else if (tenant.subscriber_network_id) {
+      targetId = this.d3Id('network', tenant.subscriber_network_id);
+    }
+    else if (tenant.subscriber_root_id) {
+      targetId = this.d3Id('subscriber', tenant.subscriber_root_id);
+    }
+    return targetId;
+  }
+
+  private getSourceId(tenant: IXosTenantModel) {
+    return this.d3Id('service', tenant.provider_service_id);
+  }
+
+  private getNodeType(n: any) {
+    return n.class_names.split(',')[0].toLowerCase();
+  }
+
+  private getNodeLabel(n: any) {
+    if (this.getNodeType(n) === 'tenant') {
+      return n.id;
+    }
+    return n.humanReadableName ? n.humanReadableName : n.name;
+  }
+
+  private removeUnwantedFineGrainedData(data: IXosFineGrainedGraphData): IXosFineGrainedGraphData {
+    data.tenants = _.filter(data.tenants, t => t.kind !== 'coarse');
+    data.networks = _.filter(data.networks, n => {
+      const subscriber = _.findIndex(data.tenants, {subscriber_network_id: n.id});
+      return subscriber > -1;
+    });
+    return data;
+  }
+
   private graphDataToCoarseGraph(data: IXosCoarseGraphData) {
-    // TODO find how to bind source/target by node ID and not by position in array (ask Simon?)
+
     const links: IXosServiceGraphLink[] = _.chain(data.tenants)
       .filter((t: IXosTenantModel) => t.kind === 'coarse')
       .map((t: IXosTenantModel) => {
         return {
           id: t.id,
-          source: this.getCoarseNodeIndexById(t.provider_service_id, data.services),
-          target: this.getCoarseNodeIndexById(t.subscriber_service_id, data.services),
+          source: this.getNodeIndexById(t.provider_service_id, data.services),
+          target: this.getNodeIndexById(t.subscriber_service_id, data.services),
           model: t
         };
       })
@@ -155,4 +225,61 @@
     });
   }
 
+  private graphDataToFineGrainedGraph(data: IXosFineGrainedGraphData) {
+
+    data = this.removeUnwantedFineGrainedData(data);
+
+    let nodes = _.reduce(Object.keys(data), (list: any[], k: string) => {
+      return list.concat(data[k]);
+    }, []);
+
+    nodes = _.chain(nodes)
+      .map(n => {
+        n.d3Id = this.d3Id(this.getNodeType(n), n.id);
+        return n;
+      })
+      .map(n => {
+        let node: IXosServiceGraphNode = {
+          id: n.d3Id,
+          label: this.getNodeLabel(n),
+          model: n,
+          type: this.getNodeType(n)
+        };
+        return node;
+      })
+      .value();
+
+    const links = _.reduce(data.tenants, (links: IXosServiceGraphLink[], tenant: IXosTenantModel) => {
+      const sourceId = this.getSourceId(tenant);
+      const targetId = this.getTargetId(tenant);
+
+      const tenantToProvider = {
+        id: `${sourceId}_${tenant.d3Id}`,
+        source: this.getNodeIndexById(sourceId, nodes),
+        target: this.getNodeIndexById(tenant.d3Id, nodes),
+        model: tenant
+      };
+
+      const tenantToSubscriber = {
+        id: `${tenant.d3Id}_${targetId}`,
+        source: this.getNodeIndexById(tenant.d3Id, nodes),
+        target: this.getNodeIndexById(targetId, nodes),
+        model: tenant
+      };
+
+      links.push(tenantToProvider);
+      links.push(tenantToSubscriber);
+      return links;
+    }, []);
+
+    if (nodes.length === 0 || links.length === 0) {
+      return;
+    }
+
+    this.d3FineGrainedGraph.next({
+      nodes: nodes,
+      links: links
+    });
+  }
+
 }