You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by ea...@apache.org on 2018/12/03 15:49:15 UTC

[1/2] qpid-dispatch git commit: DISPATCH-1195 Periodically update popup detail on topology page

Repository: qpid-dispatch
Updated Branches:
  refs/heads/master 3adde8e5d -> 55b7ae55e


http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55b7ae55/console/stand-alone/plugin/js/topology/nodes.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/topology/nodes.js b/console/stand-alone/plugin/js/topology/nodes.js
index 5d003c6..50c9a37 100644
--- a/console/stand-alone/plugin/js/topology/nodes.js
+++ b/console/stand-alone/plugin/js/topology/nodes.js
@@ -17,70 +17,74 @@ specific language governing permissions and limitations
 under the License.
 */
 
-import { utils } from '../amqp/utilities.js';
+import { utils } from "../amqp/utilities.js";
 
 /* global d3 Promise */
 export class Node {
-  constructor(id, name, nodeType, properties, routerId, x, y, nodeIndex, resultIndex, fixed, connectionContainer) {
-    this.key = id;                  // the router uri for this node (or group of clients) like: amqp:/_topo/0/<router id>/$management
-    this.name = name;               // the router id portion of the key
-    this.nodeType = nodeType;       // router.role
+  constructor(
+    id,
+    name,
+    nodeType,
+    properties,
+    routerId,
+    x,
+    y,
+    nodeIndex,
+    resultIndex,
+    fixed,
+    connectionContainer
+  ) {
+    this.key = id; // the router uri for this node (or group of clients) like: amqp:/_topo/0/<router id>/$management
+    this.name = name; // the router id portion of the key
+    this.nodeType = nodeType; // router.role
     this.properties = properties;
-    this.routerId = routerId;       // the router uri of the router we are connected to (for groups)
+    this.routerId = routerId; // the router uri of the router we are connected to (for groups)
     this.x = x;
     this.y = y;
     this.id = nodeIndex;
     this.resultIndex = resultIndex;
     this.fixed = !!+fixed;
-    this.cls = '';
+    this.cls = "";
     this.container = connectionContainer;
     this.isConsole = utils.isConsole(this);
     this.isArtemis = utils.isArtemis(this);
   }
-  title (hide) {
-    let x = '';
+  title(hide) {
+    let x = "";
     if (this.normals && this.normals.length > 1 && !hide)
-      x = ' x ' + this.normals.length;
-    if (this.isConsole)
-      return 'Dispatch console' + x;
-    else if (this.isArtemis)
-      return 'Broker - Artemis' + x;
-    else if (this.properties.product == 'qpid-cpp')
-      return 'Broker - qpid-cpp' + x;
-    else if (this.nodeType === 'edge')
-      return 'Edge Router';
-    else if (this.cdir === 'in')
-      return 'Sender' + x;
-    else if (this.cdir === 'out')
-      return 'Receiver' + x;
-    else if (this.cdir === 'both')
-      return 'Sender/Receiver' + x;
-    else if (this.nodeType === 'normal')
-      return 'client' + x;
-    else if (this.nodeType === 'on-demand')
-      return 'broker';
+      x = " x " + this.normals.length;
+    if (this.isConsole) return "Dispatch console" + x;
+    else if (this.isArtemis) return "Broker - Artemis" + x;
+    else if (this.properties.product == "qpid-cpp")
+      return "Broker - qpid-cpp" + x;
+    else if (this.nodeType === "edge") return "Edge Router";
+    else if (this.cdir === "in") return "Sender" + x;
+    else if (this.cdir === "out") return "Receiver" + x;
+    else if (this.cdir === "both") return "Sender/Receiver" + x;
+    else if (this.nodeType === "normal") return "client" + x;
+    else if (this.nodeType === "on-demand") return "broker";
     else if (this.properties.product) {
       return this.properties.product;
-    }
-    else {
-      return '';
+    } else {
+      return "";
     }
   }
-  toolTip (topology) {
-    return new Promise( (function (resolve) {
-      if (this.nodeType === 'normal' || this.nodeType === 'edge') {
-        resolve(this.clientTooltip());
-      } else
-        this.routerTooltip(topology)
-          .then( function (toolTip) {
+  toolTip(topology) {
+    return new Promise(
+      function(resolve) {
+        if (this.nodeType === "normal" || this.nodeType === "edge") {
+          resolve(this.clientTooltip());
+        } else
+          this.routerTooltip(topology).then(function(toolTip) {
             resolve(toolTip);
           });
-    }.bind(this)));
+      }.bind(this)
+    );
   }
 
-  clientTooltip () {
+  clientTooltip() {
     let type = this.title(true);
-    let title = '';
+    let title = "";
     title += `<table class="popupTable"><tr><td>Type</td><td>${type}</td></tr>`;
     if (!this.normals || this.normals.length < 2)
       title += `<tr><td>Host</td><td>${this.host}</td></tr>`;
@@ -88,77 +92,103 @@ export class Node {
       title += `<tr><td>Count</td><td>${this.normals.length}</td></tr>`;
     }
     if (!this.isConsole && !this.isArtemis)
-      title += '<tr><td colspan=2 class="more-info">Click circle for more info</td></tr></table>';
+      title +=
+        '<tr><td colspan=2 class="more-info">Click circle for more info</td></tr></table>';
     return title;
   }
 
-  routerTooltip (topology) {
-    return new Promise( (function (resolve) {
-      topology.ensureEntities(this.key, [
-        {entity: 'listener', attrs: ['role', 'port', 'http']},
-        {entity: 'router', attrs: ['name', 'version', 'hostName']}
-      ], function (foo, nodes) {
-        // update all the router title text
-        let node = nodes[this.key];
-        const err = `<table class="popupTable"><tr><td>Error</td><td>Unable to get router info for ${this.key}</td></tr></table>`;
-        if (!node) {
-          resolve(err);
-          return;
-        }
-        let listeners = node['listener'];
-        let router = node['router'];
-        if (!listeners || !router) {
-          resolve(err);
-          return;
-        }
-        let r = utils.flatten(router.attributeNames, router.results[0]);
-        let title = '<table class="popupTable">';
-        title += ('<tr><td>Router</td><td>' + r.name + '</td></tr>');
-        if (r.hostName)
-          title += ('<tr><td>Host Name</td><td>' + r.hostHame + '</td></tr>');
-        title += ('<tr><td>Version</td><td>' + r.version + '</td></tr>');
-        let ports = [];
-        for (let l=0; l<listeners.results.length; l++) {
-          let listener = utils.flatten(listeners.attributeNames, listeners.results[l]);
-          if (listener.role === 'normal') {
-            ports.push(listener.port+'');
-          }
-        }
-        if (ports.length > 0) {
-          title += ('<tr><td>Ports</td><td>' + ports.join(', ') + '</td></tr>');
-        }
-        title += '</table>';
-        resolve(title);
-        return title;
-      }.bind(this));
-    }.bind(this)));
+  routerTooltip(topology) {
+    return new Promise(
+      function(resolve) {
+        topology.ensureEntities(
+          this.key,
+          [
+            { entity: "listener", attrs: ["role", "port", "http"] },
+            { entity: "router", attrs: ["name", "version", "hostName"] }
+          ],
+          function(foo, nodes) {
+            // update all the router title text
+            let node = nodes[this.key];
+            const err = `<table class="popupTable"><tr><td>Error</td><td>Unable to get router info for ${
+              this.key
+            }</td></tr></table>`;
+            if (!node) {
+              resolve(err);
+              return;
+            }
+            let listeners = node["listener"];
+            let router = node["router"];
+            if (!listeners || !router) {
+              resolve(err);
+              return;
+            }
+            let r = utils.flatten(router.attributeNames, router.results[0]);
+            let title = '<table class="popupTable">';
+            title += "<tr><td>Router</td><td>" + r.name + "</td></tr>";
+            if (r.hostName)
+              title += "<tr><td>Host Name</td><td>" + r.hostHame + "</td></tr>";
+            title += "<tr><td>Version</td><td>" + r.version + "</td></tr>";
+            let ports = [];
+            for (let l = 0; l < listeners.results.length; l++) {
+              let listener = utils.flatten(
+                listeners.attributeNames,
+                listeners.results[l]
+              );
+              if (listener.role === "normal") {
+                ports.push(listener.port + "");
+              }
+            }
+            if (ports.length > 0) {
+              title +=
+                "<tr><td>Ports</td><td>" + ports.join(", ") + "</td></tr>";
+            }
+            title += "</table>";
+            resolve(title);
+            return title;
+          }.bind(this)
+        );
+      }.bind(this)
+    );
   }
   radius() {
     return nodeProperties[this.nodeType].radius;
   }
   uid() {
-    if (!this.uuid)
-      this.uuid = this.container;
+    if (!this.uuid) this.uuid = this.container;
     return this.normals ? `${this.uuid}-${this.normals.length}` : this.uuid;
   }
   setFixed(fixed) {
-    if (!fixed)
-      this.lat = this.lon = null;
+    if (!fixed) this.lat = this.lon = null;
     this.fixed = fixed;
   }
 }
 const nodeProperties = {
   // router types
-  'inter-router': {radius: 28, refX: {end: 32, start: -19}, linkDistance: [150, 70], charge: [-1800, -900]},
-  'edge':  {radius: 20, refX: {end: 24, start: -12}, linkDistance: [110, 55], charge: [-1350, -900]},
+  "inter-router": {
+    radius: 28,
+    refX: { end: 32, start: -19 },
+    linkDistance: [150, 70],
+    charge: [-1800, -900]
+  },
+  edge: {
+    radius: 20,
+    refX: { end: 24, start: -12 },
+    linkDistance: [110, 55],
+    charge: [-1350, -900]
+  },
   // generated nodes from connections. key is from connection.role
-  'normal':       {radius: 15, refX: {end: 20, start: -7}, linkDistance: [75, 40], charge: [-900, -900]},
+  normal: {
+    radius: 15,
+    refX: { end: 20, start: -7 },
+    linkDistance: [75, 40],
+    charge: [-900, -900]
+  }
 };
 // aliases
-nodeProperties._topo = nodeProperties['inter-router'];
-nodeProperties._edge = nodeProperties['edge'];
-nodeProperties['on-demand'] = nodeProperties['normal'];
-nodeProperties['route-container'] = nodeProperties['normal'];
+nodeProperties._topo = nodeProperties["inter-router"];
+nodeProperties._edge = nodeProperties["edge"];
+nodeProperties["on-demand"] = nodeProperties["normal"];
+nodeProperties["route-container"] = nodeProperties["normal"];
 
 export class Nodes {
   constructor(logger) {
@@ -166,8 +196,7 @@ export class Nodes {
     this.logger = logger;
   }
   static radius(type) {
-    if (nodeProperties[type].radius)
-      return nodeProperties[type].radius;
+    if (nodeProperties[type].radius) return nodeProperties[type].radius;
     return 15;
   }
   static maxRadius() {
@@ -179,8 +208,7 @@ export class Nodes {
   }
   static refX(end, r) {
     for (let key in nodeProperties) {
-      if (nodeProperties[key].radius == r)
-        return nodeProperties[key].refX[end];
+      if (nodeProperties[key].radius == r) return nodeProperties[key].refX[end];
     }
     return 0;
   }
@@ -193,97 +221,98 @@ export class Nodes {
     return Object.keys(values);
   }
   // vary the following force graph attributes based on nodeCount
-  static forceScale (nodeCount, minmax) {
+  static forceScale(nodeCount, minmax) {
     let count = Math.max(Math.min(nodeCount, 80), 6);
-    let x = d3.scale.linear()
-      .domain([6,80])
+    let x = d3.scale
+      .linear()
+      .domain([6, 80])
       .range(minmax);
     return x(count);
   }
-  linkDistance (d, nodeCount) {
+  linkDistance(d, nodeCount) {
     let range = nodeProperties[d.target.nodeType].linkDistance;
     return Nodes.forceScale(nodeCount, range);
   }
-  charge (d, nodeCount) {
+  charge(d, nodeCount) {
     let charge = nodeProperties[d.nodeType].charge;
     return Nodes.forceScale(nodeCount, charge);
   }
-  gravity (d, nodeCount) {
+  gravity(d, nodeCount) {
     return Nodes.forceScale(nodeCount, [0.0001, 0.1]);
   }
 
-  getLength () {
+  getLength() {
     return this.nodes.length;
   }
-  get (index) {
+  get(index) {
     if (index < this.getLength()) {
       return this.nodes[index];
     }
-    this.logger.error(`Attempted to get node[${index}] but there were only ${this.getLength()} nodes`);
+    this.logger.error(
+      `Attempted to get node[${index}] but there were only ${this.getLength()} nodes`
+    );
     return undefined;
   }
-  setNodesFixed (name, b) {
-    this.nodes.some(function (n) {
+  setNodesFixed(name, b) {
+    this.nodes.some(function(n) {
       if (n.name === name) {
         n.fixed(b);
         return true;
       }
     });
   }
-  nodeFor (name) {
+  nodeFor(name) {
     for (let i = 0; i < this.nodes.length; ++i) {
-      if (this.nodes[i].name == name)
-        return this.nodes[i];
+      if (this.nodes[i].name == name) return this.nodes[i];
     }
     return null;
   }
-  nodeExists (connectionContainer) {
-    return this.nodes.findIndex( function (node) {
+  nodeExists(connectionContainer) {
+    return this.nodes.findIndex(function(node) {
       return node.container === connectionContainer;
     });
   }
-  normalExists (connectionContainer) {
+  normalExists(connectionContainer) {
     let normalInfo = {};
-    for (let i=0; i<this.nodes.length; ++i) {
+    for (let i = 0; i < this.nodes.length; ++i) {
       if (this.nodes[i].normals) {
-        if (this.nodes[i].normals.some(function (normal, j) {
-          if (normal.container === connectionContainer && i !== j) {
-            normalInfo = {nodesIndex: i, normalsIndex: j};
-            return true;
-          }
-          return false;
-        }))
+        if (
+          this.nodes[i].normals.some(function(normal, j) {
+            if (normal.container === connectionContainer && i !== j) {
+              normalInfo = { nodesIndex: i, normalsIndex: j };
+              return true;
+            }
+            return false;
+          })
+        )
           break;
       }
     }
     return normalInfo;
   }
-  savePositions (nodes) {
-    if (!nodes)
-      nodes = this.nodes;
-    if (Object.prototype.toString.call(nodes) !== '[object Array]') {
+  savePositions(nodes) {
+    if (!nodes) nodes = this.nodes;
+    if (Object.prototype.toString.call(nodes) !== "[object Array]") {
       nodes = [nodes];
     }
-    this.nodes.forEach( function (d) {
+    this.nodes.forEach(function(d) {
       localStorage[d.name] = JSON.stringify({
         x: Math.round(d.x),
         y: Math.round(d.y),
-        fixed: (d.fixed & 1) ? 1 : 0,
+        fixed: d.fixed & 1 ? 1 : 0
       });
     });
   }
   // Convert node's x,y coordinates to longitude, lattitude
-  saveLonLat (backgroundMap, nodes) {
-    if (!backgroundMap || !backgroundMap.initialized)
-      return;
+  saveLonLat(backgroundMap, nodes) {
+    if (!backgroundMap || !backgroundMap.initialized) return;
     // didn't pass nodes, use all nodes
-    if (!nodes)
-      nodes = this.nodes;
+    if (!nodes) nodes = this.nodes;
     // passed a single node, wrap it in an array
-    if (Object.prototype.toString.call(nodes) !== '[object Array]') {
+    if (Object.prototype.toString.call(nodes) !== "[object Array]") {
       nodes = [nodes];
     }
-    for (let i=0; i<nodes.length; i++) {
+    for (let i = 0; i < nodes.length; i++) {
       let n = nodes[i];
       if (n.fixed) {
         let lonlat = backgroundMap.getLonLat(n.x, n.y);
@@ -297,10 +326,9 @@ export class Nodes {
     }
   }
   // convert all nodes' longitude,lattitude to x,y coordinates
-  setXY (backgroundMap) {
-    if (!backgroundMap)
-      return;
-    for (let i=0; i<this.nodes.length; i++) {
+  setXY(backgroundMap) {
+    if (!backgroundMap) return;
+    for (let i = 0; i < this.nodes.length; i++) {
       let n = this.nodes[i];
       if (n.lon && n.lat) {
         let xy = backgroundMap.getXY(n.lon, n.lat);
@@ -312,68 +340,128 @@ export class Nodes {
     }
   }
 
-  find (connectionContainer, properties, name) {
+  find(connectionContainer, properties, name) {
     properties = properties || {};
-    for (let i=0; i<this.nodes.length; ++i) {
-      if (this.nodes[i].name === name || this.nodes[i].container === connectionContainer) {
-        if (properties.product)
-          this.nodes[i].properties = properties;
+    for (let i = 0; i < this.nodes.length; ++i) {
+      if (
+        this.nodes[i].name === name ||
+        this.nodes[i].container === connectionContainer
+      ) {
+        if (properties.product) this.nodes[i].properties = properties;
         return this.nodes[i];
       }
     }
     return undefined;
   }
-  getOrCreateNode (id, name, nodeType, nodeIndex, x, y, 
-    connectionContainer, resultIndex, fixed, properties) {
+  getOrCreateNode(
+    id,
+    name,
+    nodeType,
+    nodeIndex,
+    x,
+    y,
+    connectionContainer,
+    resultIndex,
+    fixed,
+    properties
+  ) {
     properties = properties || {};
     let gotNode = this.find(connectionContainer, properties, name);
     if (gotNode) {
       return gotNode;
     }
     let routerId = utils.nameFromId(id);
-    return new Node(id, name, nodeType, properties, routerId, x, y, 
-      nodeIndex, resultIndex, fixed, connectionContainer);
-  }
-  add (obj) {
+    return new Node(
+      id,
+      name,
+      nodeType,
+      properties,
+      routerId,
+      x,
+      y,
+      nodeIndex,
+      resultIndex,
+      fixed,
+      connectionContainer
+    );
+  }
+  add(obj) {
     this.nodes.push(obj);
     return obj;
   }
-  addUsing (id, name, nodeType, nodeIndex, x, y, 
-    connectContainer, resultIndex, fixed, properties) {
-    let obj = this.getOrCreateNode(id, name, nodeType, nodeIndex, x, y, 
-      connectContainer, resultIndex, fixed, properties);
+  addUsing(
+    id,
+    name,
+    nodeType,
+    nodeIndex,
+    x,
+    y,
+    connectContainer,
+    resultIndex,
+    fixed,
+    properties
+  ) {
+    let obj = this.getOrCreateNode(
+      id,
+      name,
+      nodeType,
+      nodeIndex,
+      x,
+      y,
+      connectContainer,
+      resultIndex,
+      fixed,
+      properties
+    );
     this.nodes.push(obj);
     return obj;
   }
-  clearHighlighted () {
-    for (let i = 0; i<this.nodes.length; ++i) {
+  clearHighlighted() {
+    for (let i = 0; i < this.nodes.length; ++i) {
       this.nodes[i].highlighted = false;
     }
   }
-  initialize (nodeInfo, localStorage, width, height) {
+  initialize(nodeInfo, localStorage, width, height) {
     let nodeCount = Object.keys(nodeInfo).length;
     let yInit = 50;
     let animate = false;
     for (let id in nodeInfo) {
       let name = utils.nameFromId(id);
       // if we have any new nodes, animate the force graph to position them
-      let position = localStorage[name] ? JSON.parse(localStorage[name]) : undefined;
+      let position = localStorage[name]
+        ? JSON.parse(localStorage[name])
+        : undefined;
       if (!position) {
         animate = true;
         position = {
-          x: Math.round(width / 4 + ((width / 2) / nodeCount) * this.nodes.length),
-          y: Math.round(height / 2 + Math.sin(this.nodes.length / (Math.PI*2.0)) * height / 4),
-          fixed: false,
+          x: Math.round(
+            width / 4 + (width / 2 / nodeCount) * this.nodes.length
+          ),
+          y: Math.round(
+            height / 2 +
+              (Math.sin(this.nodes.length / (Math.PI * 2.0)) * height) / 4
+          ),
+          fixed: false
         };
       }
       if (position.y > height) {
         position.y = 200 - yInit;
         yInit *= -1;
       }
-      let parts = id.split('/');
-      this.addUsing(id, name, parts[1], this.nodes.length, position.x, position.y, name, undefined, position.fixed, {});
+      let parts = id.split("/");
+      this.addUsing(
+        id,
+        name,
+        parts[1],
+        this.nodes.length,
+        position.x,
+        position.y,
+        name,
+        undefined,
+        position.fixed,
+        {}
+      );
     }
     return animate;
   }
 }
-

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55b7ae55/console/stand-alone/plugin/js/topology/qdrTopology.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/topology/qdrTopology.js b/console/stand-alone/plugin/js/topology/qdrTopology.js
index e5cb783..b20e69e 100644
--- a/console/stand-alone/plugin/js/topology/qdrTopology.js
+++ b/console/stand-alone/plugin/js/topology/qdrTopology.js
@@ -146,20 +146,26 @@ export class TopologyController {
       });
     }
 
+    // called from the html page's popup menu
     $scope.setFixed = function(b) {
       if ($scope.contextNode) {
         $scope.contextNode.setFixed(b);
         nodes.savePositions();
         nodes.saveLonLat(backgroundMap, $scope.contextNode);
       }
-      if (!b)
-        animate = true;
+      // redraw the circles/links
       restart();
+
+      if (!b) {
+        // let the nodes move to a new position
+        animate = true;
+        force.start(); 
+      }
     };
     $scope.isFixed = function() {
       if (!$scope.contextNode)
         return false;
-      return ($scope.contextNode.fixed & 1);
+      return ($scope.contextNode.fixed);
     };
 
     let mouseX, mouseY;
@@ -255,13 +261,15 @@ export class TopologyController {
         });
 
       // the legend
-      d3.select('#topo_svg_legend svg').remove();
-      lsvg = d3.select('#topo_svg_legend')
-        .append('svg')
-        .attr('id', 'svglegend');
-      lsvg = lsvg.append('svg:g')
-        .attr('transform', `translate(${Nodes.maxRadius()}, ${Nodes.maxRadius()})`)
-        .selectAll('g');
+      //d3.select('#topo_svg_legend svg').remove();
+      if (d3.select('#svglegend').empty()) {
+        lsvg = d3.select('#topo_svg_legend')
+          .append('svg')
+          .attr('id', 'svglegend');
+        lsvg = lsvg.append('svg:g')
+          .attr('transform', `translate(${Nodes.maxRadius()}, ${Nodes.maxRadius()})`)
+          .selectAll('g');
+      }
 
       // mouse event vars
       $scope.mousedown_node = null;
@@ -342,8 +350,8 @@ export class TopologyController {
       circle = svg.append('svg:g').attr('class', 'nodes').selectAll('g');
 
       // app starts here
-      if (unknowns.length === 0)
-        restart();
+      //if (unknowns.length === 0)
+      restart();
       if (oldSelectedNode) {
         d3.selectAll('circle.inter-router').classed('selected', function (d) {
           if (d.key === oldSelectedNode.key) {
@@ -510,11 +518,12 @@ export class TopologyController {
           };
           // update the contents of the popup tooltip each time the data is polled
           QDRService.management.topology.addUpdatedAction('connectionPopupHTML', updateTooltip);
+          // request the data and update the tooltip as soon as it arrives
           QDRService.management.topology.ensureAllEntities(
             [{ entity: 'router.link', force: true},{entity: 'connection'}], function () {
               updateTooltip();
             });
-          // show the tooltip
+          // just show the tooltip with whatever data we have
           updateTooltip();
           restart();
 
@@ -572,7 +581,7 @@ export class TopologyController {
           return (d === selected_node);
         })
         .classed('fixed', function(d) {
-          return d.fixed & 1;
+          return d.fixed;
         });
       circle
         .classed('multiple', function (d) {
@@ -648,9 +657,8 @@ export class TopologyController {
           if (cur_mouse[0] != initial_mouse_down_position[0] ||
             cur_mouse[1] != initial_mouse_down_position[1]) {
             d.setFixed(true);
-            nodes.savePositions(d);
-            nodes.saveLonLat(backgroundMap, d);
-            console.log('savedLonLat for fixed node');
+            nodes.savePositions();
+            nodes.saveLonLat(backgroundMap);
             resetMouseVars();
             restart();
             return;
@@ -670,17 +678,17 @@ export class TopologyController {
           $scope.mousedown_node = null;
           if (!$scope.$$phase) $scope.$apply();
           // handle clicking on nodes that represent multiple sub-nodes
-          if (d.normals && !d.isConsole && !d.isArtemis) {
+          if (d.normals && !d.isArtemis && !d.isQpid) {
             doDialog(d);
           }
+          // apply any data changes to the interface
           restart();
 
         })
         .on('dblclick', function(d) { // circle
           d3.event.preventDefault();
           if (d.fixed) {
-            d.fixed = false;
-            nodes.setNodesFixed(d.name, false);
+            d.setFixed(false);
             restart(); // redraw the node without a dashed line
             force.start(); // let the nodes move to a new position
           }
@@ -841,7 +849,7 @@ export class TopologyController {
           return null;
         })
         .classed('fixed', function(d) {
-          return d.fixed & 1;
+          return d.fixed;
         })
         .classed('normal', function(d) {
           return d.nodeType == 'normal' || QDRService.utilities.isConsole(d);
@@ -997,11 +1005,8 @@ export class TopologyController {
         if (!savedKeys.hasOwnProperty(key))
           return 1;
         // if the number of connections for this node chaanged
-        if (!nodeInfo[key]['connection'])
-          return -1;
-        if (nodeInfo[key]['connection'].results.length != savedKeys[key]) {
-          return -1;
-        }
+        if (nodeInfo[key]['connection'].results.length !== savedKeys[key])
+          return nodeInfo[key]['connection'].results.length - savedKeys[key];
       }
       return 0;
     }
@@ -1061,25 +1066,9 @@ export class TopologyController {
           setupInitialUpdate();
         } else if (changed === -1) {
           // we lost a node (or a client), we can draw the new svg immediately
-          animate = false;
+          QDRService.management.topology.purge();
+          initForceGraph();
           saveChanged();
-          let nodeInfo = QDRService.management.topology.nodeInfo();
-          forceData.nodes = nodes = new Nodes(QDRLog);
-          animate = nodes.initialize(nodeInfo, localStorage, width, height);
-
-          let unknowns = [];
-          forceData.links = links = new Links(QDRLog);
-          if (links.initialize(nodeInfo, nodes, unknowns, localStorage, height)) {
-            animate = true;
-          }
-          if (unknowns.length > 0) {
-            resolveUnknowns(nodeInfo, unknowns);
-          }
-          else {
-            force.nodes(nodes.nodes).links(links.links).start();
-            restart();
-          }
-          //initForceGraph();
         } else {
           //QDRLog.debug("topology didn't change")
         }
@@ -1102,7 +1091,7 @@ export class TopologyController {
 
     animate = true;
     setupInitialUpdate();
-    QDRService.management.topology.startUpdating(false);
+    QDRService.management.topology.startUpdating(true);
 
   }
 }

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55b7ae55/console/stand-alone/plugin/js/topology/topoUtils.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/topology/topoUtils.js b/console/stand-alone/plugin/js/topology/topoUtils.js
index cbed6c0..71e5e4c 100644
--- a/console/stand-alone/plugin/js/topology/topoUtils.js
+++ b/console/stand-alone/plugin/js/topology/topoUtils.js
@@ -16,6 +16,9 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
+
+/* global Set */
+
 // highlight the paths between the selected node and the hovered node
 function findNextHopNode(from, d, QDRService, selected_node, nodes) {
   // d is the node that the mouse is over
@@ -35,7 +38,7 @@ function findNextHopNode(from, d, QDRService, selected_node, nodes) {
   let vAr = sInfo['router.node'].results;
   for (let hIdx = 0; hIdx < vAr.length; ++hIdx) {
     let addrT = QDRService.utilities.valFor(aAr, vAr[hIdx], 'id');
-    if (addrT == d.name) {
+    if (d.name && (addrT == d.name)) {
       let next = QDRService.utilities.valFor(aAr, vAr[hIdx], 'nextHop');
       return (next == null) ? nodes.nodeFor(addrT) : nodes.nodeFor(next);
     }
@@ -67,23 +70,44 @@ export function connectionPopupHTML (d, QDRService) {
     return;
   }
   let utils = QDRService.utilities;
-  let getConnsArray = function (d, conn) {
-    let conns = [conn];
-    if (d.cls === 'small') {
-      conns = [];
-      let normals = d.target.normals ? d.target.normals : d.source.normals;
-      for (let n=0; n<normals.length; n++) {
-        if (normals[n].resultIndex !== undefined) {
-          conns.push(utils.flatten(onode['connection'].attributeNames,
-            onode['connection'].results[normals[n].resultIndex]));
+  // return all of onode's connections that connecto to right
+  let getConnsArray = function (onode, key, right) {
+    if (right.normals) {
+      // if we want connections between a router and a client[s]
+      let connIds = new Set();
+      let connIndex = onode.connection.attributeNames.indexOf('identity');
+      for (let n=0; n<right.normals.length; n++) {
+        let normal = right.normals[n];
+        if (normal.key === key) {
+          connIds.add(normal.connectionId);
+        } else if (normal.alsoConnectsTo) {
+          normal.alsoConnectsTo.forEach( function (ac2) {
+            if (ac2.key === key)
+              connIds.add(ac2.connectionId);
+          });
         }
       }
+      return onode.connection.results.filter( function (result) {
+        return connIds.has(result[connIndex]);
+      }).map( function (c) {
+        return utils.flatten(onode.connection.attributeNames, c);
+      });
+    }
+    else {
+    // we want the connection between two routers
+      let container = utils.nameFromId(right.key);
+      let containerIndex = onode.connection.attributeNames.indexOf('container');
+      let roleIndex = onode.connection.attributeNames.indexOf('role');
+      return onode.connection.results.filter( function (conn) {
+        return conn[containerIndex] === container && conn[roleIndex] === 'inter-router';
+      }).map( function (c) {
+        return utils.flatten(onode.connection.attributeNames, c);
+      });
     }
-    return conns;
   };
   // construct HTML to be used in a popup when the mouse is moved over a link.
   // The HTML is sanitized elsewhere before it is displayed
-  let linksHTML = function (onode, conn, d) {
+  let linksHTML = function (onode, conns) {
     const max_links = 10;
     const fields = ['deliveryCount', 'undeliveredCount', 'unsettledCount', 'rejectedCount', 'releasedCount', 'modifiedCount'];
     // local function to determine if a link's connectionId is in any of the connections
@@ -103,7 +127,6 @@ export function connectionPopupHTML (d, QDRService) {
       }
       return out;
     };
-    let conns = getConnsArray(d, conn);
     // if the data for the line is from a client (small circle), we may have multiple connections
     // loop through all links for this router and accumulate those belonging to the connection(s)
     let nodeLinks = onode['router.link'];
@@ -189,50 +212,39 @@ export function connectionPopupHTML (d, QDRService) {
       });
       HTML += `<tr><td> ${joinedVals} </td></tr>`;
     }
-    // no rows were added
-    if (links.length === 0) {
-      HTML += `<tr><td align="center" colspan="${th.length}">Calculating rates, or rates were all zero</td></tr>`;
-    }
-    HTML += '</table>';
-    return HTMLHeading + HTML;
+    return links.length > 0 ? `${HTMLHeading}${HTML}</table>` : '';
   };
 
-  let left = d.left ? d.source : d.target;
-  // left is the connection with dir 'in'
-  let right = d.left ? d.target : d.source;
-  let onode = QDRService.management.topology.nodeInfo()[left.key];
-  // loop through all the connections for left, and find the one for right
-  let rightIndex = onode['connection'].results.findIndex( function (conn) {
-    return utils.valFor(onode['connection'].attributeNames, conn, 'container') === right.routerId;
-  });
-  if (rightIndex < 0) {
-    // we have a connection to a client/service
-    rightIndex = +left.resultIndex;
+  let left, right;
+  if (d.left) {
+    left = d.source;
+    right = d.target;
+  } else {
+    left = d.target;
+    right = d.source;
   }
-  if (isNaN(rightIndex)) {
-    // we have a connection to a console
-    rightIndex = +right.resultIndex;
+  if (left.normals) {
+    // swap left and right
+    [left, right] = [right, left];
   }
+  // left is a router. right is either a router or a client[s]
+  let onode = QDRService.management.topology.nodeInfo()[left.key];
+  // find all the connections for left that go to right
+  let conns = getConnsArray(onode, left.key, right);
+
   let HTML = '';
-  if (rightIndex >= 0) {
-    let conn = onode['connection'].results[rightIndex];
-    conn = utils.flatten(onode['connection'].attributeNames, conn);
-    let conns = getConnsArray(d, conn);
-    if (conns.length === 1) {
-      HTML += '<h5>Connection'+(conns.length > 1 ? 's' : '')+'</h5>';
-      HTML += '<table class="popupTable"><tr class="header"><td>Security</td><td>Authentication</td><td>Tenant</td><td>Host</td>';
+  HTML += '<h5>Connection'+(conns.length > 1 ? 's' : '')+'</h5>';
+  HTML += '<table class="popupTable"><tr class="header"><td>Security</td><td>Authentication</td><td>Tenant</td><td>Host</td>';
 
-      for (let c=0; c<conns.length; c++) {
-        HTML += ('<tr><td>' + utils.connSecurity(conns[c]) + '</td>');
-        HTML += ('<td>' + utils.connAuth(conns[c]) + '</td>');
-        HTML += ('<td>' + (utils.connTenant(conns[c]) || '--') + '</td>');
-        HTML += ('<td>' + conns[c].host + '</td>');
-        HTML += '</tr>';
-      }
-      HTML += '</table>';
-    }
-    HTML += linksHTML(onode, conn, d);
+  for (let c=0; c<Math.min(conns.length, 10); c++) {
+    HTML += ('<tr><td>' + utils.connSecurity(conns[c]) + '</td>');
+    HTML += ('<td>' + utils.connAuth(conns[c]) + '</td>');
+    HTML += ('<td>' + (utils.connTenant(conns[c]) || '--') + '</td>');
+    HTML += ('<td>' + conns[c].host + '</td>');
+    HTML += '</tr>';
   }
+  HTML += '</table>';
+  HTML += linksHTML(onode, conns);
   return HTML;
 }
 

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55b7ae55/console/stand-alone/plugin/js/topology/traffic.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/topology/traffic.js b/console/stand-alone/plugin/js/topology/traffic.js
index a6079fc..ff46d63 100644
--- a/console/stand-alone/plugin/js/topology/traffic.js
+++ b/console/stand-alone/plugin/js/topology/traffic.js
@@ -19,15 +19,25 @@ under the License.
 
 /* global d3 Promise */
 
-import { ChordData } from '../chord/data.js';
-import { MIN_CHORD_THRESHOLD } from '../chord/matrix.js';
-import { nextHop } from './topoUtils.js';
+import { ChordData } from "../chord/data.js";
+import { MIN_CHORD_THRESHOLD } from "../chord/matrix.js";
+import { nextHop } from "./topoUtils.js";
 
 const transitionDuration = 1000;
-const CHORDFILTERKEY = 'chordFilter';
+const CHORDFILTERKEY = "chordFilter";
 
-export class Traffic { // eslint-disable-line no-unused-vars
-  constructor($scope, $timeout, QDRService, converter, radius, topology, type, prefix) {
+export class Traffic {
+  // eslint-disable-line no-unused-vars
+  constructor(
+    $scope,
+    $timeout,
+    QDRService,
+    converter,
+    radius,
+    topology,
+    type,
+    prefix
+  ) {
     $scope.addressColors = {};
     this.QDRService = QDRService;
     this.type = type; // moving dots or colored path
@@ -53,16 +63,17 @@ export class Traffic { // eslint-disable-line no-unused-vars
   }
   // remove any animations that are in progress
   remove() {
-    if (this.vis)
-      this.vis.remove();
+    if (this.vis) this.vis.remove();
   }
   // called when one of the address checkboxes is toggled
   setAnimationType(type, converter, radius) {
     this.stop();
     this.remove();
     this.type = type;
-    this.vis = type === 'dots' ? new Dots(this, converter, radius) :
-      new Congestion(this);
+    this.vis =
+      type === "dots"
+        ? new Dots(this, converter, radius)
+        : new Congestion(this);
   }
   // called periodically to refresh the traffic flow
   doUpdate() {
@@ -70,7 +81,6 @@ export class Traffic { // eslint-disable-line no-unused-vars
   }
 }
 
-
 /* Base class for congestion and dots visualizations */
 class TrafficAnimation {
   constructor(traffic) {
@@ -79,18 +89,16 @@ class TrafficAnimation {
   nodeIndexFor(nodes, name) {
     for (let i = 0; i < nodes.length; i++) {
       let node = nodes[i];
-      if (node.container === name)
-        return i;
+      if (node.container === name) return i;
     }
     // not found. loop through normals
     for (let i = 0; i < nodes.length; i++) {
       let node = nodes[i];
       if (node.normals) {
-        let normalIndex = node.normals.findIndex( function (normal) {
+        let normalIndex = node.normals.findIndex(function(normal) {
           return normal.container === name;
         });
-        if (normalIndex >= 0)
-          return i;
+        if (normalIndex >= 0) return i;
       }
     }
     return -1;
@@ -98,15 +106,20 @@ class TrafficAnimation {
 }
 
 /* Color the links between router to show how heavily used the links are. */
-class Congestion extends TrafficAnimation{
+class Congestion extends TrafficAnimation {
   constructor(traffic) {
     super(traffic);
     this.init_markerDef();
   }
   init_markerDef() {
-    this.custom_markers_def = d3.select('#SVG_ID').select('defs.custom-markers');
+    this.custom_markers_def = d3
+      .select("#SVG_ID")
+      .select("defs.custom-markers");
     if (this.custom_markers_def.empty()) {
-      this.custom_markers_def = d3.select('#SVG_ID').append('svg:defs').attr('class', 'custom-markers');
+      this.custom_markers_def = d3
+        .select("#SVG_ID")
+        .append("svg:defs")
+        .attr("class", "custom-markers");
     }
   }
   findResult(node, entity, attribute, value) {
@@ -114,7 +127,10 @@ class Congestion extends TrafficAnimation{
     if (attrIndex >= 0) {
       for (let i = 0; i < node[entity].results.length; i++) {
         if (node[entity].results[i][attrIndex] === value) {
-          return this.traffic.QDRService.utilities.flatten(node[entity].attributeNames, node[entity].results[i]);
+          return this.traffic.QDRService.utilities.flatten(
+            node[entity].attributeNames,
+            node[entity].results[i]
+          );
         }
       }
     }
@@ -122,107 +138,146 @@ class Congestion extends TrafficAnimation{
   }
   doUpdate() {
     let self = this;
-    this.traffic.QDRService.management.topology.ensureAllEntities([{ entity: 'router.link', force: true }, { entity: 'connection' }], function () {
-      let links = {};
-      let nodeInfo = self.traffic.QDRService.management.topology.nodeInfo();
-      const nodes = self.traffic.topology.nodes.nodes;
-      const srv = self.traffic.QDRService;
-      // accumulate all the inter-router links in an object
-      // keyed by the svgs path id
-      for (let nodeId in nodeInfo) {
-        let node = nodeInfo[nodeId];
-        let nodeLinks = node['router.link'];
-        if (!nodeLinks)
-          continue;
-        for (let n = 0; n < nodeLinks.results.length; n++) {
-          let link = srv.utilities.flatten(nodeLinks.attributeNames, nodeLinks.results[n]);
-          if (link.linkType !== 'router-control') {
-            let f = self.nodeIndexFor(nodes, srv.utilities.nameFromId(nodeId));
-            let connection = self.findResult(node, 'connection', 'identity', link.connectionId);
-            if (connection) {
-              let t = self.nodeIndexFor(nodes, connection.container);
-              let little = Math.min(f, t);
-              let big = Math.max(f, t);
-              if (little >= 0) {
-                let key = ['#path', nodes[little].uid(srv), 
-                  nodes[big].uid(srv)].join('-');
-                if (!links[key])
-                  links[key] = [];
-                links[key].push(link);
+    this.traffic.QDRService.management.topology.ensureAllEntities(
+      [{ entity: "router.link", force: true }, { entity: "connection" }],
+      function() {
+        let links = {};
+        let nodeInfo = self.traffic.QDRService.management.topology.nodeInfo();
+        const nodes = self.traffic.topology.nodes.nodes;
+        const srv = self.traffic.QDRService;
+        // accumulate all the inter-router links in an object
+        // keyed by the svgs path id
+        for (let nodeId in nodeInfo) {
+          let node = nodeInfo[nodeId];
+          let nodeLinks = node["router.link"];
+          if (!nodeLinks) continue;
+          for (let n = 0; n < nodeLinks.results.length; n++) {
+            let link = srv.utilities.flatten(
+              nodeLinks.attributeNames,
+              nodeLinks.results[n]
+            );
+            if (link.linkType !== "router-control") {
+              let f = self.nodeIndexFor(
+                nodes,
+                srv.utilities.nameFromId(nodeId)
+              );
+              let connection = self.findResult(
+                node,
+                "connection",
+                "identity",
+                link.connectionId
+              );
+              if (connection) {
+                let t = self.nodeIndexFor(nodes, connection.container);
+                let little = Math.min(f, t);
+                let big = Math.max(f, t);
+                if (little >= 0) {
+                  let key = [
+                    "#path",
+                    nodes[little].uid(srv),
+                    nodes[big].uid(srv)
+                  ].join("-");
+                  if (!links[key]) links[key] = [];
+                  links[key].push(link);
+                }
               }
             }
           }
         }
-      }
-      // accumulate the colors/directions to be used
-      let colors = {};
-      for (let key in links) {
-        let congestion = self.congestion(links[key]);
-        let pathId = key.replace(/\./g, '\\.').replace(/ /g, '\\ ');
-        let path = d3.select(pathId);
-        if (path && !path.empty()) {
-          let dir = path.attr('marker-end') === '' ? 'start' : 'end';
-          let small = path.attr('class').indexOf('small') > -1;
-          let id = dir + '-' + congestion.substr(1) + (small ? '-s' : '');
-          colors[id] = { dir: dir, color: congestion, small: small };
-          path
-            .classed('traffic', true)
-            .attr('marker-start', function (d) {
-              return null;
-              //return d.left ? 'url(' + self.traffic.prefix + '#' + id + ')' : null;
-            })
-            .attr('marker-end', function (d) {
-              return null;
-              //return d.right ? 'url(' + self.traffic.prefix + '#' + id + ')' : null;
-            });
-          path
-            .transition()
-            .duration(1000)
-            .attr('stroke', congestion);
+        // accumulate the colors/directions to be used
+        let colors = {};
+        for (let key in links) {
+          let congestion = self.congestion(links[key]);
+          let pathId = key.replace(/\./g, "\\.").replace(/ /g, "\\ ");
+          let path = d3.select(pathId);
+          if (path && !path.empty()) {
+            let dir = path.attr("marker-end") === "" ? "start" : "end";
+            let small = path.attr("class").indexOf("small") > -1;
+            let id = dir + "-" + congestion.substr(1) + (small ? "-s" : "");
+            colors[id] = { dir: dir, color: congestion, small: small };
+            path
+              .classed("traffic", true)
+              .attr("marker-start", function() {
+                return null;
+                //return d.left ? 'url(' + self.traffic.prefix + '#' + id + ')' : null;
+              })
+              .attr("marker-end", function() {
+                return null;
+                //return d.right ? 'url(' + self.traffic.prefix + '#' + id + ')' : null;
+              });
+            path
+              .transition()
+              .duration(1000)
+              .attr("stroke", congestion);
+          }
         }
+        // create the svg:def that holds the custom markers
+        self.init_markerDef();
+        let colorKeys = Object.keys(colors);
+        let custom_markers = self.custom_markers_def
+          .selectAll("marker")
+          .data(colorKeys, function(d) {
+            return d;
+          });
+        custom_markers
+          .enter()
+          .append("svg:marker")
+          .attr("id", function(d) {
+            return d;
+          })
+          .attr("viewBox", "0 -5 10 10")
+          .attr("refX", function(d) {
+            return colors[d].dir === "end" ? 24 : colors[d].small ? -24 : -14;
+          })
+          .attr("markerWidth", 14)
+          .attr("markerHeight", 14)
+          .attr("markerUnits", "userSpaceOnUse")
+          .attr("orient", "auto")
+          .style("fill", function(d) {
+            return colors[d].color;
+          })
+          .append("svg:path")
+          .attr("d", function(d) {
+            return colors[d].dir === "end"
+              ? "M 0 -5 L 10 0 L 0 5 z"
+              : "M 10 -5 L 0 0 L 10 5 z";
+          });
+        custom_markers.exit().remove();
       }
-      // create the svg:def that holds the custom markers
-      self.init_markerDef();
-      let colorKeys = Object.keys(colors);
-      let custom_markers = self.custom_markers_def.selectAll('marker')
-        .data(colorKeys, function (d) { return d; });
-      custom_markers.enter().append('svg:marker')
-        .attr('id', function (d) { return d; })
-        .attr('viewBox', '0 -5 10 10')
-        .attr('refX', function (d) {
-          return colors[d].dir === 'end' ? 24 : (colors[d].small) ? -24 : -14;
-        })
-        .attr('markerWidth', 14)
-        .attr('markerHeight', 14)
-        .attr('markerUnits', 'userSpaceOnUse')
-        .attr('orient', 'auto')
-        .style('fill', function (d) { return colors[d].color; })
-        .append('svg:path')
-        .attr('d', function (d) {
-          return colors[d].dir === 'end' ? 'M 0 -5 L 10 0 L 0 5 z' : 'M 10 -5 L 0 0 L 10 5 z';
-        });
-      custom_markers.exit().remove();
-    });
+    );
   }
   congestion(links) {
     let v = 0;
     for (let l = 0; l < links.length; l++) {
       let link = links[l];
-      v = Math.max(v, (link.undeliveredCount + link.unsettledCount) / link.capacity);
+      v = Math.max(
+        v,
+        (link.undeliveredCount + link.unsettledCount) / link.capacity
+      );
     }
     return this.fillColor(v);
   }
   fillColor(v) {
-    let color = d3.scale.linear().domain([0, 1, 2, 3])
+    let color = d3.scale
+      .linear()
+      .domain([0, 1, 2, 3])
       .interpolate(d3.interpolateHcl)
-      .range([d3.rgb('#000000'), d3.rgb('#00FF00'), d3.rgb('#FFA500'), d3.rgb('#FF0000')]);
+      .range([
+        d3.rgb("#000000"),
+        d3.rgb("#00FF00"),
+        d3.rgb("#FFA500"),
+        d3.rgb("#FF0000")
+      ]);
     return color(Math.max(0, Math.min(3, v)));
   }
   remove() {
-    d3.select('#SVG_ID').selectAll('path.traffic')
-      .classed('traffic', false);
-    d3.select('#SVG_ID').select('defs.custom-markers')
-      .selectAll('marker').remove();
+    d3.select("#SVG_ID")
+      .selectAll("path.traffic")
+      .classed("traffic", false);
+    d3.select("#SVG_ID")
+      .select("defs.custom-markers")
+      .selectAll("marker")
+      .remove();
   }
 }
 
@@ -231,55 +286,61 @@ class Congestion extends TrafficAnimation{
 class Dots extends TrafficAnimation {
   constructor(traffic, converter, radius) {
     super(traffic);
-    this.excludedAddresses = localStorage[CHORDFILTERKEY] ? JSON.parse(localStorage[CHORDFILTERKEY]) : [];
+    this.excludedAddresses = localStorage[CHORDFILTERKEY]
+      ? JSON.parse(localStorage[CHORDFILTERKEY])
+      : [];
     this.radius = radius; // the radius of a router circle
     this.lastFlows = {}; // the number of dots animated between routers
     this.stopped = false;
     this.chordData = new ChordData(this.traffic.QDRService, true, converter); // gets ingressHistogram data
     this.chordData.setFilter(this.excludedAddresses);
     traffic.$scope.addresses = {};
-    this.chordData.getMatrix().then(function () {
-      this.traffic.$timeout(function () {
-        this.traffic.$scope.addresses = this.chordData.getAddresses();
-        for (let address in this.traffic.$scope.addresses) {
-          this.fillColor(address);
-        }
-      }.bind(this));
-    }.bind(this));
+    this.chordData.getMatrix().then(
+      function() {
+        this.traffic.$timeout(
+          function() {
+            this.traffic.$scope.addresses = this.chordData.getAddresses();
+            for (let address in this.traffic.$scope.addresses) {
+              this.fillColor(address);
+            }
+          }.bind(this)
+        );
+      }.bind(this)
+    );
     // colors
     this.colorGen = d3.scale.category10();
-    for (let i=0; i<10; i++) {
+    for (let i = 0; i < 10; i++) {
       this.colorGen(i);
     }
     let self = this;
     // event notification that an address checkbox has changed
-    traffic.$scope.addressFilterChanged = function () {
-      self.updateAddresses()
-        .then(function () {
-          // don't wait for the next polling cycle. update now
-          self.traffic.stop();
-          self.traffic.start();
-        });
+    traffic.$scope.addressFilterChanged = function() {
+      self.updateAddresses().then(function() {
+        // don't wait for the next polling cycle. update now
+        self.traffic.stop();
+        self.traffic.start();
+      });
     };
     // called by angular when mouse enters one of the address legends
-    traffic.$scope.enterLegend = function (address) {
+    traffic.$scope.enterLegend = function(address) {
       // fade all flows that aren't for this address
       self.fadeOtherAddresses(address);
     };
     // called when the mouse leaves one of the address legends
-    traffic.$scope.leaveLegend = function () {
+    traffic.$scope.leaveLegend = function() {
       self.unFadeAll();
     };
     // clicked on the address name. toggle the address checkbox
-    traffic.$scope.addressClick = function (address) {
-      self.toggleAddress(address)
-        .then(function () {
-          self.updateAddresses();
-        });
+    traffic.$scope.addressClick = function(address) {
+      self.toggleAddress(address).then(function() {
+        self.updateAddresses();
+      });
     };
   }
   remove() {
-    d3.select('#SVG_ID').selectAll('circle.flow').remove();
+    d3.select("#SVG_ID")
+      .selectAll("circle.flow")
+      .remove();
     this.lastFlows = {};
     this.stopped = true;
   }
@@ -290,88 +351,130 @@ class Dots extends TrafficAnimation {
         this.excludedAddresses.push(address);
     }
     localStorage[CHORDFILTERKEY] = JSON.stringify(this.excludedAddresses);
-    if (this.chordData)
-      this.chordData.setFilter(this.excludedAddresses);
-    return new Promise(function (resolve) {
+    if (this.chordData) this.chordData.setFilter(this.excludedAddresses);
+    return new Promise(function(resolve) {
       return resolve();
     });
   }
   toggleAddress(address) {
-    this.traffic.$scope.addresses[address] = !this.traffic.$scope.addresses[address];
-    return new Promise(function (resolve) {
+    this.traffic.$scope.addresses[address] = !this.traffic.$scope.addresses[
+      address
+    ];
+    return new Promise(function(resolve) {
       return resolve();
     });
   }
   fadeOtherAddresses(address) {
-    d3.selectAll('circle.flow').classed('fade', function (d) {
+    d3.selectAll("circle.flow").classed("fade", function(d) {
       return d.address !== address;
     });
   }
   unFadeAll() {
-    d3.selectAll('circle.flow').classed('fade', false);
+    d3.selectAll("circle.flow").classed("fade", false);
   }
   doUpdate() {
     let self = this;
     this.stopped = false;
     // we need the nextHop data to show traffic between routers that are connected by intermediaries
-    this.traffic.QDRService.management.topology.ensureAllEntities([{ entity: 'router.node', attrs: ['id', 'nextHop'] }], function () {
-      // get the ingressHistogram data for all routers
-      self.chordData.getMatrix().then(self.render.bind(self), function (e) {
-        console.log('Could not get message histogram' + e);
-      });
-    });
+    this.traffic.QDRService.management.topology.ensureAllEntities(
+      [{ entity: "router.node", attrs: ["id", "nextHop"] }],
+      function() {
+        // get the ingressHistogram data for all routers
+        self.chordData.getMatrix().then(self.render.bind(self), function(e) {
+          console.log("Could not get message histogram" + e);
+        });
+      }
+    );
   }
   render(matrix) {
     if (this.stopped === false) {
-      this.traffic.$timeout(function () {
-        this.traffic.$scope.addresses = this.chordData.getAddresses();
-      }.bind(this));
+      this.traffic.$timeout(
+        function() {
+          this.traffic.$scope.addresses = this.chordData.getAddresses();
+        }.bind(this)
+      );
       // get the rate of message flow between routers
       let hops = {}; // every hop between routers that is involved in message flow
       let matrixMessages = matrix.matrixMessages();
       // the fastest traffic rate gets 3 times as many dots as the slowest
       let minmax = matrix.getMinMax();
-      let flowScale = d3.scale.linear().domain(minmax).range([1, 1.1]);
+      let flowScale = d3.scale
+        .linear()
+        .domain(minmax)
+        .range([1, 1.1]);
       // row is ingress router, col is egress router. Value at [row][col] is the rate
-      matrixMessages.forEach(function (row, r) {
-        row.forEach(function (val, c) {
-          if (val > MIN_CHORD_THRESHOLD) {
-            // translate between matrix row/col and node index
-            let f = this.nodeIndexFor(this.traffic.topology.nodes.nodes, matrix.rows[r].egress);
-            let t = this.nodeIndexFor(this.traffic.topology.nodes.nodes, matrix.rows[r].ingress);
-            let address = matrix.getAddress(r, c);
-            if (r !== c) {
-              // accumulate the hops between the ingress and egress routers
-              nextHop(this.traffic.topology.nodes.nodes[f], 
-                this.traffic.topology.nodes.nodes[t], 
-                this.traffic.topology.nodes, 
-                this.traffic.topology.links, 
-                this.traffic.QDRService, 
-                this.traffic.topology.nodes.nodes[f],
-                function (link, fnode, tnode) {
-                  let key = '-' + link.uid;
-                  let back = fnode.index < tnode.index;
-                  if (!hops[key])
-                    hops[key] = [];
-                  hops[key].push({ val: val, back: back, address: address });
-                });
-            }
-            // Find the senders connected to nodes[f] and the receivers connected to nodes[t]
-            // and add their links to the animation
-            this.addClients(hops, this.traffic.topology.nodes.nodes, f, val, true, address);
-            this.addClients(hops, this.traffic.topology.nodes.nodes, t, val, false, address);
-          }
-        }.bind(this));
-      }.bind(this));
+      matrixMessages.forEach(
+        function(row, r) {
+          row.forEach(
+            function(val, c) {
+              if (val > MIN_CHORD_THRESHOLD) {
+                // translate between matrix row/col and node index
+                let f = this.nodeIndexFor(
+                  this.traffic.topology.nodes.nodes,
+                  matrix.rows[r].egress
+                );
+                let t = this.nodeIndexFor(
+                  this.traffic.topology.nodes.nodes,
+                  matrix.rows[r].ingress
+                );
+                let address = matrix.getAddress(r, c);
+                if (r !== c) {
+                  // accumulate the hops between the ingress and egress routers
+                  nextHop(
+                    this.traffic.topology.nodes.nodes[f],
+                    this.traffic.topology.nodes.nodes[t],
+                    this.traffic.topology.nodes,
+                    this.traffic.topology.links,
+                    this.traffic.QDRService,
+                    this.traffic.topology.nodes.nodes[f],
+                    function(link, fnode, tnode) {
+                      let key = "-" + link.uid;
+                      let back = fnode.index < tnode.index;
+                      if (!hops[key]) hops[key] = [];
+                      hops[key].push({
+                        val: val,
+                        back: back,
+                        address: address
+                      });
+                    }
+                  );
+                }
+                // Find the senders connected to nodes[f] and the receivers connected to nodes[t]
+                // and add their links to the animation
+                this.addClients(
+                  hops,
+                  this.traffic.topology.nodes.nodes,
+                  f,
+                  val,
+                  true,
+                  address
+                );
+                this.addClients(
+                  hops,
+                  this.traffic.topology.nodes.nodes,
+                  t,
+                  val,
+                  false,
+                  address
+                );
+              }
+            }.bind(this)
+          );
+        }.bind(this)
+      );
       // for each link between routers that has traffic, start an animation
       let keep = {};
       for (let id in hops) {
         let hop = hops[id];
         for (let h = 0; h < hop.length; h++) {
           let ahop = hop[h];
-          let pathId = id.replace(/\./g, '\\.').replace(/ /g, '\\ ');
-          let flowId = id.replace(/\./g, '').replace(/ /g, '') + '-' + this.addressIndex(this, ahop.address) + (ahop.back ? 'b' : '');
-          let path = d3.select('#path' + pathId);
+          let pathId = id.replace(/\./g, "\\.").replace(/ /g, "\\ ");
+          let flowId =
+            id.replace(/\./g, "").replace(/ /g, "") +
+            "-" +
+            this.addressIndex(this, ahop.address) +
+            (ahop.back ? "b" : "");
+          let path = d3.select("#path" + pathId);
           // start the animation. If the animation is already running, this will have no effect
           this.startAnimation(path, flowId, ahop, flowScale(ahop.val));
           keep[flowId] = true;
@@ -381,7 +484,9 @@ class Dots extends TrafficAnimation {
       for (let id in this.lastFlows) {
         if (this.lastFlows[id] && !keep[id]) {
           this.lastFlows[id] = 0;
-          d3.select('#SVG_ID').selectAll('circle.flow' + id).remove();
+          d3.select("#SVG_ID")
+            .selectAll("circle.flow" + id)
+            .remove();
         }
       }
     }
@@ -389,11 +494,15 @@ class Dots extends TrafficAnimation {
   // animate the d3 selection (flow) along the given path
   animateFlow(flow, path, count, back, rate) {
     let l = path.node().getTotalLength();
-    flow.transition()
-      .ease('easeLinear')
-      .duration(l * 10 / rate)
-      .attrTween('transform', this.translateDots(this.radius, path, count, back))
-      .each('end', () => {
+    flow
+      .transition()
+      .ease("easeLinear")
+      .duration((l * 10) / rate)
+      .attrTween(
+        "transform",
+        this.translateDots(this.radius, path, count, back)
+      )
+      .each("end", () => {
         if (this.stopped === false) {
           this.animateFlow(flow, path, count, back, rate);
         }
@@ -401,12 +510,12 @@ class Dots extends TrafficAnimation {
   }
   // create dots along the path between routers
   startAnimation(selection, id, hop, rate) {
-    if (selection.empty())
-      return;
+    if (selection.empty()) return;
     this.animateDots(selection, id, hop, rate);
   }
   animateDots(path, id, hop, rate) {
-    let back = hop.back, address = hop.address;
+    let back = hop.back,
+      address = hop.address;
     // the density of dots is determined by the rate of this traffic relative to the other traffic
     let len = Math.max(Math.floor(path.node().getTotalLength() / 50), 1);
     let dots = [];
@@ -415,24 +524,29 @@ class Dots extends TrafficAnimation {
     }
     // keep track of the number of dots for each link. If the length of the link is changed,
     // re-create the animation
-    if (!this.lastFlows[id])
-      this.lastFlows[id] = len;
+    if (!this.lastFlows[id]) this.lastFlows[id] = len;
     else {
       if (this.lastFlows[id] !== len) {
         this.lastFlows[id] = len;
-        d3.select('#SVG_ID').selectAll('circle.flow' + id).remove();
+        d3.select("#SVG_ID")
+          .selectAll("circle.flow" + id)
+          .remove();
       }
     }
-    let flow = d3.select('#SVG_ID').selectAll('circle.flow' + id)
-      .data(dots, function (d) { return d.i + d.address; });
+    let flow = d3
+      .select("#SVG_ID")
+      .selectAll("circle.flow" + id)
+      .data(dots, function(d) {
+        return d.i + d.address;
+      });
     let circles = flow
-      .enter().append('circle')
-      .attr('class', 'flow flow' + id)
-      .attr('fill', this.fillColor(address))
-      .attr('r', 5);
+      .enter()
+      .append("circle")
+      .attr("class", "flow flow" + id)
+      .attr("fill", this.fillColor(address))
+      .attr("r", 5);
     this.animateFlow(circles, path, dots.length, back, rate);
-    flow.exit()
-      .remove();
+    flow.exit().remove();
   }
   fillColor(n) {
     if (!(n in this.traffic.$scope.addressColors)) {
@@ -441,51 +555,53 @@ class Dots extends TrafficAnimation {
     }
     return this.traffic.$scope.addressColors[n];
   }
-  // find the link that carries traffic for this address 
+  // find the link that carries traffic for this address
   // going to nodes[f] if sender is true
   // coming from nodes[f] if sender if false.
   // Add the link's id to the hops array
   addClients(hops, nodes, f, val, sender, address) {
-    const cdir = sender ? 'out' : 'in';
-    const uuid = nodes[f].uid(this.traffic.QDRService);
+    if (!nodes[f])
+      return;
+    const cdir = sender ? "out" : "in";
+    const uuid = nodes[f].uid();
     const key = nodes[f].key;
-    const links = this.traffic.QDRService.management.topology._nodeInfo[key]['router.link'];
+    const links = this.traffic.QDRService.management.topology._nodeInfo[key][
+      "router.link"
+    ];
     if (links) {
-      const ilt = links.attributeNames.indexOf('linkType');
-      const ioa = links.attributeNames.indexOf('owningAddr');
-      const ici = links.attributeNames.indexOf('connectionId');
-      const ild = links.attributeNames.indexOf('linkDir');
-      let foundLinks = links.results.filter( function (l) {
-        return (l[ilt] === 'endpoint' || l[ilt] === 'edge-downlink') && 
+      const ilt = links.attributeNames.indexOf("linkType");
+      const ioa = links.attributeNames.indexOf("owningAddr");
+      const ici = links.attributeNames.indexOf("connectionId");
+      const ild = links.attributeNames.indexOf("linkDir");
+      let foundLinks = links.results.filter(function(l) {
+        return (
+          (l[ilt] === "endpoint" || l[ilt] === "edge-downlink") &&
           address === this.traffic.QDRService.utilities.addr_text(l[ioa]) &&
-          l[ild] === cdir;
+          l[ild] === cdir
+        );
       }, this);
       // we now have the links involved in traffic for this address that
       // ingress/egress to/from this router (f).
-      // Now find the created node that the link is to
-      for (let linkIndex=0; linkIndex<foundLinks.length; linkIndex++) {
-        let nodeIndex = nodes.findIndex( function (node) {
-          if (node.normals && node.key === key && (node.cdir === cdir || node.cdir === 'both')) {
-            return node.normals.some( function (normal) {
-              return normal.connectionId == foundLinks[linkIndex][ici];
-            });
-          } else if (node.alsoConnectsTo) {
-            return node.alsoConnectsTo.some( function (ac2) {
-              return ac2.key === key && ac2.connectionId === foundLinks[linkIndex][ici] &&
-                (ac2.dir === cdir || ac2.dir === 'both');
-            });
+      // Now find the created node that each link is associated with
+      for (let linkIndex = 0; linkIndex < foundLinks.length; linkIndex++) {
+        // use .some so the loop stops at the 1st match
+        nodes.some( function (node) {
+          if (
+            node.normals &&
+            node.normals.some(function(normal) {
+              return testNode(normal, key, cdir, foundLinks[linkIndex][ici]);
+            })
+          ) {
+            // one of the normals for this node has the traffic
+            const uuid2 = node.uid();
+            const key = ["", uuid, uuid2].join("-");
+            if (!hops[key]) 
+              hops[key] = [];
+            hops[key].push({ val: val, back: !sender, address: address });
+            return true;
           }
-          else
-            return false;
+          return false;
         });
-        if (nodeIndex >= 0) {
-          // one of the normals for this node has the traffic
-          const uuid2 = nodes[nodeIndex].uid(this.traffic.QDRService);
-          const key = ['', uuid, uuid2].join('-');
-          if (!hops[key])
-            hops[key] = [];
-          hops[key].push({ val: val, back: !sender, address: address });
-        }
       }
     }
   }
@@ -496,20 +612,36 @@ class Dots extends TrafficAnimation {
   translateDots(radius, path, count, back) {
     let pnode = path.node();
     // will be called for each element in the flow selection (for each dot)
-    return function (d) {
+    return function(d) {
       // will be called with t going from 0 to 1 for each dot
-      return function (t) {
+      return function(t) {
         // start the points at different positions depending on their value (d)
         let tt = t * 1000;
-        let f = ((tt + (d.i * 1000 / count)) % 1000) / 1000;
-        if (back)
-          f = 1 - f;
+        let f = ((tt + (d.i * 1000) / count) % 1000) / 1000;
+        if (back) f = 1 - f;
         // l needs to be calculated each tick because the path's length might be changed during the animation
         let l = pnode.getTotalLength();
         let p = pnode.getPointAtLength(f * l);
-        return 'translate(' + p.x + ',' + p.y + ')';
+        return "translate(" + p.x + "," + p.y + ")";
       };
     };
   }
 }
 
+// see if this node, or any of the nodes it also connects to
+// match the key, dir, and connectionId
+let testNode = function(node, key, dir, connectionId) {
+  // does the node match
+  if (
+    node.key === key &&
+    node.connectionId == connectionId &&
+    (node.cdir === dir || node.cdir === "both")
+  )
+    return true;
+  if (!node.alsoConnectsTo) 
+    return false;
+  // do any of the alsoConnectsTo nodes match
+  return node.alsoConnectsTo.some(function(ac2) {
+    return testNode(ac2, key, dir, connectionId);
+  });
+};

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55b7ae55/console/stand-alone/test/links.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/test/links.js b/console/stand-alone/test/links.js
index f6aff44..c3aad88 100644
--- a/console/stand-alone/test/links.js
+++ b/console/stand-alone/test/links.js
@@ -27,11 +27,11 @@ import { QDRService } from '../plugin/js/qdrService.js';
 class Log {
   constructor() {
   }
-  log (msg) {}
-  debug (msg) {}
-  error (msg) {}
-  info (msg) {}
-  warn (msg) {}
+  log (msg) {console.log(msg);}
+  debug (msg) {console.log(msg);}
+  error (msg) {console.log(msg);}
+  info (msg) {console.log(msg);}
+  warn (msg) {console.log(msg);}
 }
 var log = new Log();
 var loc = {protocol: function () { return 'http://';}};
@@ -85,7 +85,7 @@ describe('Links', function() {
   });
   describe('#initializes', function() {
     it('should initialize', function() {
-      links.initializeLinks(nodeInfo, nodes, unknowns, {}, width);
+      links.initialize(nodeInfo, nodes, unknowns, {}, width);
       assert.equal(links.links.length, 10);
     });
   });

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55b7ae55/console/stand-alone/tslint.json
----------------------------------------------------------------------
diff --git a/console/stand-alone/tslint.json b/console/stand-alone/tslint.json
index ad645d3..06ee45b 100644
--- a/console/stand-alone/tslint.json
+++ b/console/stand-alone/tslint.json
@@ -27,13 +27,7 @@ under the License.
     "max-line-length": [true, 140],
     "no-arg": true,
     "no-bitwise": true,
-    "no-console": [true,
-      "debug",
-      "info",
-      "time",
-      "timeEnd",
-      "trace"
-    ],
+    "no-console": [true, "debug", "info", "time", "timeEnd", "trace"],
     "no-construct": true,
     "no-debugger": true,
     "no-duplicate-variable": true,
@@ -43,22 +37,24 @@ under the License.
     "no-trailing-whitespace": true,
     "no-unused-variable": false,
     //"no-use-before-declare": true,
-    "one-line": [true,
+    "one-line": [
+      true,
       "check-open-brace",
       "check-catch",
       "check-else",
       "check-whitespace"
     ],
-    "quotemark": [true, "single"],
+    "quotemark": [true, "double"],
     "radix": true,
     "semicolon": true,
     "triple-equals": [true, "allow-null-check"],
     "variable-name": false,
-    "whitespace": [true,
+    "whitespace": [
+      true,
       "check-branch",
       "check-decl",
       "check-operator",
       "check-separator"
     ]
   }
-}
\ No newline at end of file
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org


[2/2] qpid-dispatch git commit: DISPATCH-1195 Periodically update popup detail on topology page

Posted by ea...@apache.org.
DISPATCH-1195 Periodically update popup detail on topology page


Project: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/repo
Commit: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/commit/55b7ae55
Tree: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/tree/55b7ae55
Diff: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/diff/55b7ae55

Branch: refs/heads/master
Commit: 55b7ae55e4f4c2f89f25a928fdd0e98049372f5e
Parents: 3adde8e
Author: Ernest Allen <ea...@redhat.com>
Authored: Mon Dec 3 10:48:50 2018 -0500
Committer: Ernest Allen <ea...@redhat.com>
Committed: Mon Dec 3 10:48:50 2018 -0500

----------------------------------------------------------------------
 console/.eslintrc.json                          |  53 +-
 console/stand-alone/main.js                     |   2 +-
 console/stand-alone/plugin/css/dispatch.css     |   3 +
 .../plugin/html/tmplClientDetail.html           | 258 +++++---
 console/stand-alone/plugin/js/amqp/topology.js  | 434 +++++++------
 console/stand-alone/plugin/js/amqp/utilities.js |   8 +-
 console/stand-alone/plugin/js/chord/data.js     |   3 +
 .../plugin/js/dlgDetailController.js            | 207 +++++--
 console/stand-alone/plugin/js/topology/links.js | 345 +++++------
 console/stand-alone/plugin/js/topology/map.js   |   4 +
 console/stand-alone/plugin/js/topology/nodes.js | 416 ++++++++-----
 .../plugin/js/topology/qdrTopology.js           |  79 ++-
 .../stand-alone/plugin/js/topology/topoUtils.js | 112 ++--
 .../stand-alone/plugin/js/topology/traffic.js   | 604 +++++++++++--------
 console/stand-alone/test/links.js               |  12 +-
 console/stand-alone/tslint.json                 |  18 +-
 16 files changed, 1470 insertions(+), 1088 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55b7ae55/console/.eslintrc.json
----------------------------------------------------------------------
diff --git a/console/.eslintrc.json b/console/.eslintrc.json
index 64d913c..f9c80bb 100644
--- a/console/.eslintrc.json
+++ b/console/.eslintrc.json
@@ -1,35 +1,22 @@
 {
-    "env": {
-        "browser": true,
-        "node": true,
-        "amd": true,
-        "jquery": true
-    },
-    "extends": "eslint:recommended",
-    "parserOptions": {
-        "ecmaVersion": 6,
-        "sourceType": "module",
-        "ecmaFeatures": {
-            "jsx": true
-        }
-    },
-    "rules": {
-        "indent": [
-            "error",
-            2
-        ],
-        "linebreak-style": [
-            "error",
-            "unix"
-        ],
-        "quotes": [
-            "error",
-            "single"
-        ],
-        "semi": [
-            "error",
-            "always"
-        ],
-        "no-console": "off"
+  "env": {
+    "browser": true,
+    "node": true,
+    "amd": true,
+    "jquery": true
+  },
+  "extends": "eslint:recommended",
+  "parserOptions": {
+    "ecmaVersion": 6,
+    "sourceType": "module",
+    "ecmaFeatures": {
+      "jsx": true
     }
-}
\ No newline at end of file
+  },
+  "rules": {
+    "indent": ["error", 2],
+    "linebreak-style": ["error", "unix"],
+    "semi": ["error", "always"],
+    "no-console": "off"
+  }
+}

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55b7ae55/console/stand-alone/main.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/main.js b/console/stand-alone/main.js
index a02b87a..4cf2d32 100644
--- a/console/stand-alone/main.js
+++ b/console/stand-alone/main.js
@@ -90,7 +90,7 @@ import { posint } from './plugin/js/posintDirective.js';
 
   QDR.module.filter('to_trusted', ['$sce', function($sce){
     return function(text) {
-      return $sce.trustAsHtml(text);
+      return $sce.trustAsHtml(text+'');
     };
   }]);
 

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55b7ae55/console/stand-alone/plugin/css/dispatch.css
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/css/dispatch.css b/console/stand-alone/plugin/css/dispatch.css
index 000b961..6b0ef2e 100644
--- a/console/stand-alone/plugin/css/dispatch.css
+++ b/console/stand-alone/plugin/css/dispatch.css
@@ -1064,6 +1064,9 @@ svg {
   circle.node.inter-router {
       fill: #EAEAEA;
   }
+  circle.node.normal {
+    fill: #FAFAFA;
+  }
   circle.node.normal.in {
       fill: #F0F000;
   }

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55b7ae55/console/stand-alone/plugin/html/tmplClientDetail.html
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/html/tmplClientDetail.html b/console/stand-alone/plugin/html/tmplClientDetail.html
index b2ef1f9..d96cda3 100644
--- a/console/stand-alone/plugin/html/tmplClientDetail.html
+++ b/console/stand-alone/plugin/html/tmplClientDetail.html
@@ -46,6 +46,64 @@
     div.details span.right {
         float: right;
     }
+
+    div.sub-table {
+        border: 1px solid #CCCCCC;
+        margin: 1em 0;
+    }
+    div.sub-table-row.body {
+        background-color: #FFFFFF;
+    }
+    div.sub-table-row {
+        border-bottom: 1px solid #CCCCCC;
+    }
+    div.sub-table-row:last-child {
+        border-bottom: 0px;
+    }
+    div.sub-table-row.header {
+        background-clip: padding-box;
+        background-color: #f5f5f5;
+        background-image: linear-gradient(to bottom,#fafafa 0,#ededed 100%);
+        background-repeat: repeat-x;
+    }
+
+    span.sub-table-col {
+        display: inline-block;
+        border-right: 1px solid #CCCCCC;
+        padding: 2px 10px 3px;
+    }
+    span.sub-table-col:last-child {
+        border-right: 0px;
+    }
+
+
+    dl.sub-table {
+        display: grid;
+        grid-template-columns: max-content auto;
+        margin-left: 0.5em;
+        background-color: white;
+        border: 1px solid #CCCCCC;
+    }
+
+    dl.sub-table dt {
+        grid-column-start: 1;
+        padding: 3px 3em 3px 10px;
+        border-bottom: 1px solid #CCCCCC;
+        border-right: 1px solid #CCCCCC;
+    }
+
+    dl.sub-table dd {
+        grid-column-start: 2;
+        padding: 3px 10px;
+        border-bottom: 1px solid #CCCCCC;
+    }
+    dl.sub-table dd:last-child, dl.sub-table dt:last-of-type {
+        border-bottom: 0px;
+    }
+
+    dl.sub-table dd.odd, dl.sub-table dt.odd {
+        background-color: #f0f0f0;
+    }
 </style>
 <!--
     This is the template for the client detail popup displayed when a group
@@ -78,47 +136,47 @@
                 <th>Links</th>
             </tr>
         </thead>
-        <tr ng-repeat-start="(key, value) in detail.infoPerId" 
+        <tr ng-repeat-start="info in detail.infoPerId" 
             ng-class="{even: $even, odd: $odd}"
-            ng-click="expandClicked(key)">
+            ng-click="expandClicked(info.container)">
             <td class="expander">
                 <span class="fa"
-                        ng-class="expanded(key) ? 'fa-angle-down' : 'fa-angle-right'"
+                        ng-class="expanded(info.container) ? 'fa-angle-down' : 'fa-angle-right'"
                 ></span>
             </td>
-            <td>{{key}}</td><!-- Id -->
-            <td class="right">{{value.encrypted}}</td>
-            <td class="right">{{value.host}}</td>
-            <td class="right">{{value.linkCount}}</td>
+            <td>{{info.container}}</td><!-- Id -->
+            <td class="right">{{info.encrypted}}</td>
+            <td class="right">{{info.host}}</td>
+            <td class="right">{{info.linkCount}}</td>
         </tr>
         <tr ng-repeat-end
-            ng-class="{hiddenRow: !expanded(key)}"
-            ng-click="expandClicked(key)"
+            ng-class="{hiddenRow: !expanded(info.container)}"
+            ng-click="expandClicked(info.container)"
         >
             <td colspan="6">
-                <table class="table table-striped table-bordered dataTable no-footer">
-                    <thead>
-                        <tr>
-                            <td ng-repeat="field in linkFields">
-                                {{field | humanify}}
-                            </td>
-                        </tr>
-                    </thead>
-                    <tbody>
-                        <tr ng-repeat="link in value.links">
-                            <td ng-repeat="field in linkFields">
-                                {{link[field] | pretty }}
-                            </td>
-                        </tr>
-                    </tbody>
-                </table>
+                <div class="sub-table">
+                    <div class="sub-table-row header">
+                        <span class="sub-table-col client" 
+                            ng-style="{ width: fieldWidth(field, info.sizes) }" 
+                            ng-repeat="field in fields.linkFields.cols">
+                            {{field | humanify}}
+                        </span>
+                    </div>
+                    <div class="sub-table-row body" ng-repeat="link in info.links">
+                        <span class="sub-table-col client" 
+                            ng-style="{ width: fieldWidth(field, info.sizes) }" 
+                            ng-repeat="field in fields.linkFields.cols">
+                            {{link[field] | pretty}}
+                        </span>
+                    </div>
+                </div>
             </td>
         </tr>
     </table>
 </script>
 
 <script type="text/ng-template" id="consoles.html">
-    here be console info
+
 </script>
 
 <script type="text/ng-template" id="edgeRouters.html">
@@ -134,84 +192,94 @@
                 <th>Accepted</td>
             </tr>
         </thead>
-        <tr ng-repeat-start="(key, value) in detail.infoPerId" 
+        <tr ng-repeat-start="info in detail.infoPerId" 
             ng-class="{even: $even, odd: $odd}"
-            ng-click="expandClicked(key)">
+            ng-click="expandClicked(info.name)">
             <td class="expander">
                 <span class="fa"
-                        ng-class="expanded(key) ? 'fa-angle-down' : 'fa-angle-right'"
+                        ng-class="expanded(info.name) ? 'fa-angle-down' : 'fa-angle-right'"
                 ></span>
             </td>
-            <td>{{key}}</td><!-- Id -->
-            <td class="right">{{value.linkRouteCount}}</td>
-            <td class="right">{{value.autoLinkCount}}</td>
-            <td class="right">{{value.connectionCount}}</td>
-            <td class="right">{{value.addrCount}}</td>
-            <td class="right">{{value.acceptedDeliveries | pretty}}</td>
+            <td>{{info.name}}</td><!-- Id -->
+            <td class="right">{{info.linkRouteCount}}</td>
+            <td class="right">{{info.autoLinkCount}}</td>
+            <td class="right">{{info.connectionCount}}</td>
+            <td class="right">{{info.addrCount}}</td>
+            <td class="right">{{info.acceptedDeliveries | pretty}}</td>
         </tr>
         <tr ng-repeat-end
-            ng-class="{hiddenRow: !expanded(key)}"
-            ng-click="expandClicked(key)"
+            ng-class="{hiddenRow: !expanded(info.name)}"
+            ng-click="expandClicked(info.name)"
         >
             <td colspan="7">
-                    <h4>Edge router details</h4>
-                    <table class="table table-striped table-bordered dataTable no-footer">
-                        <tr ng-repeat="field in detailFields">
-                            <td>{{field | humanify}}</td>
-                            <td>{{value[field] | pretty}}</td>
-                        </tr>
-                    </table>
-                    <h4>Link routes</h4>
-                    <table class="table table-striped table-bordered dataTable no-footer">
-                        <thead>
-                            <tr>
-                                <td ng-repeat="field in linkRouteFields">
-                                    {{field | humanify}}
-                                </td>
-                            </tr>
-                        </thead>
-                        <tbody>
-                            <tr ng-repeat="link in value.linkRoutes">
-                                <td ng-repeat="field in linkRouteFields">
-                                    {{link[field] | pretty }}
-                                </td>
-                            </tr>
-                        </tbody>
-                    </table>
-                    <h4>Autolinks</h4>
-                    <table class="table table-striped table-bordered dataTable no-footer">
-                        <thead>
-                            <tr>
-                                <td ng-repeat="field in autoLinkFields">
-                                    {{field | humanify}}
-                                </td>
-                            </tr>
-                        </thead>
-                        <tbody>
-                            <tr ng-repeat="link in value.autoLinks">
-                                <td ng-repeat="field in autoLinkFields">
-                                    {{link[field] | pretty }}
-                                </td>
-                            </tr>
-                        </tbody>
-                    </table>
-                    <h4>Addresses</h4>
-                    <table class="table table-striped table-bordered dataTable no-footer">
-                        <thead>
-                            <tr>
-                                <td ng-repeat="field in addressFields">
-                                    {{field | humanify}}
-                                </td>
-                            </tr>
-                        </thead>
-                        <tbody>
-                            <tr ng-repeat="link in value.addresses">
-                                <td ng-repeat="field in addressFields">
-                                    {{link[field] | pretty }}
-                                </td>
-                            </tr>
-                        </tbody>
-                    </table>
+                <h4>Details for edge router {{info.name}}</h4>
+                <dl class="sub-table">
+                    <dt ng-repeat-start="field in fields.detailFields.cols">
+                        {{field | humanify}}
+                    </dt>
+                    <dd ng-repeat-end>
+                        {{info[field] | pretty}}
+                    </dd>
+                </dl>
+    
+                <h4>Link routes</h4>
+                <div class="sub-table">
+                    <div class="sub-table-row header">
+                        <span 
+                            class="sub-table-col client" 
+                            ng-style="{ width: fieldWidth(field, info.linkRouteSizes) }" 
+                            ng-repeat="field in fields.linkRouteFields.cols">
+                            {{field | humanify}}
+                        </span>
+                    </div>
+                    <div class="sub-table-row body" ng-repeat="link in info.linkRoutes">
+                        <span 
+                            class="sub-table-col client" 
+                            ng-style="{ width: fieldWidth(field, info.linkRouteSizes) }" 
+                            ng-repeat="field in fields.linkRouteFields.cols">
+                            {{link[field] | pretty}}
+                        </span>
+                    </div>
+                </div>
+    
+                <h4>Autolinks</h4>
+                <div class="sub-table">
+                    <div class="sub-table-row header">
+                        <span 
+                            class="sub-table-col client" 
+                            ng-style="{ width: fieldWidth(field, info.autoLinkSizes) }" 
+                            ng-repeat="field in fields.autoLinkFields.cols">
+                            {{field | humanify}}
+                        </span>
+                    </div>
+                    <div class="sub-table-row body" ng-repeat="link in info.autoLinks">
+                        <span 
+                            class="sub-table-col client" 
+                            ng-style="{ width: fieldWidth(field, info.autoLinkSizes) }" 
+                            ng-repeat="field in fields.autoLinkFields.cols">
+                            {{link[field] | pretty}}
+                        </span>
+                    </div>
+                </div>
+                <h4>Addresses</h4>
+                <div class="sub-table">
+                    <div class="sub-table-row header">
+                        <span 
+                            class="sub-table-col client" 
+                            ng-style="{ width: fieldWidth(field, info.addressSizes) }" 
+                            ng-repeat="field in fields.addressFields.cols">
+                            {{field | humanify}}
+                        </span>
+                    </div>
+                    <div class="sub-table-row body" ng-repeat="link in info.addresses">
+                        <span 
+                            class="sub-table-col client" 
+                            ng-style="{ width: fieldWidth(field, info.addressSizes) }" 
+                            ng-repeat="field in fields.addressFields.cols">
+                            {{link[field] | pretty}}
+                        </span>
+                    </div>
+                </div>
             </td>
         </tr>
     </table>

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55b7ae55/console/stand-alone/plugin/js/amqp/topology.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/amqp/topology.js b/console/stand-alone/plugin/js/amqp/topology.js
index 8c88e50..02f1fe9 100644
--- a/console/stand-alone/plugin/js/amqp/topology.js
+++ b/console/stand-alone/plugin/js/amqp/topology.js
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-/* global Promise d3 */
+/* global Promise d3 Set */
 
-import { utils } from './utilities.js';
+import { utils } from "./utilities.js";
 
 class Topology {
   constructor(connectionManager) {
@@ -32,13 +32,12 @@ class Topology {
     this.updating = false;
   }
   addUpdatedAction(key, action) {
-    if (typeof action === 'function') {
+    if (typeof action === "function") {
       this.updatedActions[key] = action;
     }
   }
   delUpdatedAction(key) {
-    if (key in this.updatedActions)
-      delete this.updatedActions[key];
+    if (key in this.updatedActions) delete this.updatedActions[key];
   }
   executeUpdatedActions(error) {
     for (var action in this.updatedActions) {
@@ -52,7 +51,7 @@ class Topology {
     }
   }
   addUpdateEntities(entityAttribs) {
-    if (Object.prototype.toString.call(entityAttribs) !== '[object Array]') {
+    if (Object.prototype.toString.call(entityAttribs) !== "[object Array]") {
       entityAttribs = [entityAttribs];
     }
     for (var i = 0; i < entityAttribs.length; i++) {
@@ -61,12 +60,10 @@ class Topology {
     }
   }
   on(eventName, fn, key) {
-    if (eventName === 'updated')
-      this.addUpdatedAction(key, fn);
+    if (eventName === "updated") this.addUpdatedAction(key, fn);
   }
   unregister(eventName, key) {
-    if (eventName === 'updated')
-      this.delUpdatedAction(key);
+    if (eventName === "updated") this.delUpdatedAction(key);
   }
   nodeInfo() {
     return this._nodeInfo;
@@ -76,14 +73,17 @@ class Topology {
     for (let rId in this._nodeInfo) {
       if (!workSet.has(rId)) {
         // mark any routers that went away since the last request as removed
-        this._nodeInfo[rId]['removed'] = true;
+        this._nodeInfo[rId]["removed"] = true;
       } else {
-        if (this._nodeInfo[rId]['removed'])
-          delete this._nodeInfo[rId]['removed'];
+        if (this._nodeInfo[rId]["removed"])
+          delete this._nodeInfo[rId]["removed"];
         // copy entities
         for (let entity in workInfo[rId]) {
-          if (!this._nodeInfo[rId][entity] ||
-            (workInfo[rId][entity]['timestamp']+'' > this._nodeInfo[rId][entity]['timestamp']+'')) {
+          if (
+            !this._nodeInfo[rId][entity] ||
+            workInfo[rId][entity]["timestamp"] + "" >
+              this._nodeInfo[rId][entity]["timestamp"] + ""
+          ) {
             this._nodeInfo[rId][entity] = utils.copy(workInfo[rId][entity]);
           }
         }
@@ -97,111 +97,148 @@ class Topology {
       }
     }
   }
+  // remove any nodes that don't have connection info
+  purge() {
+    for (let id in this._nodeInfo) {
+      let node = this._nodeInfo[id];
+      if (node.removed) {
+        delete this._nodeInfo[id];
+      }
+    }
+  }
   get() {
-    return new Promise((function (resolve, reject) {
-      this.connection.sendMgmtQuery('GET-MGMT-NODES')
-        .then((function (results) {
-          let routerIds = results.response;
-          if (Object.prototype.toString.call(routerIds) === '[object Array]') {
-            // if there is only one node, it will not be returned
-            if (routerIds.length === 0) {
-              var parts = this.connection.getReceiverAddress().split('/');
-              parts[parts.length - 1] = '$management';
-              routerIds.push(parts.join('/'));
-            }
-            let finish = function (workInfo) {
-              this.saveResults(workInfo);
-              this.onDone(this._nodeInfo);
-              resolve(this._nodeInfo);
-            };
-            let connectedToEdge = function (response, workInfo) {
-              let routerId = null;
-              if (response.length === 1) {
-                let parts = response[0].split('/');
-                // we are connected to an edge router
-                if (parts[1] === '_edge') {
-                  // find the role:edge connection
-                  let conn = workInfo[response[0]].connection;
-                  if (conn) {
-                    let roleIndex = conn.attributeNames.indexOf('role');
-                    for (let i=0; i<conn.results.length; i++) {
-                      if (conn.results[i][roleIndex] === 'edge') {
-                        let container = utils.valFor(conn.attributeNames, conn.results[i], 'container');
-                        return utils.idFromName(container, '_topo');
-                      }
-                    }
-                  }
-                }
+    return new Promise(
+      function(resolve, reject) {
+        this.connection.sendMgmtQuery("GET-MGMT-NODES").then(
+          function(results) {
+            let routerIds = results.response;
+            if (
+              Object.prototype.toString.call(routerIds) === "[object Array]"
+            ) {
+              // if there is only one node, it will not be returned
+              if (routerIds.length === 0) {
+                var parts = this.connection.getReceiverAddress().split("/");
+                parts[parts.length - 1] = "$management";
+                routerIds.push(parts.join("/"));
               }
-              return routerId;
-            };
-            this.doget(routerIds)
-              .then( function (workInfo) {
-                // test for edge case
-                let routerId = connectedToEdge(routerIds, workInfo);
-                if (routerId) {
-                  this.connection.sendMgmtQuery('GET-MGMT-NODES', routerId)
-                    .then((function (results) {
-                      let response = results.response;
-                      if (Object.prototype.toString.call(response) === '[object Array]') {
-                        // special case of edge case:
-                        // we are connected to an edge router that is connected to
-                        // a router that is not connected to any other interior routers
-                        if (response.length === 0) {
-                          response = [routerId];
+              let finish = function(workInfo) {
+                this.saveResults(workInfo);
+                this.onDone(this._nodeInfo);
+                resolve(this._nodeInfo);
+              };
+              let connectedToEdge = function(response, workInfo) {
+                let routerId = null;
+                if (response.length === 1) {
+                  let parts = response[0].split("/");
+                  // we are connected to an edge router
+                  if (parts[1] === "_edge") {
+                    // find the role:edge connection
+                    let conn = workInfo[response[0]].connection;
+                    if (conn) {
+                      let roleIndex = conn.attributeNames.indexOf("role");
+                      for (let i = 0; i < conn.results.length; i++) {
+                        if (conn.results[i][roleIndex] === "edge") {
+                          let container = utils.valFor(
+                            conn.attributeNames,
+                            conn.results[i],
+                            "container"
+                          );
+                          return utils.idFromName(container, "_topo");
                         }
-                        this.doget(response)
-                          .then( function (workInfo) {
-                            finish.call(this, workInfo);
-                          }.bind(this));
-
                       }
-                    }).bind(this));
-                } else {
-                  finish.call(this, workInfo);
+                    }
+                  }
                 }
-              }.bind(this));
+                return routerId;
+              };
+              this.doget(routerIds).then(
+                function(workInfo) {
+                  // test for edge case
+                  let routerId = connectedToEdge(routerIds, workInfo);
+                  if (routerId) {
+                    this.connection
+                      .sendMgmtQuery("GET-MGMT-NODES", routerId)
+                      .then(
+                        function(results) {
+                          let response = results.response;
+                          if (
+                            Object.prototype.toString.call(response) ===
+                            "[object Array]"
+                          ) {
+                            // special case of edge case:
+                            // we are connected to an edge router that is connected to
+                            // a router that is not connected to any other interior routers
+                            if (response.length === 0) {
+                              response = [routerId];
+                            }
+                            this.doget(response).then(
+                              function(workInfo) {
+                                finish.call(this, workInfo);
+                              }.bind(this)
+                            );
+                          }
+                        }.bind(this)
+                      );
+                  } else {
+                    finish.call(this, workInfo);
+                  }
+                }.bind(this)
+              );
+            }
+          }.bind(this),
+          function(error) {
+            reject(error);
           }
-        }).bind(this), function (error) {
-          reject(error);
-        });
-    }).bind(this));
+        );
+      }.bind(this)
+    );
   }
   doget(ids) {
-    return new Promise((function (resolve) {
-      let workInfo = {};
-      for (var i = 0; i < ids.length; ++i) {
-        workInfo[ids[i]] = {};
-      }
-      var gotResponse = function (nodeName, entity, response) {
-        workInfo[nodeName][entity] = response;
-        workInfo[nodeName][entity]['timestamp'] = new Date();
-      };
-      var q = d3.queue(this.connection.availableQeueuDepth());
-      for (var id in workInfo) {
-        for (var entity in this.entityAttribs) {
-          q.defer((this.q_fetchNodeInfo).bind(this), id, entity, this.entityAttribs[entity], q, gotResponse);
+    return new Promise(
+      function(resolve) {
+        let workInfo = {};
+        for (var i = 0; i < ids.length; ++i) {
+          workInfo[ids[i]] = {};
         }
-      }
-      q.await((function () {
-        // filter out nodes that have no connection info
-        if (this.filtering) {
-          for (var id in workInfo) {
-            if (!(workInfo[id].connection)) {
-              this.flux = true;
-              delete workInfo[id];
-            }
+        var gotResponse = function(nodeName, entity, response) {
+          workInfo[nodeName][entity] = response;
+          workInfo[nodeName][entity]["timestamp"] = new Date();
+        };
+        var q = d3.queue(this.connection.availableQeueuDepth());
+        for (var id in workInfo) {
+          for (var entity in this.entityAttribs) {
+            q.defer(
+              this.q_fetchNodeInfo.bind(this),
+              id,
+              entity,
+              this.entityAttribs[entity],
+              q,
+              gotResponse
+            );
           }
         }
-        resolve(workInfo);
-      }).bind(this));
-    }).bind(this));
+        q.await(
+          function() {
+            // filter out nodes that have no connection info
+            if (this.filtering) {
+              for (var id in workInfo) {
+                if (!workInfo[id].connection) {
+                  this.flux = true;
+                  delete workInfo[id];
+                }
+              }
+            }
+            resolve(workInfo);
+          }.bind(this)
+        );
+      }.bind(this)
+    );
   }
 
   onDone(result) {
     clearTimeout(this._getTimer);
     if (this.updating)
-      this._getTimer = setTimeout((this.get).bind(this), this.updateInterval);
+      this._getTimer = setTimeout(this.get.bind(this), this.updateInterval);
     this.executeUpdatedActions(result);
   }
   startUpdating(filter) {
@@ -219,18 +256,29 @@ class Topology {
   }
   fetchEntity(node, entity, attrs, callback) {
     var results = {};
-    var gotResponse = function (nodeName, dotentity, response) {
+    var gotResponse = function(nodeName, dotentity, response) {
       results = response;
     };
     var q = d3.queue(this.connection.availableQeueuDepth());
-    q.defer((this.q_fetchNodeInfo).bind(this), node, entity, attrs, q, gotResponse);
-    q.await(function () {
+    q.defer(
+      this.q_fetchNodeInfo.bind(this),
+      node,
+      entity,
+      attrs,
+      q,
+      gotResponse
+    );
+    q.await(function() {
       callback(node, entity, results);
     });
   }
   // called from d3.queue.defer so the last argument (callback) is supplied by d3
   q_fetchNodeInfo(nodeId, entity, attrs, q, heartbeat, callback) {
-    this.getNodeInfo(nodeId, entity, attrs, q, function (nodeName, dotentity, response) {
+    this.getNodeInfo(nodeId, entity, attrs, q, function(
+      nodeName,
+      dotentity,
+      response
+    ) {
       heartbeat(nodeName, dotentity, response);
       callback(null);
     });
@@ -240,23 +288,29 @@ class Topology {
     var q = d3.queue(this.connection.availableQeueuDepth());
     var results = {};
     if (!resultCallback) {
-      resultCallback = function (nodeName, dotentity, response) {
-        if (!results[nodeName])
-          results[nodeName] = {};
+      resultCallback = function(nodeName, dotentity, response) {
+        if (!results[nodeName]) results[nodeName] = {};
         results[nodeName][dotentity] = response;
       };
     }
-    var gotAResponse = function (nodeName, dotentity, response) {
+    var gotAResponse = function(nodeName, dotentity, response) {
       resultCallback(nodeName, dotentity, response);
     };
-    if (Object.prototype.toString.call(entityAttribs) !== '[object Array]') {
+    if (Object.prototype.toString.call(entityAttribs) !== "[object Array]") {
       entityAttribs = [entityAttribs];
     }
     for (var i = 0; i < entityAttribs.length; ++i) {
       var ea = entityAttribs[i];
-      q.defer((this.q_fetchNodeInfo).bind(this), node, ea.entity, ea.attrs || [], q, gotAResponse);
+      q.defer(
+        this.q_fetchNodeInfo.bind(this),
+        node,
+        ea.entity,
+        ea.attrs || [],
+        q,
+        gotAResponse
+      );
     }
-    q.await(function () {
+    q.await(function() {
       doneCallback(results);
     });
   }
@@ -265,44 +319,56 @@ class Topology {
     var q = d3.queue(this.connection.availableQeueuDepth());
     var results = {};
     if (!resultCallback) {
-      resultCallback = function (nodeName, dotentity, response) {
-        if (!results[nodeName])
-          results[nodeName] = {};
+      resultCallback = function(nodeName, dotentity, response) {
+        if (!results[nodeName]) results[nodeName] = {};
         results[nodeName][dotentity] = response;
       };
     }
-    var gotAResponse = function (nodeName, dotentity, response) {
+    var gotAResponse = function(nodeName, dotentity, response) {
       resultCallback(nodeName, dotentity, response);
     };
-    if (Object.prototype.toString.call(entityAttribs) !== '[object Array]') {
+    if (Object.prototype.toString.call(entityAttribs) !== "[object Array]") {
       entityAttribs = [entityAttribs];
     }
     var nodes = Object.keys(this._nodeInfo);
     for (var n = 0; n < nodes.length; ++n) {
       for (var i = 0; i < entityAttribs.length; ++i) {
         var ea = entityAttribs[i];
-        q.defer((this.q_fetchNodeInfo).bind(this), nodes[n], ea.entity, ea.attrs || [], q, gotAResponse);
+        q.defer(
+          this.q_fetchNodeInfo.bind(this),
+          nodes[n],
+          ea.entity,
+          ea.attrs || [],
+          q,
+          gotAResponse
+        );
       }
     }
-    q.await(function () {
+    q.await(function() {
       doneCallback(results);
     });
   }
   // enusre all the topology nones have all these entities
   ensureAllEntities(entityAttribs, callback, extra) {
-    this.ensureEntities(Object.keys(this._nodeInfo), entityAttribs, callback, extra);
+    this.ensureEntities(
+      Object.keys(this._nodeInfo),
+      entityAttribs,
+      callback,
+      extra
+    );
   }
   // ensure these nodes have all these entities. don't fetch unless forced to
   ensureEntities(nodes, entityAttribs, callback, extra) {
-    if (Object.prototype.toString.call(nodes) !== '[object Array]') {
+    if (Object.prototype.toString.call(nodes) !== "[object Array]") {
       nodes = [nodes];
     }
     this.addUpdateEntities(entityAttribs);
-    this.doget(nodes)
-      .then( function (results) {
+    this.doget(nodes).then(
+      function(results) {
         this.saveResults(results);
         callback(extra, results);
-      }.bind(this));
+      }.bind(this)
+    );
   }
   addNodeInfo(id, entity, values) {
     // save the results in the nodeInfo object
@@ -312,7 +378,7 @@ class Topology {
       }
       // copy the values to allow garbage collection
       this._nodeInfo[id][entity] = values;
-      this._nodeInfo[id][entity]['timestamp'] = new Date();
+      this._nodeInfo[id][entity]["timestamp"] = new Date();
     }
   }
   isLargeNetwork() {
@@ -321,10 +387,9 @@ class Topology {
   getConnForLink(link) {
     // find the connection for this link
     var conns = this._nodeInfo[link.nodeId].connection;
-    if (!conns)
-      return {};
-    var connIndex = conns.attributeNames.indexOf('identity');
-    var linkCons = conns.results.filter(function (conn) {
+    if (!conns) return {};
+    var connIndex = conns.attributeNames.indexOf("identity");
+    var linkCons = conns.results.filter(function(conn) {
       return conn[connIndex] === link.connectionId;
     });
     return utils.flatten(conns.attributeNames, linkCons[0]);
@@ -356,44 +421,71 @@ class Topology {
   }
   // d3.queue'd function to make a management query for entities/attributes
   q_ensureNodeInfo(nodeId, entity, attrs, q, callback) {
-    this.getNodeInfo(nodeId, entity, attrs, q, (function (nodeName, dotentity, response) {
-      this.addNodeInfo(nodeName, dotentity, response);
-      callback(null);
-    }).bind(this));
+    this.getNodeInfo(
+      nodeId,
+      entity,
+      attrs,
+      q,
+      function(nodeName, dotentity, response) {
+        this.addNodeInfo(nodeName, dotentity, response);
+        callback(null);
+      }.bind(this)
+    );
     return {
-      abort: function () {
+      abort: function() {
         delete this._nodeInfo[nodeId];
       }
     };
   }
   getNodeInfo(nodeName, entity, attrs, q, callback) {
-    var timedOut = function (q) {
+    var timedOut = function(q) {
       q.abort();
     };
     var atimer = setTimeout(timedOut, this.timeout, q);
-    this.connection.sendQuery(nodeName, entity, attrs)
-      .then(function (response) {
+    this.connection.sendQuery(nodeName, entity, attrs).then(
+      function(response) {
         clearTimeout(atimer);
         callback(nodeName, entity, response.response);
-      }, function () {
+      },
+      function() {
         q.abort();
-      });
-  }
-  getMultipleNodeInfo(nodeNames, entity, attrs, callback, selectedNodeId, aggregate) {
+      }
+    );
+  }
+  getMultipleNodeInfo(
+    nodeNames,
+    entity,
+    attrs,
+    callback,
+    selectedNodeId,
+    aggregate
+  ) {
     var self = this;
-    if (typeof aggregate === 'undefined')
-      aggregate = true;
+    if (typeof aggregate === "undefined") aggregate = true;
     var responses = {};
-    var gotNodesResult = function (nodeName, dotentity, response) {
+    var gotNodesResult = function(nodeName, dotentity, response) {
       responses[nodeName] = response;
     };
     var q = d3.queue(this.connection.availableQeueuDepth());
-    nodeNames.forEach(function (id) {
-      q.defer((self.q_fetchNodeInfo).bind(self), id, entity, attrs, q, gotNodesResult);
+    nodeNames.forEach(function(id) {
+      q.defer(
+        self.q_fetchNodeInfo.bind(self),
+        id,
+        entity,
+        attrs,
+        q,
+        gotNodesResult
+      );
     });
-    q.await(function () {
+    q.await(function() {
       if (aggregate)
-        self.aggregateNodeInfo(nodeNames, entity, selectedNodeId, responses, callback);
+        self.aggregateNodeInfo(
+          nodeNames,
+          entity,
+          selectedNodeId,
+          responses,
+          callback
+        );
       else {
         callback(nodeNames, entity, responses);
       }
@@ -401,10 +493,15 @@ class Topology {
   }
   quiesceLink(nodeId, name) {
     var attributes = {
-      adminStatus: 'disabled',
+      adminStatus: "disabled",
       name: name
     };
-    return this.connection.sendMethod(nodeId, 'router.link', attributes, 'UPDATE');
+    return this.connection.sendMethod(
+      nodeId,
+      "router.link",
+      attributes,
+      "UPDATE"
+    );
   }
   aggregateNodeInfo(nodeNames, entity, selectedNodeId, responses, callback) {
     // aggregate the responses
@@ -420,7 +517,7 @@ class Topology {
       var result = thisNode.results[i];
       var vals = [];
       // there is a val for each attribute in this entity
-      result.forEach(function (val) {
+      result.forEach(function(val) {
         vals.push({
           sum: val,
           detail: []
@@ -428,25 +525,24 @@ class Topology {
       });
       newResponse.aggregates.push(vals);
     }
-    var nameIndex = thisNode.attributeNames.indexOf('name');
+    var nameIndex = thisNode.attributeNames.indexOf("name");
     var ent = self.connection.schema.entityTypes[entity];
     var ids = Object.keys(responses);
     ids.sort();
-    ids.forEach(function (id) {
+    ids.forEach(function(id) {
       var response = responses[id];
       var results = response.results;
-      results.forEach(function (result) {
+      results.forEach(function(result) {
         // find the matching result in the aggregates
-        var found = newResponse.aggregates.some(function (aggregate) {
+        var found = newResponse.aggregates.some(function(aggregate) {
           if (aggregate[nameIndex].sum === result[nameIndex]) {
             // result and aggregate are now the same record, add the graphable values
-            newResponse.attributeNames.forEach(function (key, i) {
+            newResponse.attributeNames.forEach(function(key, i) {
               if (ent.attributes[key] && ent.attributes[key].graph) {
-                if (id != selectedNodeId)
-                  aggregate[i].sum += result[i];
+                if (id != selectedNodeId) aggregate[i].sum += result[i];
               }
               aggregate[i].detail.push({
-                node: utils.nameFromId(id) + ':',
+                node: utils.nameFromId(id) + ":",
                 val: result[i]
               });
             });
@@ -458,13 +554,15 @@ class Topology {
           // this attribute was not found in the aggregates yet
           // because it was not in the selectedNodeId's results
           var vals = [];
-          result.forEach(function (val) {
+          result.forEach(function(val) {
             vals.push({
               sum: val,
-              detail: [{
-                node: utils.nameFromId(id),
-                val: val
-              }]
+              detail: [
+                {
+                  node: utils.nameFromId(id),
+                  val: val
+                }
+              ]
             });
           });
           newResponse.aggregates.push(vals);
@@ -475,4 +573,4 @@ class Topology {
   }
 }
 
-export default Topology;
\ No newline at end of file
+export default Topology;

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55b7ae55/console/stand-alone/plugin/js/amqp/utilities.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/amqp/utilities.js b/console/stand-alone/plugin/js/amqp/utilities.js
index c092bf4..517b4e1 100644
--- a/console/stand-alone/plugin/js/amqp/utilities.js
+++ b/console/stand-alone/plugin/js/amqp/utilities.js
@@ -46,10 +46,14 @@ var utils = {
     });
     return flat;
   },
-  flattenAll: function (entity) {
+  flattenAll: function (entity, filter) {
+    if (!filter)
+      filter = function (e) {return e;};
     let results = [];
     for (let i=0; i<entity.results.length; i++) {
-      results.push(this.flatten(entity.attributeNames, entity.results[i]));
+      let f = filter(this.flatten(entity.attributeNames, entity.results[i]));
+      if (f)
+        results.push(f);
     }
     return results;
   },

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55b7ae55/console/stand-alone/plugin/js/chord/data.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/chord/data.js b/console/stand-alone/plugin/js/chord/data.js
index 64a58fa..c1cd978 100644
--- a/console/stand-alone/plugin/js/chord/data.js
+++ b/console/stand-alone/plugin/js/chord/data.js
@@ -99,6 +99,9 @@ class ChordData { // eslint-disable-line no-unused-vars
           // each routers has a different order for the routers
           let ingressRouters = [];
           let routerNode = results[nodeId]['router.node'];
+          if (!routerNode) {
+            continue;
+          }
           let idIndex = routerNode.attributeNames.indexOf('id');
           // ingressRouters is an array of router names in the same order that the ingressHistogram values will be in
           for (let i = 0; i < routerNode.results.length; i++) {

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55b7ae55/console/stand-alone/plugin/js/dlgDetailController.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/dlgDetailController.js b/console/stand-alone/plugin/js/dlgDetailController.js
index 8efff28..0742f2a 100644
--- a/console/stand-alone/plugin/js/dlgDetailController.js
+++ b/console/stand-alone/plugin/js/dlgDetailController.js
@@ -27,44 +27,68 @@ export class DetailDialogController {
     $scope.detail = {
       template: 'loading.html',
     };
-    $scope.detailFields = [
-      'version',
-      'mode',
-      'presettledDeliveries',
-      'droppedPresettledDeliveries',
-      'acceptedDeliveries',
-      'rejectedDeliveries',
-      'releasedDeliveries',
-      'modifiedDeliveries',
-      'deliveriesIngress',
-      'deliveriesEgress',
-      'deliveriesTransit',
-      'deliveriesIngressRouteContainer',
-      'deliveriesEgressRouteContainer'
-    ];
-    $scope.linkFields = [
-      'linkType',
-      'owningAddr',
-      'priority',
-      'acceptedCount',
-      'unsettledCount'
-    ];
-    $scope.linkRouteFields = [
-      'prefix',
-      'direction',
-      'containerId'
-    ];
-    $scope.autoLinkFields = [
-      'addr',
-      'direction',
-      'containerId'
-    ];
-    $scope.addressFields = [
-      'prefix',
-      'distribution'
-    ];
+    let countChars = function (ar) {
+      let count = 0;
+      ar.forEach( function (a) {
+        count += a.length;
+      });
+      return count;
+    };
+
+    $scope.fields = {
+      detailFields: {
+        cols: [
+          'version',
+          'mode',
+          'presettledDeliveries',
+          'droppedPresettledDeliveries',
+          'acceptedDeliveries',
+          'rejectedDeliveries',
+          'releasedDeliveries',
+          'modifiedDeliveries',
+          'deliveriesIngress',
+          'deliveriesEgress',
+          'deliveriesTransit',
+          'deliveriesIngressRouteContainer',
+          'deliveriesEgressRouteContainer'
+        ]
+      },
+      linkFields: {
+        cols: [
+          'linkType',
+          'owningAddr',
+          'priority',
+          'acceptedCount',
+          'unsettledCount'
+        ]
+      },
+      linkRouteFields: {
+        cols: [
+          'prefix',
+          'direction',
+          'containerId'
+        ]
+      },
+      autoLinkFields: {
+        cols: [
+          'addr',
+          'direction',
+          'containerId'
+        ]
+      },
+      addressFields: {
+        cols: [
+          'prefix',
+          'distribution'
+        ]
+      }
+    };
+    for (let f in $scope.fields) {
+      $scope.fields[f].count = countChars($scope.fields[f].cols);
+    }
 
     $scope.okClick = function () {
+      clearInterval(updateTimer);
       $uibModalInstance.close(true);
     };
     $scope.expandClicked = function (id) {
@@ -78,6 +102,30 @@ export class DetailDialogController {
     $scope.expanded = function (id) {
       return expandedRows.has(id);
     };
+    $scope.cellWidth = function (key, val) {
+      if (key === 'autoLinkFields') {
+        return val === 'addr' ? '40%' : '20%';
+      }
+      let totalChars = $scope.fields[key].count;
+      return `${Math.round(val.length * 100 / totalChars)}%`;
+    };
+    $scope.fieldWidth = function (val, sizes) {
+      if (!sizes)
+        return '10%';
+      return `${Math.round(sizes[val] * 100 / sizes.total)}%`;
+    };
+    let updateSizes = function (fields, sizes, obj) {
+      fields.forEach( function (key) {
+        if (!sizes[key])
+          sizes[key] = QDRService.utilities.humanify(key).length;
+        sizes[key] = Math.max(sizes[key], QDRService.utilities.pretty(obj[key]).length);
+      });
+      sizes.total = 0;
+      for (let key in sizes) {
+        if (key !== 'total')
+          sizes.total += sizes[key];
+      }
+    };
 
     let groupDetail = function () {
       let q_getEdgeInfo = function (n, infoPerId, callback) {
@@ -101,15 +149,30 @@ export class DetailDialogController {
             let nodeId = QDRService.utilities.idFromName(id, '_edge');
             QDRService.management.topology.fetchEntities(nodeId, 
               [{entity: 'router.link', attrs: []},
-                {entity: 'linkRoute', attrs: $scope.linkRouteFields},
-                {entity: 'autoLink', attrs: $scope.autoLinkFields},
+                {entity: 'linkRoute', attrs: $scope.fields.linkRouteFields.cols},
+                {entity: 'autoLink', attrs: $scope.fields.autoLinkFields.cols},
                 {entity: 'address', attrs: []},
               ],
               function (results) {
                 $timeout( function () {
-                  infoPerId[id].linkRoutes = QDRService.utilities.flattenAll(results[nodeId].linkRoute);
-                  infoPerId[id].autoLinks = QDRService.utilities.flattenAll(results[nodeId].autoLink);
-                  infoPerId[id].addresses = QDRService.utilities.flattenAll(results[nodeId].address);
+                  infoPerId[id].linkRouteSizes = {};
+                  infoPerId[id].linkRoutes = QDRService.utilities.flattenAll(results[nodeId].linkRoute,
+                    function (route) {
+                      updateSizes($scope.fields.linkRouteFields.cols, infoPerId[id].linkRouteSizes, route);
+                      return route;
+                    });
+                  infoPerId[id].autoLinkSizes = {};
+                  infoPerId[id].autoLinks = QDRService.utilities.flattenAll(results[nodeId].autoLink, 
+                    function (link) {
+                      updateSizes($scope.fields.autoLinkFields.cols, infoPerId[id].autoLinkSizes, link);
+                      return link;
+                    });
+                  infoPerId[id].addressSizes = {};
+                  infoPerId[id].addresses = QDRService.utilities.flattenAll(results[nodeId].address, 
+                    function (addr) {
+                      updateSizes($scope.fields.addressFields.cols, infoPerId[id].addressSizes, addr);
+                      return addr;
+                    });
                 });
               });
           };
@@ -117,43 +180,44 @@ export class DetailDialogController {
           let q = d3.queue(10);
           for (let n=0; n<d.normals.length; n++) {
             q.defer(q_getEdgeInfo, d.normals[n], infoPerId);
+            if (expandedRows.has(d.normals[n].container)) {
+              $scope.detail.moreInfo(d.normals[n].container);
+            }
           }
           q.await(function () {
             $scope.detail.template = 'edgeRouters.html';
-            $scope.detail.title = 'for edge router';
+            $scope.detail.title = 'edge router';
             resolve({
-              description: 'Expand an edge router to see more info',
+              description: 'Select an edge router to see more info',
               infoPerId: infoPerId
             });
           });
-        } else if (d.isConsole) {
-          $scope.detail.template = 'consoles.html';
-          $scope.detail.title = 'for console';
-          resolve({
-            description: ''
-          });
         } else {
           $scope.detail.moreInfo = function () {};
+          let attrs = QDRService.utilities.copy($scope.fields.linkFields.cols);
+          attrs.unshift('connectionId');
           QDRService.management.topology.fetchEntities(d.key, 
-            [{entity: 'router.link', attrs: []}],
+            [{entity: 'router.link', attrs: attrs}],
             function (results) {
               let links = results[d.key]['router.link'];
               for (let i=0; i<d.normals.length; i++) {
                 let n = d.normals[i];
                 let conn = {};
-                let connectionIndex = links.attributeNames.indexOf('connectionId');
                 infoPerId[n.container] = conn;
                 conn.container = n.container;
-                conn.encrypted = n.encrypted;
+                conn.encrypted = n.encrypted ? 'True' : 'False';
                 conn.host = n.host;
-                conn.links = [];
-                for (let l=0; l<links.results.length; l++) {
-                  if (links.results[l][connectionIndex] === n.connectionId) {
-                    let link = QDRService.utilities.flatten(links.attributeNames, links.results[l]);
+                //conn.links = [];
+                conn.sizes = {};
+                conn.links = QDRService.utilities.flattenAll(links, function (link) {
+                  if (link.connectionId === n.connectionId) {
                     link.owningAddr = QDRService.utilities.addr_text(link.owningAddr);
-                    conn.links.push(link);
+                    updateSizes($scope.fields.linkFields.cols, conn.sizes, link);
+                    return link;
+                  } else {
+                    return null;
                   }
-                }
+                });
                 conn.linkCount = conn.links.length;
               }
               let dir = d.cdir === 'in' ? 'inbound' : d.cdir === 'both' ? 'in and outbound' : 'outbound';
@@ -172,14 +236,23 @@ export class DetailDialogController {
       }));
     };
   
-    groupDetail()
-      .then( function (det) {
-        $timeout( function () {
-          $scope.detail.title = `for ${d.normals.length} ${$scope.detail.title}${d.normals.length > 1 ? 's' : ''}`;
-          $scope.detail.description = det.description;
-          $scope.detail.infoPerId = det.infoPerId;
-        }, 10);
-      });
+    let updateDetail = function () {
+      groupDetail()
+        .then( function (det) {
+          $timeout( function () {
+            $scope.detail.title = `for ${d.normals.length} ${$scope.detail.title}${d.normals.length > 1 ? 's' : ''}`;
+            $scope.detail.description = det.description;
+            $scope.detail.infoPerId = Object.keys(det.infoPerId).map( function (id) {
+              return det.infoPerId[id];
+            }).sort( function (a, b) {
+              return a.name > b.name ? 1 : -1;
+            });
+          }, 10);
+        });
+    };
+    let updateTimer = setInterval(updateDetail, 2000);
+    updateDetail();
+
   }
 }
-DetailDialogController.$inject = ['QDRService', '$scope', '$timeout', '$uibModalInstance', 'd'];
+DetailDialogController.$inject = ['QDRService', '$scope', '$timeout', '$uibModalInstance', 'd'];
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55b7ae55/console/stand-alone/plugin/js/topology/links.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/topology/links.js b/console/stand-alone/plugin/js/topology/links.js
index 264cb8c..16b086c 100644
--- a/console/stand-alone/plugin/js/topology/links.js
+++ b/console/stand-alone/plugin/js/topology/links.js
@@ -17,22 +17,27 @@ specific language governing permissions and limitations
 under the License.
 */
 
-import { utils } from '../amqp/utilities.js';
+import { utils } from "../amqp/utilities.js";
 
 class Link {
   constructor(source, target, dir, cls, uid) {
     this.source = source;
     this.target = target;
-    this.left = (dir == 'in' || dir == 'both');
-    this.right = (dir == 'out' || dir == 'both');
+    this.left = dir == "in" || dir == "both";
+    this.right = dir == "out" || dir == "both";
     this.cls = cls;
     this.uid = uid;
   }
-  markerId (end) {
-    let selhigh = this.highlighted ? 'highlighted' : this.selected ? 'selected' : '';
-    if (selhigh === '' && (!this.left && !this.right))
-      selhigh = 'unknown';
-    return `-${selhigh}-${end === 'end' ? this.target.radius() : this.source.radius()}`;
+  markerId(end) {
+    let selhigh = this.highlighted
+      ? "highlighted"
+      : this.selected
+        ? "selected"
+        : "";
+    if (selhigh === "" && (!this.left && !this.right)) selhigh = "unknown";
+    return `-${selhigh}-${
+      end === "end" ? this.target.radius() : this.source.radius()
+    }`;
   }
 }
 
@@ -41,10 +46,9 @@ export class Links {
     this.links = [];
     this.logger = logger;
   }
-  getLinkSource (nodesIndex) {
-    for (let i=0; i<this.links.length; ++i) {
-      if (this.links[i].target === nodesIndex)
-        return i;
+  getLinkSource(nodesIndex) {
+    for (let i = 0; i < this.links.length; ++i) {
+      if (this.links[i].target === nodesIndex) return i;
     }
     return -1;
   }
@@ -52,7 +56,7 @@ export class Links {
     for (let i = 0; i < this.links.length; i++) {
       let s = this.links[i].source,
         t = this.links[i].target;
-      if (typeof this.links[i].source == 'object') {
+      if (typeof this.links[i].source == "object") {
         s = s.id;
         t = t.id;
       }
@@ -65,119 +69,144 @@ export class Links {
       }
     }
     //this.logger.debug("creating new link (" + (links.length) + ") between " + nodes[_source].name + " and " + nodes[_target].name);
-    if (this.links.some( function (l) { return l.uid === uid;}))
-      uid = uid + '.' + this.links.length;
+    if (
+      this.links.some(function(l) {
+        return l.uid === uid;
+      })
+    )
+      uid = uid + "." + this.links.length;
     return this.links.push(new Link(_source, _target, dir, cls, uid)) - 1;
   }
-  linkFor (source, target) {
+  linkFor(source, target) {
     for (let i = 0; i < this.links.length; ++i) {
-      if ((this.links[i].source == source) && (this.links[i].target == target))
+      if (this.links[i].source == source && this.links[i].target == target)
         return this.links[i];
-      if ((this.links[i].source == target) && (this.links[i].target == source))
+      if (this.links[i].source == target && this.links[i].target == source)
         return this.links[i];
     }
     // the selected node was a client/broker
     return null;
   }
 
-  getPosition (name, nodes, source, client, localStorage, height) {
-    let position = localStorage[name] ? JSON.parse(localStorage[name]) : undefined;
-    if ((typeof position == 'undefined')) {
+  getPosition(name, nodes, source, client, localStorage, height) {
+    let position = localStorage[name]
+      ? JSON.parse(localStorage[name])
+      : undefined;
+    if (typeof position == "undefined") {
       position = {
-        x: Math.round(nodes.get(source).x + 40 * Math.sin(client / (Math.PI * 2.0))),
-        y: Math.round(nodes.get(source).y + 40 * Math.cos(client / (Math.PI * 2.0))),
+        x: Math.round(
+          nodes.get(source).x + 40 * Math.sin(client / (Math.PI * 2.0))
+        ),
+        y: Math.round(
+          nodes.get(source).y + 40 * Math.cos(client / (Math.PI * 2.0))
+        ),
         fixed: false,
         animate: true
       };
-    } else 
-      position.animate = false;
+    } else position.animate = false;
     if (position.y > height) {
-      position.y = Math.round(nodes.get(source).y + 40 + Math.cos(client / (Math.PI * 2.0)));
+      position.y = Math.round(
+        nodes.get(source).y + 40 + Math.cos(client / (Math.PI * 2.0))
+      );
     }
     return position;
   }
 
-  initialize (nodeInfo, nodes, unknowns, localStorage, height) {
+  initialize(nodeInfo, nodes, unknowns, localStorage, height) {
     let connectionsPerContainer = {};
     let nodeIds = Object.keys(nodeInfo);
     // collect connection info for each router
-    for (let source=0; source<nodeIds.length; source++) {
+    for (let source = 0; source < nodeIds.length; source++) {
       let onode = nodeInfo[nodeIds[source]];
       // skip any routers without connections
-      if (!onode.connection || !onode.connection.results || onode.connection.results.length === 0)
+      if (
+        !onode.connection ||
+        !onode.connection.results ||
+        onode.connection.results.length === 0
+      )
         continue;
 
       const suid = nodes.get(source).uid();
-      for (let c=0; c<onode.connection.results.length; c++) {
-        let connection = utils.flatten(onode.connection.attributeNames, onode.connection.results[c]);
+      for (let c = 0; c < onode.connection.results.length; c++) {
+        let connection = utils.flatten(
+          onode.connection.attributeNames,
+          onode.connection.results[c]
+        );
 
         // this is a connection to another interior router
-        if (connection.role === 'inter-router') {
+        if (connection.role === "inter-router") {
           const target = getContainerIndex(connection.container, nodeInfo);
           if (target >= 0) {
             const tuid = nodes.get(target).uid();
-            this.getLink(source, target, connection.dir, '', `${suid}-${tuid}`);
+            this.getLink(source, target, connection.dir, "", `${suid}-${tuid}`);
           }
           continue;
         }
         if (!connectionsPerContainer[connection.container])
           connectionsPerContainer[connection.container] = [];
-        let linksDir = getLinkDir(connection , onode);
-        if (linksDir === 'unknown')
-          unknowns.push(nodeIds[source]);
+        let linksDir = getLinkDir(connection, onode);
+        if (linksDir === "unknown") unknowns.push(nodeIds[source]);
         connectionsPerContainer[connection.container].push({
-          source: source, 
+          source: source,
           linksDir: linksDir,
           connection: connection,
-          resultsIndex: c});
+          resultsIndex: c
+        });
       }
     }
     let unique = {};
     // create map of type:id:dir to [containers]
     for (let container in connectionsPerContainer) {
       let key = getKey(connectionsPerContainer[container]);
-      if (!unique[key])
-        unique[key] = {c: [], nodes: []};
+      if (!unique[key]) unique[key] = { c: [], nodes: [] };
       unique[key].c.push(container);
     }
     for (let key in unique) {
       let containers = unique[key].c;
-      for (let i=0; i<containers.length; i++) {
+      for (let i = 0; i < containers.length; i++) {
         let containerId = containers[i];
         let connections = connectionsPerContainer[containerId];
         let container = connections[0];
-        let name = utils.nameFromId(nodeIds[container.source]) + '.' + container.connection.identity;
-        let position = this.getPosition (name, 
-          nodes, 
-          container.source, 
-          container.resultsIndex, 
-          localStorage, 
-          height);
+        let name =
+          utils.nameFromId(nodeIds[container.source]) +
+          "." +
+          container.connection.identity;
+        let position = this.getPosition(
+          name,
+          nodes,
+          container.source,
+          container.resultsIndex,
+          localStorage,
+          height
+        );
 
-        let node = nodes.getOrCreateNode (nodeIds[container.source], 
-          name, 
-          container.connection.role, 
-          nodes.getLength(), 
-          position.x, position.y, 
-          container.connection.container, 
-          container.resultsIndex, 
-          position.fixed, 
-          container.connection.properties);
+        let node = nodes.getOrCreateNode(
+          nodeIds[container.source],
+          name,
+          container.connection.role,
+          nodes.getLength(),
+          position.x,
+          position.y,
+          container.connection.container,
+          container.resultsIndex,
+          position.fixed,
+          container.connection.properties
+        );
         node.host = container.connection.host;
         node.cdir = container.linksDir;
         node.user = container.connection.user;
         node.isEncrypted = container.connection.isEncrypted;
         node.connectionId = container.connection.identity;
-        node.uuid = `${node.routerId}${node.nodeType}${node.cdir}`;
+        node.uuid = `${containerId}-${node.routerId}-${node.nodeType}-${node.cdir}`;
         // in case a created node (or group) is connected to multiple
         // routers, we need to remember all the routers for traffic animations
-        for (let c=1; c<connections.length; c++) {
-          if (!node.alsoConnectsTo)
-            node.alsoConnectsTo = [];
+        for (let c = 1; c < connections.length; c++) {
+          if (!node.alsoConnectsTo) node.alsoConnectsTo = [];
           node.alsoConnectsTo.push({
             key: nodeIds[connections[c].source],
-            dir: connections[c].linksDir,
-            connectionId: connections[c].connection.identity});
+            cdir: connections[c].linksDir,
+            connectionId: connections[c].connection.identity
+          });
         }
         unique[key].nodes.push(node);
       }
@@ -186,184 +215,80 @@ export class Links {
       nodes.add(unique[key].nodes[0]);
       let target = nodes.nodes.length - 1;
       unique[key].nodes[0].normals = [unique[key].nodes[0]];
-      for (let n=1; n<unique[key].nodes.length; n++) {
+      for (let n = 1; n < unique[key].nodes.length; n++) {
         unique[key].nodes[0].normals.push(unique[key].nodes[n]);
       }
       let containerId = unique[key].c[0];
       let links = connectionsPerContainer[containerId];
-      for (let l=0; l<links.length; l++) {
+      for (let l = 0; l < links.length; l++) {
         let source = links[l].source;
         const suid = nodes.get(source).uid();
         const tuid = nodes.get(target).uid();
-        this.getLink(links[l].source, target, links[l].linksDir, 'small', `${suid}-${tuid}`);
+        this.getLink(
+          links[l].source,
+          target,
+          links[l].linksDir,
+          "small",
+          `${suid}-${tuid}`
+        );
       }
     }
   }
 
-  initializeLinks (nodeInfo, nodes, unknowns, localStorage, height) {
-    let animate = false;
-    let client = 1.0;
-    let nodeIds = Object.keys(nodeInfo);
-    // loop through all the routers
-    for (let source=0; source<nodeIds.length; source++) {
-      let id = nodeIds[source];
-      const suid = nodes.get(source).uid();
-      let onode = nodeInfo[id];
-      if (!onode['connection']) {
-        continue;
-      }
-      let normalsParent = {}; // 1st normal node for this parent
-      // loop through each connection for this router
-      for (let j = 0; j < onode['connection'].results.length; j++) {
-        let connection = utils.flatten(onode['connection'].attributeNames, 
-          onode['connection'].results[j]);
-        let role = connection.role;
-        let dir = connection.dir;
-        // connection to another interior router, just create a link between them
-        if (role == 'inter-router') {
-          let connId = connection.container;
-          let target = getContainerIndex(connId, nodeInfo);
-          if (target >= 0) {
-            const tuid = nodes.get(target).uid();
-            this.getLink(source, target, dir, '', suid + '-' + tuid);
-          }
-          continue;
-        }
-        let properties = connection.properties || {};
-        // handle external connections
-        let name = utils.nameFromId(id) + '.' + connection.identity;
-        // if we have any new clients, animate the force graph to position them
-        let position = localStorage[name] ? JSON.parse(localStorage[name]) : undefined;
-        if ((typeof position == 'undefined')) {
-          animate = true;
-          position = {
-            x: Math.round(nodes.get(source).x + 40 * Math.sin(client / (Math.PI * 2.0))),
-            y: Math.round(nodes.get(source).y + 40 * Math.cos(client / (Math.PI * 2.0))),
-            fixed: false
-          };
-        }
-        if (position.y > height) {
-          position.y = Math.round(nodes.get(source).y + 40 + Math.cos(client / (Math.PI * 2.0)));
-        }
-        let existingNodeIndex = nodes.nodeExists(connection.container);
-        let normalInfo = nodes.normalExists(connection.container);
-        let node = nodes.getOrCreateNode(id, name, role, nodeInfo, nodes.getLength(), position.x, position.y, connection.container, j, position.fixed, properties);
-        let nodeType = utils.isAConsole(properties, connection.identity, role, node.key) ? 'console' : 'client';
-        let linksDir = getLinkDir(connection, onode);
-        if (existingNodeIndex >= 0) {
-          // make a link between the current router (source) and the existing node
-          const tuid = nodes.get(existingNodeIndex).uid();
-          this.getLink(source, existingNodeIndex, dir, 'small', suid + '-' + tuid);
-        } else if (normalInfo.nodesIndex) {
-          // get node index of node that contained this connection in its normals array
-          let normalSource = this.getLinkSource(normalInfo.nodesIndex);
-          if (normalSource >= 0) {
-            if (linksDir === 'unknown')
-              linksDir = dir;
-            node.cdir = linksDir;
-            nodes.add(node);
-            const suidn = nodes.get(this.links[normalSource].source).uid();
-            const tuid = node.uid();
-            // create link from original node to the new node
-            this.getLink(this.links[normalSource].source, nodes.getLength()-1, linksDir, 'small', suidn + '-' + tuid);
-            // create link from this router to the new node
-            this.getLink(source, nodes.getLength()-1, linksDir, 'small', suid + '-' + tuid);
-            // remove the old node from the normals list
-            nodes.get(normalInfo.nodesIndex).normals.splice(normalInfo.normalsIndex, 1);
-          }
-        } else if (role === 'normal' || role === 'edge') {
-        // normal nodes can be collapsed into a single node if they are all the same dir
-          if (linksDir !== 'unknown') {
-            node.user = connection.user;
-            node.isEncrypted = connection.isEncrypted;
-            node.host = connection.host;
-            node.connectionId = connection.identity;
-            node.cdir = linksDir;
-            node.uuid = `${node.routerId}${node.nodeType}${node.cdir}`;
-            // determine arrow direction by using the link directions
-            if (!normalsParent[nodeType+linksDir]) {
-              normalsParent[nodeType+linksDir] = node;
-              nodes.add(node);
-              node.normals = [node];
-              node.connectsTo = {id: linksDir};
-              // now add a link
-              this.getLink(source, nodes.getLength() - 1, linksDir, 'small', suid + '-' + node.uid());
-              client++;
-            } else {
-              normalsParent[nodeType+linksDir].normals.push(node);
-            }
-          } else {
-            node.id = nodes.getLength() - 1 + unknowns.length;
-            unknowns.push(node);
-          }
-        } else {
-          nodes.add(node);
-          // now add a link
-          this.getLink(source, nodes.getLength() - 1, dir, 'small', suid + '-' + node.uid());
-          client++;
-        }
-      }
-    }
-    return animate;
-  }
-  clearHighlighted () {
+  clearHighlighted() {
     for (let i = 0; i < this.links.length; ++i) {
       this.links[i].highlighted = false;
     }
   }
 }
 
-var getContainerIndex = function (_id, nodeInfo) {
+var getContainerIndex = function(_id, nodeInfo) {
   let nodeIndex = 0;
   for (let id in nodeInfo) {
-    if (utils.nameFromId(id) === _id)
-      return nodeIndex;
+    if (utils.nameFromId(id) === _id) return nodeIndex;
     ++nodeIndex;
   }
   return -1;
 };
 
-var getLinkDir = function (connection, onode) {
-  let links = onode['router.link'];
+var getLinkDir = function(connection, onode) {
+  let links = onode["router.link"];
   if (!links) {
-    return 'unknown';
+    return "unknown";
   }
-  let inCount = 0, outCount = 0;
-  let typeIndex = links.attributeNames.indexOf('linkType');
-  let connectionIdIndex = links.attributeNames.indexOf('connectionId');
-  let dirIndex = links.attributeNames.indexOf('linkDir');
-  links.results.forEach( function (linkResult) {
-    if (linkResult[typeIndex] === 'endpoint' && linkResult[connectionIdIndex] === connection.identity)
-      if (linkResult[dirIndex] === 'in')
-        ++inCount;
-      else
-        ++outCount;
+  let inCount = 0,
+    outCount = 0;
+  let typeIndex = links.attributeNames.indexOf("linkType");
+  let connectionIdIndex = links.attributeNames.indexOf("connectionId");
+  let dirIndex = links.attributeNames.indexOf("linkDir");
+  links.results.forEach(function(linkResult) {
+    if (
+      linkResult[typeIndex] === "endpoint" &&
+      linkResult[connectionIdIndex] === connection.identity
+    )
+      if (linkResult[dirIndex] === "in") ++inCount;
+      else ++outCount;
   });
-  if (inCount > 0 && outCount > 0)
-    return 'both';
-  if (inCount > 0)
-    return 'in';
-  if (outCount > 0)
-    return 'out';
-  return 'unknown';
+  if (inCount > 0 && outCount > 0) return "both";
+  if (inCount > 0) return "in";
+  if (outCount > 0) return "out";
+  return "unknown";
 };
-var getKey = function (containers) {
+var getKey = function(containers) {
   let parts = [];
   let connection = containers[0].connection;
-  let d = {nodeType: connection.role, properties: connection.properties || {}};
-  let connectionType = 'client';
-  if (utils.isConsole(connection))
-    connectionType = 'console';
-  else if (utils.isArtemis(d))
-    connectionType = 'artemis';
-  else if (utils.isQpid(d))
-    connectionType = 'qpid';
-  else if (connection.role === 'edge')
-    connectionType = 'edge';
-  for (let c=0; c<containers.length; c++) {
+  let d = {
+    nodeType: connection.role,
+    properties: connection.properties || {}
+  };
+  let connectionType = "client";
+  if (utils.isConsole(connection)) connectionType = "console";
+  else if (utils.isArtemis(d)) connectionType = "artemis";
+  else if (utils.isQpid(d)) connectionType = "qpid";
+  else if (connection.role === "edge") connectionType = "edge";
+  for (let c = 0; c < containers.length; c++) {
     let container = containers[c];
     parts.push(`${container.source}-${container.linksDir}`);
   }
-  return `${connectionType}:${parts.join(':')}`;
+  return `${connectionType}:${parts.join(":")}`;
 };
-

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55b7ae55/console/stand-alone/plugin/js/topology/map.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/topology/map.js b/console/stand-alone/plugin/js/topology/map.js
index c14008d..972d894 100644
--- a/console/stand-alone/plugin/js/topology/map.js
+++ b/console/stand-alone/plugin/js/topology/map.js
@@ -57,6 +57,10 @@ export class BackgroundMap { // eslint-disable-line no-unused-vars
   init($scope, svg, width, height) {
     return new Promise( (function (resolve, reject) {
 
+      if (this.initialized) {
+        resolve();
+        return;
+      }
       this.svg = svg;
       this.width = width;
       this.height = height;


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org