ONOS-1479 -- GUI - augmenting topology view for extensibility:
- Added id field to property panel, as well as overloaded constructors.
- Added modify*Details() methods to UiTopoOverlay.
- Cleaned up use of string constants.
- Reworked RequestDetails in Topo view msg handler (and base).
- Fixed bug in topo UI where selected host title click caused exception on server.

Change-Id: Ib2a3cf60fae8ad8cda77a3b6933ee758262e6f3c
diff --git a/core/api/src/main/java/org/onosproject/ui/JsonUtils.java b/core/api/src/main/java/org/onosproject/ui/JsonUtils.java
index 9d3054f..2ebb554 100644
--- a/core/api/src/main/java/org/onosproject/ui/JsonUtils.java
+++ b/core/api/src/main/java/org/onosproject/ui/JsonUtils.java
@@ -38,9 +38,7 @@
      * @param sid     sequence ID
      * @param payload event payload
      * @return the object node representation
-     * @deprecated in Cardinal Release
      */
-    @Deprecated
     public static ObjectNode envelope(String type, long sid, ObjectNode payload) {
         ObjectNode event = MAPPER.createObjectNode();
         event.put("event", type);
diff --git a/core/api/src/main/java/org/onosproject/ui/UiTopoOverlay.java b/core/api/src/main/java/org/onosproject/ui/UiTopoOverlay.java
index a36d510..4c6d5d1 100644
--- a/core/api/src/main/java/org/onosproject/ui/UiTopoOverlay.java
+++ b/core/api/src/main/java/org/onosproject/ui/UiTopoOverlay.java
@@ -26,7 +26,10 @@
  */
 public class UiTopoOverlay {
 
-    private final Logger log = LoggerFactory.getLogger(getClass());
+    /**
+     * Logger for this overlay.
+     */
+    protected final Logger log = LoggerFactory.getLogger(getClass());
 
     private final String id;
 
@@ -72,7 +75,7 @@
     /**
      * Callback invoked to destroy this instance by cleaning up any
      * internal state ready for garbage collection.
-     * This default implementation does nothing.
+     * This default implementation holds no state and does nothing.
      */
     public void destroy() {
     }
@@ -85,4 +88,24 @@
      */
     public void modifySummary(PropertyPanel pp) {
     }
+
+    /**
+     * Callback to modify the contents of the details panel for
+     * a selected device.
+     * This default implementation does nothing.
+     *
+     * @param pp property panel model of summary data
+     */
+    public void modifyDeviceDetails(PropertyPanel pp) {
+    }
+
+    /**
+     * Callback to modify the contents of the details panel for
+     * a selected host.
+     * This default implementation does nothing.
+     *
+     * @param pp property panel model of summary data
+     */
+    public void modifyHostDetails(PropertyPanel pp) {
+    }
 }
diff --git a/core/api/src/main/java/org/onosproject/ui/topo/PropertyPanel.java b/core/api/src/main/java/org/onosproject/ui/topo/PropertyPanel.java
index 30b4ce7..88dad9a 100644
--- a/core/api/src/main/java/org/onosproject/ui/topo/PropertyPanel.java
+++ b/core/api/src/main/java/org/onosproject/ui/topo/PropertyPanel.java
@@ -19,6 +19,7 @@
 
 import com.google.common.collect.Sets;
 
+import java.text.DecimalFormat;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
@@ -30,6 +31,7 @@
 
     private String title;
     private String typeId;
+    private String id;
     private List<Prop> properties = new ArrayList<>();
 
     /**
@@ -56,6 +58,19 @@
     }
 
     /**
+     * Adds an ID field to the panel data, to be included in
+     * the returned JSON data to the client.
+     *
+     * @param id the identifier
+     * @return self, for chaining
+     */
+    public PropertyPanel id(String id) {
+        this.id = id;
+        return this;
+    }
+
+
+    /**
      * Returns the title text.
      *
      * @return title text
@@ -74,6 +89,15 @@
     }
 
     /**
+     * Returns the internal ID.
+     *
+     * @return the ID
+     */
+    public String id() {
+        return id;
+    }
+
+    /**
      * Returns the list of properties to be displayed.
      *
      * @return the property list
@@ -137,6 +161,8 @@
 
     // ====================
 
+    private static final DecimalFormat DF0 = new DecimalFormat("#,###");
+
     /**
      * Simple data carrier for a property, composed of a key/value pair.
      */
@@ -156,6 +182,26 @@
         }
 
         /**
+         * Constructs a property data value.
+         * @param key property key
+         * @param value property value
+         */
+        public Prop(String key, int value) {
+            this.key = key;
+            this.value = DF0.format(value);
+        }
+
+        /**
+         * Constructs a property data value.
+         * @param key property key
+         * @param value property value
+         */
+        public Prop(String key, long value) {
+            this.key = key;
+            this.value = DF0.format(value);
+        }
+
+        /**
          * Returns the property's key.
          *
          * @return the key
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TopoOverlayCache.java b/web/gui/src/main/java/org/onosproject/ui/impl/TopoOverlayCache.java
index 3d6d900..f7690e8 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/TopoOverlayCache.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TopoOverlayCache.java
@@ -74,12 +74,20 @@
         return isNullOrEmpty(id) ? NONE : overlays.get(id);
     }
 
+    /**
+     * Returns the current overlay instance.
+     * Note that this method always returns a reference; when there is no
+     * overlay selected the "NULL" overlay instance is returned.
+     *
+     * @return the current overlay
+     */
     public UiTopoOverlay currentOverlay() {
         return current;
     }
 
     /**
-     * Returns the number of overlays in the cache.
+     * Returns the number of overlays in the cache. Remember that this
+     * includes the "NULL" overlay, representing "no overlay selected".
      *
      * @return number of overlays
      */
