ONOS-2325 - GUI -- Rewrite / bug fix for table row flashing. Multi-rows act like a unit and row flashes when a single new element is added.

Change-Id: I7be876281c0c86b927366223fc87372ea21034ec
diff --git a/web/gui/src/main/webapp/app/directives.js b/web/gui/src/main/webapp/app/directives.js
index b3c34ab..0b4f495 100644
--- a/web/gui/src/main/webapp/app/directives.js
+++ b/web/gui/src/main/webapp/app/directives.js
@@ -15,7 +15,7 @@
  */
 
 /*
- ONOS GUI -- Our own Angular directives defined here.
+ ONOS GUI -- General Purpose Angular directives defined here.
  */
 
 (function () {
@@ -59,5 +59,14 @@
                     });
                 }
             };
+        }])
+
+        .directive('ngRepeatComplete', [function () {
+            return function (scope) {
+                if (scope.$last) {
+                    scope.$emit('ngRepeatComplete');
+                    scope.$broadcast('ngRepeatComplete');
+                }
+            };
         }]);
 }());
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 ad441cd..6fee2bf 100644
--- a/web/gui/src/main/webapp/app/fw/util/fn.js
+++ b/web/gui/src/main/webapp/app/fw/util/fn.js
@@ -189,8 +189,9 @@
     // 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++) {
