Topo2 - Update device model on status change

Change-Id: I1387c3a5296ef4a4c27908251d43cf34bef4fdf4
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js b/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js
index 311c170..b49de81 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js
@@ -63,6 +63,10 @@
                 return this.addModel(data);
             }
         },
+        remove: function (model) {
+            var index = _.indexOf(this.models, model);
+            this.models.splice(index, 1);
+        },
         get: function (id) {
 
             if (!id) {
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Device.js b/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
index 9b82c90..ddb9b55 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
@@ -60,11 +60,11 @@
                     events: {
                         'click': 'onClick'
                     },
-                    onChange: function () {
-
-                        // Update class names when the model changes
+                    onChange: function (change) {
                         if (this.el) {
                             this.el.attr('class', this.svgClassName());
+                            var rect = this.el.select('.icon-rect');
+                            rect.style('fill', this.devGlyphColor());
                         }
                     },
                     nodeType: 'device',
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Force.js b/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
index 0c1ffb7..094e169 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
@@ -150,10 +150,12 @@
 
     function modelEvent(data) {
         $log.debug('>> topo2UiModelEvent event:', data);
+
         // TODO: Interpret the event and update our topo model state (if needed)
         // To Decide: Can we assume that the server will only send events
         //    related to objects that we are currently showing?
         //    (e.g. filtered by subregion contents?)
+        t2rs.update(data);
     }
 
     function showMastership(masterId) {
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js b/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
index 2f6f0d2..596524f 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
@@ -22,7 +22,8 @@
 (function () {
     'use strict';
 
-    var instance;
+    var instance,
+        updateTimer;
 
     // default settings for force layout
     var defaultSettings = {
@@ -89,10 +90,10 @@
     angular.module('ovTopo2')
     .factory('Topo2LayoutService',
         [
-            '$log', 'WebSocketService', 'SvgUtilService', 'Topo2RegionService',
+            '$log', '$timeout', 'WebSocketService', 'SvgUtilService', 'Topo2RegionService',
             'Topo2D3Service', 'Topo2ViewService', 'Topo2SelectService', 'Topo2ZoomService',
             'Topo2ViewController',
-            function ($log, wss, sus, t2rs, t2d3, t2vs, t2ss, t2zs,
+            function ($log, $timeout, wss, sus, t2rs, t2d3, t2vs, t2ss, t2zs,
                       ViewController) {
 
                 var Layout = ViewController.extend({
@@ -189,11 +190,12 @@
                         t2zs.panAndZoom([x, y], scale, 1000);
                     },
                     tick: function () {
+
                         this.link
-                            .attr("x1", function (d) { return d.source.x; })
-                            .attr("y1", function (d) { return d.source.y; })
-                            .attr("x2", function (d) { return d.target.x; })
-                            .attr("y2", function (d) { return d.target.y; });
+                            .attr("x1", function (d) { return d.get('source').x; })
+                            .attr("y1", function (d) { return d.get('source').y; })
+                            .attr("x2", function (d) { return d.get('target').x; })
+                            .attr("y2", function (d) { return d.get('target').y; });
 
                         this.node
                             .attr({
@@ -209,6 +211,13 @@
                         this.force.start();
                     },
                     update: function () {
+
+                        if (updateTimer) {
+                            $timeout.cancel(updateTimer);
+                        }
+                        updateTimer = $timeout(this._update.bind(this), 150);
+                    },
+                    _update: function () {
                         this.updateNodes();
                         this.updateLinks();
                     },
@@ -237,26 +246,13 @@
                         entering.filter('.device').each(t2d3.nodeEnter);
                         entering.filter('.sub-region').each(t2d3.nodeEnter);
                         entering.filter('.host').each(t2d3.hostEnter);
-
-                        // operate on exiting nodes:
-                        // Note that the node is removed after 2 seconds.
-                        // Sub element animations should be shorter than 2 seconds.
-                        // var exiting = this.node.exit()
-                        //     .transition()
-                        //     .duration(300)
-                        //     .style('opacity', 0)
-                        //     .remove();
-
-                        // exiting node specifics:
-                        // exiting.filter('.host').each(t2d3.hostExit);
-                        // exiting.filter('.device').each(t2d3.nodeExit);
                     },
                     updateLinks: function () {
 
                         var regionLinks = t2rs.regionLinks();
 
                         this.link = this.elements.linkG.selectAll('.link')
-                            .data(regionLinks);
+                            .data(regionLinks, function (d) { return d.get('key'); });
 
                         // operate on entering links:
                         var entering = this.link.enter()
@@ -275,9 +271,13 @@
 
                         // operate on exiting links:
                         this.link.exit()
-                            .style('opacity', 1)
+                            .attr('stroke-dasharray', '3 3')
+                            .style('opacity', 0.5)
                             .transition()
-                            .duration(300)
+                            .duration(1500)
+                            .attr({
+                                'stroke-dasharray': '3 12',
+                            })
                             .style('opacity', 0.0)
                             .remove();
                     },
@@ -336,7 +336,6 @@
                         d.fixed = true;
                         d3.select(this).classed('fixed', true);
                         instance.sendUpdateMeta(d);
-                        $log.debug(d);
                         t2ss.clickConsumed(true);
                     },
                     transitionDownRegion: function () {
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Link.js b/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
index 73ac150..09377e9 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
@@ -59,8 +59,6 @@
         var attrs = angular.extend({}, linkPoints, {
             key: this.get('id'),
             class: 'link',
-            srcPort: this.get('srcPort'),
-            tgtPort: this.get('dstPort'),
             position: {
                 x1: 0,
                 y1: 0,
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js b/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
index 7171477..c713dc0 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
@@ -44,14 +44,6 @@
         }
     };
 
-    function devGlyphColor(d) {
-        var o = this.get('online'),
-            id = this.get('master'),
-            otag = o ? 'online' : 'offline';
-        return o ? sus.cat7().getColor(id, 0, ts.theme()) :
-            dColTheme[ts.theme()][otag];
-    }
-
     angular.module('ovTopo2')
     .factory('Topo2NodeModel', [
         'Topo2Model', 'FnService', 'Topo2PrefsService',
@@ -117,10 +109,10 @@
                     });
                 },
                 mouseoverHandler: function () {
-                    this.set('hovered', true);
+                    this.set('hovered', true, { silent: true });
                 },
                 mouseoutHandler: function () {
-                    this.set('hovered', false);
+                    this.set('hovered', false, { silent: true });
                 },
                 icon: function () {
                     return 'unknown';
@@ -146,15 +138,23 @@
                         box = text.node().getBBox();
                     return box.width + labelPad * 2;
                 },
+                devGlyphColor: function () {
+                    var o = this.get('online'),
+                        id = this.get('master'),
+                        otag = o ? 'online' : 'offline';
+                    return o ? sus.cat7().getColor(id, 0, ts.theme()) :
+                        dColTheme[ts.theme()][otag];
+                },
                 addLabelElements: function (label) {
                     var rect = this.el.append('rect')
                         .attr('class', 'node-container');
                     var glythRect = this.el.append('rect')
+                        .attr('class', 'icon-rect')
                         .attr('y', -halfDevIcon)
                         .attr('x', -halfDevIcon)
                         .attr('width', devIconDim)
                         .attr('height', devIconDim)
-                        .style('fill', devGlyphColor.bind(this));
+                        .style('fill', this.devGlyphColor.bind(this));
 
                     var text = this.el.append('text').text(label)
                         .attr('text-anchor', 'left')
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Region.js b/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
index 7938fd6..09c6ce7 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
@@ -104,6 +104,9 @@
                 getLink: function (linkId) {
                     return this.model.get('links').get(linkId);
                 },
+                getDevice: function (deviceId) {
+                    return this.model.get('devices').get(deviceId);
+                },
                 filterRegionNodes: function (predicate) {
                     var nodes = this.regionNodes();
                     return _.filter(nodes, predicate);
@@ -144,12 +147,14 @@
                 },
 
                 update: function (event) {
+
                     if (this[event.type]) {
                         this[event.type](event);
-                        this.layout.update();
                     } else {
                         $log.error("Unhanded topology update", event);
                     }
+
+                    this.layout.update()
                 },
 
                 // Topology update event handlers
@@ -162,6 +167,22 @@
                 LINK_REMOVED: function (event) {
                     var link = this.getLink(event.subject);
                     link.remove();
+                    this.model.get('links').remove(link);
+                },
+                DEVICE_ADDED_OR_UPDATED: function (event) {
+
+                    var device;
+
+                    if (event.memo === 'added') {
+                        device = this.model.get('devices').add(event.data);
+                        $log('Added device', device)
+                    } else if (event.memo === 'updated') {
+                        device = this.getDevice(event.subject);
+                        device.set(event.data);
+                    }
+                },
+                DEVICE_REMOVED: function (event) {
+                    device.remove();
                 }
             });