@@ -88,16 +96,13 @@
     }
 
 
-
+    // overlay instance representing "no overlay selected"
     private static class NullOverlay extends UiTopoOverlay {
         public NullOverlay() {
             super(null);
         }
 
-        @Override
-        public void init() {
-        }
-
+        // override activate and deactivate, so no log messages are written
         @Override
         public void activate() {
         }
@@ -105,9 +110,5 @@
         @Override
         public void deactivate() {
         }
-
-        @Override
-        public void destroy() {
-        }
     }
 }
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandler.java
index ddc61f8..9a360bf 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandler.java
@@ -85,6 +85,7 @@
  */
 public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase {
 
+    // incoming event types
     private static final String REQ_DETAILS = "requestDetails";
     private static final String UPDATE_META = "updateMeta";
     private static final String ADD_HOST_INTENT = "addHostIntent";
@@ -107,6 +108,33 @@
     private static final String TOPO_SELECT_OVERLAY = "topoSelectOverlay";
     private static final String TOPO_STOP = "topoStop";
 
+    // outgoing event types
+    private static final String SHOW_SUMMARY = "showSummary";
+    private static final String SHOW_DETAILS = "showDetails";
+    private static final String SPRITE_LIST_RESPONSE = "spriteListResponse";
+    private static final String SPRITE_DATA_RESPONSE = "spriteDataResponse";
+    private static final String UPDATE_INSTANCE = "updateInstance";
+
+    // fields
+    private static final String ID = "id";
+    private static final String IDS = "ids";
+    private static final String HOVER = "hover";
+    private static final String DEVICE = "device";
+    private static final String HOST = "host";
+    private static final String CLASS = "class";
+    private static final String UNKNOWN = "unknown";
+    private static final String ONE = "one";
+    private static final String TWO = "two";
+    private static final String SRC = "src";
+    private static final String DST = "dst";
+    private static final String DATA = "data";
+    private static final String NAME = "name";
+    private static final String NAMES = "names";
+    private static final String ACTIVATE = "activate";
+    private static final String DEACTIVATE = "deactivate";
+    private static final String PRIMARY = "primary";
+    private static final String SECONDARY = "secondary";
+
 
     private static final String APP_ID = "org.onosproject.gui";
 
@@ -244,8 +272,8 @@
 
         @Override
         public void process(long sid, ObjectNode payload) {
-            String deact = string(payload, "deactivate");
-            String act = string(payload, "activate");
+            String deact = string(payload, DEACTIVATE);
+            String act = string(payload, ACTIVATE);
             overlayCache.switchOverlay(deact, act);
         }
     }
@@ -295,8 +323,8 @@
             ObjectNode root = objectNode();
             ArrayNode names = arrayNode();
             get(SpriteService.class).getNames().forEach(names::add);
-            root.set("names", names);
-            sendMessage("spriteListResponse", sid, root);
+            root.set(NAMES, names);
+            sendMessage(SPRITE_LIST_RESPONSE, sid, root);
         }
     }
 
