[CORD-2424] Adding Instances and Networks to the graph

Change-Id: Ib30081f4995930d979447af59124896f1308f54d
diff --git a/src/app/service-graph/components/graph/graph.component.scss b/src/app/service-graph/components/graph/graph.component.scss
index 40b6f11..5d773a1 100644
--- a/src/app/service-graph/components/graph/graph.component.scss
+++ b/src/app/service-graph/components/graph/graph.component.scss
@@ -73,12 +73,12 @@
 
     .node {
       cursor: pointer;
+      fill: $background-color;
     }
 
     .node.service {
       > rect {
         stroke: $color-accent;
-        fill: $background-color;
       }
       > .icon {
         fill: $color-accent;
@@ -87,11 +87,28 @@
 
     .node.serviceinstance {
       > rect {
-        stroke: green;
-        fill: $background-color;
+        stroke: $serviceinstances-fg;
       }
       > .icon {
-        fill: green;
+        fill: $serviceinstances-fg;
+      }
+    }
+
+    .node.instance {
+      > rect {
+        stroke: $instances-fg;
+      }
+      > .icon {
+        fill: $instances-fg;
+      }
+    }
+
+    .node.network {
+      > rect {
+        stroke: $networks-fg;
+      }
+      > .icon {
+        fill: $networks-fg;
       }
     }
 
@@ -114,12 +131,21 @@
     }
 
     line.ownership {
-      stroke: green;
+      stroke: $serviceinstances-fg;
       stroke-dasharray: 5;
     }
 
     line.serviceinstancelink {
-      stroke: green;
+      stroke: $serviceinstances-fg;
+    }
+
+    line.instance_ownership {
+      stroke: $instances-fg;
+      stroke-dasharray: 5;
+    }
+
+    line.port {
+      stroke: $networks-fg;
     }
   }
   .arrow-marker {
diff --git a/src/app/service-graph/components/graph/graph.component.ts b/src/app/service-graph/components/graph/graph.component.ts
index 5f84384..ed36654 100644
--- a/src/app/service-graph/components/graph/graph.component.ts
+++ b/src/app/service-graph/components/graph/graph.component.ts
@@ -26,7 +26,7 @@
 import {IXosServiceGraphIcons} from '../../services/d3-helpers/graph-icons.service';
 import {IXosNodePositioner} from '../../services/node-positioner.service';
 import {IXosNodeRenderer} from '../../services/renderer/node.renderer';
-import {IXosSgNode} from '../../interfaces';
+import {IXosSgLink, IXosSgNode} from '../../interfaces';
 import {IXosGraphConfig} from '../../services/graph.config';
 
 class XosServiceGraphCtrl {
@@ -74,7 +74,6 @@
         graph => {
           this.graph = graph;
           if (this.graph.nodes().length > 0) {
-            this.loader = false;
             this.renderGraph(this.graph);
           }
         },
@@ -154,6 +153,7 @@
 
     this.XosNodePositioner.positionNodes(svgDim, nodes)
       .then((nodes: IXosSgNode[]) => {
+        this.loader = false;
 
         this.forceLayout
           .nodes(nodes)
@@ -162,6 +162,15 @@
           .linkDistance(config.force.linkDistance)
           .charge(config.force.charge)
           .gravity(config.force.gravity)
+          .linkStrength((link: IXosSgLink) => {
+            switch (link.type) {
+              case 'ownership':
+              case 'instance_ownership':
+                // NOTE make "ownsership" links stronger than other for positioning
+                return 1;
+            }
+            return 0.1;
+          })
           .start();
 
         // render nodes
diff --git a/src/app/service-graph/services/d3-helpers/graph-icons.service.ts b/src/app/service-graph/services/d3-helpers/graph-icons.service.ts
index 9baeb57..de03a62 100644
--- a/src/app/service-graph/services/d3-helpers/graph-icons.service.ts
+++ b/src/app/service-graph/services/d3-helpers/graph-icons.service.ts
@@ -34,6 +34,18 @@
     'M13,0a1,1,0,0,1,.53.2l9.4,5.45c.54.32.54.45,0,.78C19.78,8.2,16.7,10,13.63,11.74a1.12,1.12,0,0,1-1.24,0C9.28,9.92,6.15,8.12,3,6.31c-.55-.31-.55-.46,0-.78L12.45.19A.86.86,0,0,1,13,0Z\n' +
     'M24.73,14.16c0,1.81,0,3.61,0,5.42a.8.8,0,0,1-.46.79l-9.36,5.41c-.64.38-.79.29-.79-.47,0-3.53,0-7.07,0-10.6a.92.92,0,0,1,.52-.94q4.63-2.65,9.26-5.35l.24-.14c.43-.2.58-.11.58.36Z',
     transform: ''
+  },
+  {
+    name: 'instance',
+    path: 'M11.87,19.94v5.47c0,.61-.18.72-.69.42l-9.6-5.55a.62.62,0,0,1-.32-.64c0-3.64,0-7.29,0-10.94,0-.7.16-.79.79-.42,2.89,1.67,5.77,3.39,8.69,5a1.81,1.81,0,0,1,1.14,2C11.8,16.81,11.87,18.38,11.87,19.94Z\n' +
+    'M13,0a1,1,0,0,1,.53.2l9.4,5.45c.54.32.54.45,0,.78C19.78,8.2,16.7,10,13.63,11.74a1.12,1.12,0,0,1-1.24,0C9.28,9.92,6.15,8.12,3,6.31c-.55-.31-.55-.46,0-.78L12.45.19A.86.86,0,0,1,13,0Z\n' +
+    'M24.73,14.16c0,1.81,0,3.61,0,5.42a.8.8,0,0,1-.46.79l-9.36,5.41c-.64.38-.79.29-.79-.47,0-3.53,0-7.07,0-10.6a.92.92,0,0,1,.52-.94q4.63-2.65,9.26-5.35l.24-.14c.43-.2.58-.11.58.36Z',
+    transform: ''
+  },
+  {
+    name: 'network',
+    path: 'M15.62,1.56a4.69,4.69,0,0,1,4.69,4.63,2.83,2.83,0,0,0,0,.29l0,1.15L21.33,8a3.11,3.11,0,0,1-1,6.06H4.69a3.13,3.13,0,0,1-.05-6.25l.22,0,1.19.07.39-1.12a3.12,3.12,0,0,1,2.94-2.1,3,3,0,0,1,.54.06l1.1.19.54-1a4.7,4.7,0,0,1,4.06-2.41m0-1.56A6.2,6.2,0,0,0,10.2,3.21a4.48,4.48,0,0,0-.82-.09A4.67,4.67,0,0,0,5,6.28l-.28,0a4.69,4.69,0,1,0,0,9.37H20.31a4.67,4.67,0,0,0,1.54-9.09c0-.1,0-.19,0-.28A6.25,6.25,0,0,0,15.62,0Z',
+    transform: ''
   }
 ];
 
diff --git a/src/app/service-graph/services/graph.config.ts b/src/app/service-graph/services/graph.config.ts
index 7db2d97..e6ddfbd 100644
--- a/src/app/service-graph/services/graph.config.ts
+++ b/src/app/service-graph/services/graph.config.ts
@@ -34,6 +34,33 @@
     'XosGraphStore'
   ];
 
+  private instanceEnabled = false;
+  private instanceBinding = {
+    key: 'i',
+    modifiers: ['shift'],
+    cb: () => {
+      // NOTE anytime the graph change the observable is updated,
+      // no need to manually retrigger here
+      this.XosGraphStore.toggleInstances();
+      this.toggleNetworkShortcuts();
+    },
+    label: 'i',
+    description: 'Toggle Instances'
+  };
+
+  private networkEnabled = false;
+  private networkBinding = {
+    key: 'n',
+    modifiers: ['shift'],
+    cb: () => {
+      // NOTE anytime the graph change the observable is updated,
+      // no need to manually retrigger here
+      this.XosGraphStore.toggleNetwork();
+    },
+    label: 'n',
+    description: 'Toggle Networks'
+  };
+
   constructor (
     private $log: ng.ILogService,
     private $cookies: ng.cookies.ICookiesService,
@@ -64,13 +91,13 @@
       key: 's',
       modifiers: ['shift'],
       cb: () => {
-        // NOTE anytime the graph change the observable is updated,
-        // no need to manually retrigger here
         this.XosGraphStore.toggleServiceInstances();
+        this.toggleInstanceShortcuts();
       },
       label: 's',
       description: 'Toggle ServiceInstances'
     });
