ONOS-2325 - GUI -- Table View rows now flash yellow when their information updates. Minor device details panel bug fix.

Change-Id: I78eb0f90af00ce4484255d7e9e0c3c8a10a0eda7
diff --git a/web/gui/src/main/webapp/app/fw/util/fn.js b/web/gui/src/main/webapp/app/fw/util/fn.js
index 3e2fb29..ad441cd 100644
--- a/web/gui/src/main/webapp/app/fw/util/fn.js
+++ b/web/gui/src/main/webapp/app/fw/util/fn.js
@@ -172,6 +172,32 @@
         return true;
     }
 
+    // returns true if the two objects have all the same properties
+    function sameObjProps(obj1, obj2) {
+        var key;
+        for (key in obj1) {
+            if (obj1.hasOwnProperty(key)) {
+                if (!(obj1[key] === obj2[key])) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    // returns true if the array contains the object
+    // does NOT use strict object reference equality,
+        // instead checks each property individually for equality
+    function containsObj(arr, obj) {
+        var i;
+        for (i = 0; i < arr.length; i++) {
+            if (sameObjProps(arr[i], obj)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     // return the given string with the first character capitalized.
     function cap(s) {
         return s.toLowerCase().replace(/^[a-z]/, function (m) {
@@ -227,6 +253,8 @@
                 inArray: inArray,
                 removeFromArray: removeFromArray,
                 isEmptyObject: isEmptyObject,
+                sameObjProps: sameObjProps,
+                containsObj: containsObj,
                 cap: cap,
                 noPx: noPx,
                 noPxStyle: noPxStyle,
diff --git a/web/gui/src/main/webapp/app/fw/widget/table.css b/web/gui/src/main/webapp/app/fw/widget/table.css
index ae5ef03..846ce9e 100644
--- a/web/gui/src/main/webapp/app/fw/widget/table.css
+++ b/web/gui/src/main/webapp/app/fw/widget/table.css
@@ -63,6 +63,17 @@
     background-color: #304860;
 }
 
+/* highlighting */
+div.summary-list tr {
+    transition: background-color 500ms;
+}
+.light div.summary-list tr.data-change {
+    background-color: #FDFFDC;
+}
+.dark div.summary-list tr.data-change {
+    background-color: #5A5600;
+}
+
 div.summary-list td {
     padding: 6px;
     text-align: left;
diff --git a/web/gui/src/main/webapp/app/fw/widget/table.js b/web/gui/src/main/webapp/app/fw/widget/table.js
index 21baaa3..0b3191b 100644
--- a/web/gui/src/main/webapp/app/fw/widget/table.js
+++ b/web/gui/src/main/webapp/app/fw/widget/table.js
@@ -26,6 +26,7 @@
     // constants
     var tableIconTdSize = 33,
         pdg = 22,
+        flashTime = 2000,
         colWidth = 'col-width',
         tableIcon = 'table-icon',
         asc = 'asc',
@@ -208,7 +209,46 @@
                 scope.$on('$destroy', function () {
                     resetSort();
                 });
-            }
+            };
+        }])
+
+        .directive('onosFlashChanges', ['$log', '$parse', '$timeout',
+            function ($log, $parse, $timeout) {
+            return function (scope, element, attrs) {
+                var rowData = $parse(attrs.row)(scope),
+                    id = attrs.rowId,
+                    tr = d3.select(element[0]),
+                    multiRows = d3.selectAll('.multi-row'),
+                    promise;
+
+                scope.$watchCollection('changedData', function (newData) {
+                    angular.forEach(newData, function (item) {
+                        function classMultiRows(b) {
+                            if (!multiRows.empty()) {
+                                multiRows.each(function () {
+                                    d3.select(this).classed('data-change', b);
+                                });
+                            }
+                        }
+
+                        if (rowData[id] === item[id]) {
+                            tr.classed('data-change', true);
+                            classMultiRows(true);
+
+                            promise = $timeout(function () {
+                                tr.classed('data-change', false);
+                                classMultiRows(false);
+                            }, flashTime);
+                        }
+
+                    });
+                });
+                scope.$on('$destroy', function () {
+                    if (promise) {
+                        $timeout.cancel(promise);
+                    }
+                });
+            };
         }]);
 
 }());
diff --git a/web/gui/src/main/webapp/app/fw/widget/tableBuilder.js b/web/gui/src/main/webapp/app/fw/widget/tableBuilder.js
index 1196dee..7a38feb 100644
--- a/web/gui/src/main/webapp/app/fw/widget/tableBuilder.js
+++ b/web/gui/src/main/webapp/app/fw/widget/tableBuilder.js
@@ -30,7 +30,8 @@
     // {
     //    scope: $scope,     <- controller scope
     //    tag: 'device',     <- table identifier
-    //    selCb: selCb       <- row selection callback (optional)
+    //    selCb: selCb,      <- row selection callback (optional)
+    //    respCb: respCb,    <- websocket response callback (optional)
     //    query: params      <- query parameters in URL (optional)
     // }
     //          Note: selCb() is passed the row data model of the selected row,
@@ -45,31 +46,54 @@
             resp = o.tag + 'DataResponse',
             onSel = fs.isF(o.selCb),
             onResp = fs.isF(o.respCb),
+            oldTableData = [],
             promise;
 
         o.scope.tableData = [];
+        o.scope.changedData = [];
         o.scope.sortParams = {};
         o.scope.autoRefresh = true;
         o.scope.autoRefreshTip = 'Toggle auto refresh';
 
+        // === websocket functions --------------------
+        // response
         function respCb(data) {
             o.scope.tableData = data[root];
             onResp && onResp();
             o.scope.$apply();
-        }
 
+            // checks if data changed for row flashing
+            if (!angular.equals(o.scope.tableData, oldTableData)) {
+                o.scope.changedData = [];
+                // only flash the row if the data already exists
+                if (oldTableData.length) {
+                    angular.forEach(o.scope.tableData, function (item) {
+                        if (!fs.containsObj(oldTableData, item)) {
+                            o.scope.changedData.push(item);
+                        }
+                    });
+                }
+                angular.copy(o.scope.tableData, oldTableData);
+            }
+        }
+        handlers[resp] = respCb;
+        wss.bindHandlers(handlers);
+
+        // request
         function sortCb(params) {
             var p = angular.extend({}, params, o.query);
             wss.sendEvent(req, p);
         }
         o.scope.sortCallback = sortCb;
 
+        // === selecting a row functions ----------------
         function selCb($event, selRow) {
             o.scope.selId = (o.scope.selId === selRow.id) ? null : selRow.id;
             onSel && onSel($event, selRow);
         }
         o.scope.selectCallback = selCb;
 
+        // === autoRefresh functions ------------------
         function startRefresh() {
             promise = $interval(function () {
                 if (fs.debugOn('widget')) {
@@ -92,10 +116,7 @@
         }
         o.scope.toggleRefresh = toggleRefresh;
 
-        handlers[resp] = respCb;
-        wss.bindHandlers(handlers);
-
-        // Cleanup on destroyed scope
+        // === Cleanup on destroyed scope -----------------
         o.scope.$on('$destroy', function () {
             wss.unbindHandlers(handlers);
             stopRefresh();
diff --git a/web/gui/src/main/webapp/app/view/app/app.html b/web/gui/src/main/webapp/app/view/app/app.html
index 4d202bf..8f80a07 100644
--- a/web/gui/src/main/webapp/app/view/app/app.html
+++ b/web/gui/src/main/webapp/app/view/app/app.html
@@ -60,7 +60,8 @@
 
                 <tr ng-repeat="app in tableData track by $index"
                     ng-click="selectCallback($event, app)"
-                    ng-class="{selected: app.id === selId}">
+                    ng-class="{selected: app.id === selId}"
+                    onos-flash-changes row="{{app}}" row-id="id">
                     <td class="table-icon">
                         <div icon icon-id="{{app._iconid_state}}"></div>
                     </td>
diff --git a/web/gui/src/main/webapp/app/view/cluster/cluster.html b/web/gui/src/main/webapp/app/view/cluster/cluster.html
index a7c84bf..27bad4e 100644
--- a/web/gui/src/main/webapp/app/view/cluster/cluster.html
+++ b/web/gui/src/main/webapp/app/view/cluster/cluster.html
@@ -48,7 +48,8 @@
                     </td>
                 </tr>
 
-                <tr ng-repeat="node in tableData track by $index">
+                <tr ng-repeat="node in tableData track by $index"
+                    onos-flash-changes row="{{node}}" row-id="id">
                     <td class="table-icon">
                         <div icon icon-id="{{node._iconid_state}}"></div>
                     </td>
diff --git a/web/gui/src/main/webapp/app/view/device/device.html b/web/gui/src/main/webapp/app/view/device/device.html
index 6f09fe4..c6f12b2 100644
--- a/web/gui/src/main/webapp/app/view/device/device.html
+++ b/web/gui/src/main/webapp/app/view/device/device.html
@@ -54,7 +54,8 @@
 
                 <tr ng-repeat="dev in tableData track by $index"
                     ng-click="selectCallback($event, dev)"
-                    ng-class="{selected: dev.id === selId}">
+                    ng-class="{selected: dev.id === selId}"
+                    onos-flash-changes row="{{dev}}" row-id="id">
                     <td class="table-icon">
                         <div icon icon-id="{{dev._iconid_available}}"></div>
                     </td>
diff --git a/web/gui/src/main/webapp/app/view/device/device.js b/web/gui/src/main/webapp/app/view/device/device.js
index ecfcc98..0e16dec 100644
--- a/web/gui/src/main/webapp/app/view/device/device.js
+++ b/web/gui/src/main/webapp/app/view/device/device.js
@@ -250,6 +250,7 @@
     .directive('deviceDetailsPanel', ['$rootScope', '$window',
     function ($rootScope, $window) {
         return function (scope) {
+            var unbindWatch;
 
             function heightCalc() {
                 pStartY = fs.noPxStyle(d3.select('.tabular-header'), 'height')
@@ -268,7 +269,7 @@
                 }
             });
 
-            $rootScope.$watchCollection(
+            unbindWatch = $rootScope.$watchCollection(
                 function () {
                     return {
                         h: $window.innerHeight,
@@ -283,6 +284,7 @@
             );
 
             scope.$on('$destroy', function () {
+                unbindWatch();
                 ps.destroyPanel(pName);
             });
         };
diff --git a/web/gui/src/main/webapp/app/view/flow/flow.css b/web/gui/src/main/webapp/app/view/flow/flow.css
index 9aadffa..4aa9621 100644
--- a/web/gui/src/main/webapp/app/view/flow/flow.css
+++ b/web/gui/src/main/webapp/app/view/flow/flow.css
@@ -61,6 +61,24 @@
     background-color: #333;
 }
 
+/* highlighted color */
+.light #ov-flow tr:nth-child(6n + 1).data-change,
+.light #ov-flow tr:nth-child(6n + 2).data-change,
+.light #ov-flow tr:nth-child(6n + 3).data-change,
+.light #ov-flow tr:nth-child(6n + 4).data-change,
+.light #ov-flow tr:nth-child(6n + 5).data-change,
+.light #ov-flow tr:nth-child(6n).data-change {
+    background-color: #FDFFDC;
+}
+.dark #ov-flow tr:nth-child(6n + 1).data-change,
+.dark #ov-flow tr:nth-child(6n + 2).data-change,
+.dark #ov-flow tr:nth-child(6n + 3).data-change,
+.dark #ov-flow tr:nth-child(6n + 4).data-change,
+.dark #ov-flow tr:nth-child(6n + 5).data-change,
+.dark #ov-flow tr:nth-child(6n).data-change {
+    background-color: #5A5600;
+}
+
 #ov-flow td.selector,
 #ov-flow td.treatment {
     padding-left: 36px;
diff --git a/web/gui/src/main/webapp/app/view/flow/flow.html b/web/gui/src/main/webapp/app/view/flow/flow.html
index 87109b6..3555d6b 100644
--- a/web/gui/src/main/webapp/app/view/flow/flow.html
+++ b/web/gui/src/main/webapp/app/view/flow/flow.html
@@ -55,7 +55,8 @@
                     </td>
                 </tr>
 
-                <tr ng-repeat-start="flow in tableData track by $index">
+                <tr ng-repeat-start="flow in tableData track by $index"
+                    onos-flash-changes row="{{flow}}" row-id="id">
                     <td>{{flow.id}}</td>
                     <td>{{flow.appId}}</td>
                     <td>{{flow.groupId}}</td>
@@ -67,10 +68,10 @@
                     <td>{{flow.packets}}</td>
                     <td>{{flow.bytes}}</td>
                 </tr>
-                <tr>
+                <tr class="multi-row">
                     <td class="selector" colspan="10">{{flow.selector}}</td>
                 </tr>
-                <tr ng-repeat-end>
+                <tr class="multi-row" ng-repeat-end>
                     <td class="treatment" colspan="10">{{flow.treatment}}</td>
                 </tr>
             </table>
diff --git a/web/gui/src/main/webapp/app/view/group/group.css b/web/gui/src/main/webapp/app/view/group/group.css
index cf6b9af..42f1c31 100644
--- a/web/gui/src/main/webapp/app/view/group/group.css
+++ b/web/gui/src/main/webapp/app/view/group/group.css
@@ -57,6 +57,20 @@
     background-color: #333;
 }
 
+/* highlighted color */
+.light #ov-group tr:nth-child(4n + 1).data-change,
+.light #ov-group tr:nth-child(4n + 2).data-change,
+.light #ov-group tr:nth-child(4n + 3).data-change,
+.light #ov-group tr:nth-child(4n).data-change {
+    background-color: #FDFFDC;
+}
+.dark #ov-group tr:nth-child(4n + 1).data-change,
+.dark #ov-group tr:nth-child(4n + 2).data-change,
+.dark #ov-group tr:nth-child(4n + 3).data-change,
+.dark #ov-group tr:nth-child(4n).data-change {
+    background-color: #5A5600;
+}
+
 #ov-group td.buckets {
     padding-left: 36px;
     opacity: 0.65;
diff --git a/web/gui/src/main/webapp/app/view/group/group.html b/web/gui/src/main/webapp/app/view/group/group.html
index 13bde14..e47783c 100644
--- a/web/gui/src/main/webapp/app/view/group/group.html
+++ b/web/gui/src/main/webapp/app/view/group/group.html
@@ -67,7 +67,8 @@
                     </td>
                 </tr>
 
-                <tr ng-repeat-start="group in tableData track by $index">
+                <tr ng-repeat-start="group in tableData track by $index"
+                    onos-flash-changes row="{{group}}" row-id="id">
                     <td>{{group.id}}</td>
                     <td>{{group.app_id}}</td>
                     <td>{{group.state}}</td>
@@ -75,7 +76,7 @@
                     <td>{{group.packets}}</td>
                     <td>{{group.bytes}}</td>
                 </tr>
-                <tr ng-repeat-end>
+                <tr class="multi-row" ng-repeat-end>
                     <td class="buckets" colspan="6"
                         ng-bind-html="group.buckets"></td>
                 </tr>
diff --git a/web/gui/src/main/webapp/app/view/host/host.html b/web/gui/src/main/webapp/app/view/host/host.html
index 7a32216..6298ebd 100644
--- a/web/gui/src/main/webapp/app/view/host/host.html
+++ b/web/gui/src/main/webapp/app/view/host/host.html
@@ -33,7 +33,8 @@
                     </td>
                 </tr>
 
-                <tr ng-repeat="host in tableData track by $index">
+                <tr ng-repeat="host in tableData track by $index"
+                    onos-flash-changes row="{{host}}" row-id="id">
                     <td class="table-icon">
                         <div icon icon-id="{{host._iconid_type}}"></div>
                     </td>
diff --git a/web/gui/src/main/webapp/app/view/intent/intent.css b/web/gui/src/main/webapp/app/view/intent/intent.css
index 9f9923b..ed9cd48 100644
--- a/web/gui/src/main/webapp/app/view/intent/intent.css
+++ b/web/gui/src/main/webapp/app/view/intent/intent.css
@@ -47,6 +47,23 @@
     background-color: #333;
 }
 
+.light #ov-intent tr:nth-child(6n + 1).data-change,
+.light #ov-intent tr:nth-child(6n + 2).data-change,
+.light #ov-intent tr:nth-child(6n + 3).data-change,
+.light #ov-intent tr:nth-child(6n + 4).data-change,
+.light #ov-intent tr:nth-child(6n + 5).data-change,
+.light #ov-intent tr:nth-child(6n).data-change {
+    background-color: #FDFFDC;
+}
+.dark #ov-intent tr:nth-child(6n + 1).data-change,
+.dark #ov-intent tr:nth-child(6n + 2).data-change,
+.dark #ov-intent tr:nth-child(6n + 3).data-change,
+.dark #ov-intent tr:nth-child(6n + 4).data-change,
+.dark #ov-intent tr:nth-child(6n + 5).data-change,
+.dark #ov-intent tr:nth-child(6n).data-change {
+    background-color: #5A5600;
+}
+
 #ov-intent td.resources,
 #ov-intent td.details {
     padding-left: 36px;
diff --git a/web/gui/src/main/webapp/app/view/intent/intent.html b/web/gui/src/main/webapp/app/view/intent/intent.html
index 4fcb453..193267e 100644
--- a/web/gui/src/main/webapp/app/view/intent/intent.html
+++ b/web/gui/src/main/webapp/app/view/intent/intent.html
@@ -48,17 +48,18 @@
                     </td>
                 </tr>
 
-                <tr ng-repeat-start="intent in tableData track by $index">
+                <tr ng-repeat-start="intent in tableData track by $index"
+                    onos-flash-changes row="{{intent}}" row-id="key">
                     <td>{{intent.appId}}</td>
                     <td>{{intent.key}}</td>
                     <td>{{intent.type}}</td>
                     <td>{{intent.priority}}</td>
                     <td>{{intent.state}}</td>
                 </tr>
-                <tr>
+                <tr class="multi-row">
                     <td class="resources" colspan="5">{{intent.resources}}</td>
                 </tr>
-                <tr ng-repeat-end>
+                <tr class="multi-row" ng-repeat-end>
                     <td class="details" colspan="5">{{intent.details}}</td>
                 </tr>
             </table>
diff --git a/web/gui/src/main/webapp/app/view/link/link.html b/web/gui/src/main/webapp/app/view/link/link.html
index fa8306f..69a4737 100644
--- a/web/gui/src/main/webapp/app/view/link/link.html
+++ b/web/gui/src/main/webapp/app/view/link/link.html
@@ -49,7 +49,8 @@
                     </td>
                 </tr>
 
-                <tr ng-repeat="link in tableData track by $index">
+                <tr ng-repeat="link in tableData track by $index"
+                    onos-flash-changes row="{{link}}" row-id="one">
                     <td class="table-icon">
                         <div icon icon-id="{{link._iconid_state}}"></div>
                     </td>
diff --git a/web/gui/src/main/webapp/app/view/port/port.html b/web/gui/src/main/webapp/app/view/port/port.html
index 522abd5..d18d883 100644
--- a/web/gui/src/main/webapp/app/view/port/port.html
+++ b/web/gui/src/main/webapp/app/view/port/port.html
@@ -69,7 +69,8 @@
                     </td>
                 </tr>
 
-                <tr ng-repeat="port in tableData track by $index">
+                <tr ng-repeat="port in tableData track by $index"
+                    onos-flash-changes row="{{port}}" row-id="id">
                     <td>{{port.id}}</td>
                     <td>{{port.pkt_rx}}</td>
                     <td>{{port.pkt_tx}}</td>
diff --git a/web/gui/src/main/webapp/app/view/settings/settings.html b/web/gui/src/main/webapp/app/view/settings/settings.html
index a10dd1c..b4226f4 100644
--- a/web/gui/src/main/webapp/app/view/settings/settings.html
+++ b/web/gui/src/main/webapp/app/view/settings/settings.html
@@ -33,7 +33,8 @@
                     </td>
                 </tr>
 
-                <tr ng-repeat="prop in tableData track by $index">
+                <tr ng-repeat="prop in tableData track by $index"
+                    onos-flash-changes row="{{prop}}" row-id="id">
                     <td>{{prop.component}}</td>
                     <td>{{prop.id}}</td>
                     <td>{{prop.type}}</td>