+        var i,
+            len = arr.length;
+        for (i = 0; i < len; i++) {
             if (sameObjProps(arr[i], obj)) {
                 return true;
             }
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 ad06205..327aedb 100644
--- a/web/gui/src/main/webapp/app/fw/widget/table.js
+++ b/web/gui/src/main/webapp/app/fw/widget/table.js
@@ -217,37 +217,49 @@
             function ($log, $parse, $timeout, fs) {
 
             return function (scope, element, attrs) {
-                var rowData = $parse(attrs.row)(scope),
-                    id = attrs.rowId,
-                    tr = d3.select(element[0]),
-                    multi = 'multiRow' in attrs,
-                    promise;
+                var idProp = attrs.idProp,
+                    table = d3.select(element[0]),
+                    trs, promise;
 
-                scope.$watchCollection('changedData', function (newData) {
-                    var multiRows = null;
-                    if (multi) {
-                        // This is a way to identify which rows need to be
-                        // highlighted with the first one. It uses the unique
-                        // identifier as a class selection. If the unique ID
-                        // has invalid characters (like ':') then it won't work.
-                        multiRows = d3.selectAll('.multi-row-' + rowData[id]);
-                    }
-
+                function highlightRows() {
+                    var changedRows = [];
                     function classRows(b) {
-                        tr.classed('data-change', b);
-                        if (multiRows) {
-                            multiRows.classed('data-change', b);
+                        if (changedRows.length) {
+                            angular.forEach(changedRows, function (tr) {
+                                tr.classed('data-change', b);
+                            });
                         }
                     }
+                    // timeout because 'row-id' was the un-interpolated value
+                    // "{{link.one}}" for example, instead of link.one evaluated
+                    // timeout executes on the next digest -- after evaluation
+                    $timeout(function () {
+                        if (scope.tableData.length) {
+                            trs = table.selectAll('tr');
+                        }
 
-                    if (fs.find(rowData[id], newData, id) > -1) {
-                        classRows(true);
+                        if (trs && !trs.empty()) {
+                            trs.each(function () {
+                                var tr = d3.select(this);
+                                if (fs.find(tr.attr('row-id'),
+                                        scope.changedData,
+                                        idProp) > -1) {
+                                    changedRows.push(tr);
+                                }
+                            });
+                            classRows(true);
+                            promise = $timeout(function () {
+                                classRows(false);
+                            }, flashTime);
+                            trs = undefined;
+                        }
+                    });
+                }
 
-                        promise = $timeout(function () {
-                            classRows(false);
-                        }, flashTime);
-                    }
-                });
+                // new items added:
+                scope.$on('ngRepeatComplete', highlightRows);
+                // items changed in existing set:
+                scope.$watchCollection('changedData', highlightRows);
 
                 scope.$on('$destroy', function () {
                     if (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 7a38feb..c731600 100644
--- a/web/gui/src/main/webapp/app/fw/widget/tableBuilder.js
+++ b/web/gui/src/main/webapp/app/fw/widget/tableBuilder.js
@@ -60,7 +60,6 @@
         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)) {
@@ -75,6 +74,7 @@
                 }
                 angular.copy(o.scope.tableData, oldTableData);
             }
+            o.scope.$apply();
         }
         handlers[resp] = respCb;
         wss.bindHandlers(handlers);
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 8f80a07..0db8d9b 100644
--- a/web/gui/src/main/webapp/app/view/app/app.html
+++ b/web/gui/src/main/webapp/app/view/app/app.html
@@ -51,7 +51,7 @@
         </div>
 
         <div class="table-body">
-            <table>
+            <table onos-flash-changes id-prop="id">
                 <tr ng-if="!tableData.length" class="no-data">
                     <td colspan="5">
                         No Applications found
@@ -61,7 +61,7 @@
                 <tr ng-repeat="app in tableData track by $index"
                     ng-click="selectCallback($event, app)"
                     ng-class="{selected: app.id === selId}"
-                    onos-flash-changes row="{{app}}" row-id="id">
+                    ng-repeat-complete row-id="{{app.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 27bad4e..68b1b09 100644
--- a/web/gui/src/main/webapp/app/view/cluster/cluster.html
+++ b/web/gui/src/main/webapp/app/view/cluster/cluster.html
@@ -41,7 +41,7 @@
         </div>
 
         <div class="table-body">
-            <table>
+            <table onos-flash-changes id-prop="id">
                 <tr ng-if="!tableData.length" class="no-data">
                     <td colspan="5">
                         No Cluster Nodes found
@@ -49,7 +49,7 @@
                 </tr>
 
                 <tr ng-repeat="node in tableData track by $index"
-                    onos-flash-changes row="{{node}}" row-id="id">
+                    ng-repeat-complete row-id="{{node.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 c6f12b2..609423d 100644
--- a/web/gui/src/main/webapp/app/view/device/device.html
+++ b/web/gui/src/main/webapp/app/view/device/device.html
@@ -45,7 +45,7 @@
         </div>
 
         <div class="table-body">
-            <table>
+            <table onos-flash-changes id-prop="id">
                 <tr ng-if="!tableData.length" class="no-data">
                     <td colspan="9">
                         No Devices found
@@ -55,7 +55,7 @@
                 <tr ng-repeat="dev in tableData track by $index"
                     ng-click="selectCallback($event, dev)"
                     ng-class="{selected: dev.id === selId}"
-                    onos-flash-changes row="{{dev}}" row-id="id">
+                    ng-repeat-complete row-id="{{dev.id}}">
                     <td class="table-icon">
                         <div icon icon-id="{{dev._iconid_available}}"></div>
                     </td>
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 d9d3618..1338a89 100644
--- a/web/gui/src/main/webapp/app/view/flow/flow.html
+++ b/web/gui/src/main/webapp/app/view/flow/flow.html
@@ -48,7 +48,7 @@
         </div>
 
         <div class="table-body">
-            <table>
+            <table onos-flash-changes id-prop="id">
                 <tr ng-if="!tableData.length" class="no-data">
                     <td colspan="10">
                         No Flows found
@@ -56,7 +56,7 @@
                 </tr>
 
                 <tr ng-repeat-start="flow in tableData track by $index"
-                    onos-flash-changes row="{{flow}}" row-id="id" multi-row>
+                    ng-repeat-complete row-id="{{flow.id}}">
                     <td>{{flow.id}}</td>
                     <td>{{flow.appId}}</td>
                     <td>{{flow.groupId}}</td>
@@ -68,10 +68,10 @@
                     <td>{{flow.packets}}</td>
                     <td>{{flow.bytes}}</td>
                 </tr>
-                <tr class="multi-row-{{flow.id}}">
+                <tr row-id="{{flow.id}}">
                     <td class="selector" colspan="10">{{flow.selector}}</td>
                 </tr>
-                <tr class="multi-row-{{flow.id}}" ng-repeat-end>
+                <tr row-id="{{flow.id}}" 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.html b/web/gui/src/main/webapp/app/view/group/group.html
index 5387f34..6f2362e 100644
--- a/web/gui/src/main/webapp/app/view/group/group.html
+++ b/web/gui/src/main/webapp/app/view/group/group.html
@@ -60,7 +60,7 @@
         </div>
 
         <div class="table-body">
-            <table>
+            <table onos-flash-changes id-prop="id">
                 <tr ng-if="!tableData.length" class="no-data">
                     <td colspan="6">
                         No Groups found
@@ -68,7 +68,7 @@
                 </tr>
 
                 <tr ng-repeat-start="group in tableData track by $index"
-                    onos-flash-changes row="{{group}}" row-id="id" multi-row>
+                    ng-repeat-complete row-id="{{group.id}}">
                     <td>{{group.id}}</td>
                     <td>{{group.app_id}}</td>
                     <td>{{group.state}}</td>
@@ -76,7 +76,7 @@
                     <td>{{group.packets}}</td>
                     <td>{{group.bytes}}</td>
                 </tr>
-                <tr class="multi-row-{{group.id}}" ng-repeat-end>
+                <tr row-id="{{group.id}}" 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 6298ebd..5103b45 100644
--- a/web/gui/src/main/webapp/app/view/host/host.html
+++ b/web/gui/src/main/webapp/app/view/host/host.html
@@ -26,7 +26,7 @@
         </div>
 
         <div class="table-body">
-            <table>
+            <table onos-flash-changes id-prop="id">
                 <tr ng-if="!tableData.length" class="no-data">
                     <td colspan="6">
                         No Hosts found
@@ -34,7 +34,7 @@
                 </tr>
 
                 <tr ng-repeat="host in tableData track by $index"
-                    onos-flash-changes row="{{host}}" row-id="id">
+                    ng-repeat-complete row-id="{{host.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.html b/web/gui/src/main/webapp/app/view/intent/intent.html
index eeaf21e..581d12a 100644
--- a/web/gui/src/main/webapp/app/view/intent/intent.html
+++ b/web/gui/src/main/webapp/app/view/intent/intent.html
@@ -41,7 +41,7 @@
         </div>
 
         <div class="table-body">
-            <table>
+            <table onos-flash-changes id-prop="key">
                 <tr ng-if="!tableData.length" class="no-data">
                     <td colspan="5">
                         No Intents found
@@ -49,17 +49,17 @@
                 </tr>
 
                 <tr ng-repeat-start="intent in tableData track by $index"
-                    onos-flash-changes row="{{intent}}" row-id="key" multi-row>
+                    ng-repeat-complete row-id="{{intent.key}}">
                     <td>{{intent.appId}}</td>
                     <td>{{intent.key}}</td>
                     <td>{{intent.type}}</td>
                     <td>{{intent.priority}}</td>
                     <td>{{intent.state}}</td>
                 </tr>
-                <tr class="multi-row-{{intent.key}}">
+                <tr row-id="{{intent.key}}">
                     <td class="resources" colspan="5">{{intent.resources}}</td>
                 </tr>
-                <tr class="multi-row-{{intent.key}}" ng-repeat-end>
+                <tr row-id="{{intent.key}}" 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 69a4737..3ed80a5 100644
--- a/web/gui/src/main/webapp/app/view/link/link.html
+++ b/web/gui/src/main/webapp/app/view/link/link.html
@@ -42,7 +42,7 @@
         </div>
 
         <div class="table-body">
-            <table>
+            <table onos-flash-changes id-prop="one">
                 <tr ng-if="!tableData.length" class="no-data">
                     <td colspan="6">
                         No Links found
@@ -50,7 +50,7 @@
                 </tr>
 
                 <tr ng-repeat="link in tableData track by $index"
-                    onos-flash-changes row="{{link}}" row-id="one">
+                    ng-repeat-complete row-id="{{link.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.css b/web/gui/src/main/webapp/app/view/port/port.css
index 6639539..7fcfee3 100644
--- a/web/gui/src/main/webapp/app/view/port/port.css
+++ b/web/gui/src/main/webapp/app/view/port/port.css
@@ -42,4 +42,8 @@
 
 #ov-port td {
     text-align: right;
-}
\ No newline at end of file
+}
+
+#ov-port tr.no-data td {
+    text-align: center;
+}
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 d18d883..2a6d59e 100644
--- a/web/gui/src/main/webapp/app/view/port/port.html
+++ b/web/gui/src/main/webapp/app/view/port/port.html
@@ -62,7 +62,7 @@
         </div>
 
         <div class="table-body">
-            <table>
+            <table onos-flash-changes id-prop="id">
                 <tr ng-if="!tableData.length" class="no-data">
                     <td colspan="8">
                         No Ports found
@@ -70,7 +70,7 @@
                 </tr>
 
                 <tr ng-repeat="port in tableData track by $index"
-                    onos-flash-changes row="{{port}}" row-id="id">
+                    ng-repeat-complete row-id="{{port.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 b4226f4..0faa1c2 100644
--- a/web/gui/src/main/webapp/app/view/settings/settings.html
+++ b/web/gui/src/main/webapp/app/view/settings/settings.html
@@ -26,7 +26,7 @@
         </div>
 
         <div class="table-body">
-            <table>
+            <table onos-flash-changes id-prop="id">
                 <tr ng-if="!tableData.length" class="no-data">
                     <td colspan="6">
                         No Settings found
@@ -34,7 +34,7 @@
                 </tr>
 
                 <tr ng-repeat="prop in tableData track by $index"
-                    onos-flash-changes row="{{prop}}" row-id="id">
+                    ng-repeat-complete row-id="{{prop.id}}">
                     <td>{{prop.component}}</td>
                     <td>{{prop.id}}</td>
                     <td>{{prop.type}}</td>