+
   }
 
   public toggleFullscreen() {
@@ -80,4 +107,26 @@
       this.$rootScope.$broadcast('xos.sg.update');
     }, 500);
   }
+
+  private toggleInstanceShortcuts(): void {
+    if (!this.instanceEnabled) {
+      this.XosKeyboardShortcut.registerKeyBinding(this.instanceBinding);
+    }
+    else {
+      this.XosKeyboardShortcut.removeKeyBinding(this.instanceBinding);
+      this.XosKeyboardShortcut.removeKeyBinding(this.networkBinding);
+    }
+    this.instanceEnabled = !this.instanceEnabled;
+  }
+
+  private toggleNetworkShortcuts(): void {
+    if (!this.networkEnabled) {
+      this.XosKeyboardShortcut.registerKeyBinding(this.networkBinding);
+    }
+    else {
+      this.XosKeyboardShortcut.removeKeyBinding(this.networkBinding);
+    }
+
+    this.networkEnabled = !this.networkEnabled;
+  }
 }
diff --git a/src/app/service-graph/services/graph.store.ts b/src/app/service-graph/services/graph.store.ts
index 4d46c88..52100e7 100644
--- a/src/app/service-graph/services/graph.store.ts
+++ b/src/app/service-graph/services/graph.store.ts
@@ -30,6 +30,8 @@
   nodesFromGraph(graph: Graph): IXosSgNode[];
   linksFromGraph(graph: Graph): IXosSgLink[];
   toggleServiceInstances(): Graph;