@@ -307,10 +335,10 @@
 
         @Override
         public void process(long sid, ObjectNode payload) {
-            String name = string(payload, "name");
+            String name = string(payload, NAME);
             ObjectNode root = objectNode();
-            root.set("data", get(SpriteService.class).get(name));
-            sendMessage("spriteDataResponse", sid, root);
+            root.set(DATA, get(SpriteService.class).get(name));
+            sendMessage(SPRITE_DATA_RESPONSE, sid, root);
         }
     }
 
@@ -321,14 +349,20 @@
 
         @Override
         public void process(long sid, ObjectNode payload) {
-            String type = string(payload, "class", "unknown");
-            String id = string(payload, "id");
+            String type = string(payload, CLASS, UNKNOWN);
+            String id = string(payload, ID);
+            PropertyPanel pp = null;
 
-            if (type.equals("device")) {
-                sendMessage(deviceDetails(deviceId(id), sid));
-            } else if (type.equals("host")) {
-                sendMessage(hostDetails(hostId(id), sid));
+            if (type.equals(DEVICE)) {
+                pp = deviceDetails(deviceId(id), sid);
+                overlayCache.currentOverlay().modifyDeviceDetails(pp);
+            } else if (type.equals(HOST)) {
+                pp = hostDetails(hostId(id), sid);
+                overlayCache.currentOverlay().modifyHostDetails(pp);
             }
+
+            ObjectNode json = JsonUtils.envelope(SHOW_DETAILS, sid, json(pp));
+            sendMessage(json);
         }
     }
 
@@ -364,8 +398,8 @@
         @Override
         public void process(long sid, ObjectNode payload) {
             // TODO: add protection against device ids and non-existent hosts.
-            HostId one = hostId(string(payload, "one"));
-            HostId two = hostId(string(payload, "two"));
+            HostId one = hostId(string(payload, ONE));
+            HostId two = hostId(string(payload, TWO));
 
             HostToHostIntent intent = HostToHostIntent.builder()
                     .appId(appId)
@@ -386,8 +420,8 @@
         @Override
         public void process(long sid, ObjectNode payload) {
             // TODO: add protection against device ids and non-existent hosts.
-            Set<HostId> src = getHostIds((ArrayNode) payload.path("src"));
-            HostId dst = hostId(string(payload, "dst"));
+            Set<HostId> src = getHostIds((ArrayNode) payload.path(SRC));
+            HostId dst = hostId(string(payload, DST));
             Host dstHost = hostService.getHost(dst);
 
             Set<ConnectPoint> ingressPoints = getHostLocations(src);
@@ -421,12 +455,12 @@
             // Cancel any other traffic monitoring mode.
             stopTrafficMonitoring();
 
-            if (!payload.has("ids")) {
+            if (!payload.has(IDS)) {
                 return;
             }
 
             // Get the set of selected hosts and their intents.
-            ArrayNode ids = (ArrayNode) payload.path("ids");
+            ArrayNode ids = (ArrayNode) payload.path(IDS);
             selectedHosts = getHosts(ids);
             selectedDevices = getDevices(ids);
             selectedIntents = intentFilter.findPathIntents(
@@ -435,7 +469,7 @@
 
             if (haveSelectedIntents()) {
                 // Send a message to highlight all links of all monitored intents.
-                sendMessage(trafficMessage(new TrafficClass("primary", selectedIntents)));
+                sendMessage(trafficMessage(new TrafficClass(PRIMARY, selectedIntents)));
             }
 
             // TODO: Re-introduce once the client click vs hover gesture stuff is sorted out.
@@ -548,7 +582,7 @@
     private synchronized void requestSummary(long sid) {
         PropertyPanel pp = summmaryMessage(sid);
         overlayCache.currentOverlay().modifySummary(pp);
-        ObjectNode json = JsonUtils.envelope("showSummary", sid, json(pp));
+        ObjectNode json = JsonUtils.envelope(SHOW_SUMMARY, sid, json(pp));
         sendMessage(json);
     }
 
@@ -668,12 +702,12 @@
         startTrafficMonitoring();
 
         // Get the set of selected hosts and their intents.
-        ArrayNode ids = (ArrayNode) payload.path("ids");
+        ArrayNode ids = (ArrayNode) payload.path(IDS);
         Set<Host> hosts = new HashSet<>();
         Set<Device> devices = getDevices(ids);
 
         // If there is a hover node, include it in the hosts and find intents.
-        String hover = JsonUtils.string(payload, "hover");
+        String hover = JsonUtils.string(payload, HOVER);
         if (!isNullOrEmpty(hover)) {
             addHover(hosts, devices, hover);
         }
@@ -699,8 +733,8 @@
         secondary.removeAll(primary);
 
         // Send a message to highlight all links of all monitored intents.
-        sendMessage(trafficMessage(new TrafficClass("primary", primary),
-                                   new TrafficClass("secondary", secondary)));
+        sendMessage(trafficMessage(new TrafficClass(PRIMARY, primary),
+                                   new TrafficClass(SECONDARY, secondary)));
     }
 
     // Requests next or previous related intent.
@@ -720,7 +754,7 @@
     // selected intent highlighted.
     private void sendSelectedIntent() {
         Intent selectedIntent = selectedIntents.get(currentIntentIndex);
-        log.info("Requested next intent {}", selectedIntent.id());
+        log.debug("Requested next intent {}", selectedIntent.id());
 
         Set<Intent> primary = new HashSet<>();
         primary.add(selectedIntent);
@@ -729,8 +763,8 @@
         secondary.remove(selectedIntent);
 
         // Send a message to highlight all links of the selected intent.
-        sendMessage(trafficMessage(new TrafficClass("primary", primary),
-                                   new TrafficClass("secondary", secondary)));
+        sendMessage(trafficMessage(new TrafficClass(PRIMARY, primary),
+                                   new TrafficClass(SECONDARY, secondary)));
     }
 
     // Requests monitoring of traffic for the selected intent.
@@ -740,13 +774,13 @@
                 currentIntentIndex = 0;
             }
             Intent selectedIntent = selectedIntents.get(currentIntentIndex);
-            log.info("Requested traffic for selected {}", selectedIntent.id());
+            log.debug("Requested traffic for selected {}", selectedIntent.id());
 
             Set<Intent> primary = new HashSet<>();
             primary.add(selectedIntent);
 
             // Send a message to highlight all links of the selected intent.
-            sendMessage(trafficMessage(new TrafficClass("primary", primary, true)));
+            sendMessage(trafficMessage(new TrafficClass(PRIMARY, primary, true)));
         }
     }
 
