ONOS-293 Added summary pane and related keyboard shortcuts; also tweaked key help sizes and dropped instances toggle from mast. Fixed ONOS-295 bug.

Change-Id: I694901957451cf88df06e6fca3a8d71de144f68e
diff --git a/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewMessages.java b/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewMessages.java
index 6c12400..91a820c 100644
--- a/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewMessages.java
+++ b/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewMessages.java
@@ -23,6 +23,7 @@
 import org.onlab.onos.cluster.ClusterService;
 import org.onlab.onos.cluster.ControllerNode;
 import org.onlab.onos.cluster.NodeId;
+import org.onlab.onos.core.CoreService;
 import org.onlab.onos.mastership.MastershipService;
 import org.onlab.onos.net.Annotated;
 import org.onlab.onos.net.Annotations;
@@ -56,6 +57,8 @@
 import org.onlab.onos.net.provider.ProviderId;
 import org.onlab.onos.net.statistic.Load;
 import org.onlab.onos.net.statistic.StatisticService;
+import org.onlab.onos.net.topology.Topology;
+import org.onlab.onos.net.topology.TopologyService;
 import org.onlab.osgi.ServiceDirectory;
 import org.onlab.packet.IpAddress;
 import org.slf4j.Logger;
@@ -117,8 +120,10 @@
     protected final IntentService intentService;
     protected final FlowRuleService flowService;
     protected final StatisticService statService;
+    protected final TopologyService topologyService;
 
     protected final ObjectMapper mapper = new ObjectMapper();
+    private final String version;
 
     // TODO: extract into an external & durable state; good enough for now and demo
     private static Map<String, ObjectNode> metaUi = new ConcurrentHashMap<>();
@@ -138,6 +143,9 @@
         intentService = directory.get(IntentService.class);
         flowService = directory.get(FlowRuleService.class);
         statService = directory.get(StatisticService.class);
+        topologyService = directory.get(TopologyService.class);
+
+        version = directory.get(CoreService.class).version().toString();
     }
 
     // Retrieves the payload from the specified event.
@@ -419,6 +427,22 @@
         metaUi.put(string(payload, "id"), (ObjectNode) payload.path("memento"));
     }
 
+    // Returns summary response.
+    protected ObjectNode summmaryMessage(long sid) {
+        Topology topology = topologyService.currentTopology();
+        return envelope("showSummary", sid,
+                        json("ONOS Summary", "node",
+                             new Prop("Devices", format(topology.deviceCount())),
+                             new Prop("Links", format(topology.linkCount())),
+                             new Prop("Hosts", format(hostService.getHostCount())),
+                             new Prop("Topology SCCs", format(topology.clusterCount())),
+                             new Prop("Paths", format(topology.pathCount())),
+                             new Separator(),
+                             new Prop("Intents", format(intentService.getIntentCount())),
+                             new Prop("Flows", format(flowService.getFlowRuleCount())),
+                             new Prop("Version", version.replace(".SNAPSHOT", "*"))));
+    }
+
     // Returns device details response.
     protected ObjectNode deviceDetails(DeviceId deviceId, long sid) {
         Device device = deviceService.getDevice(deviceId);
@@ -435,12 +459,12 @@
                              new Prop("S/W Version", device.swVersion()),
                              new Prop("Serial Number", device.serialNumber()),
                              new Separator(),
+                             new Prop("Master", master(deviceId)),
                              new Prop("Latitude", annot.value("latitude")),
                              new Prop("Longitude", annot.value("longitude")),
-                             new Prop("Ports", Integer.toString(portCount)),
-                             new Prop("Flows", Integer.toString(flowCount)),
                              new Separator(),
-                             new Prop("Master", master(deviceId))));
+                             new Prop("Ports", Integer.toString(portCount)),
+                             new Prop("Flows", Integer.toString(flowCount))));
     }
 
     protected int getFlowCount(DeviceId deviceId) {
@@ -641,6 +665,12 @@
         return format.format(value) + " " + unit;
     }
 