+  toggleInstances(): Graph;
+  toggleNetwork(): Graph;
 }
 
 export class XosGraphStore implements IXosGraphStore {
@@ -41,16 +43,22 @@
 
   // state
   private serviceInstanceShown: boolean = false;
+  private instanceShown: boolean = false;
+  private networkShown: boolean = false;
 
   // graphs
   private serviceGraph: any;
   private ServiceGraphSubject: BehaviorSubject<any>;
 
   // datastore
+  private InstanceSubscription: Subscription;
+  private NetworkSubscription: Subscription;
+  private PortSubscription: Subscription;
   private ServiceSubscription: Subscription;
   private ServiceDependencySubscription: Subscription;
   private ServiceInstanceSubscription: Subscription;
   private ServiceInstanceLinkSubscription: Subscription;
+  private TenantWithContainerSubscription: Subscription;
 
   // debounced
   private efficientNext = this.XosDebouncer.debounce(this.callNext, 500, this, false);
@@ -76,6 +84,7 @@
   public nodesFromGraph(graph: Graph): IXosSgNode[] {
     return _.map(graph.nodes(), (n: string) => {
       const nodeData = graph.node(n);
+
       return {
         id: n,
         type: this.getModelType(nodeData),
@@ -87,16 +96,9 @@
   public linksFromGraph(graph: Graph): IXosSgLink[] {
     const nodes = this.nodesFromGraph(graph);
 
-    // NOTE we'll need some intelligence here to differentiate between:
-    // - ServiceDependency
-    // - ServiceInstanceLinks
-    // - Owners
-
     return _.map(graph.edges(), l => {
       const link = graph.edge(l);
       const linkType = this.getModelType(link);
-
-      // FIXME consider ownership links
       let sourceId, targetId;
 
       switch (linkType) {
@@ -105,17 +107,28 @@
           targetId = this.getServiceId(link.provider_service_id);
           break;
         case 'serviceinstancelink':
+          // NOTE ServiceInstanceLink can actually also connect to a service and a network
           sourceId = this.getServiceInstanceId(link.subscriber_service_instance_id);
           targetId = this.getServiceInstanceId(link.provider_service_instance_id);
           break;
         case 'ownership':
           sourceId = this.getServiceId(link.service);
           targetId = this.getServiceInstanceId(link.service_instance);
+          break;
+        case 'instance_ownership':
+          sourceId = this.getServiceInstanceId(link.id);
+          targetId = this.getInstanceId(link.instance_id);
+          break;
+        case 'port':
+          sourceId = this.getInstanceId(link.instance_id);
+          targetId = this.getNetworkId(link.network_id);
+          break;
       }
 
       // NOTE help while debugging
       if (!sourceId || !targetId) {
         this.$log.warn(`Link ${l.v}-${l.w} has missing source or target:`, l, link);
+        // TODO return null and then filter out so that we don't break the rendering
       }
 
       return {
@@ -136,6 +149,18 @@
 
       // remove nodes from the graph
       this.removeElementsFromGraph('serviceinstance'); // NOTE links are automatically removed by the graph library
+
+      if (this.instanceShown) {
+        // NOTE if we remove ServiceInstances we also need to remove Instances
+        this.removeElementsFromGraph('instance');
+        this.instanceShown = false;
+      }
+
+      if (this.networkShown) {
+        // NOTE if we remove ServiceInstances we also need to remove Networks
+        this.removeElementsFromGraph('network');
+        this.networkShown = false;
+      }
     }
     else {
       // NOTE subscribe to ServiceInstance and ServiceInstanceLink observables
@@ -146,6 +171,43 @@
     return this.serviceGraph;
   }
 
+  public toggleInstances(): Graph {
+    if (this.instanceShown) {
+
+      this.InstanceSubscription.unsubscribe();
+      this.TenantWithContainerSubscription.unsubscribe();
+
+      this.removeElementsFromGraph('instance'); // NOTE links are automatically removed by the graph library
+
+      if (this.networkShown) {
+        // NOTE if we remove Instances we also need to remove Networks
+        this.removeElementsFromGraph('network');
+        this.networkShown = false;
+      }
+    }
+    else {
+      this.loadInstances();
+      this.loadInstanceLinks();
+    }
+    this.instanceShown = !this.instanceShown;
+    return this.serviceGraph;
+  }
+
+  public toggleNetwork() {
+    if (this.networkShown) {
+      this.NetworkSubscription.unsubscribe();
+      this.PortSubscription.unsubscribe();
+      this.removeElementsFromGraph('network');
+    }
+    else {
+      this.loadNetworks();
+      this.loadPorts(); // Ports define the connection of an Instance to a Network
+    }
+
+    this.networkShown = !this.networkShown;
+    return this.serviceGraph;
+  }
+
   public get(): Observable<Graph> {
     return this.ServiceGraphSubject.asObservable();
   }
@@ -194,6 +256,22 @@
     this.serviceGraph.setEdge(sourceId, targetId, link);
   }
 
+  private addInstanceOwner(tenantWithContainer: any) {
+    // NOTE some TenantWithContainer don't have an instance
+    if (tenantWithContainer.instance_id) {
+      const sourceId = this.getServiceInstanceId(tenantWithContainer.id);
+      const targetId = this.getInstanceId(tenantWithContainer.instance_id);
+      this.serviceGraph.setEdge(sourceId, targetId, angular.merge(tenantWithContainer, {type: 'instance_ownership'}));
+    }
+  }
+
+  private addNetworkLink(port: any) {
+    // ports are connected to 1 Instance and 1 Network
+    const sourceId = this.getInstanceId(port.instance_id);
+    const targetId = this.getNetworkId(port.network_id);
+    this.serviceGraph.setEdge(sourceId, targetId, angular.merge(port, {type: 'port'}));
+  }
+
   private removeElementsFromGraph(type: string) {
     _.forEach(this.serviceGraph.nodes(), (n: string) => {
       const node = this.serviceGraph.node(n);
@@ -209,7 +287,7 @@
   // helpers
   private getModelType(node: IXosBaseModel): string {
     if (node.type) {
-      // NOTE we'll add "ownership" links
+      // NOTE handling "ownership" links
       return node.type;
     }
     return node.class_names.split(',')[0].toLowerCase();
@@ -223,6 +301,14 @@
     return `serviceinstance~${id}`;
   }
 
+  private getInstanceId(id: number): string {
+    return `instance~${id}`;
+  }
+
+  private getNetworkId(id: number): string {
+    return `network~${id}`;
+  }
+
   private getNodeId(node: IXosBaseModel): string {
 
     const nodeType = this.getModelType(node);
@@ -231,6 +317,10 @@
         return this.getServiceId(node.id);
       case 'serviceinstance':
         return this.getServiceInstanceId(node.id);
+      case 'instance':
+        return this.getInstanceId(node.id);
+      case 'network':
+        return this.getNetworkId(node.id);
     }
   }
 
@@ -303,6 +393,74 @@
       );
   }
 
+  private loadInstances() {
+    this.InstanceSubscription = this.XosModelStore.query('Instance', '/core/instances')
+      .subscribe(
+        (res) => {
+          if (res.length > 0) {
+            _.forEach(res, n => {
+              this.addNode(n);
+            });
+            this.efficientNext(this.ServiceGraphSubject, this.serviceGraph);
+          }
+        },
+        (err) => {
+          this.$log.error(`[XosServiceGraphStore] Instance Observable: `, err);
+        }
+      );
+  }
+
+  private loadInstanceLinks() {
+    this.TenantWithContainerSubscription = this.XosModelStore.query('TnantWithContainer', '/core/tenantwithcontainers')
+      .subscribe(
+        (res) => {
+          if (res.length > 0) {
+            _.forEach(res, n => {
+              this.addInstanceOwner(n);
+            });
+            this.efficientNext(this.ServiceGraphSubject, this.serviceGraph);
+          }
+        },
+        (err) => {
+          this.$log.error(`[XosServiceGraphStore] Instance Observable: `, err);
+        }
+      );
+  }
+
+  private loadNetworks() {
+    this.NetworkSubscription = this.XosModelStore.query('Network', '/core/networks')
+      .subscribe(
+        (res) => {
+          if (res.length > 0) {
+            _.forEach(res, n => {
+              this.addNode(n);
+            });
+            this.efficientNext(this.ServiceGraphSubject, this.serviceGraph);
+          }
+        },
+        (err) => {
+          this.$log.error(`[XosServiceGraphStore] Network Observable: `, err);
+        }
+      );
+  }
+
+  private loadPorts() {
+    this.PortSubscription = this.XosModelStore.query('Port', '/core/ports')
+      .subscribe(
+        (res) => {
+          if (res.length > 0) {
+            _.forEach(res, n => {
+              this.addNetworkLink(n);
+            });
+            this.efficientNext(this.ServiceGraphSubject, this.serviceGraph);
+          }
+        },
+        (err) => {
+          this.$log.error(`[XosServiceGraphStore] Network Observable: `, err);
+        }
+      );
+  }
+
   private callNext(subject: BehaviorSubject<any>, data: any) {
     subject.next(data);
   }
diff --git a/src/app/service-graph/services/renderer/node.renderer.ts b/src/app/service-graph/services/renderer/node.renderer.ts
index fb29046..18719f1 100644
--- a/src/app/service-graph/services/renderer/node.renderer.ts
+++ b/src/app/service-graph/services/renderer/node.renderer.ts
@@ -60,6 +60,8 @@
 
     this.renderServiceNodes(entering.filter('.service'));
     this.renderServiceInstanceNodes(entering.filter('.serviceinstance'));
+    this.renderInstanceNodes(entering.filter('.instance'));
+    this.renderNetworkNodes(entering.filter('.network'));
 
     node.exit().remove();
   }
@@ -106,6 +108,46 @@
     this.handleLabels(nodes); // eventually improve, padding top is wrong
   }
 
+  private renderInstanceNodes(nodes: d3.selection) {
+    nodes
+      .append('rect')
+      .attr({
+        rx: config.node.radius,
+        ry: config.node.radius
+      });
+
+    nodes
+      .append('path')
+      .attr({
+        d: this.XosServiceGraphIcons.get('instance').path,
+        transform: this.XosServiceGraphIcons.get('instance').transform,
+        class: 'icon'
+      });
+
+    this.positionServiceNodeGroup(nodes);
+    this.handleLabels(nodes);
+  }
+
+  private renderNetworkNodes(nodes: d3.selection) {
+    nodes
+      .append('rect')
+      .attr({
+        rx: config.node.radius,
+        ry: config.node.radius
+      });
+
+    nodes
+      .append('path')
+      .attr({
+        d: this.XosServiceGraphIcons.get('network').path,
+        transform: this.XosServiceGraphIcons.get('network').transform,
+        class: 'icon'
+      });
+
+    this.positionServiceNodeGroup(nodes);
+    this.handleLabels(nodes);
+  }
+
   private positionServiceNodeGroup(nodes: d3.selection) {
     const self = this;
     nodes.each(function (d: IXosSgNode) {
@@ -231,6 +273,7 @@
   }
 
   private getNodeLabel(n: any): string {
+    // NOTE for 'instances' display instance_name instead of name?
     return n.data.name ? n.data.name.toUpperCase() : n.data.id;
     // return n.data.name ? n.data.name.toUpperCase() + ` - ${n.data.id}` : n.data.id;
   }