ONOS-4137 Adding a "ready" check-mark glyph to the node instances.

Introduced a new INSTANCE_READY type of ClusterEvent.

Change-Id: I7f77ebae56cb18c196cd3ec7f2735faa4ca363db
diff --git a/core/api/src/main/java/org/onosproject/cluster/ClusterEvent.java b/core/api/src/main/java/org/onosproject/cluster/ClusterEvent.java
index 7bdc1d7..1799dc3 100644
--- a/core/api/src/main/java/org/onosproject/cluster/ClusterEvent.java
+++ b/core/api/src/main/java/org/onosproject/cluster/ClusterEvent.java
@@ -42,6 +42,11 @@
         INSTANCE_ACTIVATED,
 
         /**
+         * Signifies that a cluster instance became ready.
+         */
+        INSTANCE_READY,
+
+        /**
          * Signifies that a cluster instance became inactive.
          */
         INSTANCE_DEACTIVATED
diff --git a/core/store/dist/src/main/java/org/onosproject/store/cluster/impl/DistributedClusterStore.java b/core/store/dist/src/main/java/org/onosproject/store/cluster/impl/DistributedClusterStore.java
index 5c34270..ac8692b 100644
--- a/core/store/dist/src/main/java/org/onosproject/store/cluster/impl/DistributedClusterStore.java
+++ b/core/store/dist/src/main/java/org/onosproject/store/cluster/impl/DistributedClusterStore.java
@@ -43,6 +43,7 @@
 import org.slf4j.Logger;
 
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -53,6 +54,9 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.cluster.ClusterEvent.Type.INSTANCE_ACTIVATED;
+import static org.onosproject.cluster.ClusterEvent.Type.INSTANCE_DEACTIVATED;
+import static org.onosproject.cluster.ClusterEvent.Type.INSTANCE_READY;
 import static org.slf4j.LoggerFactory.getLogger;
 
 @Component(immediate = true)
@@ -196,8 +200,12 @@
     }
 
     private void updateState(NodeId nodeId, State newState) {
-        nodeStates.put(nodeId, newState);
-        nodeStateLastUpdatedTimes.put(nodeId, DateTime.now());
+        State currentState = nodeStates.get(nodeId);
+        if (!Objects.equals(currentState, newState)) {
+            nodeStates.put(nodeId, newState);
+            nodeStateLastUpdatedTimes.put(nodeId, DateTime.now());
+            notifyStateChange(nodeId, currentState, newState);
+        }
     }
 
     private void heartbeat() {
@@ -215,12 +223,10 @@
                 if (phi >= PHI_FAILURE_THRESHOLD) {
                     if (currentState.isActive()) {
                         updateState(node.id(), State.INACTIVE);
-                        notifyStateChange(node.id(), State.ACTIVE, State.INACTIVE);
                     }
                 } else {
                     if (currentState == State.INACTIVE) {
                         updateState(node.id(), State.ACTIVE);
-                        notifyStateChange(node.id(), State.INACTIVE, State.ACTIVE);
                     }
                 }
             });
@@ -230,11 +236,12 @@
     }
 
     private void notifyStateChange(NodeId nodeId, State oldState, State newState) {
-        ControllerNode node = allNodes.get(nodeId);
-        if (newState.isActive()) {
-            notifyDelegate(new ClusterEvent(ClusterEvent.Type.INSTANCE_ACTIVATED, node));
-        } else {
-            notifyDelegate(new ClusterEvent(ClusterEvent.Type.INSTANCE_DEACTIVATED, node));
+        if (oldState != newState) {
+            ControllerNode node = allNodes.get(nodeId);
+            ClusterEvent.Type type = newState == State.READY ? INSTANCE_READY :
+                    newState == State.ACTIVE ? INSTANCE_ACTIVATED :
+                            INSTANCE_DEACTIVATED;
+            notifyDelegate(new ClusterEvent(type, node));
         }
     }
 
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
index e1b4bea..29c4992 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
@@ -230,6 +230,7 @@
                 .put("id", node.id().toString())
                 .put("ip", node.ip().toString())
                 .put("online", clusterService.getState(node.id()).isActive())
+                .put("ready", clusterService.getState(node.id()).isReady())
                 .put("uiAttached", node.equals(clusterService.getLocalNode()))
                 .put("switches", switchCount);
 
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.css b/web/gui/src/main/webapp/app/view/topo/topo.css
index 1270979..ceb0e4b 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.css
+++ b/web/gui/src/main/webapp/app/view/topo/topo.css
@@ -767,3 +767,7 @@
     fill: #447;
 }
 
+.notReady .readyBadge {
+    visibility: hidden;
+}
+
diff --git a/web/gui/src/main/webapp/app/view/topo/topoInst.js b/web/gui/src/main/webapp/app/view/topo/topoInst.js
index 7e92997..99d381c 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoInst.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoInst.js
@@ -155,10 +155,15 @@
     }
 
     function attachUiBadge(svg) {
-        gs.addGlyph(svg, 'uiAttached', 30, true, [12, instCfg.uiDy])
+        gs.addGlyph(svg, 'uiAttached', 24, true, [28, instCfg.uiDy])
             .classed('badgeIcon uiBadge', true);
     }
 
+    function attachReadyBadge(svg) {
+        gs.addGlyph(svg, 'checkMark', 16, true, [12, instCfg.uiDy + 4])
+            .classed('badgeIcon readyBadge', true);
+    }
+
     function instColor(id, online) {
         return sus.cat7().getColor(id, !online, ts.theme());
     }
@@ -183,6 +188,7 @@
 
             // update online state
             el.classed('online', d.online);
+            el.classed('notReady', !d.ready);
 
             // update ui-attached state
             svg.select('use.uiBadge').remove();
@@ -190,6 +196,8 @@
                 attachUiBadge(svg);
             }
 
+            attachReadyBadge(svg, d.ready);
+
             function updAttr(id, value) {
                 svg.select('text.instLabel.'+id).text(value);
             }
@@ -204,6 +212,7 @@
             .append('div')
             .attr('class', 'onosInst')
             .classed('online', function (d) { return d.online; })
+            .classed('notReady', function (d) { return !d.ready; })
             .on('click', clickInst);
 
         entering.each(function (d) {
@@ -228,6 +237,8 @@
                 attachUiBadge(svg);
             }
 
+            attachReadyBadge(svg);
+
             var left = c.nodeOx + c.nodeDim,
                 len = rectAttr.width - left,
                 hlen = len / 2,