+    // Formats the given number into a string.
+    private String format(Number number) {
+        DecimalFormat format = new DecimalFormat("#,###");
+        return format.format(number);
+    }
+
     private boolean isInfrastructureEgress(Link link) {
         return link.src().elementId() instanceof DeviceId;
     }
diff --git a/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewWebSocket.java b/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewWebSocket.java
index 104fa95..9562040 100644
--- a/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewWebSocket.java
+++ b/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewWebSocket.java
@@ -42,7 +42,11 @@
 import org.onlab.osgi.ServiceDirectory;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 import java.util.Timer;
 import java.util.TimerTask;
@@ -70,8 +74,17 @@
 
     private static final String APP_ID = "org.onlab.onos.gui";
 
+    private static final long SUMMARY_FREQUENCY_SEC = 2000;
     private static final long TRAFFIC_FREQUENCY_SEC = 1000;
 
+    private static final Comparator<? super ControllerNode> NODE_COMPARATOR =
+            new Comparator<ControllerNode>() {
+                @Override
+                public int compare(ControllerNode o1, ControllerNode o2) {
+                    return o1.id().toString().compareTo(o2.id().toString());
+                }
+            };
+
     private final ApplicationId appId;
 
     private Connection connection;
@@ -83,10 +96,14 @@
     private final HostListener hostListener = new InternalHostListener();
     private final IntentListener intentListener = new InternalIntentListener();
 
-    // Intents that are being monitored for the GUI
-    private ObjectNode monitorRequest;
-    private final Timer timer = new Timer("intent-traffic-monitor");
-    private final TimerTask timerTask = new IntentTrafficMonitor();
+    // Timers and objects being monitored
+    private final Timer timer = new Timer("topology-view");
+
+    private TimerTask trafficTask;
+    private ObjectNode trafficEvent;
+
+    private TimerTask summaryTask;
+    private ObjectNode summaryEvent;
 
     private long lastActive = System.currentTimeMillis();
     private boolean listenersRemoved = false;
@@ -140,7 +157,6 @@
         this.connection = connection;
         this.control = (FrameConnection) connection;
         addListeners();
-        timer.schedule(timerTask, TRAFFIC_FREQUENCY_SEC, TRAFFIC_FREQUENCY_SEC);
 
         sendAllInstances();
         sendAllDevices();
@@ -181,6 +197,7 @@
             updateMetaUi(event);
         } else if (type.equals("addHostIntent")) {
             createHostIntent(event);
+
         } else if (type.equals("requestTraffic")) {
             requestTraffic(event);
         } else if (type.equals("requestAllTraffic")) {
@@ -189,6 +206,11 @@
             requestDeviceLinkFlows(event);
         } else if (type.equals("cancelTraffic")) {
             cancelTraffic(event);
+
+        } else if (type.equals("requestSummary")) {
+            requestSummary(event);
+        } else if (type.equals("cancelSummary")) {
+            cancelSummary(event);
         }
     }
 
@@ -205,7 +227,9 @@
 
     // Sends all controller nodes to the client as node-added messages.
     private void sendAllInstances() {
-        for (ControllerNode node : clusterService.getNodes()) {
+        List<ControllerNode> nodes = new ArrayList<>(clusterService.getNodes());
+        Collections.sort(nodes, NODE_COMPARATOR);
+        for (ControllerNode node : nodes) {
             sendMessage(instanceMessage(new ClusterEvent(INSTANCE_ADDED, node)));
         }
     }
@@ -255,22 +279,37 @@
         HostToHostIntent hostIntent = new HostToHostIntent(appId, one, two,
                                                            DefaultTrafficSelector.builder().build(),
                                                            DefaultTrafficTreatment.builder().build());
-        monitorRequest = event;
+        trafficEvent = event;
         intentService.submit(hostIntent);
     }
 
