[CORD-2742] Remove elements from the service graph

Change-Id: Ibcb9fac4428f0b168ff66fab3219b8357e732dc5
diff --git a/conf/browsersync.conf.js b/conf/browsersync.conf.js
index ff3bb4f..23dee02 100644
--- a/conf/browsersync.conf.js
+++ b/conf/browsersync.conf.js
@@ -18,6 +18,7 @@
 
 const conf = require('./gulp.conf');
 const proxy = require('./proxy').proxy;
+const wsProxy = require('./proxy').wsProxy;
 
 module.exports = function () {
   return {
@@ -29,11 +30,15 @@
       middleware: function(req, res, next) {
         if (
           req.url.indexOf('xosapi') !== -1
-          || req.url.indexOf('socket.io') !== -1
           || req.url.indexOf('extensions') !== -1
         ) {
           proxy.web(req, res);
         }
+        else if (
+          req.url.indexOf('socket.io') !== -1
+        ) {
+          wsProxy.web(req, res);
+        }
         else {
           next();
         }
diff --git a/conf/proxy.js b/conf/proxy.js
index ad94635..387bd57 100644
--- a/conf/proxy.js
+++ b/conf/proxy.js
@@ -19,17 +19,27 @@
 const httpProxy = require('http-proxy');
 
 const target = process.env.PROXY || '127.0.0.1:9101';
+const wsTarget = process.env.WS || '127.0.0.1:3000';
 
 const proxy = httpProxy.createProxyServer({
   target: `http://${target}`,
   ws: true
 });
-
 proxy.on('error', function(error, req, res) {
   res.writeHead(500, {'Content-Type': 'text/plain'});
   console.error('[Proxy]', error);
 });
 
+const wsProxy = httpProxy.createProxyServer({
+  target: `http://${wsTarget}`,
+  ws: true
+});
+wsProxy.on('error', function(error, req, res) {
+  res.writeHead(500, {'Content-Type': 'text/plain'});
+  console.error('[Proxy]', error);
+});
+
 module.exports = {
-  proxy
+  proxy,
+  wsProxy
 };
\ No newline at end of file
diff --git a/src/app/datasources/stores/model.store.ts b/src/app/datasources/stores/model.store.ts
index 09bd715..4f430e6 100644
--- a/src/app/datasources/stores/model.store.ts
+++ b/src/app/datasources/stores/model.store.ts
@@ -68,7 +68,7 @@
       this.efficientNext(this._collections[modelName]);
     }
 
-    // NOTE do we need to subscriber every time we query?
+    // NOTE do we need to subscribe every time we query?
     this.webSocket.list()
       .filter((e: IWSEvent) => e.model === modelName)
       .subscribe(
diff --git a/src/app/service-graph/services/graph.store.spec.ts b/src/app/service-graph/services/graph.store.spec.ts
index 53f24fc..ad67f12 100644
--- a/src/app/service-graph/services/graph.store.spec.ts
+++ b/src/app/service-graph/services/graph.store.spec.ts
@@ -22,6 +22,7 @@
 import {Subject} from 'rxjs/Subject';
 import {Graph} from 'graphlib';
 import {XosDebouncer} from '../../core/services/helpers/debounce.helper';
+import {IWSEvent} from '../../datasources/websocket/global';
 
 interface ITestXosGraphStore extends IXosGraphStore {
 
@@ -94,6 +95,7 @@
 const subject_servicedependency = new Subject();
 const subject_serviceinstances = new Subject();
 const subject_serviceinstancelinks = new Subject();
+const subject_websocket = new Subject();
 
 let MockModelStore = {
   query: jasmine.createSpy('XosModelStore.query')
@@ -113,6 +115,11 @@
     })
 };
 
+let MockWebSocket = {
+  list: jasmine.createSpy('WebSocket.list')
+    .and.returnValue(subject_websocket)
+};
+
 
 describe('The XosGraphStore service', () => {
 
@@ -120,6 +127,7 @@
     angular.module('XosGraphStore', [])
       .service('XosGraphStore', XosGraphStore)
       .value('XosModelStore', MockModelStore)
+      .value('WebSocket', MockWebSocket)
       .service('XosDebouncer', XosDebouncer);
 
     angular.mock.module('XosGraphStore');
@@ -134,25 +142,128 @@
 
   }));
 
-  it('should load services and service-dependency and add nodes to the graph', (done) => {
-    let event = 0;
-    service.get().subscribe(
-      (graph: Graph) => {
-        if (event === 1) {
-          expect(graph.nodes().length).toBe(services.length);
-          expect(graph.nodes()).toEqual(['service~1', 'service~2']);
-          expect(graph.edges().length).toBe(servicedependencies.length);
-          expect(graph.edges()).toEqual([{v: 'service~1', w: 'service~2'}]);
-          done();
+  describe('when started', () => {
+
+    let subscription;
+
+    it('should load services and service-dependency and add nodes to the graph', (done) => {
+      let event = 0;
+      subscription = service.get().subscribe(
+        (graph: Graph) => {
+          if (event === 1) {
+            expect(graph.nodes().length).toBe(services.length);
+            expect(graph.nodes()).toEqual(['service~1', 'service~2']);
+            expect(graph.edges().length).toBe(servicedependencies.length);
+            expect(graph.edges()).toEqual([{v: 'service~1', w: 'service~2'}]);
+            done();
+          }
+          else {
+            event = event + 1;
+          }
         }
-        else {
-          event = event + 1;
+      );
+      subject_services.next(services);
+      subject_servicedependency.next(servicedependencies);
+      scope.$apply();
+    });
+
+    afterEach(() => {
+      subscription.unsubscribe();
+    });
+  });
+
+  describe('when an observed model is removed', () => {
+
+    let subscription;
+
+    beforeEach(() => {
+      subject_services.next([]);
+      subject_servicedependency.next([]);
+      subject_services.next(services);
+      subject_servicedependency.next(servicedependencies);
+      service.addServiceInstances();
+      subject_serviceinstances.next([serviceInstances[0]]);
+    });
+
+    it('should be removed from the graph', (done) => {
+      let event = 0;
+      subscription = service.get().subscribe(
+        (graph: Graph) => {
+          if (event === 1) {
+            expect(graph.nodes().length).toBe(0);
+            expect(graph.nodes()).toEqual([]);
+            expect(graph.edges().length).toBe(0);
+            expect(graph.edges()).toEqual([]);
+            done();
+          }
+          else {
+            event = event + 1;
+          }
         }
-      }
-    );
-    subject_services.next(services);
-    subject_servicedependency.next(servicedependencies);
-    scope.$apply();
+      );
+
+      const removeService1: IWSEvent = {
+        model: 'Service',
+        deleted: true,
+        msg: {
+          changed_fields: [],
+          pk: 1,
+          object: {
+            id: 1,
+            class_names: 'Service, XOSBase'
+          }
+        }
+      };
+
+      const removeService2: IWSEvent = {
+        model: 'Service',
+        deleted: true,
+        msg: {
+          changed_fields: [],
+          pk: 2,
+          object: {
+            id: 2,
+            class_names: 'Service, XOSBase'
+          }
+        }
+      };
+
+      const removeServiceDependency1: IWSEvent = {
+        model: 'ServiceDependency',
+        deleted: true,
+        msg: {
+          changed_fields: [],
+          pk: 1,
+          object: {
+            id: 1,
+            class_names: 'ServiceDependency, XOSBase'
+          }
+        }
+      };
+
+      const removeServiceInstnace1: IWSEvent = {
+        model: 'VSGServiceInstance',
+        deleted: true,
+        msg: {
+          changed_fields: [],
+          pk: 1,
+          object: {
+            id: 1,
+            class_names: 'VSGServiceInstance,TenantWithContainer,ServiceInstance'
+          }
+        }
+      };
+
+      subject_websocket.next(removeService1);
+      subject_websocket.next(removeService2);
+      subject_websocket.next(removeServiceDependency1);
+      subject_websocket.next(removeServiceInstnace1);
+      scope.$apply();
+    });
+
+    afterEach(() => {
+      subscription.unsubscribe();
+    });
   });
 
   describe(`the getModelType`, () => {
diff --git a/src/app/service-graph/services/graph.store.ts b/src/app/service-graph/services/graph.store.ts
index 0dd6653..0edba0f 100644
--- a/src/app/service-graph/services/graph.store.ts
+++ b/src/app/service-graph/services/graph.store.ts
@@ -23,6 +23,7 @@
 import {BehaviorSubject} from 'rxjs/BehaviorSubject';
 import {Observable} from 'rxjs/Observable';
 import {IXosBaseModel, IXosSgLink, IXosSgNode} from '../interfaces';
+import {IWSEvent, IWSEventService} from '../../datasources/websocket/global';
 
 
 export interface IXosGraphStore {
@@ -41,7 +42,8 @@
   static $inject = [
     '$log',
     'XosModelStore',
-    'XosDebouncer'
+    'XosDebouncer',
+    'WebSocket'
   ];
 
   // graphs
@@ -64,7 +66,8 @@
   constructor (
     private $log: ng.ILogService,
     private XosModelStore: IXosModelStoreService,
-    private XosDebouncer: IXosDebouncer
+    private XosDebouncer: IXosDebouncer,
+    private webSocket: IWSEventService,
   ) {
     this.$log.info('[XosGraphStore] Setup');
 
@@ -72,6 +75,34 @@
     this.ServiceGraphSubject = new BehaviorSubject(this.serviceGraph);
 
     this.loadData();
+
+    // handle model deletion
+    this.webSocket.list()
+      .filter((e: IWSEvent) => {
+
+        const model = this.getModelType(e.msg.object);
+
+        switch (model) {
+          case 'service':
+          case 'serviceinstance':
+          case 'instance':
+          case 'network':
+            return true;
+          case 'servicedependency':
+          case 'serviceinstanceLink':
+            // NOTE ServiceDependency are considered links, they are automatically removed by the graph library
+            return false;
+          default:
+            return false;
+        }
+      })
+      .subscribe((event: IWSEvent) => {
+        if (event.deleted) {
+          const nodeId = this.getNodeId(event.msg.object);
+          this.serviceGraph.removeNode(nodeId);
+          this.efficientNext(this.ServiceGraphSubject, this.serviceGraph);
+        }
+      });
   }
 
   $onDestroy() {