@@ -805,7 +839,7 @@
         @Override
         public void event(MastershipEvent event) {
             msgSender.execute(() -> {
-                sendAllInstances("updateInstance");
+                sendAllInstances(UPDATE_INSTANCE);
                 Device device = deviceService.getDevice(event.subject());
                 if (device != null) {
                     sendMessage(deviceMessage(new DeviceEvent(DEVICE_UPDATED, device)));
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 2deabc0..d32ad1d 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
@@ -440,57 +440,61 @@
                    JsonUtils.node(payload, "memento"));
     }
 
-    // Returns summary response.
+    // -----------------------------------------------------------------------
+    // Create models of the data to return, that overlays can adjust / augment
+
+    // Returns property panel model for summary response.
     protected PropertyPanel summmaryMessage(long sid) {
         Topology topology = topologyService.currentTopology();
         PropertyPanel pp = new PropertyPanel("ONOS Summary", "node")
-            .add(new PropertyPanel.Prop("Devices", format(topology.deviceCount())))
-            .add(new PropertyPanel.Prop("Links", format(topology.linkCount())))
-            .add(new PropertyPanel.Prop("Hosts", format(hostService.getHostCount())))
-            .add(new PropertyPanel.Prop("Topology SCCs", format(topology.clusterCount())))
+            .add(new PropertyPanel.Prop("Devices", topology.deviceCount()))
+            .add(new PropertyPanel.Prop("Links", topology.linkCount()))
+            .add(new PropertyPanel.Prop("Hosts", hostService.getHostCount()))
+            .add(new PropertyPanel.Prop("Topology SCCs", topology.clusterCount()))
             .add(new PropertyPanel.Separator())
-            .add(new PropertyPanel.Prop("Intents", format(intentService.getIntentCount())))
-            .add(new PropertyPanel.Prop("Tunnels", format(tunnelService.tunnelCount())))
-            .add(new PropertyPanel.Prop("Flows", format(flowService.getFlowRuleCount())))
+            .add(new PropertyPanel.Prop("Intents", intentService.getIntentCount()))
+            .add(new PropertyPanel.Prop("Tunnels", tunnelService.tunnelCount()))
+            .add(new PropertyPanel.Prop("Flows", flowService.getFlowRuleCount()))
             .add(new PropertyPanel.Prop("Version", version));
 
         return pp;
     }
 
-    // Returns device details response.
-    protected ObjectNode deviceDetails(DeviceId deviceId, long sid) {
+    // Returns property panel model for device details response.
+    protected PropertyPanel deviceDetails(DeviceId deviceId, long sid) {
         Device device = deviceService.getDevice(deviceId);
         Annotations annot = device.annotations();
         String name = annot.value(AnnotationKeys.NAME);
         int portCount = deviceService.getPorts(deviceId).size();
         int flowCount = getFlowCount(deviceId);
         int tunnelCount = getTunnelCount(deviceId);
-        return JsonUtils.envelope("showDetails", sid,
-                                  json(isNullOrEmpty(name) ? deviceId.toString() : name,
-                                       device.type().toString().toLowerCase(),
-                                       new Prop("URI", deviceId.toString()),
-                                       new Prop("Vendor", device.manufacturer()),
-                                       new Prop("H/W Version", device.hwVersion()),
-                                       new Prop("S/W Version", device.swVersion()),
-                                       new Prop("Serial Number", device.serialNumber()),
-                                       new Prop("Protocol", annot.value(AnnotationKeys.PROTOCOL)),
-                                       new Separator(),
-                                       new Prop("Master", master(deviceId)),
-                                       new Prop("Latitude", annot.value(AnnotationKeys.LATITUDE)),
-                                       new Prop("Longitude", annot.value(AnnotationKeys.LONGITUDE)),
-                                       new Separator(),
-                                       new Prop("Ports", Integer.toString(portCount)),
-                                       new Prop("Flows", Integer.toString(flowCount)),
-                                       new Prop("Tunnels", Integer.toString(tunnelCount))
-                                  ));
+
+        String title = isNullOrEmpty(name) ? deviceId.toString() : name;
+        String typeId = device.type().toString().toLowerCase();
+
+        PropertyPanel pp = new PropertyPanel(title, typeId)
+                .id(deviceId.toString())
+                .add(new PropertyPanel.Prop("URI", deviceId.toString()))
+                .add(new PropertyPanel.Prop("Vendor", device.manufacturer()))
+                .add(new PropertyPanel.Prop("H/W Version", device.hwVersion()))
+                .add(new PropertyPanel.Prop("S/W Version", device.swVersion()))
+                .add(new PropertyPanel.Prop("Serial Number", device.serialNumber()))
+                .add(new PropertyPanel.Prop("Protocol", annot.value(AnnotationKeys.PROTOCOL)))
+                .add(new PropertyPanel.Separator())
+                .add(new PropertyPanel.Prop("Latitude", annot.value(AnnotationKeys.LATITUDE)))
+                .add(new PropertyPanel.Prop("Longitude", annot.value(AnnotationKeys.LONGITUDE)))
+                .add(new PropertyPanel.Separator())
+                .add(new PropertyPanel.Prop("Ports", portCount))
+                .add(new PropertyPanel.Prop("Flows", flowCount))
+                .add(new PropertyPanel.Prop("Tunnels", tunnelCount));
+
+        return pp;
     }
 
     protected int getFlowCount(DeviceId deviceId) {
         int count = 0;
-        Iterator<FlowEntry> it = flowService.getFlowEntries(deviceId).iterator();
-        while (it.hasNext()) {
+        for (FlowEntry flowEntry : flowService.getFlowEntries(deviceId)) {
             count++;
-            it.next();
         }
         return count;
     }
@@ -503,8 +507,8 @@
             OpticalTunnelEndPoint dst = (OpticalTunnelEndPoint) tunnel.dst();
             DeviceId srcDevice = (DeviceId) src.elementId().get();
             DeviceId dstDevice = (DeviceId) dst.elementId().get();
-            if (srcDevice.toString().equals(deviceId.toString())
-             || dstDevice.toString().equals(deviceId.toString())) {
+            if (srcDevice.toString().equals(deviceId.toString()) ||
+                dstDevice.toString().equals(deviceId.toString())) {
                 count++;
             }
         }
@@ -516,9 +520,8 @@
         List<FlowEntry> entries = new ArrayList<>();
         Set<Link> links = new HashSet<>(linkService.getDeviceEgressLinks(deviceId));
         Set<Host> hosts = hostService.getConnectedHosts(deviceId);
-        Iterator<FlowEntry> it = flowService.getFlowEntries(deviceId).iterator();
-        while (it.hasNext()) {
-            entries.add(it.next());
+        for (FlowEntry flowEntry : flowService.getFlowEntries(deviceId)) {
+            entries.add(flowEntry);
         }
 
         // Add all edge links to the set
@@ -555,24 +558,30 @@
 
 
     // Returns host details response.
-    protected ObjectNode hostDetails(HostId hostId, long sid) {
+    protected PropertyPanel hostDetails(HostId hostId, long sid) {
         Host host = hostService.getHost(hostId);
         Annotations annot = host.annotations();
         String type = annot.value(AnnotationKeys.TYPE);
         String name = annot.value(AnnotationKeys.NAME);
         String vlan = host.vlan().toString();
-        return JsonUtils.envelope("showDetails", sid,
-                                  json(isNullOrEmpty(name) ? hostId.toString() : name,
-                                       isNullOrEmpty(type) ? "endstation" : type,
-                                       new Prop("MAC", host.mac().toString()),
-                                       new Prop("IP", host.ipAddresses().toString().replaceAll("[\\[\\]]", "")),
-                                       new Prop("VLAN", vlan.equals("-1") ? "none" : vlan),
-                                       new Separator(),
-                                       new Prop("Latitude", annot.value(AnnotationKeys.LATITUDE)),
-                                       new Prop("Longitude", annot.value(AnnotationKeys.LONGITUDE))));
+
+        String title = isNullOrEmpty(name) ? hostId.toString() : name;
+        String typeId = isNullOrEmpty(type) ? "endstation" : type;
+
+        PropertyPanel pp = new PropertyPanel(title, typeId)
+                .id(hostId.toString())
+                .add(new PropertyPanel.Prop("MAC", host.mac().toString()))
+                .add(new PropertyPanel.Prop("IP", host.ipAddresses().toString().replaceAll("[\\[\\]]", "")))
+                .add(new PropertyPanel.Prop("VLAN", vlan.equals("-1") ? "none" : vlan))
+                .add(new PropertyPanel.Separator())
+                .add(new PropertyPanel.Prop("Latitude", annot.value(AnnotationKeys.LATITUDE)))
+                .add(new PropertyPanel.Prop("Longitude", annot.value(AnnotationKeys.LONGITUDE)));
+
+        return pp;
     }
 
 
+    // TODO: migrate to Traffic overlay
     // Produces JSON message to trigger flow traffic overview visualization
     protected ObjectNode trafficSummaryMessage(StatsType type) {
         ObjectNode payload = objectNode();
@@ -827,21 +836,19 @@
         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);
-    }
-
     // Produces compact string representation of a link.
     private static String compactLinkString(Link link) {
         return String.format(COMPACT, link.src().elementId(), link.src().port(),
                              link.dst().elementId(), link.dst().port());
     }
 
+    // translates the property panel into JSON, for returning to the client
     protected ObjectNode json(PropertyPanel pp) {
         ObjectNode result = objectNode()
-                .put("title", pp.title()).put("type", pp.typeId());
+                .put("title", pp.title())
+                .put("type", pp.typeId())
+                .put("id", pp.id());
+
         ObjectNode pnode = objectNode();
         ArrayNode porder = arrayNode();
         for (PropertyPanel.Prop p : pp.properties()) {
diff --git a/web/gui/src/main/webapp/app/view/topo/topoPanel.js b/web/gui/src/main/webapp/app/view/topo/topoPanel.js
index cac8736..9869f64 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoPanel.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoPanel.js
@@ -219,6 +219,11 @@
     // === -----------------------------------------------------
     //  Functions for populating the detail panel
 
+    var isDevice = {
+        switch: 1,
+        roadm: 1
+    };
+
     function displaySingle(data) {
         detail.setup();
 
@@ -228,16 +233,21 @@
             title = detail.appendHeader('h2')
                 .classed('clickable', true),
             table = detail.appendBody('table'),
-            tbody = table.append('tbody');
+            tbody = table.append('tbody'),
+            navFn;
 
         gs.addGlyph(svg, (data.type || 'unknown'), 40);
-        title.text(data.id);
-        svg.on('click', function () {
-            ns.navTo(devPath, { devId: data.id });
-        });
-        title.on('click', function () {
-            ns.navTo(devPath, { devId: data.id });
-        });
+        title.text(data.title);
+
+        // only add navigation when displaying a device
+        if (isDevice[data.type]) {
+            navFn = function () {
+                ns.navTo(devPath, { devId: data.id });
+            };
+
+            svg.on('click', navFn);
+            title.on('click', navFn);
+        }
 
         listProps(tbody, data);
         addBtnFooter();