+    private synchronized long startMonitoring(ObjectNode event) {
+        if (trafficTask == null) {
+            trafficEvent = event;
+            trafficTask = new TrafficMonitor();
+            timer.schedule(trafficTask, TRAFFIC_FREQUENCY_SEC, TRAFFIC_FREQUENCY_SEC);
+        }
+        return number(event, "sid");
+    }
+
+    private synchronized void stopMonitoring() {
+        if (trafficTask != null) {
+            trafficTask.cancel();
+            trafficTask = null;
+            trafficEvent = null;
+        }
+    }
+
     // Subscribes for host traffic messages.
     private synchronized void requestAllTraffic(ObjectNode event) {
         ObjectNode payload = payload(event);
-        long sid = number(event, "sid");
-        monitorRequest = event;
+        long sid = startMonitoring(event);
         sendMessage(trafficSummaryMessage(sid));
     }
 
     private void requestDeviceLinkFlows(ObjectNode event) {
         ObjectNode payload = payload(event);
-        long sid = number(event, "sid");
-        monitorRequest = event;
+        long sid = startMonitoring(event);
 
         // Get the set of selected hosts and their intents.
         ArrayNode ids = (ArrayNode) payload.path("ids");
@@ -294,8 +333,7 @@
             return;
         }
 
-        long sid = number(event, "sid");
-        monitorRequest = event;
+        long sid = startMonitoring(event);
 
         // Get the set of selected hosts and their intents.
         ArrayNode ids = (ArrayNode) payload.path("ids");
@@ -325,9 +363,30 @@
     // Cancels sending traffic messages.
     private void cancelTraffic(ObjectNode event) {
         sendMessage(trafficMessage(number(event, "sid")));
-        monitorRequest = null;
+        stopMonitoring();
     }
 
+
+    // Subscribes for summary messages.
+    private synchronized void requestSummary(ObjectNode event) {
+        if (summaryTask == null) {
+            summaryEvent = event;
+            summaryTask = new SummaryMonitor();
+            timer.schedule(summaryTask, SUMMARY_FREQUENCY_SEC, SUMMARY_FREQUENCY_SEC);
+        }
+        sendMessage(summmaryMessage(number(event, "sid")));
+    }
+
+    // Cancels sending summary messages.
+    private synchronized void cancelSummary(ObjectNode event) {
+        if (summaryTask != null) {
+            summaryTask.cancel();
+            summaryTask = null;
+            summaryEvent = null;
+        }
+    }
+
+
     // Adds all internal listeners.
     private void addListeners() {
         clusterService.addListener(clusterListener);
@@ -385,26 +444,36 @@
     private class InternalIntentListener implements IntentListener {
         @Override
         public void event(IntentEvent event) {
-            if (monitorRequest != null) {
-                requestTraffic(monitorRequest);
+            if (trafficEvent != null) {
+                requestTraffic(trafficEvent);
             }
         }
     }
 
-    private class IntentTrafficMonitor extends TimerTask {
+    private class TrafficMonitor extends TimerTask {
         @Override
         public void run() {
-            if (monitorRequest != null) {
-                String type = string(monitorRequest, "event", "unknown");
+            if (trafficEvent != null) {
+                String type = string(trafficEvent, "event", "unknown");
                 if (type.equals("requestAllTraffic")) {
-                    requestAllTraffic(monitorRequest);
+                    requestAllTraffic(trafficEvent);
                 } else if (type.equals("requestDeviceLinkFlows")) {
-                    requestDeviceLinkFlows(monitorRequest);
+                    requestDeviceLinkFlows(trafficEvent);
                 } else {
-                    requestTraffic(monitorRequest);
+                    requestTraffic(trafficEvent);
                 }
             }
         }
     }
+
+    private class SummaryMonitor extends TimerTask {
+        @Override
+        public void run() {
+            if (summaryEvent != null) {
+                requestSummary(summaryEvent);
+            }
+        }
+    }
+
 }