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/06/25 21:26:47 UTC

[1/8] qpid-dispatch git commit: DISPATCH-1049 Add console tests

Repository: qpid-dispatch
Updated Branches:
  refs/heads/master af997544e -> b5deb0357


http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/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
new file mode 100644
index 0000000..48d4cf8
--- /dev/null
+++ b/console/stand-alone/plugin/js/topology/topoUtils.js
@@ -0,0 +1,225 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+// 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
+  // from is the selected_node ....
+  if (!from)
+    return null;
+
+  if (from == d)
+    return selected_node;
+
+  let sInfo = QDRService.management.topology.nodeInfo()[from.key];
+
+  // find the hovered name in the selected name's .router.node results
+  if (!sInfo['router.node'])
+    return null;
+  let aAr = sInfo['router.node'].attributeNames;
+  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) {
+      let next = QDRService.utilities.valFor(aAr, vAr[hIdx], 'nextHop');
+      return (next == null) ? nodes.nodeFor(addrT) : nodes.nodeFor(next);
+    }
+  }
+  return null;
+}
+export function nextHop(thisNode, d, nodes, links, QDRService, selected_node, cb) {
+  if ((thisNode) && (thisNode != d)) {
+    let target = findNextHopNode(thisNode, d, QDRService, selected_node, nodes);
+    if (target) {
+      let hnode = nodes.nodeFor(thisNode.name);
+      let hlLink = links.linkFor(hnode, target);
+      if (hlLink) {
+        if (cb) {
+          cb(hlLink, hnode, target);
+        }
+      }
+      else
+        target = null;
+    }
+    nextHop(target, d, nodes, links, QDRService, selected_node, cb);
+  }
+}
+
+export function connectionPopupHTML (d, QDRService) {
+  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(QDRService.utilities.flatten(onode['connection'].attributeNames,
+            onode['connection'].results[normals[n].resultIndex]));
+        }
+      }
+    }
+    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) {
+    const max_links = 10;
+    const fields = ['undelivered', 'unsettled', 'rejected', 'released', 'modified'];
+    // local function to determine if a link's connectionId is in any of the connections
+    let isLinkFor = function (connectionId, conns) {
+      for (let c=0; c<conns.length; c++) {
+        if (conns[c].identity === connectionId)
+          return true;
+      }
+      return false;
+    };
+    let fnJoin = function (ar, sepfn) {
+      let out = '';
+      out = ar[0];
+      for (let i=1; i<ar.length; i++) {
+        let sep = sepfn(ar[i]);
+        out += (sep[0] + sep[1]);
+      }
+      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'];
+    if (!nodeLinks)
+      return '';
+    let links = [];
+    let hasAddress = false;
+    for (let n=0; n<nodeLinks.results.length; n++) {
+      let link = QDRService.utilities.flatten(nodeLinks.attributeNames, nodeLinks.results[n]);
+      if (link.linkType !== 'router-control') {
+        if (isLinkFor(link.connectionId, conns)) {
+          if (link.owningAddr)
+            hasAddress = true;
+          links.push(link);
+        }
+      }
+    }
+    // we may need to limit the number of links displayed, so sort descending by the sum of the field values
+    links.sort( function (a, b) {
+      let asum = a.undeliveredCount + a.unsettledCount + a.rejectedCount + a.releasedCount + a.modifiedCount;
+      let bsum = b.undeliveredCount + b.unsettledCount + b.rejectedCount + b.releasedCount + b.modifiedCount;
+      return asum < bsum ? 1 : asum > bsum ? -1 : 0;
+    });
+    let HTMLHeading = '<h5>Links</h5>';
+    let HTML = '<table class="popupTable">';
+    // copy of fields since we may be prepending an address
+    let th = fields.slice();
+    // convert to actual attribute names
+    let td = fields.map( function (f) {return f + 'Count';});
+    th.unshift('dir');
+    td.unshift('linkDir');
+    // add an address field if any of the links had an owningAddress
+    if (hasAddress) {
+      th.unshift('address');
+      td.unshift('owningAddr');
+    }
+    HTML += ('<tr class="header"><td>' + th.join('</td><td>') + '</td></tr>');
+    // add rows to the table for each link
+    for (let l=0; l<links.length; l++) {
+      if (l>=max_links) {
+        HTMLHeading = `<h4>Top ${max_links} Links</h4>`;
+        break;
+      }
+      let link = links[l];
+      let vals = td.map( function (f) {
+        if (f === 'owningAddr') {
+          let identity = QDRService.utilities.identity_clean(link.owningAddr);
+          return QDRService.utilities.addr_text(identity);
+        }
+        return link[f];
+      });
+      let joinedVals = fnJoin(vals, function (v1) {
+        return ['</td><td' + (isNaN(+v1) ? '': ' align="right"') + '>', QDRService.utilities.pretty(v1 || '0')];
+      });
+      HTML += `<tr><td> ${joinedVals} </td></tr>`;
+    }
+    HTML += '</table>';
+    return HTMLHeading + HTML;
+  };
+  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];
+  let connSecurity = function (conn) {
+    if (!conn.isEncrypted)
+      return 'no-security';
+    if (conn.sasl === 'GSSAPI')
+      return 'Kerberos';
+    return conn.sslProto + '(' + conn.sslCipher + ')';
+  };
+  let connAuth = function (conn) {
+    if (!conn.isAuthenticated)
+      return 'no-auth';
+    let sasl = conn.sasl;
+    if (sasl === 'GSSAPI')
+      sasl = 'Kerberos';
+    else if (sasl === 'EXTERNAL')
+      sasl = 'x.509';
+    else if (sasl === 'ANONYMOUS')
+      return 'anonymous-user';
+    if (!conn.user)
+      return sasl;
+    return conn.user + '(' + sasl + ')';
+  };
+  let connTenant = function (conn) {
+    if (!conn.tenant) {
+      return '';
+    }
+    if (conn.tenant.length > 1)
+      return conn.tenant.replace(/\/$/, '');
+  };
+  // loop through all the connections for left, and find the one for right
+  let rightIndex = onode['connection'].results.findIndex( function (conn) {
+    return QDRService.utilities.valFor(onode['connection'].attributeNames, conn, 'container') === right.routerId;
+  });
+  if (rightIndex < 0) {
+    // we have a connection to a client/service
+    rightIndex = +left.resultIndex;
+  }
+  if (isNaN(rightIndex)) {
+    // we have a connection to a console
+    rightIndex = +right.resultIndex;
+  }
+  let HTML = '';
+  if (rightIndex >= 0) {
+    let conn = onode['connection'].results[rightIndex];
+    conn = QDRService.utilities.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>';
+
+      for (let c=0; c<conns.length; c++) {
+        HTML += ('<tr><td>' + connSecurity(conns[c]) + '</td>');
+        HTML += ('<td>' + connAuth(conns[c]) + '</td>');
+        HTML += ('<td>' + (connTenant(conns[c]) || '--') + '</td>');
+        HTML += ('<td>' + conns[c].host + '</td>');
+        HTML += '</tr>';
+      }
+      HTML += '</table>';
+    }
+    HTML += linksHTML(onode, conn, d);
+  }
+  return HTML;
+}

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/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
new file mode 100644
index 0000000..3778c4e
--- /dev/null
+++ b/console/stand-alone/plugin/js/topology/traffic.js
@@ -0,0 +1,445 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+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';
+
+const transitionDuration = 1000;
+const CHORDFILTERKEY = 'chordFilter';
+
+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
+    this.prefix = prefix; // url prefix used in svg url()s
+    this.topology = topology; // contains the list of router nodes
+    this.$scope = $scope;
+    this.$timeout = $timeout;
+    // internal variables
+    this.interval = null; // setInterval handle
+    this.setAnimationType(type, converter, radius);
+  }
+  // stop updating the traffic data
+  stop() {
+    if (this.interval) {
+      clearInterval(this.interval);
+      this.interval = null;
+    }
+  }
+  // start updating the traffic data
+  start() {
+    this.doUpdate();
+    this.interval = setInterval(this.doUpdate.bind(this), transitionDuration);
+  }
+  // remove any animations that are in progress
+  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);
+  }
+  // called periodically to refresh the traffic flow
+  doUpdate() {
+    this.vis.doUpdate();
+  }
+}
+
+
+/* Base class for congestion and dots visualizations */
+class TrafficAnimation {
+  constructor(traffic) {
+    this.traffic = traffic;
+  }
+  nodeIndexFor(nodes, name) {
+    for (let i = 0; i < nodes.length; i++) {
+      let node = nodes[i];
+      if (node.container === name)
+        return i;
+    }
+    return -1;
+  }
+}
+
+/* Color the links between router to show how heavily used the links are. */
+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');
+    if (this.custom_markers_def.empty()) {
+      this.custom_markers_def = d3.select('#SVG_ID').append('svg:defs').attr('class', 'custom-markers');
+    }
+  }
+  findResult(node, entity, attribute, value) {
+    let attrIndex = node[entity].attributeNames.indexOf(attribute);
+    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 null;
+  }
+  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();
+      // 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'];
+        for (let n = 0; n < nodeLinks.results.length; n++) {
+          let link = self.traffic.QDRService.utilities.flatten(nodeLinks.attributeNames, nodeLinks.results[n]);
+          if (link.linkType !== 'router-control') {
+            let f = self.nodeIndexFor(self.traffic.topology.nodes.nodes, self.traffic.QDRService.utilities.nameFromId(nodeId));
+            let connection = self.findResult(node, 'connection', 'identity', link.connectionId);
+            if (connection) {
+              let t = self.nodeIndexFor(self.traffic.topology.nodes.nodes, connection.container);
+              let little = Math.min(f, t);
+              let big = Math.max(f, t);
+              let key = ['#path', little, big].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 path = d3.select(key);
+        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
+            .attr('stroke', congestion)
+            .classed('traffic', true)
+            .attr('marker-start', function (d) {
+              return d.left ? 'url(' + self.traffic.prefix + '#' + id + ')' : '';
+            })
+            .attr('marker-end', function (d) {
+              return d.right ? 'url(' + self.traffic.prefix + '#' + id + ')' : '';
+            });
+        }
+      }
+      // 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', 4)
+        .attr('markerHeight', 4)
+        .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);
+    }
+    return this.fillColor(v);
+  }
+  fillColor(v) {
+    let color = d3.scale.linear().domain([0, 1, 2, 3])
+      .interpolate(d3.interpolateHcl)
+      .range([d3.rgb('#CCCCCC'), 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();
+  }
+}
+
+/* Create animated dots moving along the links between routers
+   to show message flow */
+class Dots extends TrafficAnimation {
+  constructor(traffic, converter, radius) {
+    super(traffic);
+    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.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));
+    // colors
+    this.colorGen = d3.scale.category10();
+    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();
+        });
+    };
+    // called by angular when mouse enters one of the address legends
+    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 () {
+      self.unFadeAll();
+    };
+    // clicked on the address name. toggle the address checkbox
+    traffic.$scope.addressClick = function (address) {
+      self.toggleAddress(address)
+        .then(function () {
+          self.updateAddresses();
+        });
+    };
+  }
+  remove() {
+    for (let id in this.lastFlows) {
+      d3.select('#SVG_ID').selectAll('circle.flow' + id).remove();
+    }
+    this.lastFlows = {};
+  }
+  updateAddresses() {
+    this.excludedAddresses = [];
+    for (let address in this.traffic.$scope.addresses) {
+      if (!this.traffic.$scope.addresses[address])
+        this.excludedAddresses.push(address);
+    }
+    localStorage[CHORDFILTERKEY] = JSON.stringify(this.excludedAddresses);
+    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) {
+      return resolve();
+    });
+  }
+  fadeOtherAddresses(address) {
+    d3.selectAll('circle.flow').classed('fade', function (d) {
+      return d.address !== address;
+    });
+  }
+  unFadeAll() {
+    d3.selectAll('circle.flow').classed('fade', false);
+  }
+  doUpdate() {
+    let self = this;
+    // 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);
+      });
+    });
+  }
+  render(matrix) {
+    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.75]);
+    // 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));
+    // 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 flowId = id + '-' + this.addressIndex(this, ahop.address) + (ahop.back ? 'b' : '');
+        let path = d3.select('#path' + id);
+        // 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;
+      }
+    }
+    // remove any existing animations that we don't have data for anymore
+    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();
+      }
+    }
+  }
+  // animate the d3 selection (flow) along the given path
+  animateFlow(flow, path, count, back, rate) {
+    let self = this;
+    let l = path.node().getTotalLength();
+    flow.transition()
+      .ease('easeLinear')
+      .duration(l * 10 / rate)
+      .attrTween('transform', self.translateDots(self.radius, path, count, back))
+      .each('end', function () { self.animateFlow(flow, path, count, back, rate); });
+  }
+  // create dots along the path between routers
+  startAnimation(path, id, hop, rate) {
+    if (!path.node())
+      return;
+    this.animateDots(path, id, hop, rate);
+  }
+  animateDots(path, id, hop, rate) {
+    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 = [];
+    for (let i = 0, offset = this.addressIndex(this, address); i < len; ++i) {
+      dots[i] = { i: i + 10 * offset, address: address };
+    }
+    // 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;
+    else {
+      if (this.lastFlows[id] !== len) {
+        this.lastFlows[id] = len;
+        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 circles = flow
+      .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();
+  }
+  fillColor(n) {
+    if (!(n in this.traffic.$scope.addressColors)) {
+      let ci = Object.keys(this.traffic.$scope.addressColors).length;
+      this.traffic.$scope.addressColors[n] = this.colorGen(ci);
+    }
+    return this.traffic.$scope.addressColors[n];
+  }
+  addClients(hops, nodes, f, val, sender, address) {
+    let cdir = sender ? 'out' : 'in';
+    for (let n = 0; n < nodes.length; n++) {
+      let node = nodes[n];
+      if (node.normals && node.key === nodes[f].key && node.cdir === cdir) {
+        let key = ['', f, n].join('-');
+        if (!hops[key])
+          hops[key] = [];
+        hops[key].push({ val: val, back: node.cdir === 'in', address: address });
+        return;
+      }
+    }
+  }
+  addressIndex(vis, address) {
+    return Object.keys(vis.traffic.$scope.addresses).indexOf(address);
+  }
+  // calculate the translation for each dot along the path
+  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) {
+      // will be called with t going from 0 to 1 for each dot
+      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;
+        // 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 + ')';
+      };
+    };
+  }
+}
+

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/topology/traffic.ts
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/topology/traffic.ts b/console/stand-alone/plugin/js/topology/traffic.ts
deleted file mode 100644
index 3f1e5c2..0000000
--- a/console/stand-alone/plugin/js/topology/traffic.ts
+++ /dev/null
@@ -1,443 +0,0 @@
-/*
-Licensed to the Apache Software Foundation (ASF) under one
-or more contributor license agreements.  See the NOTICE file
-distributed with this work for additional information
-regarding copyright ownership.  The ASF licenses this file
-to you under the Apache License, Version 2.0 (the
-"License"); you may not use this file except in compliance
-with the License.  You may obtain a copy of the License at
-
-  http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing,
-software distributed under the License is distributed on an
-"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-KIND, either express or implied.  See the License for the
-specific language governing permissions and limitations
-under the License.
-*/
-'use strict';
-
-/* global d3 ChordData MIN_CHORD_THRESHOLD Promise */
-declare var d3: any;
-declare var ChordData: any;
-declare var MIN_CHORD_THRESHOLD: number;
-
-const transitionDuration = 1000;
-const CHORDFILTERKEY = 'chordFilter';
-
-class Traffic { // eslint-disable-line no-unused-vars
-  [x: string]: any;
-  constructor($scope, $timeout, QDRService, converter, radius, topology, nextHop, type, prefix) {
-    $scope.addressColors = {};
-    this.QDRService = QDRService;
-    this.type = type; // moving dots or colored path
-    this.prefix = prefix; // url prefix used in svg url()s
-    this.topology = topology; // contains the list of router nodes
-    this.nextHop = nextHop; // fn that returns the route through the network between two routers
-    this.$scope = $scope;
-    this.$timeout = $timeout;
-    // internal variables
-    this.interval = null; // setInterval handle
-    this.setAnimationType(type, converter, radius);
-  }
-  // stop updating the traffic data
-  stop() {
-    if (this.interval) {
-      clearInterval(this.interval);
-      this.interval = null;
-    }
-  }
-  // start updating the traffic data
-  start() {
-    this.doUpdate();
-    this.interval = setInterval(this.doUpdate.bind(this), transitionDuration);
-  }
-  // remove any animations that are in progress
-  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);
-  }
-  // called periodically to refresh the traffic flow
-  doUpdate() {
-    this.vis.doUpdate();
-  }
-}
-
-
-/* Base class for congestion and dots visualizations */
-class TrafficAnimation {
-  [x: string]: any;
-  constructor(traffic) {
-    this.traffic = traffic;
-  }
-  nodeIndexFor(nodes, name) {
-    for (let i = 0; i < nodes.length; i++) {
-      let node = nodes[i];
-      if (node.container === name) {
-        return i;
-      }
-    }
-    return -1;
-  }
-}
-
-/* Color the links between router to show how heavily used the links are. */
-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');
-    if (this.custom_markers_def.empty()) {
-      this.custom_markers_def = d3.select('#SVG_ID').append('svg:defs').attr('class', 'custom-markers');
-    }
-  }
-  findResult(node, entity, attribute, value) {
-    let attrIndex = node[entity].attributeNames.indexOf(attribute);
-    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 null;
-  }
-  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();
-      // accumulate all the inter-router links in an object
-      // keyed by the svgs path id
-      for (const nodeId of Object.keys(nodeInfo)) {
-        let node = nodeInfo[nodeId];
-        let nodeLinks = node['router.link'];
-        for (let n = 0; n < nodeLinks.results.length; n++) {
-          let link = self.traffic.QDRService.utilities.flatten(nodeLinks.attributeNames, nodeLinks.results[n]);
-          if (link.linkType !== 'router-control') {
-            let f = self.nodeIndexFor(self.traffic.topology.nodes, self.traffic.QDRService.management.topology.nameFromId(nodeId));
-            let connection = self.findResult(node, 'connection', 'identity', link.connectionId);
-            if (connection) {
-              let t = self.nodeIndexFor(self.traffic.topology.nodes, connection.container);
-              let little = Math.min(f, t);
-              let big = Math.max(f, t);
-              let key = ['#path', little, big].join('-');
-              if (!links[key]) {
-                links[key] = [];
-              }
-              links[key].push(link);
-            }
-          }
-        }
-      }
-      // accumulate the colors/directions to be used
-      let colors = {};
-      for (const key of Object.keys(links)) {
-        let congestion = self.congestion(links[key]);
-        let path = d3.select(key);
-        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
-            .attr('stroke', congestion)
-            .classed('traffic', true)
-            .attr('marker-start', function (d) {
-              return d.left ? 'url(' + self.traffic.prefix + '#' + id + ')' : '';
-            })
-            .attr('marker-end', function (d) {
-              return d.right ? 'url(' + self.traffic.prefix + '#' + id + ')' : '';
-            });
-        }
-      }
-      // 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', 4)
-        .attr('markerHeight', 4)
-        .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);
-    }
-    return this.fillColor(v);
-  }
-  fillColor(v) {
-    let color = d3.scale.linear().domain([0, 1, 2, 3])
-      .interpolate(d3.interpolateHcl)
-      .range([d3.rgb('#CCCCCC'), d3.rgb('#00FF00'), d3.rgb('#FFA500'), d3.rgb('#FF0000')]);
-    return color(v);
-  }
-  remove() {
-    d3.select('#SVG_ID').selectAll('path.traffic')
-      .classed('traffic', false);
-    d3.select('#SVG_ID').select('defs.custom-markers')
-      .selectAll('marker').remove();
-  }
-}
-
-/* Create animated dots moving along the links between routers
-   to show message flow */
-class Dots extends TrafficAnimation {
-  constructor(traffic, converter, radius) {
-    super(traffic);
-    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.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 (const address of Object.keys(this.traffic.$scope.addresses)) {
-          this.fillColor(address);
-        }
-      }.bind(this));
-    }.bind(this));
-    // colors
-    this.colorGen = d3.scale.category10();
-    let self = this;
-    // event notification that an address checkbox has changed
-    traffic.$scope.addressFilterChanged = function () {
-      self.updateAddresses();
-      // 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) {
-      // 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 () {
-      self.unFadeAll();
-    };
-    // clicked on the address name. toggle the address checkbox
-    traffic.$scope.addressClick = function (address) {
-      self.toggleAddress(address);
-      self.updateAddresses();
-    };
-  }
-  remove() {
-    for (const id of Object.keys(this.lastFlows)) {
-      d3.select('#SVG_ID').selectAll('circle.flow' + id).remove();
-    }
-    this.lastFlows = {};
-  }
-  updateAddresses() {
-    this.excludedAddresses = [];
-    for (let address in this.traffic.$scope.addresses) {
-      if (!this.traffic.$scope.addresses[address]) {
-        this.excludedAddresses.push(address);
-      }
-    }
-    localStorage[CHORDFILTERKEY] = JSON.stringify(this.excludedAddresses);
-    if (this.chordData) {
-      this.chordData.setFilter(this.excludedAddresses);
-    }
-  }
-  toggleAddress(address) {
-    this.traffic.$scope.addresses[address] = !this.traffic.$scope.addresses[address];
-  }
-  fadeOtherAddresses(address) {
-    d3.selectAll('circle.flow').classed('fade', function (d) {
-      return d.address !== address;
-    });
-  }
-  unFadeAll() {
-    d3.selectAll('circle.flow').classed('fade', false);
-  }
-  doUpdate() {
-    let self = this;
-    // 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);
-      });
-    });
-  }
-  render(matrix) {
-    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.75]);
-    // 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, matrix.rows[r].egress);
-          let t = this.nodeIndexFor(this.traffic.topology.nodes, matrix.rows[r].ingress);
-          let address = matrix.getAddress(r, c);
-          if (r !== c) {
-            // accumulate the hops between the ingress and egress routers
-            this.traffic.nextHop(this.traffic.topology.nodes[f], this.traffic.topology.nodes[t], 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, f, val, true, address);
-          this.addClients(hops, this.traffic.topology.nodes, t, val, false, address);
-        }
-      }.bind(this));
-    }.bind(this));
-    // for each link between routers that has traffic, start an animation
-    let keep = {};
-    for (const id of Object.keys(hops)) {
-      let hop = hops[id];
-      for (let h = 0; h < hop.length; h++) {
-        let ahop = hop[h];
-        let flowId = id + '-' + this.addressIndex(this, ahop.address) + (ahop.back ? 'b' : '');
-        let path = d3.select('#path' + id);
-        // 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;
-      }
-    }
-    // remove any existing animations that we don't have data for anymore
-    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();
-      }
-    }
-  }
-  // animate the d3 selection (flow) along the given path
-  animateFlow(flow, path, count, back, rate) {
-    let self = this;
-    let l = path.node().getTotalLength();
-    flow.transition()
-      .ease('easeLinear')
-      .duration(l * 10 / rate)
-      .attrTween('transform', self.translateDots(self.radius, path, count, back))
-      .each('end', function () { self.animateFlow(flow, path, count, back, rate); });
-  }
-  // create dots along the path between routers
-  startAnimation(path, id, hop, rate) {
-    if (!path.node()) {
-      return;
-    }
-    this.animateDots(path, id, hop, rate);
-  }
-  animateDots(path, id, hop, rate) {
-    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 = [];
-    for (let i = 0, offset = this.addressIndex(this, address); i < len; ++i) {
-      dots[i] = { i: i + 10 * offset, address: address };
-    }
-    // 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;
-    } else {
-      if (this.lastFlows[id] !== len) {
-        this.lastFlows[id] = len;
-        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 circles = flow
-      .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();
-  }
-  fillColor(n) {
-    if (!(n in this.traffic.$scope.addressColors)) {
-      let ci = Object.keys(this.traffic.$scope.addressColors).length;
-      this.traffic.$scope.addressColors[n] = this.colorGen(ci);
-    }
-    return this.traffic.$scope.addressColors[n];
-  }
-  addClients(hops, nodes, f, val, sender, address) {
-    let cdir = sender ? 'out' : 'in';
-    for (let n = 0; n < nodes.length; n++) {
-      let node = nodes[n];
-      if (node.normals && node.key === nodes[f].key && node.cdir === cdir) {
-        let key = ['', f, n].join('-');
-        if (!hops[key]) {
-          hops[key] = [];
-        }
-        hops[key].push({ val: val, back: node.cdir === 'in', address: address });
-        return;
-      }
-    }
-  }
-  addressIndex(vis, address) {
-    return Object.keys(vis.traffic.$scope.addresses).indexOf(address);
-  }
-  // calculate the translation for each dot along the path
-  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) {
-      // will be called with t going from 0 to 1 for each dot
-      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;
-        }
-        // 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 + ')';
-      };
-    };
-  }
-}
-
-

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/test/filter.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/test/filter.js b/console/stand-alone/test/filter.js
new file mode 100644
index 0000000..d72915c
--- /dev/null
+++ b/console/stand-alone/test/filter.js
@@ -0,0 +1,73 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+var assert = require('assert');
+import { aggregateAddresses, separateAddresses } from '../plugin/js/chord/filters.js';
+/* global describe it */
+
+describe('Filters', function() {
+  const MIN_VALUE = 1,
+    MAX_VALUE = 2;
+  const values = [
+    {ingress: 'A', egress: 'B', address: 'toB', messages: MIN_VALUE, key: 'BAtoB'},
+    {ingress: 'B', egress: 'A', address: 'toA', messages: MAX_VALUE, key: 'ABtoA'}
+  ];
+
+  describe('#aggregateAddresses', function() {
+    let m = aggregateAddresses(values, []);
+    it('should create a matrix', function() {
+      assert.ok(m.hasValues());
+    });
+    it('that has two rows', function() {
+      assert.equal(m.rows.length, 2);
+    });
+    it('and has 1 chord per router', function() {
+      assert.equal(m.getChordList().length, 2);
+    });
+    it('and contains all addresses when there is no filter', function () {
+      let minmax = m.getMinMax();
+      assert.ok(minmax[0] === MIN_VALUE && minmax[1] === MAX_VALUE);
+    });
+    it('should filter out an address', function () {
+      let m = aggregateAddresses(values, ['toB']);
+      // if the toB address was filtered, the min value in the matrix should be 2 (for the toA address)
+      assert.equal(m.getMinMax()[0], MAX_VALUE);
+    });
+  });
+  describe('#separateAddresses', function() {
+    let m = separateAddresses(values, []);
+    it('should create a matrix', function() {
+      assert.ok(m.hasValues());
+    });
+    it('that has a row per router/address combination', function() {
+      assert.equal(m.rows.length, 4);
+    });
+    it('and has 1 chord per router/address combination', function() {
+      assert.equal(m.getChordList().length, 4);
+    });
+    it('and contains all addresses when there is no filter', function () {
+      let minmax = m.getMinMax();
+      assert.ok(minmax[0] === MIN_VALUE && minmax[1] === MAX_VALUE);
+    });
+    it('should filter out an address', function () {
+      let m = separateAddresses(values, ['toB']);
+      // if the toB address was filtered, the min value in the matrix should be 2 (for the toA address)
+      assert.equal(m.getMinMax()[0], MAX_VALUE);
+    });
+  });
+});

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/test/links.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/test/links.js b/console/stand-alone/test/links.js
new file mode 100644
index 0000000..f07482f
--- /dev/null
+++ b/console/stand-alone/test/links.js
@@ -0,0 +1,82 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+/* global describe it before */
+var assert = require('assert');
+var fs = require('fs');
+var expect = require('chai').expect;
+import { Links } from '../plugin/js/topology/links.js';
+import { Nodes } from '../plugin/js/topology/nodes.js';
+import { QDRService } from '../plugin/js/qdrService.js';
+
+class Log {
+  constructor() {
+  }
+  log (msg) {}
+  debug (msg) {}
+  error (msg) {}
+  info (msg) {}
+  warn (msg) {}
+}
+var log = new Log();
+var loc = {protocol: function () { return 'http://';}};
+var timeout = {};
+var qdrService = new QDRService(log, timeout, loc);
+var links = new Links(qdrService, log);
+var nodes = new Nodes(qdrService, log);
+var nodeInfo;
+var unknowns = [];
+const width = 1024;
+const height = 768;
+
+before(function(done){
+  fs.readFile('./test/nodes.json', 'utf8', function(err, fileContents) {
+    if (err) throw err;
+    nodeInfo = JSON.parse(fileContents);
+    done();
+  });
+});
+
+describe('Nodes', function() {
+  describe('#exists', function() {
+    it('should exist', function() {
+      expect(nodes).to.exist;
+    });
+  });
+  describe('#initializes', function() {
+    it('should initialize', function() {
+      nodes.initialize(nodeInfo, {}, width, height);
+      assert.equal(nodes.nodes.length, 6);
+    });
+  });
+
+});
+describe('Links', function() {
+  describe('#exists', function() {
+    it('should exist', function() {
+      expect(links).to.exist;
+    });
+  });
+  describe('#initializes', function() {
+    it('should initialize', function() {
+      links.initializeLinks(nodeInfo, nodes, unknowns, {}, width);
+      assert.equal(links.links.length, 10);
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/test/matrix.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/test/matrix.js b/console/stand-alone/test/matrix.js
new file mode 100644
index 0000000..449273a
--- /dev/null
+++ b/console/stand-alone/test/matrix.js
@@ -0,0 +1,51 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+var assert = require('assert');
+import { valuesMatrix } from '../plugin/js/chord/matrix.js';
+/* global describe it */
+
+describe('Matrix', function() {
+  describe('#zeroInit', function() {
+    const ROW_COUNT = 10;
+    let matrix = new valuesMatrix(false);
+    matrix.zeroInit(ROW_COUNT);
+    it('should create the requested number of rows', function() {
+      assert.equal(matrix.rows.length, ROW_COUNT);
+    });
+    it('should create the requested number of cols per row', function() {
+      let allEqual = true;
+      matrix.rows.forEach( function (row) {
+        allEqual = allEqual && row.cols.length === ROW_COUNT;
+      });
+      assert.ok(allEqual);
+    });
+  });
+  describe('#hasValues', function () {
+    it('should not have any values to start', function () {
+      let matrix = new valuesMatrix(false);
+      assert.ok(!matrix.hasValues());
+    });
+    it('should have a value after adding one', function () {
+      let matrix = new valuesMatrix(false);
+      matrix.addRow('chordName', 'ingress', 'egress', 'address');
+      matrix.addValue(0, 0, {messages: 1234, address: 'address'});
+      assert.ok(matrix.hasValues());
+    });
+  });
+});

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/test/nodes.json
----------------------------------------------------------------------
diff --git a/console/stand-alone/test/nodes.json b/console/stand-alone/test/nodes.json
new file mode 100644
index 0000000..7aa7631
--- /dev/null
+++ b/console/stand-alone/test/nodes.json
@@ -0,0 +1 @@
+{"amqp:/_topo/0/D/$management":{"router.node":{"results":[["D","(self)"],["C",null],["A","C"],["B","C"],["E",null],["F",null]],"attributeNames":["id","nextHop"]},"connection":{"attributeNames":["name","identity","host","role","dir","container","sasl","isAuthenticated","user","isEncrypted","sslProto","sslCipher","properties","sslSsf","tenant","type","ssl","opened"],"results":[["connection/127.0.0.1:43430","3","127.0.0.1:43430","inter-router","in","C","ANONYMOUS",true,"anonymous",false,null,null,{"product":"qpid-dispatch-router","version":"1.2.0-SNAPSHOT"},0,null,"org.apache.qpid.dispatch.connection",false,true],["connection/0.0.0.0:2002","4","0.0.0.0:2002","inter-router","out","E","ANONYMOUS",true,null,false,null,null,{"product":"qpid-dispatch-router","version":"1.2.0-SNAPSHOT"},0,null,"org.apache.qpid.dispatch.connection",false,true],["connection/0.0.0.0:2003","5","0.0.0.0:2003","inter-router","out","F","ANONYMOUS",true,null,false,null,null,{"product":"qpid-dispatch-router","version
 ":"1.2.0-SNAPSHOT"},0,null,"org.apache.qpid.dispatch.connection",false,true],["connection/127.0.0.1","35","127.0.0.1","normal","in","3df1ec8f-c39d-ce46-bb71-939b171b9c3d",null,false,"anonymous",false,null,null,{"console_identifier":"Dispatch console"},0,null,"org.apache.qpid.dispatch.connection",false,true],["connection/127.0.0.1","37","127.0.0.1","normal","in","a3aa9724-cc48-b442-90e2-7ee59965825e",null,false,"anonymous",false,null,null,{"console_identifier":"Dispatch console"},0,null,"org.apache.qpid.dispatch.connection",false,true]]},"router.link":{"attributeNames":["name","identity","type","linkName","linkType","linkDir","owningAddr","capacity","peer","undeliveredCount","unsettledCount","deliveryCount","connectionId","adminStatus","operStatus","presettledCount","droppedPresettledCount","acceptedCount","rejectedCount","releasedCount","modifiedCount","ingressHistogram"],"results":[["qdlink.iiY_WYQqdKOZjjf","1","org.apache.qpid.dispatch.router.link","qdlink.iiY_WYQqdKOZjjf","router
 -control","out","Lqdhello",250,null,0,0,69650,"3","enabled","up",69650,0,0,0,0,0,null],["qdlink.ad3E15bOZ4387CG","2","org.apache.qpid.dispatch.router.link","qdlink.ad3E15bOZ4387CG","router-control","in",null,250,null,0,0,69643,"3","enabled","up",69643,0,0,0,0,0,null],["qdlink.RfxRiUQpPAmV7ow","3","org.apache.qpid.dispatch.router.link","qdlink.RfxRiUQpPAmV7ow","inter-router","out",null,250,null,0,224,214732069,"3","enabled","up",0,0,214731845,0,0,0,null],["qdlink.0x5Gje+NwvMzFpo","4","org.apache.qpid.dispatch.router.link","qdlink.0x5Gje+NwvMzFpo","inter-router","in",null,250,null,0,232,214183382,"3","enabled","up",94314,0,214088836,0,0,0,null],["qdlink.g8g0G5os5DBBLZt","5","org.apache.qpid.dispatch.router.link","qdlink.g8g0G5os5DBBLZt","router-control","in",null,250,null,0,0,65399,"4","enabled","up",65399,0,0,0,0,0,null],["qdlink.GNu2meWTw_cQnmz","6","org.apache.qpid.dispatch.router.link","qdlink.GNu2meWTw_cQnmz","router-control","out","Lqdhello",250,null,0,0,73850,"4","enabled","up"
 ,73850,0,0,0,0,0,null],["qdlink.uCAaTkz40dYjyzL","7","org.apache.qpid.dispatch.router.link","qdlink.uCAaTkz40dYjyzL","inter-router","in",null,250,null,0,0,23717,"4","enabled","up",23717,0,0,0,0,0,null],["qdlink.7IApVdoiBsie1s4","8","org.apache.qpid.dispatch.router.link","qdlink.7IApVdoiBsie1s4","inter-router","out",null,250,null,0,0,23717,"4","enabled","up",0,0,23717,0,0,0,null],["qdlink.WhYVBxoYjPd28OH","9","org.apache.qpid.dispatch.router.link","qdlink.WhYVBxoYjPd28OH","router-control","in",null,250,null,0,0,65412,"5","enabled","up",65412,0,0,0,0,0,null],["qdlink.ZforTKcJOgvL1rw","10","org.apache.qpid.dispatch.router.link","qdlink.ZforTKcJOgvL1rw","router-control","out","Lqdhello",250,null,0,0,73851,"5","enabled","up",73851,0,0,0,0,0,null],["qdlink.g2xEaALSGR7GbpB","11","org.apache.qpid.dispatch.router.link","qdlink.g2xEaALSGR7GbpB","inter-router","in",null,250,null,0,223,214661483,"5","enabled","up",23729,0,214637531,0,0,0,null],["qdlink.gKc+W6yyLIBAvIH","12","org.apache.qpid.dis
 patch.router.link","qdlink.gKc+W6yyLIBAvIH","inter-router","out",null,250,null,0,232,214112797,"5","enabled","up",0,0,214112565,0,0,0,null],["eef4b65e-88b0-ee48-ae83-714ff4b9483e","41","org.apache.qpid.dispatch.router.link","eef4b65e-88b0-ee48-ae83-714ff4b9483e","endpoint","out","Ltemp.s_bSvFZ+MBL6Tgt",250,null,0,0,817,"35","enabled","up",817,0,0,0,0,0,[227,210,58,58,58,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],["d2636e9c-fa68-8245-991d-3d45a7d32f05","42","org.apache.qpid.dispatch.router.link","d2636e9c-fa68-8245-991d-3d45a7d32f05","endpoint","in",null,250,null,0,0,817,"35","enabled","up",0,0,817,0,0,0,null],["30a1137c-5f31-2b44-b02c-6701d89f50c7","43","org.apache.qpid.dispatch.router.link","30a1137c-5f31-2b44-b02c-6701d89f50c7","endpoint","out","Ltemp.M+hukZfxvBQl62r",250,null,0
 ,0,120,"37","enabled","up",120,0,0,0,0,0,[36,31,7,7,8,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],["e524f82a-e2dd-854d-9e7f-620b0dad4e47","44","org.apache.qpid.dispatch.router.link","e524f82a-e2dd-854d-9e7f-620b0dad4e47","endpoint","in",null,250,null,0,2,123,"37","enabled","up",0,0,121,0,0,0,null]]},"router":{"attributeNames":["name","version","name"],"results":[["D","1.2.0-SNAPSHOT","D"]]},"listener":{"results":[["normal","5673",true],["inter-router","2001",false]],"attributeNames":["role","port","http"]}},"amqp:/_topo/0/C/$management":{"router.node":{"results":[["C","(self)"],["A",null],["B",null],["D",null],["E","D"],["F","D"]],"attributeNames":["id","nextHop"]},"connection":{"attributeNames":["name","identity","host","role","dir","container","sasl","isAuthenticated","user","isEn
 crypted","sslProto","sslCipher","properties","sslSsf","tenant","type","ssl","opened"],"results":[["connection/127.0.0.1:42042","2","127.0.0.1:42042","inter-router","in","A","ANONYMOUS",true,"anonymous",false,null,null,{"product":"qpid-dispatch-router","version":"1.2.0-SNAPSHOT"},0,null,"org.apache.qpid.dispatch.connection",false,true],["connection/127.0.0.1:42044","3","127.0.0.1:42044","inter-router","in","B","ANONYMOUS",true,"anonymous",false,null,null,{"product":"qpid-dispatch-router","version":"1.2.0-SNAPSHOT"},0,null,"org.apache.qpid.dispatch.connection",false,true],["connection/0.0.0.0:2001","4","0.0.0.0:2001","inter-router","out","D","ANONYMOUS",true,null,false,null,null,{"product":"qpid-dispatch-router","version":"1.2.0-SNAPSHOT"},0,null,"org.apache.qpid.dispatch.connection",false,true]]},"router.link":{"attributeNames":["name","identity","type","linkName","linkType","linkDir","owningAddr","capacity","peer","undeliveredCount","unsettledCount","deliveryCount","connectionId","a
 dminStatus","operStatus","presettledCount","droppedPresettledCount","acceptedCount","rejectedCount","releasedCount","modifiedCount","ingressHistogram"],"results":[["qdlink.m3KDAkh4XWFmTF4","1","org.apache.qpid.dispatch.router.link","qdlink.m3KDAkh4XWFmTF4","router-control","out","Lqdhello",250,null,0,0,73865,"2","enabled","up",73865,0,0,0,0,0,null],["qdlink.5A74B_72F2tpPTO","2","org.apache.qpid.dispatch.router.link","qdlink.5A74B_72F2tpPTO","router-control","in",null,250,null,0,0,65405,"2","enabled","up",65405,0,0,0,0,0,null],["qdlink.GwfDttXBw7jsEci","3","org.apache.qpid.dispatch.router.link","qdlink.GwfDttXBw7jsEci","inter-router","out",null,250,null,0,0,46871,"2","enabled","up",0,0,46871,0,0,0,null],["qdlink.ZDs9rTrIFNLMjmK","4","org.apache.qpid.dispatch.router.link","qdlink.ZDs9rTrIFNLMjmK","inter-router","in",null,250,null,0,0,46871,"2","enabled","up",46871,0,0,0,0,0,null],["qdlink.bqvnMED2goA3BuE","5","org.apache.qpid.dispatch.router.link","qdlink.bqvnMED2goA3BuE","router-cont
 rol","out","Lqdhello",250,null,0,0,73866,"3","enabled","up",73866,0,0,0,0,0,null],["qdlink.yvvxh_IOqw4DsKe","6","org.apache.qpid.dispatch.router.link","qdlink.yvvxh_IOqw4DsKe","router-control","in",null,250,null,0,0,65420,"3","enabled","up",65420,0,0,0,0,0,null],["qdlink.MBLypLUMaK4ymNV","7","org.apache.qpid.dispatch.router.link","qdlink.MBLypLUMaK4ymNV","inter-router","out",null,250,null,0,214,214661484,"3","enabled","up",0,0,214661270,0,0,0,null],["qdlink.mGkw33coTYTwtn5","8","org.apache.qpid.dispatch.router.link","qdlink.mGkw33coTYTwtn5","inter-router","in",null,250,null,0,245,214112811,"3","enabled","up",23730,0,214088836,0,0,0,null],["qdlink.iiY_WYQqdKOZjjf","9","org.apache.qpid.dispatch.router.link","qdlink.iiY_WYQqdKOZjjf","router-control","in",null,250,null,0,0,69650,"4","enabled","up",69650,0,0,0,0,0,null],["qdlink.ad3E15bOZ4387CG","10","org.apache.qpid.dispatch.router.link","qdlink.ad3E15bOZ4387CG","router-control","out","Lqdhello",250,null,0,0,69643,"4","enabled","up",696
 43,0,0,0,0,0,null],["qdlink.RfxRiUQpPAmV7ow","11","org.apache.qpid.dispatch.router.link","qdlink.RfxRiUQpPAmV7ow","inter-router","in",null,250,null,0,214,214732071,"4","enabled","up",0,0,214731857,0,0,0,null],["qdlink.0x5Gje+NwvMzFpo","12","org.apache.qpid.dispatch.router.link","qdlink.0x5Gje+NwvMzFpo","inter-router","out",null,250,null,0,245,214183397,"4","enabled","up",94316,0,214088836,0,0,0,null]]}},"amqp:/_topo/0/A/$management":{"connection":{"attributeNames":["name","identity","host","role","dir","container","sasl","isAuthenticated","user","isEncrypted","sslProto","sslCipher","properties","sslSsf","tenant","type","ssl","opened"],"results":[["connection/0.0.0.0:2000","2","0.0.0.0:2000","inter-router","out","C","ANONYMOUS",true,null,false,null,null,{"product":"qpid-dispatch-router","version":"1.2.0-SNAPSHOT"},0,null,"org.apache.qpid.dispatch.connection",false,true]]},"router.node":{"results":[["A","(self)"],["C",null],["B","C"],["D","C"],["E","C"],["F","C"]],"attributeNames":["i
 d","nextHop"]},"router.link":{"attributeNames":["name","identity","type","linkName","linkType","linkDir","owningAddr","capacity","peer","undeliveredCount","unsettledCount","deliveryCount","connectionId","adminStatus","operStatus","presettledCount","droppedPresettledCount","acceptedCount","rejectedCount","releasedCount","modifiedCount","ingressHistogram"],"results":[["qdlink.m3KDAkh4XWFmTF4","1","org.apache.qpid.dispatch.router.link","qdlink.m3KDAkh4XWFmTF4","router-control","in",null,250,null,0,0,73865,"2","enabled","up",73865,0,0,0,0,0,null],["qdlink.5A74B_72F2tpPTO","2","org.apache.qpid.dispatch.router.link","qdlink.5A74B_72F2tpPTO","router-control","out","Lqdhello",250,null,0,0,65405,"2","enabled","up",65405,0,0,0,0,0,null],["qdlink.GwfDttXBw7jsEci","3","org.apache.qpid.dispatch.router.link","qdlink.GwfDttXBw7jsEci","inter-router","in",null,250,null,0,0,46871,"2","enabled","up",0,0,46871,0,0,0,null],["qdlink.ZDs9rTrIFNLMjmK","4","org.apache.qpid.dispatch.router.link","qdlink.ZDs9
 rTrIFNLMjmK","inter-router","out",null,250,null,0,0,46870,"2","enabled","up",46870,0,0,0,0,0,null]]}},"amqp:/_topo/0/B/$management":{"connection":{"attributeNames":["name","identity","host","role","dir","container","sasl","isAuthenticated","user","isEncrypted","sslProto","sslCipher","properties","sslSsf","tenant","type","ssl","opened"],"results":[["connection/127.0.0.1:51142","3","127.0.0.1:51142","normal","in","e7bad48b-3f38-496f-bc3e-b2696e289b9f","ANONYMOUS",true,"anonymous",false,null,null,{},0,null,"org.apache.qpid.dispatch.connection",false,true],["connection/127.0.0.1:51140","2","127.0.0.1:51140","normal","in","33709f9b-70bd-4fc4-938a-74740b6d8c24","ANONYMOUS",true,"anonymous",false,null,null,{},0,null,"org.apache.qpid.dispatch.connection",false,true],["connection/0.0.0.0:2000","4","0.0.0.0:2000","inter-router","out","C","ANONYMOUS",true,null,false,null,null,{"product":"qpid-dispatch-router","version":"1.2.0-SNAPSHOT"},0,null,"org.apache.qpid.dispatch.connection",false,true]]
 },"router.node":{"results":[["B","(self)"],["C",null],["A","C"],["D","C"],["E","C"],["F","C"]],"attributeNames":["id","nextHop"]},"router.link":{"attributeNames":["name","identity","type","linkName","linkType","linkDir","owningAddr","capacity","peer","undeliveredCount","unsettledCount","deliveryCount","connectionId","adminStatus","operStatus","presettledCount","droppedPresettledCount","acceptedCount","rejectedCount","releasedCount","modifiedCount","ingressHistogram"],"results":[["e7bad48b-3f38-496f-bc3e-b2696e289b9f-toF","1","org.apache.qpid.dispatch.router.link","e7bad48b-3f38-496f-bc3e-b2696e289b9f-toF","endpoint","in","M0toF",250,null,0,243,214089079,"3","enabled","up",0,0,214088836,0,0,0,null],["33709f9b-70bd-4fc4-938a-74740b6d8c24-toB","2","org.apache.qpid.dispatch.router.link","33709f9b-70bd-4fc4-938a-74740b6d8c24-toB","endpoint","out","M0toB",250,null,205,9,214637549,"2","enabled","up",0,0,214637540,0,0,0,[0,0,0,0,0,214637540,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],["qdlink.bqvnMED2goA3BuE","3","org.apache.qpid.dispatch.router.link","qdlink.bqvnMED2goA3BuE","router-control","in",null,250,null,0,0,73865,"4","enabled","up",73865,0,0,0,0,0,null],["qdlink.yvvxh_IOqw4DsKe","4","org.apache.qpid.dispatch.router.link","qdlink.yvvxh_IOqw4DsKe","router-control","out","Lqdhello",250,null,0,0,65420,"4","enabled","up",65420,0,0,0,0,0,null],["qdlink.MBLypLUMaK4ymNV","5","org.apache.qpid.dispatch.router.link","qdlink.MBLypLUMaK4ymNV","inter-router","in",null,250,null,0,214,214661484,"4","enabled","up",0,0,214661270,0,0,0,null],["qdlink.mGkw33coTYTwtn5","6","org.apache.qpid.dispatch.router.link","qdlink.mGkw33coTYTwtn5","inter-router","out",null,250,null,0,245,214112810,"4","enabled","up",23729,0,214088836,0,0,0,null]]}},"amqp:/_topo/0/E/$management":{"connection":{"
 attributeNames":["name","identity","host","role","dir","container","sasl","isAuthenticated","user","isEncrypted","sslProto","sslCipher","properties","sslSsf","tenant","type","ssl","opened"],"results":[["connection/127.0.0.1:32944","1","127.0.0.1:32944","inter-router","in","D","ANONYMOUS",true,"anonymous",false,null,null,{"product":"qpid-dispatch-router","version":"1.2.0-SNAPSHOT"},0,null,"org.apache.qpid.dispatch.connection",false,true]]},"router.node":{"results":[["E","(self)"],["D",null],["C","D"],["F","D"],["A","D"],["B","D"]],"attributeNames":["id","nextHop"]},"listener":{"results":[["normal","22004",false],["inter-router","2002",false]],"attributeNames":["role","port","http"]},"router":{"attributeNames":["name","version","name"],"results":[["E","1.2.0-SNAPSHOT","E"]]},"router.link":{"attributeNames":["name","identity","type","linkName","linkType","linkDir","owningAddr","capacity","peer","undeliveredCount","unsettledCount","deliveryCount","connectionId","adminStatus","operStatus
 ","presettledCount","droppedPresettledCount","acceptedCount","rejectedCount","releasedCount","modifiedCount","ingressHistogram"],"results":[["qdlink.g8g0G5os5DBBLZt","1","org.apache.qpid.dispatch.router.link","qdlink.g8g0G5os5DBBLZt","router-control","out","Lqdhello",250,null,0,0,65399,"1","enabled","up",65399,0,0,0,0,0,null],["qdlink.GNu2meWTw_cQnmz","2","org.apache.qpid.dispatch.router.link","qdlink.GNu2meWTw_cQnmz","router-control","in",null,250,null,0,0,73850,"1","enabled","up",73850,0,0,0,0,0,null],["qdlink.uCAaTkz40dYjyzL","3","org.apache.qpid.dispatch.router.link","qdlink.uCAaTkz40dYjyzL","inter-router","out",null,250,null,0,0,23717,"1","enabled","up",23717,0,0,0,0,0,null],["qdlink.7IApVdoiBsie1s4","4","org.apache.qpid.dispatch.router.link","qdlink.7IApVdoiBsie1s4","inter-router","in",null,250,null,0,0,23718,"1","enabled","up",0,0,23718,0,0,0,null]]}},"amqp:/_topo/0/F/$management":{"router.node":{"results":[["F","(self)"],["D",null],["C","D"],["E","D"],["A","D"],["B","D"]],"a
 ttributeNames":["id","nextHop"]},"connection":{"attributeNames":["name","identity","host","role","dir","container","sasl","isAuthenticated","user","isEncrypted","sslProto","sslCipher","properties","sslSsf","tenant","type","ssl","opened"],"results":[["connection/127.0.0.1:57110","1","127.0.0.1:57110","inter-router","in","D","ANONYMOUS",true,"anonymous",false,null,null,{"product":"qpid-dispatch-router","version":"1.2.0-SNAPSHOT"},0,null,"org.apache.qpid.dispatch.connection",false,true],["connection/127.0.0.1:50830","2","127.0.0.1:50830","normal","in","776690cc-4dd4-4a21-8432-960ae8b5232a","ANONYMOUS",true,"anonymous",false,null,null,{},0,null,"org.apache.qpid.dispatch.connection",false,true],["connection/127.0.0.1:50832","3","127.0.0.1:50832","normal","in","dbd0eed0-6b6a-471b-bebf-267e237c6f61","ANONYMOUS",true,"anonymous",false,null,null,{},0,null,"org.apache.qpid.dispatch.connection",false,true]]},"router.link":{"attributeNames":["name","identity","type","linkName","linkType","linkD
 ir","owningAddr","capacity","peer","undeliveredCount","unsettledCount","deliveryCount","connectionId","adminStatus","operStatus","presettledCount","droppedPresettledCount","acceptedCount","rejectedCount","releasedCount","modifiedCount","ingressHistogram"],"results":[["qdlink.WhYVBxoYjPd28OH","1","org.apache.qpid.dispatch.router.link","qdlink.WhYVBxoYjPd28OH","router-control","out","Lqdhello",250,null,0,0,65412,"1","enabled","up",65412,0,0,0,0,0,null],["qdlink.ZforTKcJOgvL1rw","2","org.apache.qpid.dispatch.router.link","qdlink.ZforTKcJOgvL1rw","router-control","in",null,250,null,0,0,73851,"1","enabled","up",73851,0,0,0,0,0,null],["qdlink.g2xEaALSGR7GbpB","3","org.apache.qpid.dispatch.router.link","qdlink.g2xEaALSGR7GbpB","inter-router","out",null,250,null,0,232,214661501,"1","enabled","up",23729,0,214637540,0,0,0,null],["qdlink.gKc+W6yyLIBAvIH","4","org.apache.qpid.dispatch.router.link","qdlink.gKc+W6yyLIBAvIH","inter-router","in",null,250,null,0,245,214112811,"1","enabled","up",0,0,
 214112566,0,0,0,null],["776690cc-4dd4-4a21-8432-960ae8b5232a-toF","5","org.apache.qpid.dispatch.router.link","776690cc-4dd4-4a21-8432-960ae8b5232a-toF","endpoint","out","M0toF",250,null,236,9,214088845,"2","enabled","up",0,0,214088836,0,0,0,[0,0,0,0,0,214088836,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],["dbd0eed0-6b6a-471b-bebf-267e237c6f61-toB","6","org.apache.qpid.dispatch.router.link","dbd0eed0-6b6a-471b-bebf-267e237c6f61-toB","endpoint","in","M0toB",250,null,0,232,214637772,"3","enabled","up",0,0,214637540,0,0,0,null]]}}}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/test/utilities.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/test/utilities.js b/console/stand-alone/test/utilities.js
new file mode 100644
index 0000000..56c751e
--- /dev/null
+++ b/console/stand-alone/test/utilities.js
@@ -0,0 +1,192 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+/* global describe it */
+var assert = require('assert');
+var expect = require('chai').expect;
+import { QDRService } from '../plugin/js/qdrService.js';
+
+class Log {
+  constructor() {
+  }
+  log (msg) {}
+  debug (msg) {}
+  error (msg) {}
+  info (msg) {}
+  warn (msg) {}
+}
+var log = new Log();
+var loc = {protocol: function () { return 'http://';}};
+var timeout = function (f) {f();};
+var qdrService = new QDRService(log, timeout, loc);
+
+describe('Management utilities', function() {
+  describe('#nameFromId', function() {
+    it('should extract name from id', function() {
+      let name = qdrService.utilities.nameFromId('amqp:/topo/0/routerName/$management');
+      assert.equal(name, 'routerName');
+    });
+    it('should extract name with / from id', function() {
+      let name = qdrService.utilities.nameFromId('amqp:/topo/0/router/Name/$management');
+      assert.equal(name, 'router/Name');
+    });
+  });
+  describe('#valFor', function() {
+    let aAr = ['name', 'value'];
+    let vAr = [['mary', 'lamb']];
+    it('should return correct value for key', function() {
+      let name = qdrService.utilities.valFor(aAr, vAr[0], 'name');
+      assert.equal(name, 'mary');
+    });
+    it('should return null if key is not found', function() {
+      let name = qdrService.utilities.valFor(aAr, vAr, 'address');
+      assert.equal(name, null);
+    });
+  });
+  describe('#pretty', function() {
+    it('should return unchanged if not a number', function() {
+      let val = qdrService.utilities.pretty('foo');
+      assert.equal(val, 'foo');
+    });
+    it('should add commas to numbers', function() {
+      let val = qdrService.utilities.pretty('1234');
+      assert.equal(val, '1,234');
+    });
+  });
+  describe('#humanify', function() {
+    it('should handle empty strings', function() {
+      let val = qdrService.utilities.humanify('');
+      assert.equal(val, '');
+    });
+    it('should handle undefined input', function() {
+      let val = qdrService.utilities.humanify();
+      assert.equal(val, undefined);
+    });
+    it('should capitalize the first letter', function() {
+      let val = qdrService.utilities.humanify('foo');
+      assert.equal(val, 'Foo');
+    });
+    it('should split on all capital letters', function() {
+      let val = qdrService.utilities.humanify('fooBarBaz');
+      assert.equal(val, 'Foo Bar Baz');
+    });
+  });
+  describe('#addr_class', function() {
+    it('should handle unknown address types', function() {
+      let val = qdrService.utilities.addr_class(' ');
+      assert.equal(val, 'unknown:  ');
+    });
+    it('should handle undefined input', function() {
+      let val = qdrService.utilities.addr_class();
+      assert.equal(val, '-');
+    });
+    it('should identify mobile addresses', function() {
+      let val = qdrService.utilities.addr_class('Mfoo');
+      assert.equal(val, 'mobile');
+    });
+    it('should identify router addresses', function() {
+      let val = qdrService.utilities.addr_class('Rfoo');
+      assert.equal(val, 'router');
+    });
+    it('should identify area addresses', function() {
+      let val = qdrService.utilities.addr_class('Afoo');
+      assert.equal(val, 'area');
+    });
+    it('should identify local addresses', function() {
+      let val = qdrService.utilities.addr_class('Lfoo');
+      assert.equal(val, 'local');
+    });
+    it('should identify link-incoming C addresses', function() {
+      let val = qdrService.utilities.addr_class('Cfoo');
+      assert.equal(val, 'link-incoming');
+    });
+    it('should identify link-incoming E addresses', function() {
+      let val = qdrService.utilities.addr_class('Efoo');
+      assert.equal(val, 'link-incoming');
+    });
+    it('should identify link-outgoing D addresses', function() {
+      let val = qdrService.utilities.addr_class('Dfoo');
+      assert.equal(val, 'link-outgoing');
+    });
+    it('should identify link-outgoing F addresses', function() {
+      let val = qdrService.utilities.addr_class('Dfoo');
+      assert.equal(val, 'link-outgoing');
+    });
+    it('should identify topo addresses', function() {
+      let val = qdrService.utilities.addr_class('Tfoo');
+      assert.equal(val, 'topo');
+    });
+  });
+  describe('#addr_text', function() {
+    it('should handle undefined input', function() {
+      let val = qdrService.utilities.addr_text();
+      assert.equal(val, '-');
+    });
+    it('should identify mobile addresses', function() {
+      let val = qdrService.utilities.addr_text('M0foo');
+      assert.equal(val, 'foo');
+    });
+    it('should identify non-mobile addresses', function() {
+      let val = qdrService.utilities.addr_text('Rfoo');
+      assert.equal(val, 'foo');
+    });
+  });
+  describe('#identity_clean', function() {
+    it('should handle undefined input', function() {
+      let val = qdrService.utilities.identity_clean();
+      assert.equal(val, '-');
+    });
+    it('should handle identities with no /', function() {
+      let val = qdrService.utilities.identity_clean('foo');
+      assert.equal(val, 'foo');
+    });
+    it('should return everything after the 1st /', function() {
+      let val = qdrService.utilities.identity_clean('foo/bar');
+      assert.equal(val, 'bar');
+    });
+  });
+  describe('#copy', function() {
+    it('should handle undefined input', function() {
+      let val = qdrService.utilities.copy();
+      assert.equal(val, undefined);
+    });
+    it('should copy all object values instead making references', function() {
+      let input = {a: 'original value'};
+      let output = qdrService.utilities.copy(input);
+      input.a = 'changed value';
+      assert.equal(output.a, 'original value');
+    });
+  });
+  describe('#flatten', function() {
+    it('should return an object when passed undefined input', function() {
+      let val = qdrService.utilities.flatten();
+      assert.equal(typeof val, 'object');
+    });
+    it('and the returned object should be empty', function() {
+      let val = qdrService.utilities.flatten();
+      assert.equal(Object.keys(val).length, 0);
+    });
+    it('should flatten the arrays into an object', function() {
+      let attributes = ['first', 'second'];
+      let value = ['1st', '2nd'];
+      let val = qdrService.utilities.flatten(attributes, value);
+      assert.equal(val.second, '2nd');
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/tsconfig.json
----------------------------------------------------------------------
diff --git a/console/stand-alone/tsconfig.json b/console/stand-alone/tsconfig.json
index 056e773..d89a961 100644
--- a/console/stand-alone/tsconfig.json
+++ b/console/stand-alone/tsconfig.json
@@ -1,7 +1,14 @@
 {
   "compilerOptions": {
-    "allowJs": true,
-    "checkJs": true,
+    "module": "commonjs",
+    "preserveConstEnums": true,
+    "newLine": "LF",
+    "target": "es5",
+    "moduleResolution": "node",
+    "noImplicitReturns": true,
+    "strict": true,
+    "declaration": true,
+    "declarationDir": "./typings",
     "lib": [
       "dom",
       "dom.iterable",
@@ -10,22 +17,14 @@
       "es7",
       "esnext",
       "esnext.asynciterable",
-      "es2015.iterable",
-      "es2017"
+      "es2015.iterable"
     ]
   },
   "compileOnSave": true,
   "include": [
-    "plugin/*"
+    "typings/*"
   ],
   "exclude": [
     "node_modules/*"
-  ],
-  "typeAcquisition": {
-    "enable": true
-  },
-  "files": [
-    "plugin/**/*.ts"
   ]
-
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/vendor-js.txt
----------------------------------------------------------------------
diff --git a/console/stand-alone/vendor-js.txt b/console/stand-alone/vendor-js.txt
index 5af21f4..4e11d07 100644
--- a/console/stand-alone/vendor-js.txt
+++ b/console/stand-alone/vendor-js.txt
@@ -17,7 +17,7 @@
 
 -- The following files get packaged into a single vendor.min.js
 
-node_modules/bluebird/js/browser/bluebird.min.js
+node_modules/babel-polyfill/dist/polyfill.min.js
 node_modules/jquery/dist/jquery.min.js
 node_modules/jquery-ui-dist/jquery-ui.min.js
 node_modules/jquery.fancytree/dist/jquery.fancytree-all.min.js
@@ -26,18 +26,18 @@ node_modules/angular-animate/angular-animate.min.js
 node_modules/angular-sanitize/angular-sanitize.min.js
 node_modules/angular-route/angular-route.min.js
 node_modules/angular-resource/angular-resource.min.js
-node_modules/bootstrap/dist/js/bootstrap.min.js
+node_modules/angular-ui-slider/src/slider.js
+node_modules/angular-ui-grid/ui-grid.min.js
 node_modules/angular-ui-bootstrap/dist/ui-bootstrap.js
 node_modules/angular-ui-bootstrap/dist/ui-bootstrap-tpls.js
+node_modules/angular-bootstrap-checkbox/angular-bootstrap-checkbox.js
+node_modules/bootstrap/dist/js/bootstrap.min.js
 node_modules/d3/d3.min.js
 node_modules/d3-queue/build/d3-queue.min.js
 node_modules/d3-time/build/d3-time.min.js
 node_modules/d3-time-format/build/d3-time-format.min.js
 node_modules/d3-path/build/d3-path.min.js
 node_modules/c3/c3.min.js
-node_modules/angular-ui-slider/src/slider.js
-node_modules/angular-ui-grid/ui-grid.min.js
-node_modules/angular-bootstrap-checkbox/angular-bootstrap-checkbox.js
 node_modules/notifyjs-browser/dist/notify.js
 node_modules/patternfly/dist/js/patternfly.min.js
-node_modules/dispatch-management/dist/dispatch-management.min.js
+node_modules/rhea/dist/rhea.js


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


[4/8] qpid-dispatch git commit: DISPATCH-1049 Add console tests

Posted by ea...@apache.org.
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/qdrList.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/qdrList.js b/console/stand-alone/plugin/js/qdrList.js
index db3a43f..6dafe20 100644
--- a/console/stand-alone/plugin/js/qdrList.js
+++ b/console/stand-alone/plugin/js/qdrList.js
@@ -16,713 +16,673 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-'use strict';
 /* global angular d3 */
 
-/**
- * @module QDR
- */
-var QDR = (function(QDR) {
-
-  /**
-   * Controller for the main interface
-   */
-  QDR.module.controller('QDR.ListController', ['$scope', '$location', '$uibModal', '$filter', '$timeout', 'QDRService', 'QDRChartService', 'uiGridConstants', '$sce',
-    function ($scope, $location, $uibModal, $filter, $timeout, QDRService, QDRChartService, uiGridConstants, $sce) {
-
-      QDR.log.debug('QDR.ListControll started with location of ' + $location.path() + ' and connection of  ' + QDRService.management.connection.is_connected());
-      let updateIntervalHandle = undefined,
-        updateInterval = 5000,
-        last_updated = 0,
-        updateNow = false,
-        ListExpandedKey = 'QDRListExpanded',
-        SelectedEntityKey = 'QDRSelectedEntity',
-        ActivatedKey = 'QDRActivatedKey';
-      $scope.details = {};
-
-      $scope.tmplListTree = QDR.templatePath + 'tmplListTree.html';
-      $scope.selectedEntity = localStorage[SelectedEntityKey] || 'address';
-      $scope.ActivatedKey = localStorage[ActivatedKey] || null;
-      if ($scope.selectedEntity == 'undefined')
-        $scope.selectedEntity = undefined;
-      $scope.selectedNode = localStorage['QDRSelectedNode'];
-      $scope.selectedNodeId = localStorage['QDRSelectedNodeId'];
-      $scope.selectedRecordName = localStorage['QDRSelectedRecordName'];
-      $scope.nodes = [];
-      $scope.currentNode = undefined;
-      $scope.modes = [
-        {
-          content: '<a><i class="icon-list"></i> Attributes</a>',
-          id: 'attributes',
-          op: 'READ',
-          title: 'View router attributes',
-          isValid: function () { return true; }
-        },
-        {
-          content: '<a><i class="icon-edit"></i> Update</a>',
-          id: 'operations',
-          op: 'UPDATE',
-          title: 'Update this attribute',
-          isValid: function () {
-            return $scope.operations.indexOf(this.op) > -1;
-          }
-        },
-        {
-          content: '<a><i class="icon-plus"></i> Create</a>',
-          id: 'operations',
-          op: 'CREATE',
-          title: 'Create a new attribute',
-          isValid: function () { return $scope.operations.indexOf(this.op) > -1; }
-        },
-        {
-          content: '<a><i class="icon-remove"></i> Delete</a>',
-          id: 'delete',
-          op: 'DELETE',
-          title: 'Delete',
-          isValid: function () { return $scope.operations.indexOf(this.op) > -1; }
-        },
-        {
-          content: '<a><i class="icon-eye-open"></i> Fetch</a>',
-          id: 'log',
-          op: 'GET-LOG',
-          title: 'Fetch recent log entries',
-          isValid: function () { return ($scope.selectedEntity === 'log'); }
+import { QDRFolder, QDRLeaf, QDRCore, QDRLogger, QDRTemplatePath, QDRRedirectWhenConnected} from './qdrGlobals.js';
+
+export class ListController {
+  constructor(QDRService, QDRChartService, $scope, $log, $location, $uibModal, $filter, $timeout, uiGridConstants, $sce) {
+    this.controllerName = 'QDR.ListController';
+
+    let QDRLog = new QDRLogger($log, 'ListController');
+
+    QDRLog.debug('QDR.ListControll started with location of ' + $location.path() + ' and connection of  ' + QDRService.management.connection.is_connected());
+    let updateIntervalHandle = undefined,
+      updateInterval = 5000,
+      last_updated = 0,
+      updateNow = false,
+      ListExpandedKey = 'QDRListExpanded',
+      SelectedEntityKey = 'QDRSelectedEntity',
+      ActivatedKey = 'QDRActivatedKey';
+    $scope.details = {};
+
+    $scope.tmplListTree = QDRTemplatePath + 'tmplListTree.html';
+    $scope.selectedEntity = localStorage[SelectedEntityKey] || 'address';
+    $scope.ActivatedKey = localStorage[ActivatedKey] || null;
+    if ($scope.selectedEntity == 'undefined')
+      $scope.selectedEntity = undefined;
+    $scope.selectedNode = localStorage['QDRSelectedNode'];
+    $scope.selectedNodeId = localStorage['QDRSelectedNodeId'];
+    $scope.selectedRecordName = localStorage['QDRSelectedRecordName'];
+    $scope.nodes = [];
+    $scope.currentNode = undefined;
+    $scope.modes = [
+      {
+        content: '<a><i class="icon-list"></i> Attributes</a>',
+        id: 'attributes',
+        op: 'READ',
+        title: 'View router attributes',
+        isValid: function () { return true; }
+      },
+      {
+        content: '<a><i class="icon-edit"></i> Update</a>',
+        id: 'operations',
+        op: 'UPDATE',
+        title: 'Update this attribute',
+        isValid: function () {
+          return $scope.operations.indexOf(this.op) > -1;
         }
-      ];
-      $scope.operations = [];
-      $scope.currentMode = $scope.modes[0];
-      $scope.isModeSelected = function (mode) {
-        return mode === $scope.currentMode;
-      };
-      $scope.fetchingLog = false;
-      $scope.selectMode = function (mode) {
-        $scope.currentMode = mode;
-        if (mode.id === 'log') {
-          $scope.logResults = [];
-          $scope.fetchingLog = true;
-          let entity; // undefined since it is not supported in the GET-LOG call
-          QDRService.management.connection.sendMethod($scope.currentNode.id, entity, {}, $scope.currentMode.op)
-            .then( function (response) {
-              let statusCode = response.context.message.application_properties.statusCode;
-              if (statusCode < 200 || statusCode >= 300) {
-                QDR.Core.notification('error', response.context.message.statusDescription);
-                QDR.log.error('Error ' + response.context.message.statusDescription);
-                return;
-              }
-              $timeout( function () {
-                $scope.fetchingLog = false;
-                $scope.logResults = response.response.filter( function (entry) {
-                  return entry[0] === $scope.detailsObject.module;
-                }).sort( function (a, b) {
-                  return b[5] - a[5];
-                }).map( function (entry) {
-                  return {
-                    type: entry[1],
-                    message: entry[2],
-                    source: entry[3],
-                    line: entry[4],
-                    time: Date(entry[5]).toString()
-                  };
-                });
+      },
+      {
+        content: '<a><i class="icon-plus"></i> Create</a>',
+        id: 'operations',
+        op: 'CREATE',
+        title: 'Create a new attribute',
+        isValid: function () { return $scope.operations.indexOf(this.op) > -1; }
+      },
+      {
+        content: '<a><i class="icon-remove"></i> Delete</a>',
+        id: 'delete',
+        op: 'DELETE',
+        title: 'Delete',
+        isValid: function () { return $scope.operations.indexOf(this.op) > -1; }
+      },
+      {
+        content: '<a><i class="icon-eye-open"></i> Fetch</a>',
+        id: 'log',
+        op: 'GET-LOG',
+        title: 'Fetch recent log entries',
+        isValid: function () { return ($scope.selectedEntity === 'log'); }
+      }
+    ];
+    $scope.operations = [];
+    $scope.currentMode = $scope.modes[0];
+    $scope.isModeSelected = function (mode) {
+      return mode === $scope.currentMode;
+    };
+    $scope.fetchingLog = false;
+    $scope.selectMode = function (mode) {
+      $scope.currentMode = mode;
+      if (mode.id === 'log') {
+        $scope.logResults = [];
+        $scope.fetchingLog = true;
+        let entity; // undefined since it is not supported in the GET-LOG call
+        QDRService.management.connection.sendMethod($scope.currentNode.id, entity, {}, $scope.currentMode.op)
+          .then( function (response) {
+            let statusCode = response.context.message.application_properties.statusCode;
+            if (statusCode < 200 || statusCode >= 300) {
+              QDRCore.notification('error', response.context.message.statusDescription);
+              QDRLog.error('Error ' + response.context.message.statusDescription);
+              return;
+            }
+            $timeout( function () {
+              $scope.fetchingLog = false;
+              $scope.logResults = response.response.filter( function (entry) {
+                return entry[0] === $scope.detailsObject.module;
+              }).sort( function (a, b) {
+                return b[5] - a[5];
+              }).map( function (entry) {
+                return {
+                  type: entry[1],
+                  message: entry[2],
+                  source: entry[3],
+                  line: entry[4],
+                  time: Date(entry[5]).toString()
+                };
               });
             });
-        }
-      };
-      $scope.isValid = function (mode) {
-        return mode.isValid();
-      };
-
-      $scope.expandAll = function () {
-        $('#entityTree').fancytree('getTree').visit(function(node){
-          node.setExpanded(true);
-        });
-      };
-      $scope.contractAll = function () {
-        $('#entityTree').fancytree('getTree').visit(function(node){
-          node.setExpanded(false);
-        });
-      };
-
-      if (!QDRService.management.connection.is_connected()) {
-      // we are not connected. we probably got here from a bookmark or manual page reload
-        QDR.redirectWhenConnected($location, 'list');
-        return;
+          });
       }
-
-      let excludedEntities = ['management', 'org.amqp.management', 'operationalEntity', 'entity', 'configurationEntity', 'dummy', 'console'];
-      let aggregateEntities = ['router.address'];
-
-      let classOverrides = {
-        'connection': function (row, nodeId) {
-          let isConsole = QDRService.utilities.isAConsole (row.properties.value, row.identity.value, row.role.value, nodeId);
-          return isConsole ? 'console' : row.role.value === 'inter-router' ? 'inter-router' : 'external';
-        },
-        'router.link': function (row, nodeId) {
-          let link = {nodeId: nodeId, connectionId: row.connectionId.value};
-
-          let isConsole = QDRService.utilities.isConsole(QDRService.management.topology.getConnForLink(link));
-          return isConsole ? 'console' : row.linkType.value;
-        },
-        'router.address': function (row) {
-          let identity = QDRService.utilities.identity_clean(row.identity.value);
-          let address = QDRService.utilities.addr_text(identity);
-          let cls = QDRService.utilities.addr_class(identity);
-          if (address === '$management')
-            cls = 'internal ' + cls;
-          return cls;
+    };
+    $scope.isValid = function (mode) {
+      return mode.isValid();
+    };
+
+    $scope.expandAll = function () {
+      $('#entityTree').fancytree('getTree').visit(function(node){
+        node.setExpanded(true);
+      });
+    };
+    $scope.contractAll = function () {
+      $('#entityTree').fancytree('getTree').visit(function(node){
+        node.setExpanded(false);
+      });
+    };
+
+    if (!QDRService.management.connection.is_connected()) {
+    // we are not connected. we probably got here from a bookmark or manual page reload
+      QDRRedirectWhenConnected($location, 'list');
+      return;
+    }
+
+    let excludedEntities = ['management', 'org.amqp.management', 'operationalEntity', 'entity', 'configurationEntity', 'dummy', 'console'];
+    let aggregateEntities = ['router.address'];
+
+    let classOverrides = {
+      'connection': function (row, nodeId) {
+        let isConsole = QDRService.utilities.isAConsole (row.properties.value, row.identity.value, row.role.value, nodeId);
+        return isConsole ? 'console' : row.role.value === 'inter-router' ? 'inter-router' : 'external';
+      },
+      'router.link': function (row, nodeId) {
+        let link = {nodeId: nodeId, connectionId: row.connectionId.value};
+
+        let isConsole = QDRService.utilities.isConsole(QDRService.management.topology.getConnForLink(link));
+        return isConsole ? 'console' : row.linkType.value;
+      },
+      'router.address': function (row) {
+        let identity = QDRService.utilities.identity_clean(row.identity.value);
+        let address = QDRService.utilities.addr_text(identity);
+        let cls = QDRService.utilities.addr_class(identity);
+        if (address === '$management')
+          cls = 'internal ' + cls;
+        return cls;
+      }
+    };
+
+    var lookupOperations = function () {
+      let ops = QDRService.management.schema().entityTypes[$scope.selectedEntity].operations.filter( function (op) { return op !== 'READ';});
+      $scope.operation = ops.length ? ops[0] : '';
+      return ops;
+    };
+    let entityTreeChildren = [];
+    let expandedList = angular.fromJson(localStorage[ListExpandedKey]) || [];
+    let saveExpanded = function () {
+    // save the list of entities that are expanded
+      let tree = $('#entityTree').fancytree('getTree');
+      let list = [];
+      tree.visit( function (tnode) {
+        if (tnode.isExpanded()) {
+          list.push(tnode.key);
         }
-      };
-
-      var lookupOperations = function () {
-        let ops = QDRService.management.schema().entityTypes[$scope.selectedEntity].operations.filter( function (op) { return op !== 'READ';});
-        $scope.operation = ops.length ? ops[0] : '';
-        return ops;
-      };
-      let entityTreeChildren = [];
-      let expandedList = angular.fromJson(localStorage[ListExpandedKey]) || [];
-      let saveExpanded = function () {
-      // save the list of entities that are expanded
-        let tree = $('#entityTree').fancytree('getTree');
-        let list = [];
-        tree.visit( function (tnode) {
-          if (tnode.isExpanded()) {
-            list.push(tnode.key);
-          }
-        });
-        console.log('saving expanded list');
-        console.log(list);
-        localStorage[ListExpandedKey] = JSON.stringify(list);
-      };
-
-      var onTreeNodeBeforeActivate = function (event, data) {
-      // if node is toplevel entity
-        if (data.node.data.typeName === 'entity') {
-          return false;
-          /*
-          // if the current active node is not this one and not one of its children
-          let active = data.tree.getActiveNode();
-          if (active && !data.node.isActive() && data.node.isExpanded()) {  // there is an active node and it's not this one
-            let any = false;
-            let children = data.node.getChildren();
-            if (children) {
-              any = children.some( function (child) {
-                return child.key === active.key;
-              });
-            }
-            if (!any) // none of the clicked on node's children was active
-              return false;  // don't activate, just collapse this top level node
+      });
+      console.log('saving expanded list');
+      console.log(list);
+      localStorage[ListExpandedKey] = JSON.stringify(list);
+    };
+
+    var onTreeNodeBeforeActivate = function (event, data) {
+    // if node is toplevel entity
+      if (data.node.data.typeName === 'entity') {
+        return false;
+        /*
+        // if the current active node is not this one and not one of its children
+        let active = data.tree.getActiveNode();
+        if (active && !data.node.isActive() && data.node.isExpanded()) {  // there is an active node and it's not this one
+          let any = false;
+          let children = data.node.getChildren();
+          if (children) {
+            any = children.some( function (child) {
+              return child.key === active.key;
+            });
           }
-          */
-        }
-        return true;
-      };
-      var onTreeNodeExpanded = function () {
-        saveExpanded();
-        updateExpandedEntities();
-      };
-      var onTreeNodeCollapsed = function () {
-        saveExpanded();
-      };
-      // a tree node was selected
-      var onTreeNodeActivated = function (event, data) {
-        $scope.ActivatedKey = data.node.key;
-        let selectedNode = data.node;
-        $scope.selectedTreeNode = data.node;
-        if ($scope.currentMode.id === 'operations')
-          $scope.currentMode = $scope.modes[0];
-        else if ($scope.currentMode.id === 'log')
-          $scope.selectMode($scope.currentMode);
-        else if ($scope.currentMode.id === 'delete') {
-          // clicked on a tree node while on the delete screen -> switch to attribute screen
-          $scope.currentMode = $scope.modes[0];
+          if (!any) // none of the clicked on node's children was active
+            return false;  // don't activate, just collapse this top level node
         }
-        if (selectedNode.data.typeName === 'entity') {
-          $scope.selectedEntity = selectedNode.key;
-          $scope.operations = lookupOperations();
+        */
+      }
+      return true;
+    };
+    var onTreeNodeExpanded = function () {
+      saveExpanded();
+      updateExpandedEntities();
+    };
+    var onTreeNodeCollapsed = function () {
+      saveExpanded();
+    };
+    // a tree node was selected
+    var onTreeNodeActivated = function (event, data) {
+      $scope.ActivatedKey = data.node.key;
+      let selectedNode = data.node;
+      $scope.selectedTreeNode = data.node;
+      if ($scope.currentMode.id === 'operations')
+        $scope.currentMode = $scope.modes[0];
+      else if ($scope.currentMode.id === 'log')
+        $scope.selectMode($scope.currentMode);
+      else if ($scope.currentMode.id === 'delete') {
+        // clicked on a tree node while on the delete screen -> switch to attribute screen
+        $scope.currentMode = $scope.modes[0];
+      }
+      if (selectedNode.data.typeName === 'entity') {
+        $scope.selectedEntity = selectedNode.key;
+        $scope.operations = lookupOperations();
+        updateNow = true;
+      } else if (selectedNode.data.typeName === 'attribute') {
+        if (!selectedNode.parent)
+          return;
+        let sameEntity = $scope.selectedEntity === selectedNode.parent.key;
+        $scope.selectedEntity = selectedNode.parent.key;
+        $scope.operations = lookupOperations();
+        $scope.selectedRecordName = selectedNode.key;
+        updateDetails(selectedNode.data.details);   // update the table on the right
+        if (!sameEntity) {
           updateNow = true;
-        } else if (selectedNode.data.typeName === 'attribute') {
-          if (!selectedNode.parent)
-            return;
-          let sameEntity = $scope.selectedEntity === selectedNode.parent.key;
-          $scope.selectedEntity = selectedNode.parent.key;
-          $scope.operations = lookupOperations();
-          $scope.selectedRecordName = selectedNode.key;
-          updateDetails(selectedNode.data.details);   // update the table on the right
-          if (!sameEntity) {
-            updateNow = true;
-          }
-        } else if (selectedNode.data.typeName === 'none') {
-          $scope.selectedEntity = selectedNode.parent.key;
-          $scope.selectedRecordName = $scope.selectedEntity;
-          updateDetails(fromSchema($scope.selectedEntity));
-        }
-      };
-      var getExpanded = function (tree) {
-        let list = [];
-        tree.visit( function (tnode) {
-          if (tnode.isExpanded()) {
-            list.push(tnode);
-          }
-        });
-        return list;
-      };
-      // fill in an empty results recoord based on the entities schema
-      var fromSchema = function (entityName) {
-        let row = {};
-        let schemaEntity = QDRService.management.schema().entityTypes[entityName];
-        for (let attr in schemaEntity.attributes) {
-          let entity = schemaEntity.attributes[attr];
-          let value = '';
-          if (angular.isDefined(entity['default'])) {
-            if (entity['type'] === 'integer')
-              value = parseInt(entity['default']); // some default values that are marked as integer are passed as string
-            else
-              value = entity['default'];
-          }
-          row[attr] = {
-            value: value,
-            type: entity.type,
-            graph: false,
-            title: entity.description,
-            aggregate: false,
-            aggregateTip: '',
-            'default': entity['default']
-          };
         }
-        return row;
-      };
-      $scope.hasCreate = function () {
-        let schemaEntity = QDRService.management.schema().entityTypes[$scope.selectedEntity];
-        return (schemaEntity.operations.indexOf('CREATE') > -1);
-      };
-
-      var getActiveChild = function (node) {
-        let active = node.children.filter(function (child) {
-          return child.isActive();
-        });
-        if (active.length > 0)
-          return active[0].key;
-        return null;
-      };
-      // the data for the selected entity is available, populate the tree on the left
-      var updateTreeChildren = function (entity, tableRows, expand) {
-        let tree = $('#entityTree').fancytree('getTree'), node, newNode;
-        if (tree && tree.getNodeByKey) {
-          node = tree.getNodeByKey(entity);
+      } else if (selectedNode.data.typeName === 'none') {
+        $scope.selectedEntity = selectedNode.parent.key;
+        $scope.selectedRecordName = $scope.selectedEntity;
+        updateDetails(fromSchema($scope.selectedEntity));
+      }
+    };
+    var getExpanded = function (tree) {
+      let list = [];
+      tree.visit( function (tnode) {
+        if (tnode.isExpanded()) {
+          list.push(tnode);
         }
-        if (!tree || !node) {
-          return;
+      });
+      return list;
+    };
+    // fill in an empty results recoord based on the entities schema
+    var fromSchema = function (entityName) {
+      let row = {};
+      let schemaEntity = QDRService.management.schema().entityTypes[entityName];
+      for (let attr in schemaEntity.attributes) {
+        let entity = schemaEntity.attributes[attr];
+        let value = '';
+        if (angular.isDefined(entity['default'])) {
+          if (entity['type'] === 'integer')
+            value = parseInt(entity['default']); // some default values that are marked as integer are passed as string
+          else
+            value = entity['default'];
         }
-        let wasActive = node.isActive();
-        let wasExpanded = node.isExpanded();
-        let activeChildKey = getActiveChild(node);
-        node.removeChildren();
-        if (tableRows.length == 0) {
-          newNode = {
-            extraClasses:   'no-data',
-            typeName:   'none',
-            title:      'no data',
-            key:        node.key + '.1'
-          };
-          node.addNode(newNode);
-          if (expand) {
-            updateDetails(fromSchema(entity));
-            $scope.selectedRecordName = entity;
-          }
-        } else {
-          let children = tableRows.map( function (row) {
-            let addClass = entity;
-            if (classOverrides[entity]) {
-              addClass += ' ' + classOverrides[entity](row, $scope.currentNode.id);
-            }
-            let child = {
-              typeName:   'attribute',
-              extraClasses:   addClass,
-              tooltip:    addClass,
-              key:        row.name.value,
-              title:      row.name.value,
-              details:    row
-            };
-            return child;
-          });
-          node.addNode(children);
+        row[attr] = {
+          value: value,
+          type: entity.type,
+          graph: false,
+          title: entity.description,
+          aggregate: false,
+          aggregateTip: '',
+          'default': entity['default']
+        };
+      }
+      return row;
+    };
+    $scope.hasCreate = function () {
+      let schemaEntity = QDRService.management.schema().entityTypes[$scope.selectedEntity];
+      return (schemaEntity.operations.indexOf('CREATE') > -1);
+    };
+
+    var getActiveChild = function (node) {
+      let active = node.children.filter(function (child) {
+        return child.isActive();
+      });
+      if (active.length > 0)
+        return active[0].key;
+      return null;
+    };
+    // the data for the selected entity is available, populate the tree on the left
+    var updateTreeChildren = function (entity, tableRows, expand) {
+      let tree = $('#entityTree').fancytree('getTree'), node, newNode;
+      if (tree && tree.getNodeByKey) {
+        node = tree.getNodeByKey(entity);
+      }
+      if (!tree || !node) {
+        return;
+      }
+      let wasActive = node.isActive();
+      let wasExpanded = node.isExpanded();
+      let activeChildKey = getActiveChild(node);
+      node.removeChildren();
+      if (tableRows.length == 0) {
+        newNode = {
+          extraClasses:   'no-data',
+          typeName:   'none',
+          title:      'no data',
+          key:        node.key + '.1'
+        };
+        node.addNode(newNode);
+        if (expand) {
+          updateDetails(fromSchema(entity));
+          $scope.selectedRecordName = entity;
         }
-        // top level node was expanded
-        if (wasExpanded)
-          node.setExpanded(true, {noAnimation: true, noEvents: true, noFocus: true});
-        // if the parent node was active, but none of the children were active, active the 1st child
-        if (wasActive) {
-          if (!activeChildKey) {
-            activeChildKey = node.children[0].key;
+      } else {
+        let children = tableRows.map( function (row) {
+          let addClass = entity;
+          if (classOverrides[entity]) {
+            addClass += ' ' + classOverrides[entity](row, $scope.currentNode.id);
           }
-        }
-        if (!tree.getActiveNode())
-          activeChildKey = $scope.ActivatedKey;
-        // re-active the previously active child node
-        if (activeChildKey) {
-          newNode = tree.getNodeByKey(activeChildKey);
-          // the node may not be there after the update
-          if (newNode)
-            newNode.setActive(true, {noFocus: true}); // fires the onTreeNodeActivated event for this node
-        }
-        //resizer();
-      };
-
-
-      var resizer = function () {
-      // this forces the tree and the grid to be the size of the browser window.
-      // the effect is that the tree and the grid will have vertical scroll bars if needed.
-      // the alternative is to let the tree and grid determine the size of the page and have
-      // the scroll bar on the window
-        // don't allow HTML in the tree titles
-        $('.fancytree-title').each( function () {
-          let unsafe = $(this).html();
-          $(this).html(unsafe.replace(/</g, '&lt;').replace(/>/g, '&gt;'));
+          let child = {
+            typeName:   'attribute',
+            extraClasses:   addClass,
+            tooltip:    addClass,
+            key:        row.name.value,
+            title:      row.name.value,
+            details:    row
+          };
+          return child;
         });
-        let h = $scope.detailFields.length * 30 + 46;
-        $('.ui-grid-viewport').height(h);
-        $scope.details.excessRows = $scope.detailFields.length;
-        $scope.gridApi.grid.handleWindowResize();
-        $scope.gridApi.core.refresh();
-      };
-      $(window).resize(resizer);
-
-      var schemaProps = function (entityName, key, currentNode) {
-        let typeMap = {integer: 'number', string: 'text', path: 'text', boolean: 'boolean', map: 'textarea'};
-
-        let entity = QDRService.management.schema().entityTypes[entityName];
-        let value = entity.attributes[key];
-        // skip identity and depricated fields
-        if (!value)
-          return {input: 'input', type: 'disabled', required: false, selected: '', rawtype: 'string', disabled: true, 'default': ''};
-        let description = value.description || '';
-        let val = value['default'];
-        let disabled = (key == 'identity' || description.startsWith('Deprecated'));
-        // special cases
-        if (entityName == 'log' && key == 'module') {
-          return {input: 'input', type: 'disabled', required: false, selected: '', rawtype: 'string', disabled: true, 'default': ''};
-        }
-        if (entityName === 'linkRoutePattern' && key === 'connector') {
-        // turn input into a select. the values will be populated later
-          value.type = [];
-          // find all the connector names and populate the select
-          QDRService.management.topology.fetchEntity(currentNode.id, 'connector', ['name'], function (nodeName, dotentity, response) {
-            $scope.detailFields.some( function (field) {
-              if (field.name === 'connector') {
-                field.rawtype = response.results.map (function (result) {return result[0];});
-                return true;
-              }
-            });
-          });
+        node.addNode(children);
+      }
+      // top level node was expanded
+      if (wasExpanded)
+        node.setExpanded(true, {noAnimation: true, noEvents: true, noFocus: true});
+      // if the parent node was active, but none of the children were active, active the 1st child
+      if (wasActive) {
+        if (!activeChildKey) {
+          activeChildKey = node.children[0].key;
         }
-        return {    name:       key,
-          humanName:  QDRService.utilities.humanify(key),
-          description:value.description,
-          type:       disabled ? 'disabled' : typeMap[value.type],
-          rawtype:    value.type,
-          input:      typeof value.type == 'string' ? value.type == 'boolean' ? 'boolean' : 'input'
-            : 'select',
-          selected:   val ? val : undefined,
-          'default':  value['default'],
-          value:      val,
-          required:   value.required,
-          unique:     value.unique,
-          disabled:   disabled
-        };
-      };
-      $scope.getAttributeValue = function (attribute) {
-        let value = attribute.attributeValue;
-        if ($scope.currentMode.op === 'CREATE' && attribute.name === 'identity')
-          value = '<assigned by system>';
-        return value;
-      };
-
-      // update the table on the right
-      var updateDetails = function (row) {
-        let details = [];
-        $scope.detailsObject = {};
-        let attrs = Object.keys(row).sort();
-        attrs.forEach( function (attr) {
-          let changed = $scope.detailFields.filter(function (old) {
-            return (old.name === attr) ? old.graph && old.rawValue != row[attr].value : false;
-          });
-          let schemaEntity = schemaProps($scope.selectedEntity, attr, $scope.currentNode);
-          details.push( {
-            attributeName:  QDRService.utilities.humanify(attr),
-            attributeValue: attr === 'port' ? row[attr].value : QDRService.utilities.pretty(row[attr].value),
-            name:           attr,
-            changed:        changed.length,
-            rawValue:       row[attr].value,
-            graph:          row[attr].graph,
-            title:          row[attr].title,
-            chartExists:    (QDRChartService.isAttrCharted($scope.currentNode.id, $scope.selectedEntity, row.name.value, attr)),
-            aggchartExists: (QDRChartService.isAttrCharted($scope.currentNode.id, $scope.selectedEntity, row.name.value, attr, true)),
-            aggregateValue: QDRService.utilities.pretty(row[attr].aggregate),
-            aggregateTip:   row[attr].aggregateTip,
-
-            input:          schemaEntity.input,
-            type:           schemaEntity.type,
-            required:       schemaEntity.required,
-            unique:         schemaEntity.unique,
-            selected:       schemaEntity.selected,
-            rawtype:        schemaEntity.rawtype,
-            disabled:       schemaEntity.disabled,
-            'default':      schemaEntity['default']
+      }
+      if (!tree.getActiveNode())
+        activeChildKey = $scope.ActivatedKey;
+      // re-active the previously active child node
+      if (activeChildKey) {
+        newNode = tree.getNodeByKey(activeChildKey);
+        // the node may not be there after the update
+        if (newNode)
+          newNode.setActive(true, {noFocus: true}); // fires the onTreeNodeActivated event for this node
+      }
+      //resizer();
+    };
+
+
+    var resizer = function () {
+    // this forces the tree and the grid to be the size of the browser window.
+    // the effect is that the tree and the grid will have vertical scroll bars if needed.
+    // the alternative is to let the tree and grid determine the size of the page and have
+    // the scroll bar on the window
+      // don't allow HTML in the tree titles
+      $('.fancytree-title').each( function () {
+        let unsafe = $(this).html();
+        $(this).html(unsafe.replace(/</g, '&lt;').replace(/>/g, '&gt;'));
+      });
+      let h = $scope.detailFields.length * 30 + 46;
+      $('.ui-grid-viewport').height(h);
+      $scope.details.excessRows = $scope.detailFields.length;
+      $scope.gridApi.grid.handleWindowResize();
+      $scope.gridApi.core.refresh();
+      
+    };
+    $(window).resize(resizer);
+
+    var schemaProps = function (entityName, key, currentNode) {
+      let typeMap = {integer: 'number', string: 'text', path: 'text', boolean: 'boolean', map: 'textarea'};
+
+      let entity = QDRService.management.schema().entityTypes[entityName];
+      let value = entity.attributes[key];
+      // skip identity and depricated fields
+      if (!value)
+        return {input: 'input', type: 'disabled', required: false, selected: '', rawtype: 'string', disabled: true, 'default': ''};
+      let description = value.description || '';
+      let val = value['default'];
+      let disabled = (key == 'identity' || description.startsWith('Deprecated'));
+      // special cases
+      if (entityName == 'log' && key == 'module') {
+        return {input: 'input', type: 'disabled', required: false, selected: '', rawtype: 'string', disabled: true, 'default': ''};
+      }
+      if (entityName === 'linkRoutePattern' && key === 'connector') {
+      // turn input into a select. the values will be populated later
+        value.type = [];
+        // find all the connector names and populate the select
+        QDRService.management.topology.fetchEntity(currentNode.id, 'connector', ['name'], function (nodeName, dotentity, response) {
+          $scope.detailFields.some( function (field) {
+            if (field.name === 'connector') {
+              field.rawtype = response.results.map (function (result) {return result[0];});
+              return true;
+            }
           });
-          $scope.detailsObject[attr] = row[attr].value;
         });
-        $scope.detailFields = details;
-        aggregateColumn();
-        resizer();
-      };
-
-      // called from html ng-style="getTableHeight()"
-      $scope.getTableHeight = function () {
-        return {
-          height: (Math.max($scope.detailFields.length, 15) * 30 + 46) + 'px'
-        };
-      };
-      var updateExpandedEntities = function () {
-        let tree = $('#entityTree').fancytree('getTree');
-        if (tree) {
-          let q = d3.queue(10);
-          let expanded = getExpanded(tree);
-          expanded.forEach( function (node) {
-            q.defer(q_updateTableData, node.key, node.key === $scope.selectedEntity);
-          });
+      }
+      return {
+        name:       key,
+        humanName:  QDRService.utilities.humanify(key),
+        description:value.description,
+        type:       disabled ? 'disabled' : typeMap[value.type],
+        rawtype:    value.type,
+        input:      typeof value.type == 'string' ? value.type == 'boolean' ? 'boolean' : 'input'
+          : 'select',
+        selected:   val ? val : undefined,
+        'default':  value['default'],
+        value:      val,
+        required:   value.required,
+        unique:     value.unique,
+        disabled:   disabled
+      };
+    };
+    $scope.getAttributeValue = function (attribute) {
+      let value = attribute.attributeValue;
+      if ($scope.currentMode.op === 'CREATE' && attribute.name === 'identity')
+        value = '<assigned by system>';
+      return value;
+    };
+
+    // update the table on the right
+    var updateDetails = function (row) {
+      let details = [];
+      $scope.detailsObject = {};
+      let attrs = Object.keys(row).sort();
+      attrs.forEach( function (attr) {
+        let changed = $scope.detailFields.filter(function (old) {
+          return (old.name === attr) ? old.graph && old.rawValue != row[attr].value : false;
+        });
+        let schemaEntity = schemaProps($scope.selectedEntity, attr, $scope.currentNode);
+        details.push( {
+          attributeName:  QDRService.utilities.humanify(attr),
+          attributeValue: attr === 'port' ? row[attr].value : QDRService.utilities.pretty(row[attr].value),
+          name:           attr,
+          changed:        changed.length,
+          rawValue:       row[attr].value,
+          graph:          row[attr].graph,
+          title:          row[attr].title,
+          chartExists:    (QDRChartService.isAttrCharted($scope.currentNode.id, $scope.selectedEntity, row.name.value, attr)),
+          aggchartExists: (QDRChartService.isAttrCharted($scope.currentNode.id, $scope.selectedEntity, row.name.value, attr, true)),
+          aggregateValue: QDRService.utilities.pretty(row[attr].aggregate),
+          aggregateTip:   row[attr].aggregateTip,
+
+          input:          schemaEntity.input,
+          type:           schemaEntity.type,
+          required:       schemaEntity.required,
+          unique:         schemaEntity.unique,
+          selected:       schemaEntity.selected,
+          rawtype:        schemaEntity.rawtype,
+          disabled:       schemaEntity.disabled,
+          'default':      schemaEntity['default']
+        });
+        $scope.detailsObject[attr] = row[attr].value;
+      });
+      $scope.detailFields = details;
+      aggregateColumn();
+      resizer();
+    };
+
+    // called from html ng-style="getTableHeight()"
+    $scope.getTableHeight = function () {
+      return {
+        height: (Math.max($scope.detailFields.length, 15) * 30 + 46) + 'px'
+      };
+    };
+    var updateExpandedEntities = function () {
+      let tree = $('#entityTree').fancytree('getTree');
+      if (tree) {
+        let q = d3.queue(10);
+        let expanded = getExpanded(tree);
+        expanded.forEach( function (node) {
+          q.defer(q_updateTableData, node.key, node.key === $scope.selectedEntity);
+        });
 
-          q.await(function (error) {
-            if (error)
-              QDR.log.error(error.message);
+        q.await(function (error) {
+          if (error)
+            QDRLog.error(error.message);
 
-            if (!tree.getActiveNode()) {
-              if ($scope.ActivatedKey) {
-                let node = tree.getNodeByKey($scope.ActivatedKey);
-                if (node) {
-                  node.setActive(true, {noEvents: true});
-                }
+          if (!tree.getActiveNode()) {
+            if ($scope.ActivatedKey) {
+              let node = tree.getNodeByKey($scope.ActivatedKey);
+              if (node) {
+                node.setActive(true, {noEvents: true});
               }
-              if (!tree.getActiveNode()) {
-                let first = tree.getFirstChild();
-                if (first) {
-                  let child = first.getFirstChild();
-                  if (child)
-                    first = child;
-                }
-                first.setActive(true);
+            }
+            if (!tree.getActiveNode()) {
+              let first = tree.getFirstChild();
+              if (first) {
+                let child = first.getFirstChild();
+                if (child)
+                  first = child;
               }
+              first.setActive(true);
             }
+          }
 
-            d3.selectAll('.ui-effects-placeholder').style('height', '0px');
-            resizer();
+          d3.selectAll('.ui-effects-placeholder').style('height', '0px');
+          resizer();
 
-            last_updated = Date.now();
-          });
-        }
-      };
-
-      // The selection dropdown (list of routers) was changed.
-      $scope.selectNode = function(node) {
-        $scope.selectedNode = node.name;
-        $scope.selectedNodeId = node.id;
-        $timeout( function () {
-          setCurrentNode();
-          updateNow = true;
+          last_updated = Date.now();
         });
-      };
+      }
+    };
 
-      $scope.$watch('ActivatedKey', function(newValue, oldValue) {
-        if (newValue !== oldValue) {
-          localStorage[ActivatedKey] = $scope.ActivatedKey;
-        }
-      });
-      $scope.$watch('selectedEntity', function(newValue, oldValue) {
-        if (newValue !== oldValue) {
-          localStorage['QDRSelectedEntity'] = $scope.selectedEntity;
-          $scope.operations = lookupOperations();
-        }
-      });
-      $scope.$watch('selectedNode', function(newValue, oldValue) {
-        if (newValue !== oldValue) {
-          localStorage['QDRSelectedNode'] = $scope.selectedNode;
-          localStorage['QDRSelectedNodeId'] = $scope.selectedNodeId;
-        }
-      });
-      $scope.$watch('selectedRecordName', function(newValue, oldValue) {
-        if (newValue != oldValue) {
-          localStorage['QDRSelectedRecordName'] = $scope.selectedRecordName;
-        }
+    // The selection dropdown (list of routers) was changed.
+    $scope.selectNode = function(node) {
+      $scope.selectedNode = node.name;
+      $scope.selectedNodeId = node.id;
+      $timeout( function () {
+        setCurrentNode();
+        updateNow = true;
       });
+    };
 
-      /* Called periodically to refresh the data on the page */
-      var q_updateTableData = function (entity, expand, callback) {
-      // don't update the data when on the operations tabs
-        if ($scope.currentMode.id !== 'attributes') {
-          callback(null);
-          return;
-        }
-        var gotNodeInfo = function (nodeName, dotentity, response) {
-          let tableRows = [];
-          let records = response.results;
-          let aggregates = response.aggregates;
-          let attributeNames = response.attributeNames;
-          // If !attributeNmes then  there was an error getting the records for this entity
-          if (attributeNames) {
-            let nameIndex = attributeNames.indexOf('name');
-            let identityIndex = attributeNames.indexOf('identity');
-            let ent = QDRService.management.schema().entityTypes[entity];
-            for (let i=0; i<records.length; ++i) {
-              let record = records[i];
-              let aggregate = aggregates ? aggregates[i] : undefined;
-              let row = {};
-              let rowName;
-              if (nameIndex > -1) {
-                rowName = record[nameIndex];
-                if (!rowName && identityIndex > -1) {
-                  rowName = record[nameIndex] = (dotentity + '/' + record[identityIndex]);
-                }
-              }
-              if (!rowName) {
-                let msg = 'response attributeNames did not contain a name field';
-                QDR.log.error(msg);
-                callback(Error(msg));
-                return;
+    $scope.$watch('ActivatedKey', function(newValue, oldValue) {
+      if (newValue !== oldValue) {
+        localStorage[ActivatedKey] = $scope.ActivatedKey;
+      }
+    });
+    $scope.$watch('selectedEntity', function(newValue, oldValue) {
+      if (newValue !== oldValue) {
+        localStorage['QDRSelectedEntity'] = $scope.selectedEntity;
+        $scope.operations = lookupOperations();
+      }
+    });
+    $scope.$watch('selectedNode', function(newValue, oldValue) {
+      if (newValue !== oldValue) {
+        localStorage['QDRSelectedNode'] = $scope.selectedNode;
+        localStorage['QDRSelectedNodeId'] = $scope.selectedNodeId;
+      }
+    });
+    $scope.$watch('selectedRecordName', function(newValue, oldValue) {
+      if (newValue != oldValue) {
+        localStorage['QDRSelectedRecordName'] = $scope.selectedRecordName;
+      }
+    });
+
+    /* Called periodically to refresh the data on the page */
+    var q_updateTableData = function (entity, expand, callback) {
+    // don't update the data when on the operations tabs
+      if ($scope.currentMode.id !== 'attributes') {
+        callback(null);
+        return;
+      }
+      var gotNodeInfo = function (nodeName, dotentity, response) {
+        let tableRows = [];
+        let records = response.results;
+        let aggregates = response.aggregates;
+        let attributeNames = response.attributeNames;
+        // If !attributeNmes then  there was an error getting the records for this entity
+        if (attributeNames) {
+          let nameIndex = attributeNames.indexOf('name');
+          let identityIndex = attributeNames.indexOf('identity');
+          let ent = QDRService.management.schema().entityTypes[entity];
+          for (let i=0; i<records.length; ++i) {
+            let record = records[i];
+            let aggregate = aggregates ? aggregates[i] : undefined;
+            let row = {};
+            let rowName;
+            if (nameIndex > -1) {
+              rowName = record[nameIndex];
+              if (!rowName && identityIndex > -1) {
+                rowName = record[nameIndex] = (dotentity + '/' + record[identityIndex]);
               }
-              for (let j=0; j<attributeNames.length; ++j) {
-                let col = attributeNames[j];
-                row[col] = {value: record[j], type: undefined, graph: false, title: '', aggregate: '', aggregateTip: ''};
-                if (ent) {
-                  let att = ent.attributes[col];
-                  if (att) {
-                    row[col].type = att.type;
-                    row[col].graph = att.graph;
-                    row[col].title = att.description;
-
-                    if (aggregate) {
-                      if (att.graph) {
-                        row[col].aggregate = att.graph ? aggregate[j].sum : '';
-                        let tip = [];
-                        aggregate[j].detail.forEach( function (line) {
-                          tip.push(line);
-                        });
-                        row[col].aggregateTip = angular.toJson(tip);
-                      }
+            }
+            if (!rowName) {
+              let msg = 'response attributeNames did not contain a name field';
+              QDRLog.error(msg);
+              callback(Error(msg));
+              return;
+            }
+            for (let j=0; j<attributeNames.length; ++j) {
+              let col = attributeNames[j];
+              row[col] = {value: record[j], type: undefined, graph: false, title: '', aggregate: '', aggregateTip: ''};
+              if (ent) {
+                let att = ent.attributes[col];
+                if (att) {
+                  row[col].type = att.type;
+                  row[col].graph = att.graph;
+                  row[col].title = att.description;
+
+                  if (aggregate) {
+                    if (att.graph) {
+                      row[col].aggregate = att.graph ? aggregate[j].sum : '';
+                      let tip = [];
+                      aggregate[j].detail.forEach( function (line) {
+                        tip.push(line);
+                      });
+                      row[col].aggregateTip = angular.toJson(tip);
                     }
                   }
                 }
               }
-              tableRows.push(row);
-            }
-            tableRows.sort( function (a, b) { return a.name.value.localeCompare(b.name.value); });
-            selectRow({entity: dotentity, rows: tableRows, expand: expand});
-          }
-          callback(null);  // let queue handler know we are done
-        };
-        // if this entity should show an aggregate column, send the request to get the info for this entity from all the nedes
-        if (aggregateEntities.indexOf(entity) > -1) {
-          let nodeIdList = QDRService.management.topology.nodeIdList();
-          QDRService.management.topology.getMultipleNodeInfo(nodeIdList, entity, [], gotNodeInfo, $scope.selectedNodeId);
-        } else {
-          QDRService.management.topology.fetchEntity($scope.selectedNodeId, entity, [], gotNodeInfo);
-        }
-      };
-
-      // tableRows are the records that were returned, this populates the left hand tree on the page
-      var selectRow = function (info) {
-        updateTreeChildren(info.entity, info.rows, info.expand);
-      };
-      $scope.detailFields = [];
-
-      $scope.addToGraph = function(rowEntity) {
-        let chart = QDRChartService.registerChart(
-          {nodeId: $scope.selectedNodeId,
-            entity: $scope.selectedEntity,
-            name:   $scope.selectedRecordName,
-            attr:    rowEntity.name,
-            forceCreate: true});
-
-        doDialog('tmplChartConfig.html', chart);
-      };
-
-      $scope.addAllToGraph = function(rowEntity) {
-        let chart = QDRChartService.registerChart({
-          nodeId:     $scope.selectedNodeId,
-          entity:     $scope.selectedEntity,
-          name:       $scope.selectedRecordName,
-          attr:       rowEntity.name,
-          type:       'rate',
-          rateWindow: updateInterval,
-          visibleDuration: 0.25,
-          forceCreate: true,
-          aggregate:   true});
-        doDialog('tmplChartConfig.html', chart);
-      };
-
-      // The ui-popover dynamic html
-      $scope.aggregateTip = '';
-      // disable popover tips for non-integer cells
-      $scope.aggregateTipEnabled = function (row) {
-        let tip = row.entity.aggregateTip;
-        return (tip && tip.length) ? 'true' : 'false';
-      };
-      // convert the aggregate data into a table for the popover tip
-      $scope.genAggregateTip = function (row) {
-        let tip = row.entity.aggregateTip;
-        if (tip && tip.length) {
-          let data = angular.fromJson(tip);
-          let table = '<table class=\'tiptable\'><tbody>';
-          data.forEach (function (row) {
-            table += '<tr>';
-            table += '<td>' + row.node + '</td><td align=\'right\'>' + QDRService.utilities.pretty(row.val) + '</td>';
-            table += '</tr>';
-          });
-          table += '</tbody></table>';
-          $scope.aggregateTip = $sce.trustAsHtml(table);
-        }
-      };
-      var aggregateColumn = function () {
-        if ((aggregateEntities.indexOf($scope.selectedEntity) > -1 && $scope.detailCols.length != 3) ||
-        (aggregateEntities.indexOf($scope.selectedEntity) == -1 && $scope.detailCols.length != 2)) {
-        // column defs have to be reassigned and not spliced, so no push/pop
-          $scope.detailCols = [
-            {
-              field: 'attributeName',
-              displayName: 'Attribute',
-              cellTemplate: '<div title="{{row.entity.title}}" class="listAttrName ui-grid-cell-contents">{{COL_FIELD CUSTOM_FILTERS | pretty}}<button ng-if="row.entity.graph" title="Click to view/add a graph" ng-click="grid.appScope.addToGraph(row.entity)" ng-class="{\'btn-success\': row.entity.chartExists}" class="btn"><i ng-class="{\'icon-bar-chart\': row.entity.graph == true }"></i></button></div>'
-            },
-            {
-              field: 'attributeValue',
-              displayName: 'Value',
-              cellTemplate: '<div class="ui-grid-cell-contents" ng-class="{\'changed\': row.entity.changed == 1}">{{COL_FIELD CUSTOM_FILTERS | pretty}}</div>'
             }
-          ];
-          if (aggregateEntities.indexOf($scope.selectedEntity) > -1) {
-            $scope.detailCols.push(
-              {
-                width: '10%',
-                field: 'aggregateValue',
-                displayName: 'Aggregate',
-                cellTemplate: '<div popover-enable="{{grid.appScope.aggregateTipEnabled(row)}}" uib-popover-html="grid.appScope.aggregateTip" popover-append-to-body="true" ng-mouseover="grid.appScope.genAggregateTip(row)" popover-trigger="\'mouseenter\'" class="listAggrValue ui-grid-cell-contents" ng-class="{\'changed\': row.entity.changed == 1}">{{COL_FIELD CUSTOM_FILTERS}} <button title="Click to view/add a graph" ng-if="row.entity.graph" ng-click="grid.appScope.addAllToGraph(row.entity)" ng-class="{\'btn-success\': row.entity.aggchartExists}" class="btn"><i ng-class="{\'icon-bar-chart\': row.entity.graph == true }"></i></button></div>',
-                cellClass: 'aggregate'
-              }
-            );
+            tableRows.push(row);
           }
+          tableRows.sort( function (a, b) { return a.name.value.localeCompare(b.name.value); });
+          selectRow({entity: dotentity, rows: tableRows, expand: expand});
         }
-        if ($scope.selectedRecordName === '')
-          $scope.detailCols = [];
-
-        $scope.details.columnDefs = $scope.detailCols;
-        if ($scope.gridApi)
-          $scope.gridApi.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
-      };
-
-      $scope.gridApi = undefined;
-      // the table on the right of the page contains a row for each field in the selected record in the table on the left
-      $scope.desiredTableHeight = 340;
-      $scope.detailCols = [];
-      $scope.details = {
-        data: 'detailFields',
-        columnDefs: [
+        callback(null);  // let queue handler know we are done
+      };
+      // if this entity should show an aggregate column, send the request to get the info for this entity from all the nedes
+      if (aggregateEntities.indexOf(entity) > -1) {
+        let nodeIdList = QDRService.management.topology.nodeIdList();
+        QDRService.management.topology.getMultipleNodeInfo(nodeIdList, entity, [], gotNodeInfo, $scope.selectedNodeId);
+      } else {
+        QDRService.management.topology.fetchEntity($scope.selectedNodeId, entity, [], gotNodeInfo);
+      }
+    };
+
+    // tableRows are the records that were returned, this populates the left hand tree on the page
+    var selectRow = function (info) {
+      updateTreeChildren(info.entity, info.rows, info.expand);
+    };
+    $scope.detailFields = [];
+
+    $scope.addToGraph = function(rowEntity) {
+      let chart = QDRChartService.registerChart(
+        {nodeId: $scope.selectedNodeId,
+          entity: $scope.selectedEntity,
+          name:   $scope.selectedRecordName,
+          attr:    rowEntity.name,
+          forceCreate: true});
+
+      doDialog('tmplChartConfig.html', chart);
+    };
+
+    $scope.addAllToGraph = function(rowEntity) {
+      let chart = QDRChartService.registerChart({
+        nodeId:     $scope.selectedNodeId,
+        entity:     $scope.selectedEntity,
+        name:       $scope.selectedRecordName,
+        attr:       rowEntity.name,
+        type:       'rate',
+        rateWindow: updateInterval,
+        visibleDuration: 0.25,
+        forceCreate: true,
+        aggregate:   true});
+      doDialog('tmplChartConfig.html', chart);
+    };
+
+    // The ui-popover dynamic html
+    $scope.aggregateTip = '';
+    // disable popover tips for non-integer cells
+    $scope.aggregateTipEnabled = function (row) {
+      let tip = row.entity.aggregateTip;
+      return (tip && tip.length) ? 'true' : 'false';
+    };
+    // convert the aggregate data into a table for the popover tip
+    $scope.genAggregateTip = function (row) {
+      let tip = row.entity.aggregateTip;
+      if (tip && tip.length) {
+        let data = angular.fromJson(tip);
+        let table = '<table class=\'tiptable\'><tbody>';
+        data.forEach (function (row) {
+          table += '<tr>';
+          table += '<td>' + row.node + '</td><td align=\'right\'>' + QDRService.utilities.pretty(row.val) + '</td>';
+          table += '</tr>';
+        });
+        table += '</tbody></table>';
+        $scope.aggregateTip = $sce.trustAsHtml(table);
+      }
+    };
+    var aggregateColumn = function () {
+      if ((aggregateEntities.indexOf($scope.selectedEntity) > -1 && $scope.detailCols.length != 3) ||
+      (aggregateEntities.indexOf($scope.selectedEntity) == -1 && $scope.detailCols.length != 2)) {
+      // column defs have to be reassigned and not spliced, so no push/pop
+        $scope.detailCols = [
           {
             field: 'attributeName',
             displayName: 'Attribute',
@@ -733,216 +693,252 @@ var QDR = (function(QDR) {
             displayName: 'Value',
             cellTemplate: '<div class="ui-grid-cell-contents" ng-class="{\'changed\': row.entity.changed == 1}">{{COL_FIELD CUSTOM_FILTERS | pretty}}</div>'
           }
-        ],
-        enableColumnResize: true,
-        enableHorizontalScrollbar: 0,
-        enableVerticalScrollbar: 0,
-        multiSelect: false,
-        jqueryUIDraggable: true,
-        excessRows: 20,
-        onRegisterApi: function(gridApi) {
-          $scope.gridApi = gridApi;
+        ];
+        if (aggregateEntities.indexOf($scope.selectedEntity) > -1) {
+          $scope.detailCols.push(
+            {
+              width: '10%',
+              field: 'aggregateValue',
+              displayName: 'Aggregate',
+              cellTemplate: '<div popover-enable="{{grid.appScope.aggregateTipEnabled(row)}}" uib-popover-html="grid.appScope.aggregateTip" popover-append-to-body="true" ng-mouseover="grid.appScope.genAggregateTip(row)" popover-trigger="\'mouseenter\'" class="listAggrValue ui-grid-cell-contents" ng-class="{\'changed\': row.entity.changed == 1}">{{COL_FIELD CUSTOM_FILTERS}} <button title="Click to view/add a graph" ng-if="row.entity.graph" ng-click="grid.appScope.addAllToGraph(row.entity)" ng-class="{\'btn-success\': row.entity.aggchartExists}" class="btn"><i ng-class="{\'icon-bar-chart\': row.entity.graph == true }"></i></button></div>',
+              cellClass: 'aggregate'
+            }
+          );
         }
-      };
-
-      $scope.$on('$destroy', function() {
-        clearTimeout(updateIntervalHandle);
-        $(window).off('resize', resizer);
-      });
-
-      function gotMethodResponse (entity, context) {
-        let statusCode = context.message.application_properties.statusCode, note;
-        if (statusCode < 200 || statusCode >= 300) {
-          note = 'Failed to ' + $filter('Pascalcase')($scope.currentMode.op) + ' ' + entity + ': ' + context.message.application_properties.statusDescription;
-          QDR.Core.notification('error', note);
-          QDR.log.error('Error ' + note);
-        } else {
-          note = entity + ' ' + $filter('Pascalcase')($scope.currentMode.op) + 'd';
-          QDR.Core.notification('success', note);
-          QDR.log.debug('Success ' + note);
-          $scope.selectMode($scope.modes[0]);
+      }
+      if ($scope.selectedRecordName === '')
+        $scope.detailCols = [];
+
+      $scope.details.columnDefs = $scope.detailCols;
+      if ($scope.gridApi)
+        $scope.gridApi.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
+    };
+
+    $scope.gridApi = undefined;
+    // the table on the right of the page contains a row for each field in the selected record in the table on the left
+    $scope.desiredTableHeight = 340;
+    $scope.detailCols = [];
+    $scope.details = {
+      data: 'detailFields',
+      columnDefs: [
+        {
+          field: 'attributeName',
+          displayName: 'Attribute',
+          cellTemplate: '<div title="{{row.entity.title}}" class="listAttrName ui-grid-cell-contents">{{COL_FIELD CUSTOM_FILTERS | pretty}}<button ng-if="row.entity.graph" title="Click to view/add a graph" ng-click="grid.appScope.addToGraph(row.entity)" ng-class="{\'btn-success\': row.entity.chartExists}" class="btn"><i ng-class="{\'icon-bar-chart\': row.entity.graph == true }"></i></button></div>'
+        },
+        {
+          field: 'attributeValue',
+          displayName: 'Value',
+          cellTemplate: '<div class="ui-grid-cell-contents" ng-class="{\'changed\': row.entity.changed == 1}">{{COL_FIELD CUSTOM_FILTERS | pretty}}</div>'
         }
+      ],
+      enableColumnResize: true,
+      enableHorizontalScrollbar: 0,
+      enableVerticalScrollbar: 0,
+      multiSelect: false,
+      jqueryUIDraggable: true,
+      excessRows: 20,
+      onRegisterApi: function(gridApi) {
+        $scope.gridApi = gridApi;
       }
-      $scope.ok = function () {
-        let attributes = {};
-        $scope.detailFields.forEach( function (field) {
-          let value = field.rawValue;
-          if (field.input === 'input') {
-            if (field.type === 'text' || field.type === 'disabled')
-              value = field.attributeValue;
-          } else if (field.input === 'select') {
-            value = field.selected;
-          } else if (field.input === 'boolean') {
-            value = field.rawValue;
-          }
-          if (value === '')
-            value = undefined;
-
-          if ((value && value != field['default']) || field.required || (field.name === 'role')) {
-            if (field.name !== 'identity')
-              attributes[field.name] = value;
-          }
-        });
-        QDRService.management.connection.sendMethod($scope.currentNode.id, $scope.selectedEntity, attributes, $scope.currentMode.op)
-          .then(function (response) {gotMethodResponse($scope.selectedEntity, response.context);});
-      };
-      $scope.remove = function () {
-        let identity = $scope.selectedTreeNode.data.details.identity.value;
-        let attributes = {type: $scope.selectedEntity, identity: identity};
-        QDRService.management.connection.sendMethod($scope.currentNode.id, $scope.selectedEntity, attributes, $scope.currentMode.op)
-          .then(function (response) {gotMethodResponse($scope.selectedEntity, response.context);});
-      };
-
-      function doDialog(template, chart) {
-
-        $uibModal.open({
-          backdrop: true,
-          keyboard: true,
-          backdropClick: true,
-          templateUrl: QDR.templatePath + template,
-          controller: 'QDR.ChartDialogController',
-          resolve: {
-            chart: function() {
-              return chart;
-            },
-            updateTick: function () {
-              return function () {};
-            },
-            dashboard: function () {
-              return $scope;
-            },
-            adding: function () {
-              return true;
-            }
-          }
-        }).result.then(function() {
-          QDRChartService.unRegisterChart(chart);
-        });
+    };
+
+    $scope.$on('$destroy', function() {
+      clearTimeout(updateIntervalHandle);
+      $(window).off('resize', resizer);
+    });
+
+    function gotMethodResponse (entity, context) {
+      let statusCode = context.message.application_properties.statusCode, note;
+      if (statusCode < 200 || statusCode >= 300) {
+        note = 'Failed to ' + $filter('Pascalcase')($scope.currentMode.op) + ' ' + entity + ': ' + context.message.application_properties.statusDescription;
+        QDRCore.notification('error', note);
+        QDRLog.error('Error ' + note);
+      } else {
+        note = entity + ' ' + $filter('Pascalcase')($scope.currentMode.op) + 'd';
+        QDRCore.notification('success', note);
+        QDRLog.debug('Success ' + note);
+        $scope.selectMode($scope.modes[0]);
       }
-      var setCurrentNode = function () {
-        let currentNode;
-        $scope.nodes.some( function (node, i) {
-          if (node.name === $scope.selectedNode) {
-            currentNode = $scope.nodes[i];
-            return true;
-          }
-        });
-        if ($scope.currentNode !== currentNode)
-          $scope.currentNode = currentNode;
-      };
+    }
+    $scope.ok = function () {
+      let attributes = {};
+      $scope.detailFields.forEach( function (field) {
+        let value = field.rawValue;
+        if (field.input === 'input') {
+          if (field.type === 'text' || field.type === 'disabled')
+            value = field.attributeValue;
+        } else if (field.input === 'select') {
+          value = field.selected;
+        } else if (field.input === 'boolean') {
+          value = field.rawValue;
+        }
+        if (value === '')
+          value = undefined;
 
-      let treeReady = false;
-      let serviceReady = false;
-      $scope.largeNetwork = QDRService.management.topology.isLargeNetwork();
-      if ($scope.largeNetwork)
-        aggregateEntities = [];
-
-      // called after we know for sure the schema is fetched and the routers are all ready
-      QDRService.management.topology.addUpdatedAction('initList', function () {
-        QDRService.management.topology.stopUpdating();
-        QDRService.management.topology.delUpdatedAction('initList');
-
-        $scope.nodes = QDRService.management.topology.nodeList().sort(function (a, b) { return a.name.toLowerCase() > b.name.toLowerCase();});
-        // unable to get node list? Bail.
-        if ($scope.nodes.length == 0) {
-          $location.path('/' + QDR.pluginName + '/connect');
-          $location.search('org', 'list');
+        if ((value && value != field['default']) || field.required || (field.name === 'role')) {
+          if (field.name !== 'identity')
+            attributes[field.name] = value;
         }
-        if (!angular.isDefined($scope.selectedNode)) {
-        //QDR.log.debug("selectedNode was " + $scope.selectedNode);
-          if ($scope.nodes.length > 0) {
-            $scope.selectedNode = $scope.nodes[0].name;
-            $scope.selectedNodeId = $scope.nodes[0].id;
-          //QDR.log.debug("forcing selectedNode to " + $scope.selectedNode);
+      });
+      QDRService.management.connection.sendMethod($scope.currentNode.id, $scope.selectedEntity, attributes, $scope.currentMode.op)
+        .then(function (response) {gotMethodResponse($scope.selectedEntity, response.context);});
+    };
+    $scope.remove = function () {
+      let identity = $scope.selectedTreeNode.data.details.identity.value;
+      let attributes = {type: $scope.selectedEntity, identity: identity};
+      QDRService.management.connection.sendMethod($scope.currentNode.id, $scope.selectedEntity, attributes, $scope.currentMode.op)
+        .then(function (response) {gotMethodResponse($scope.selectedEntity, response.context);});
+    };
+
+    function doDialog(template, chart) {
+
+      $uibModal.open({
+        backdrop: true,
+        keyboard: true,
+        backdropClick: true,
+        templateUrl: QDRTemplatePath + template,
+        controller: 'QDR.ChartDialogController',
+        resolve: {
+          chart: function() {
+            return chart;
+          },
+          updateTick: function () {
+            return function () {};
+          },
+          dashboard: function () {
+            return $scope;
+          },
+          adding: function () {
+            return true;
           }
         }
-        setCurrentNode();
-        if ($scope.currentNode == undefined) {
-          if ($scope.nodes.length > 0) {
-            $scope.selectedNode = $scope.nodes[0].name;
-            $scope.selectedNodeId = $scope.nodes[0].id;
-            $scope.currentNode = $scope.nodes[0];
-          }
+      }).result.then(function() {
+        QDRChartService.unRegisterChart(chart);
+      });
+    }
+    var setCurrentNode = function () {
+      let currentNode;
+      $scope.nodes.some( function (node, i) {
+        if (node.name === $scope.selectedNode) {
+          currentNode = $scope.nodes[i];
+          return true;
+        }
+      });
+      if ($scope.currentNode !== currentNode)
+        $scope.currentNode = currentNode;
+    };
+
+    let treeReady = false;
+    let serviceReady = false;
+    $scope.largeNetwork = QDRService.management.topology.isLargeNetwork();
+    if ($scope.largeNetwork)
+      aggregateEntities = [];
+
+    // called after we know for sure the schema is fetched and the routers are all ready
+    QDRService.management.topology.addUpdatedAction('initList', function () {
+      QDRService.management.topology.stopUpdating();
+      QDRService.management.topology.delUpdatedAction('initList');
+
+      $scope.nodes = QDRService.management.topology.nodeList().sort(function (a, b) { return a.name.toLowerCase() > b.name.toLowerCase();});
+      // unable to get node list? Bail.
+      if ($scope.nodes.length == 0) {
+        $location.path('/connect');
+        $location.search('org', 'list');
+      }
+      if (!angular.isDefined($scope.selectedNode)) {
+      //QDRLog.debug("selectedNode was " + $scope.selectedNode);
+        if ($scope.nodes.length > 0) {
+          $scope.selectedNode = $scope.nodes[0].name;
+          $scope.selectedNodeId = $scope.nodes[0].id;
+        //QDRLog.debug("forcing selectedNode to " + $scope.selectedNode);
+        }
+      }
+      setCurrentNode();
+      if ($scope.currentNode == undefined) {
+        if ($scope.nodes.length > 0) {
+          $scope.selectedNode = $scope.nodes[0].name;
+          $scope.selectedNodeId = $scope.nodes[0].id;
+          $scope.currentNode = $scope.nodes[0];
+        }
+      }
+      let sortedEntities = Object.keys(QDRService.management.schema().entityTypes).sort();
+      sortedEntities.forEach( function (entity) {
+        if (excludedEntities.indexOf(entity) == -1) {
+          if (!angular.isDefined($scope.selectedEntity))
+            $scope.selectedEntity = entity;
+          $scope.operations = lookupOperations();
+          let e = new QDRFolder(entity);
+          e.typeName = 'entity';
+          e.key = entity;
+          e.expanded = (expandedList.indexOf(entity) > -1);
+          let placeHolder = new QDRLeaf('loading...');
+          placeHolder.addClass = 'loading';
+          e.children = [placeHolder];
+          entityTreeChildren.push(e);
         }
-        let sortedEntities = Object.keys(QDRService.management.schema().entityTypes).sort();
-        sortedEntities.forEach( function (entity) {
-          if (excludedEntities.indexOf(entity) == -1) {
-            if (!angular.isDefined($scope.selectedEntity))
-              $scope.selectedEntity = entity;
-            $scope.operations = lookupOperations();
-            let e = new QDR.Folder(entity);
-            e.typeName = 'entity';
-            e.key = entity;
-            e.expanded = (expandedList.indexOf(entity) > -1);
-            let placeHolder = new QDR.Leaf('loading...');
-            placeHolder.addClass = 'loading';
-            e.children = [placeHolder];
-            entityTreeChildren.push(e);
+      });
+      serviceReady = true;
+      initTree();
+    });
+    // called by ng-init="treeReady()" in tmplListTree.html
+    $scope.treeReady = function () {
+      treeReady = true;
+      initTree();
+    };
+
+    // this gets called once tree is initialized
+    var onTreeInitialized = function () {
+      updateExpandedEntities();
+    };
+
+    var initTree = function () {
+      if (!treeReady || !serviceReady)
+        return;
+      $('#entityTree').fancytree({
+        activate: onTreeNodeActivated,
+        expand: onTreeNodeExpanded,
+        collapse: onTreeNodeCollapsed,
+        beforeActivate: onTreeNodeBeforeActivate,
+        beforeSelect: function(event, data){
+          // A node is about to be selected: prevent this for folders:
+          if( data.node.isFolder() ){
+            return false;
           }
-        });
-        serviceReady = true;
-        initTree();
+        },
+        init: onTreeInitialized,
+        selectMode: 1,
+        autoCollapse: $scope.largeNetwork,
+        activeVisible: !$scope.largeNetwork,
+        clickFolderMode: 3,
+        debugLevel: 0,
+        extraClasses: {
+          expander: 'fa-angle',
+          connector: 'fancytree-no-connector'
+        },
+        source: entityTreeChildren
       });
-      // called by ng-init="treeReady()" in tmplListTree.html
-      $scope.treeReady = function () {
-        treeReady = true;
-        initTree();
-      };
-
-      // this gets called once tree is initialized
-      var onTreeInitialized = function () {
-        updateExpandedEntities();
-      };
+    };
+    QDRService.management.topology.ensureAllEntities({entity: 'connection'}, function () {
+      QDRService.management.topology.setUpdateEntities(['connection']);
+      // keep the list of routers up to date
+      QDRService.management.topology.startUpdating(true);
+    });
+
+    updateIntervalHandle = setInterval(function () {
+      if (!treeReady || !serviceReady)
+        return;
 
-      var initTree = function () {
-        if (!treeReady || !serviceReady)
-          return;
-        $('#entityTree').fancytree({
-          activate: onTreeNodeActivated,
-          expand: onTreeNodeExpanded,
-          collapse: onTreeNodeCollapsed,
-          beforeActivate: onTreeNodeBeforeActivate,
-          beforeSelect: function(event, data){
-            // A node is about to be selected: prevent this for folders:
-            if( data.node.isFolder() ){
-              return false;
-            }
-          },
-          init: onTreeInitialized,
-          selectMode: 1,
-          autoCollapse: $scope.largeNetwork,
-          activeVisible: !$scope.largeNetwork,
-          clickFolderMode: 3,
-          debugLevel: 0,
-          extraClasses: {
-            expander: 'fa-angle',
-            connector: 'fancytree-no-connector'
-          },
-          source: entityTreeChildren
+      let now = Date.now();
+      if (((now - last_updated) >= updateInterval) || updateNow) {
+        updateNow = false;
+        $timeout( function () {
+          updateExpandedEntities();
+          resizer();
         });
-      };
-      QDRService.management.topology.ensureAllEntities({entity: 'connection'}, function () {
-        QDRService.management.topology.setUpdateEntities(['connection']);
-        // keep the list of routers up to date
-        QDRService.management.topology.startUpdating(true);
-      });
-
-      updateIntervalHandle = setInterval(function () {
-        if (!treeReady || !serviceReady)
-          return;
-
-        let now = Date.now();
-        if (((now - last_updated) >= updateInterval) || updateNow) {
-          updateNow = false;
-          $timeout( function () {
-            updateExpandedEntities();
-            resizer();
-          });
-        }
-      }, 100);
-
-    }]);
-
-  return QDR;
+      }
+    }, 100);
 
-} (QDR || {}));
\ No newline at end of file
+  }
+}
+ListController.$inject = ['QDRService', 'QDRChartService', '$scope', '$log', '$location', '$uibModal', '$filter', '$timeout', 'uiGridConstants', '$sce'];

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/qdrListChart.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/qdrListChart.js b/console/stand-alone/plugin/js/qdrListChart.js
deleted file mode 100644
index 711046e..0000000
--- a/console/stand-alone/plugin/js/qdrListChart.js
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
-Licensed to the Apache Software Foundation (ASF) under one
-or more contributor license agreements.  See the NOTICE file
-distributed with this work for additional information
-regarding copyright ownership.  The ASF licenses this file
-to you under the Apache License, Version 2.0 (the
-"License"); you may not use this file except in compliance
-with the License.  You may obtain a copy of the License at
-
-  http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing,
-software distributed under the License is distributed on an
-"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-KIND, either express or implied.  See the License for the
-specific language governing permissions and limitations
-under the License.
-*/
-'use strict';
-/* global angular */
-
-/**
- * @module QDR
- */
-
-var QDR = (function(QDR) {
-
-  QDR.module.controller('QDR.ListChartController', function ($scope, $uibModalInstance, $uibModal, $location, QDRService, QDRChartService, chart, nodeName) {
-
-    $scope.chart = chart;
-    $scope.dialogSvgChart = null;
-    let updateTimer = null;
-    $scope.svgDivId = 'dialogChart';    // the div id for the svg chart
-
-    $scope.showChartsPage = function () {
-      cleanup();
-      $uibModalInstance.close(true);
-      $location.path(QDR.pluginRoot + '/charts');
-    };
-
-    $scope.addHChart = function () {
-      QDRChartService.addHDash($scope.chart);
-      cleanup();
-      $uibModalInstance.close(true);
-    };
-
-    $scope.addToDashboardLink = function () {
-      let href = '#/' + QDR.pluginName + '/charts';
-      let size = angular.toJson({
-        size_x: 2,
-        size_y: 2
-      });
-
-      let params = angular.toJson({chid: $scope.chart.id()});
-      let title = 'Dispatch - ' + nodeName;
-      return '/hawtio/#/dashboard/add?tab=dashboard' +
-        '&href=' + encodeURIComponent(href) +
-        '&routeParams=' + encodeURIComponent(params) +
-        '&title=' + encodeURIComponent(title) +
-        '&size=' + encodeURIComponent(size);
-    };
-
-
-    $scope.addChartsPage = function () {
-      QDRChartService.addDashboard($scope.chart);
-    };
-
-    $scope.delChartsPage = function () {
-      QDRChartService.delDashboard($scope.chart);
-    };
-
-    $scope.isOnChartsPage = function () {
-      return $scope.chart.dashboard;
-    };
-
-    var showChart = function () {
-      // we need a width and height before generating the chart
-      let div = angular.element('#pfDialogChart');
-      if (!div.width()) {
-        setTimeout(showChart, 100);
-        return;
-      }
-      $scope.pfDialogSvgChart = new QDRChartService.pfAreaChart($scope.chart, 'pfDialogChart');
-      updateDialogChart();
-    };
-    showChart();
-
-    var updateDialogChart = function () {
-      if ($scope.pfDialogSvgChart) {
-        $scope.pfDialogSvgChart.tick();
-      }
-      if (updateTimer)
-        clearTimeout(updateTimer);
-      updateTimer = setTimeout(updateDialogChart, 1000);
-    };
-
-    var cleanup = function () {
-      if (updateTimer) {
-        clearTimeout(updateTimer);
-        updateTimer = null;
-      }
-      if (!$scope.chart.hdash && !$scope.chart.dashboard)
-        QDRChartService.unRegisterChart($scope.chart);     // remove the chart
-
-    };
-    $scope.ok = function () {
-      cleanup();
-      $uibModalInstance.close(true);
-    };
-
-    $scope.editChart = function () {
-      doDialog('tmplChartConfig.html', chart);
-    };
-
-    function doDialog(template, chart) {
-
-      $uibModal.open({
-        backdrop: true,
-        keyboard: true,
-        backdropClick: true,
-        templateUrl: QDR.templatePath + template,
-        controller: 'QDR.ChartDialogController',
-        resolve: {
-          chart: function() {
-            return chart;
-          },
-          updateTick: function () {
-            return function () {};
-          },
-          dashboard: function () {
-            return $scope;
-          },
-          adding: function () {
-            return true;
-          }
-        }
-      }).result.then(function() {
-        $scope.ok();
-      });
-    }
-
-  });
-
-  return QDR;
-
-} (QDR || {}));


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


[7/8] qpid-dispatch git commit: DISPATCH-1049 Add console tests

Posted by ea...@apache.org.
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/package-lock.json
----------------------------------------------------------------------
diff --git a/console/stand-alone/package-lock.json b/console/stand-alone/package-lock.json
index a94c1cf..daacf41 100644
--- a/console/stand-alone/package-lock.json
+++ b/console/stand-alone/package-lock.json
@@ -10,11 +10,19 @@
       "integrity": "sha1-z6I7xYQPkQTOMqZedNt+epdLvuE=",
       "dev": true,
       "requires": {
-        "acorn": "5.6.1",
+        "acorn": "5.7.1",
         "css": "2.2.3",
         "normalize-path": "2.1.1",
         "source-map": "0.5.7",
         "through2": "2.0.3"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.5.7",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+          "dev": true
+        }
       }
     },
     "@gulp-sourcemaps/map-sources": {
@@ -33,12 +41,43 @@
       "integrity": "sha512-mQjDxyOM1Cpocd+vm1kZBP7smwKZ4TNokFeds9LV7OZibmPJFEzY3+xZMrKfUdNT71lv8GoCPD6upKwHxubClw==",
       "dev": true
     },
+    "@types/mocha": {
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.2.tgz",
+      "integrity": "sha512-tfg9rh2qQhBW6SBqpvfqTgU6lHWGhQURoTrn7NeDF+CEkO9JGYbkzU23EXu6p3bnvDxLeeSX8ohAA23urvWeNw==",
+      "dev": true
+    },
+    "@types/node": {
+      "version": "10.3.4",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-10.3.4.tgz",
+      "integrity": "sha512-YMLlzdeNnAyLrQew39IFRkMacAR5BqKGIEei9ZjdHsIZtv+ZWKYTu1i7QJhetxQ9ReXx8w5f+cixdHZG3zgMQA==",
+      "dev": true
+    },
+    "JSONStream": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.3.tgz",
+      "integrity": "sha512-3Sp6WZZ/lXl+nTDoGpGWHEpTnnC6X5fnkolYZR6nwIfzbxxvA8utPWe1gCt7i0m9uVGsSz2IS8K8mJ7HmlduMg==",
+      "dev": true,
+      "requires": {
+        "jsonparse": "1.3.1",
+        "through": "2.3.8"
+      }
+    },
     "acorn": {
-      "version": "5.6.1",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.6.1.tgz",
-      "integrity": "sha512-XH4o5BK5jmw9PzSGK7mNf+/xV+mPxQxGZoeC36OVsJZYV77JAG9NnI7T90hoUpI/C1TOfXWTvugRdZ9ZR3iE2Q==",
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.1.tgz",
+      "integrity": "sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ==",
       "dev": true
     },
+    "acorn-dynamic-import": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz",
+      "integrity": "sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==",
+      "dev": true,
+      "requires": {
+        "acorn": "5.7.1"
+      }
+    },
     "acorn-jsx": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz",
@@ -56,6 +95,17 @@
         }
       }
     },
+    "acorn-node": {
+      "version": "1.5.2",
+      "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.5.2.tgz",
+      "integrity": "sha512-krFKvw/d1F17AN3XZbybIUzEY4YEPNiGo05AfP3dBlfVKrMHETKpgjpuZkSF8qDNt9UkQcqj7am8yJLseklCMg==",
+      "dev": true,
+      "requires": {
+        "acorn": "5.7.1",
+        "acorn-dynamic-import": "3.0.0",
+        "xtend": "4.0.1"
+      }
+    },
     "ajv": {
       "version": "5.5.2",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
@@ -125,17 +175,17 @@
       "integrity": "sha512-yzcHpPMLQl0232nDzm5P4iAFTFQ9dMw0QgFLuKYbDj9M0xJ62z0oudYD/Lvh1pWfRsukiytP4Xj6BHOSrSXP8A=="
     },
     "angular-ui-grid": {
-      "version": "4.4.11",
-      "resolved": "https://registry.npmjs.org/angular-ui-grid/-/angular-ui-grid-4.4.11.tgz",
-      "integrity": "sha512-tOZrlgmmV8LXS5yDXxry53uibZxFOC3dNNUOH+5AA0SwVTg1B0rE4+4zdU7NfW6bKClshoPE7mik8/VrS1rXAQ==",
+      "version": "4.5.1",
+      "resolved": "https://registry.npmjs.org/angular-ui-grid/-/angular-ui-grid-4.5.1.tgz",
+      "integrity": "sha512-asiuS57fEpakHjQ2R2po8FNFuSnc7B+Tj3j7FuroFBUQrjLnaF/5jNnpOVVBM2DzPcyOEjbE47bLk1PXG63fOQ==",
       "requires": {
-        "angular": "1.7.0"
+        "angular": "1.7.2"
       },
       "dependencies": {
         "angular": {
-          "version": "1.7.0",
-          "resolved": "https://registry.npmjs.org/angular/-/angular-1.7.0.tgz",
-          "integrity": "sha512-3LboCLjrOuC7dWh953O0+dI3dJ7PexYRSCIrfqoN5qoHyja/wak3eWoxPKb2Sl2qwiPbrUV5KJXwgpUQ48McBQ=="
+          "version": "1.7.2",
+          "resolved": "https://registry.npmjs.org/angular/-/angular-1.7.2.tgz",
+          "integrity": "sha512-JcKKJbBdybUsmQ6x1M3xWyTYQ/ioVKJhSByEAjqrhmlOfvMFdhfMqAx5KIo8rLGk4DFolYPcCSgssjgTVjCtRQ=="
         }
       }
     },
@@ -193,10 +243,13 @@
       "dev": true
     },
     "ansi-styles": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
-      "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
-      "dev": true
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "dev": true,
+      "requires": {
+        "color-convert": "1.9.2"
+      }
     },
     "ansi-wrap": {
       "version": "0.1.0",
@@ -239,13 +292,10 @@
       }
     },
     "arr-diff": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
-      "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
-      "dev": true,
-      "requires": {
-        "arr-flatten": "1.1.0"
-      }
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+      "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
+      "dev": true
     },
     "arr-filter": {
       "version": "1.1.2",
@@ -277,12 +327,24 @@
       "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
       "dev": true
     },
+    "array-differ": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz",
+      "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=",
+      "dev": true
+    },
     "array-each": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz",
       "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=",
       "dev": true
     },
+    "array-filter": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz",
+      "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=",
+      "dev": true
+    },
     "array-initial": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz",
@@ -318,6 +380,18 @@
         }
       }
     },
+    "array-map": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz",
+      "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=",
+      "dev": true
+    },
+    "array-reduce": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz",
+      "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=",
+      "dev": true
+    },
     "array-slice": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz",
@@ -370,6 +444,49 @@
       "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
       "dev": true
     },
+    "asn1.js": {
+      "version": "4.10.1",
+      "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
+      "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
+      "dev": true,
+      "requires": {
+        "bn.js": "4.11.8",
+        "inherits": "2.0.3",
+        "minimalistic-assert": "1.0.1"
+      }
+    },
+    "assert": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
+      "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
+      "dev": true,
+      "requires": {
+        "util": "0.10.3"
+      },
+      "dependencies": {
+        "inherits": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
+          "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
+          "dev": true
+        },
+        "util": {
+          "version": "0.10.3",
+          "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
+          "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
+          "dev": true,
+          "requires": {
+            "inherits": "2.0.1"
+          }
+        }
+      }
+    },
+    "assertion-error": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+      "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+      "dev": true
+    },
     "assign-symbols": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
@@ -386,6 +503,14 @@
         "once": "1.4.0",
         "process-nextick-args": "1.0.7",
         "stream-exhaust": "1.0.2"
+      },
+      "dependencies": {
+        "process-nextick-args": {
+          "version": "1.0.7",
+          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
+          "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
+          "dev": true
+        }
       }
     },
     "async-each": {
@@ -418,6 +543,33 @@
         "chalk": "1.1.3",
         "esutils": "2.0.2",
         "js-tokens": "3.0.2"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "2.2.1",
+            "escape-string-regexp": "1.0.5",
+            "has-ansi": "2.0.0",
+            "strip-ansi": "3.0.1",
+            "supports-color": "2.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true
+        }
       }
     },
     "babel-core": {
@@ -455,6 +607,12 @@
           "requires": {
             "ms": "2.0.0"
           }
+        },
+        "source-map": {
+          "version": "0.5.7",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+          "dev": true
         }
       }
     },
@@ -472,6 +630,14 @@
         "lodash": "4.17.10",
         "source-map": "0.5.7",
         "trim-right": "1.0.1"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.5.7",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+          "dev": true
+        }
       }
     },
     "babel-helper-builder-binary-assignment-operator-visitor": {
@@ -922,6 +1088,23 @@
         "babel-types": "6.26.0"
       }
     },
+    "babel-polyfill": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz",
+      "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=",
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "core-js": "2.5.7",
+        "regenerator-runtime": "0.10.5"
+      },
+      "dependencies": {
+        "regenerator-runtime": {
+          "version": "0.10.5",
+          "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz",
+          "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg="
+        }
+      }
+    },
     "babel-preset-env": {
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.7.0.tgz",
@@ -960,6 +1143,38 @@
         "semver": "5.5.0"
       }
     },
+    "babel-preset-es2015": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz",
+      "integrity": "sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=",
+      "dev": true,
+      "requires": {
+        "babel-plugin-check-es2015-constants": "6.22.0",
+        "babel-plugin-transform-es2015-arrow-functions": "6.22.0",
+        "babel-plugin-transform-es2015-block-scoped-functions": "6.22.0",
+        "babel-plugin-transform-es2015-block-scoping": "6.26.0",
+        "babel-plugin-transform-es2015-classes": "6.24.1",
+        "babel-plugin-transform-es2015-computed-properties": "6.24.1",
+        "babel-plugin-transform-es2015-destructuring": "6.23.0",
+        "babel-plugin-transform-es2015-duplicate-keys": "6.24.1",
+        "babel-plugin-transform-es2015-for-of": "6.23.0",
+        "babel-plugin-transform-es2015-function-name": "6.24.1",
+        "babel-plugin-transform-es2015-literals": "6.22.0",
+        "babel-plugin-transform-es2015-modules-amd": "6.24.1",
+        "babel-plugin-transform-es2015-modules-commonjs": "6.26.2",
+        "babel-plugin-transform-es2015-modules-systemjs": "6.24.1",
+        "babel-plugin-transform-es2015-modules-umd": "6.24.1",
+        "babel-plugin-transform-es2015-object-super": "6.24.1",
+        "babel-plugin-transform-es2015-parameters": "6.24.1",
+        "babel-plugin-transform-es2015-shorthand-properties": "6.24.1",
+        "babel-plugin-transform-es2015-spread": "6.22.0",
+        "babel-plugin-transform-es2015-sticky-regex": "6.24.1",
+        "babel-plugin-transform-es2015-template-literals": "6.22.0",
+        "babel-plugin-transform-es2015-typeof-symbol": "6.23.0",
+        "babel-plugin-transform-es2015-unicode-regex": "6.24.1",
+        "babel-plugin-transform-regenerator": "6.26.0"
+      }
+    },
     "babel-register": {
       "version": "6.26.0",
       "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz",
@@ -979,7 +1194,6 @@
       "version": "6.26.0",
       "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
       "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
-      "dev": true,
       "requires": {
         "core-js": "2.5.7",
         "regenerator-runtime": "0.11.1"
@@ -1120,12 +1334,6 @@
             "kind-of": "6.0.2"
           }
         },
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
-        },
         "kind-of": {
           "version": "6.0.2",
           "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
@@ -1134,16 +1342,39 @@
         }
       }
     },
+    "base64-js": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
+      "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==",
+      "dev": true
+    },
+    "beeper": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz",
+      "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=",
+      "dev": true
+    },
     "binary-extensions": {
       "version": "1.11.0",
       "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz",
       "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=",
       "dev": true
     },
-    "bluebird": {
-      "version": "3.5.1",
-      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
-      "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA=="
+    "bl": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
+      "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==",
+      "dev": true,
+      "requires": {
+        "readable-stream": "2.3.6",
+        "safe-buffer": "5.1.2"
+      }
+    },
+    "bn.js": {
+      "version": "4.11.8",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
+      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
+      "dev": true
     },
     "bootstrap": {
       "version": "3.3.7",
@@ -1171,16 +1402,195 @@
         "repeat-element": "1.1.2"
       }
     },
+    "brorand": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
+      "dev": true
+    },
+    "browser-pack": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.1.0.tgz",
+      "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==",
+      "dev": true,
+      "requires": {
+        "JSONStream": "1.3.3",
+        "combine-source-map": "0.8.0",
+        "defined": "1.0.0",
+        "safe-buffer": "5.1.2",
+        "through2": "2.0.3",
+        "umd": "3.0.3"
+      }
+    },
+    "browser-resolve": {
+      "version": "1.11.3",
+      "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz",
+      "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==",
+      "dev": true,
+      "requires": {
+        "resolve": "1.1.7"
+      },
+      "dependencies": {
+        "resolve": {
+          "version": "1.1.7",
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
+          "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=",
+          "dev": true
+        }
+      }
+    },
+    "browser-stdout": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+      "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+      "dev": true
+    },
+    "browserify": {
+      "version": "16.2.2",
+      "resolved": "https://registry.npmjs.org/browserify/-/browserify-16.2.2.tgz",
+      "integrity": "sha512-fMES05wq1Oukts6ksGUU2TMVHHp06LyQt0SIwbXIHm7waSrQmNBZePsU0iM/4f94zbvb/wHma+D1YrdzWYnF/A==",
+      "dev": true,
+      "requires": {
+        "JSONStream": "1.3.3",
+        "assert": "1.4.1",
+        "browser-pack": "6.1.0",
+        "browser-resolve": "1.11.3",
+        "browserify-zlib": "0.2.0",
+        "buffer": "5.1.0",
+        "cached-path-relative": "1.0.1",
+        "concat-stream": "1.6.2",
+        "console-browserify": "1.1.0",
+        "constants-browserify": "1.0.0",
+        "crypto-browserify": "3.12.0",
+        "defined": "1.0.0",
+        "deps-sort": "2.0.0",
+        "domain-browser": "1.2.0",
+        "duplexer2": "0.1.4",
+        "events": "2.1.0",
+        "glob": "7.1.2",
+        "has": "1.0.3",
+        "htmlescape": "1.1.1",
+        "https-browserify": "1.0.0",
+        "inherits": "2.0.3",
+        "insert-module-globals": "7.2.0",
+        "labeled-stream-splicer": "2.0.1",
+        "mkdirp": "0.5.1",
+        "module-deps": "6.1.0",
+        "os-browserify": "0.3.0",
+        "parents": "1.0.1",
+        "path-browserify": "0.0.1",
+        "process": "0.11.10",
+        "punycode": "1.4.1",
+        "querystring-es3": "0.2.1",
+        "read-only-stream": "2.0.0",
+        "readable-stream": "2.3.6",
+        "resolve": "1.8.1",
+        "shasum": "1.0.2",
+        "shell-quote": "1.6.1",
+        "stream-browserify": "2.0.1",
+        "stream-http": "2.8.3",
+        "string_decoder": "1.1.1",
+        "subarg": "1.0.0",
+        "syntax-error": "1.4.0",
+        "through2": "2.0.3",
+        "timers-browserify": "1.4.2",
+        "tty-browserify": "0.0.1",
+        "url": "0.11.0",
+        "util": "0.10.4",
+        "vm-browserify": "1.1.0",
+        "xtend": "4.0.1"
+      }
+    },
+    "browserify-aes": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+      "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
+      "dev": true,
+      "requires": {
+        "buffer-xor": "1.0.3",
+        "cipher-base": "1.0.4",
+        "create-hash": "1.2.0",
+        "evp_bytestokey": "1.0.3",
+        "inherits": "2.0.3",
+        "safe-buffer": "5.1.2"
+      }
+    },
+    "browserify-cipher": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
+      "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
+      "dev": true,
+      "requires": {
+        "browserify-aes": "1.2.0",
+        "browserify-des": "1.0.1",
+        "evp_bytestokey": "1.0.3"
+      }
+    },
+    "browserify-des": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.1.tgz",
+      "integrity": "sha512-zy0Cobe3hhgpiOM32Tj7KQ3Vl91m0njwsjzZQK1L+JDf11dzP9qIvjreVinsvXrgfjhStXwUWAEpB9D7Gwmayw==",
+      "dev": true,
+      "requires": {
+        "cipher-base": "1.0.4",
+        "des.js": "1.0.0",
+        "inherits": "2.0.3"
+      }
+    },
+    "browserify-rsa": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+      "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
+      "dev": true,
+      "requires": {
+        "bn.js": "4.11.8",
+        "randombytes": "2.0.6"
+      }
+    },
+    "browserify-sign": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz",
+      "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=",
+      "dev": true,
+      "requires": {
+        "bn.js": "4.11.8",
+        "browserify-rsa": "4.0.1",
+        "create-hash": "1.2.0",
+        "create-hmac": "1.1.7",
+        "elliptic": "6.4.0",
+        "inherits": "2.0.3",
+        "parse-asn1": "5.1.1"
+      }
+    },
+    "browserify-zlib": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+      "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
+      "dev": true,
+      "requires": {
+        "pako": "1.0.6"
+      }
+    },
     "browserslist": {
       "version": "3.2.8",
       "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.2.8.tgz",
       "integrity": "sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==",
       "dev": true,
       "requires": {
-        "caniuse-lite": "1.0.30000847",
+        "caniuse-lite": "1.0.30000856",
         "electron-to-chromium": "1.3.48"
       }
     },
+    "buffer": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.1.0.tgz",
+      "integrity": "sha512-YkIRgwsZwJWTnyQrsBTWefizHh+8GYj3kbL1BTiAQ/9pwpino0G7B2gp5tx/FUBqUlvtxV85KNR3mwfAtv15Yw==",
+      "dev": true,
+      "requires": {
+        "base64-js": "1.3.0",
+        "ieee754": "1.1.12"
+      }
+    },
     "buffer-equal": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz",
@@ -1193,6 +1603,12 @@
       "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ==",
       "dev": true
     },
+    "buffer-xor": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
+      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
+      "dev": true
+    },
     "bufferstreams": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/bufferstreams/-/bufferstreams-1.1.3.tgz",
@@ -1208,6 +1624,12 @@
       "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
       "dev": true
     },
+    "builtin-status-codes": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
+      "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
+      "dev": true
+    },
     "c3": {
       "version": "0.4.23",
       "resolved": "https://registry.npmjs.org/c3/-/c3-0.4.23.tgz",
@@ -1231,16 +1653,14 @@
         "to-object-path": "0.3.0",
         "union-value": "1.0.0",
         "unset-value": "1.0.0"
-      },
-      "dependencies": {
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
-        }
       }
     },
+    "cached-path-relative": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.1.tgz",
+      "integrity": "sha1-0JxLUoAKpMB44t2BqGmqyQ0uVOc=",
+      "dev": true
+    },
     "caller-path": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz",
@@ -1263,22 +1683,34 @@
       "dev": true
     },
     "caniuse-lite": {
-      "version": "1.0.30000847",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000847.tgz",
-      "integrity": "sha512-Weo+tRtVWcN2da782Ebx/27hFNEb+KP+uP6tdqAa+2S5bp1zOJhVH9tEpDygagrfvU4QjeuPwi/5VGsgT4SLaA==",
+      "version": "1.0.30000856",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000856.tgz",
+      "integrity": "sha512-x3mYcApHMQemyaHuH/RyqtKCGIYTgEA63fdi+VBvDz8xUSmRiVWTLeyKcoGQCGG6UPR9/+4qG4OKrTa6aSQRKg==",
       "dev": true
     },
+    "chai": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz",
+      "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=",
+      "dev": true,
+      "requires": {
+        "assertion-error": "1.1.0",
+        "check-error": "1.0.2",
+        "deep-eql": "3.0.1",
+        "get-func-name": "2.0.0",
+        "pathval": "1.1.0",
+        "type-detect": "4.0.8"
+      }
+    },
     "chalk": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
-      "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+      "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
       "dev": true,
       "requires": {
-        "ansi-styles": "2.2.1",
+        "ansi-styles": "3.2.1",
         "escape-string-regexp": "1.0.5",
-        "has-ansi": "2.0.0",
-        "strip-ansi": "3.0.1",
-        "supports-color": "2.0.0"
+        "supports-color": "5.4.0"
       }
     },
     "chardet": {
@@ -1287,6 +1719,12 @@
       "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=",
       "dev": true
     },
+    "check-error": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
+      "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
+      "dev": true
+    },
     "chokidar": {
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
@@ -1302,9 +1740,30 @@
         "is-glob": "2.0.1",
         "path-is-absolute": "1.0.1",
         "readdirp": "2.1.0"
-      }
-    },
-    "circular-json": {
+      },
+      "dependencies": {
+        "glob-parent": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
+          "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+          "dev": true,
+          "requires": {
+            "is-glob": "2.0.1"
+          }
+        }
+      }
+    },
+    "cipher-base": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
+      "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3",
+        "safe-buffer": "5.1.2"
+      }
+    },
+    "circular-json": {
       "version": "0.3.3",
       "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz",
       "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==",
@@ -1330,12 +1789,6 @@
           "requires": {
             "is-descriptor": "0.1.6"
           }
-        },
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
         }
       }
     },
@@ -1346,6 +1799,14 @@
       "dev": true,
       "requires": {
         "source-map": "0.5.7"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.5.7",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+          "dev": true
+        }
       }
     },
     "cli-cursor": {
@@ -1401,14 +1862,6 @@
         "inherits": "2.0.3",
         "process-nextick-args": "2.0.0",
         "readable-stream": "2.3.6"
-      },
-      "dependencies": {
-        "process-nextick-args": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
-          "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
-          "dev": true
-        }
       }
     },
     "co": {
@@ -1456,18 +1909,18 @@
       }
     },
     "color-convert": {
-      "version": "1.9.1",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz",
-      "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==",
+      "version": "1.9.2",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz",
+      "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==",
       "dev": true,
       "requires": {
-        "color-name": "1.1.3"
+        "color-name": "1.1.1"
       }
     },
     "color-name": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz",
+      "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=",
       "dev": true
     },
     "color-support": {
@@ -1476,6 +1929,32 @@
       "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
       "dev": true
     },
+    "combine-source-map": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz",
+      "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=",
+      "dev": true,
+      "requires": {
+        "convert-source-map": "1.1.3",
+        "inline-source-map": "0.6.2",
+        "lodash.memoize": "3.0.4",
+        "source-map": "0.5.7"
+      },
+      "dependencies": {
+        "convert-source-map": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz",
+          "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=",
+          "dev": true
+        },
+        "source-map": {
+          "version": "0.5.7",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+          "dev": true
+        }
+      }
+    },
     "commander": {
       "version": "2.15.1",
       "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
@@ -1513,16 +1992,23 @@
       "dev": true,
       "requires": {
         "source-map": "0.6.1"
-      },
-      "dependencies": {
-        "source-map": {
-          "version": "0.6.1",
-          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-          "dev": true
-        }
       }
     },
+    "console-browserify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
+      "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=",
+      "dev": true,
+      "requires": {
+        "date-now": "0.1.4"
+      }
+    },
+    "constants-browserify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
+      "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=",
+      "dev": true
+    },
     "convert-source-map": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz",
@@ -1548,8 +2034,7 @@
     "core-js": {
       "version": "2.5.7",
       "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
-      "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==",
-      "dev": true
+      "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw=="
     },
     "core-util-is": {
       "version": "1.0.2",
@@ -1557,6 +2042,43 @@
       "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
       "dev": true
     },
+    "create-ecdh": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
+      "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==",
+      "dev": true,
+      "requires": {
+        "bn.js": "4.11.8",
+        "elliptic": "6.4.0"
+      }
+    },
+    "create-hash": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+      "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+      "dev": true,
+      "requires": {
+        "cipher-base": "1.0.4",
+        "inherits": "2.0.3",
+        "md5.js": "1.3.4",
+        "ripemd160": "2.0.2",
+        "sha.js": "2.4.11"
+      }
+    },
+    "create-hmac": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+      "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+      "dev": true,
+      "requires": {
+        "cipher-base": "1.0.4",
+        "create-hash": "1.2.0",
+        "inherits": "2.0.3",
+        "ripemd160": "2.0.2",
+        "safe-buffer": "5.1.2",
+        "sha.js": "2.4.11"
+      }
+    },
     "cross-spawn": {
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
@@ -1568,6 +2090,25 @@
         "which": "1.3.1"
       }
     },
+    "crypto-browserify": {
+      "version": "3.12.0",
+      "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
+      "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
+      "dev": true,
+      "requires": {
+        "browserify-cipher": "1.0.1",
+        "browserify-sign": "4.0.4",
+        "create-ecdh": "4.0.3",
+        "create-hash": "1.2.0",
+        "create-hmac": "1.1.7",
+        "diffie-hellman": "5.0.3",
+        "inherits": "2.0.3",
+        "pbkdf2": "3.0.16",
+        "public-encrypt": "4.0.2",
+        "randombytes": "2.0.6",
+        "randomfill": "1.0.4"
+      }
+    },
     "css": {
       "version": "2.2.3",
       "resolved": "https://registry.npmjs.org/css/-/css-2.2.3.tgz",
@@ -1628,6 +2169,24 @@
         "d3-time": "1.0.8"
       }
     },
+    "dargs": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/dargs/-/dargs-5.1.0.tgz",
+      "integrity": "sha1-7H6lDHhWTNNsnV7Bj2Yyn63ieCk=",
+      "dev": true
+    },
+    "date-now": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
+      "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=",
+      "dev": true
+    },
+    "dateformat": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz",
+      "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=",
+      "dev": true
+    },
     "debug": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
@@ -1659,6 +2218,15 @@
       "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
       "dev": true
     },
+    "deep-eql": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
+      "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
+      "dev": true,
+      "requires": {
+        "type-detect": "4.0.8"
+      }
+    },
     "deep-is": {
       "version": "0.1.3",
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
@@ -1695,7 +2263,7 @@
       "dev": true,
       "requires": {
         "foreach": "2.0.5",
-        "object-keys": "1.0.11"
+        "object-keys": "1.0.12"
       }
     },
     "define-property": {
@@ -1737,12 +2305,6 @@
             "kind-of": "6.0.2"
           }
         },
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
-        },
         "kind-of": {
           "version": "6.0.2",
           "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
@@ -1751,6 +2313,12 @@
         }
       }
     },
+    "defined": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
+      "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
+      "dev": true
+    },
     "del": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz",
@@ -1763,6 +2331,51 @@
         "p-map": "1.2.0",
         "pify": "3.0.0",
         "rimraf": "2.6.2"
+      },
+      "dependencies": {
+        "globby": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
+          "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
+          "dev": true,
+          "requires": {
+            "array-union": "1.0.2",
+            "glob": "7.1.2",
+            "object-assign": "4.1.1",
+            "pify": "2.3.0",
+            "pinkie-promise": "2.0.1"
+          },
+          "dependencies": {
+            "pify": {
+              "version": "2.3.0",
+              "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+              "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+              "dev": true
+            }
+          }
+        }
+      }
+    },
+    "deps-sort": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.0.tgz",
+      "integrity": "sha1-CRckkC6EZYJg65EHSMzNGvbiH7U=",
+      "dev": true,
+      "requires": {
+        "JSONStream": "1.3.3",
+        "shasum": "1.0.2",
+        "subarg": "1.0.0",
+        "through2": "2.0.3"
+      }
+    },
+    "des.js": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz",
+      "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3",
+        "minimalistic-assert": "1.0.1"
       }
     },
     "detect-file": {
@@ -1786,18 +2399,40 @@
       "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=",
       "dev": true
     },
+    "detective": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/detective/-/detective-5.1.0.tgz",
+      "integrity": "sha512-TFHMqfOvxlgrfVzTEkNBSh9SvSNX/HfF4OFI2QFGCyPm02EsyILqnUeb5P6q7JZ3SFNTBL5t2sePRgrN4epUWQ==",
+      "dev": true,
+      "requires": {
+        "acorn-node": "1.5.2",
+        "defined": "1.0.0",
+        "minimist": "1.2.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        }
+      }
+    },
     "diff": {
       "version": "3.5.0",
       "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
       "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
       "dev": true
     },
-    "dispatch-management": {
-      "version": "0.1.21",
-      "resolved": "https://registry.npmjs.org/dispatch-management/-/dispatch-management-0.1.21.tgz",
-      "integrity": "sha512-BfD3w/61q4mshOKIQKpzS5NBZNiUkJgdL65soyBghsU+ICFgshXWxwHsIl4fPaTezR3IuHAaWqS4wGnzAfhDlg==",
+    "diffie-hellman": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+      "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
+      "dev": true,
       "requires": {
-        "d3-queue": "3.0.7"
+        "bn.js": "4.11.8",
+        "miller-rabin": "4.0.1",
+        "randombytes": "2.0.6"
       }
     },
     "doctrine": {
@@ -1809,6 +2444,21 @@
         "esutils": "2.0.2"
       }
     },
+    "domain-browser": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
+      "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==",
+      "dev": true
+    },
+    "duplexer2": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
+      "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=",
+      "dev": true,
+      "requires": {
+        "readable-stream": "2.3.6"
+      }
+    },
     "duplexify": {
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz",
@@ -1837,6 +2487,21 @@
       "integrity": "sha1-07DYWTgUBE4JLs4hCPw6ya6kuQA=",
       "dev": true
     },
+    "elliptic": {
+      "version": "6.4.0",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz",
+      "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=",
+      "dev": true,
+      "requires": {
+        "bn.js": "4.11.8",
+        "brorand": "1.1.0",
+        "hash.js": "1.1.4",
+        "hmac-drbg": "1.0.1",
+        "inherits": "2.0.3",
+        "minimalistic-assert": "1.0.1",
+        "minimalistic-crypto-utils": "1.0.1"
+      }
+    },
     "end-of-stream": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
@@ -1847,9 +2512,9 @@
       }
     },
     "error-ex": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz",
-      "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=",
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
       "dev": true,
       "requires": {
         "is-arrayish": "0.2.1"
@@ -1926,7 +2591,7 @@
         "file-entry-cache": "2.0.0",
         "functional-red-black-tree": "1.0.1",
         "glob": "7.1.2",
-        "globals": "11.5.0",
+        "globals": "11.7.0",
         "ignore": "3.3.8",
         "imurmurhash": "0.1.4",
         "inquirer": "3.3.0",
@@ -1957,30 +2622,10 @@
           "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
           "dev": true
         },
-        "ansi-styles": {
-          "version": "3.2.1",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-          "dev": true,
-          "requires": {
-            "color-convert": "1.9.1"
-          }
-        },
-        "chalk": {
-          "version": "2.4.1",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
-          "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
-          "dev": true,
-          "requires": {
-            "ansi-styles": "3.2.1",
-            "escape-string-regexp": "1.0.5",
-            "supports-color": "5.4.0"
-          }
-        },
         "globals": {
-          "version": "11.5.0",
-          "resolved": "https://registry.npmjs.org/globals/-/globals-11.5.0.tgz",
-          "integrity": "sha512-hYyf+kI8dm3nORsiiXUQigOU62hDLfJ9G01uyGMxhc6BKsircrUhC4uJPQPUSuq2GrTmiiEt7ewxlMdBewfmKQ==",
+          "version": "11.7.0",
+          "resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz",
+          "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==",
           "dev": true
         },
         "strip-ansi": {
@@ -1991,15 +2636,6 @@
           "requires": {
             "ansi-regex": "3.0.0"
           }
-        },
-        "supports-color": {
-          "version": "5.4.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
-          "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
-          "dev": true,
-          "requires": {
-            "has-flag": "3.0.0"
-          }
         }
       }
     },
@@ -2025,7 +2661,7 @@
       "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==",
       "dev": true,
       "requires": {
-        "acorn": "5.6.1",
+        "acorn": "5.7.1",
         "acorn-jsx": "3.0.1"
       }
     },
@@ -2075,6 +2711,52 @@
         "es5-ext": "0.10.45"
       }
     },
+    "events": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz",
+      "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg==",
+      "dev": true
+    },
+    "evp_bytestokey": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
+      "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+      "dev": true,
+      "requires": {
+        "md5.js": "1.3.4",
+        "safe-buffer": "5.1.2"
+      }
+    },
+    "execa": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz",
+      "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==",
+      "dev": true,
+      "requires": {
+        "cross-spawn": "6.0.5",
+        "get-stream": "3.0.0",
+        "is-stream": "1.1.0",
+        "npm-run-path": "2.0.2",
+        "p-finally": "1.0.0",
+        "signal-exit": "3.0.2",
+        "strip-eof": "1.0.0"
+      },
+      "dependencies": {
+        "cross-spawn": {
+          "version": "6.0.5",
+          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+          "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+          "dev": true,
+          "requires": {
+            "nice-try": "1.0.4",
+            "path-key": "2.0.1",
+            "semver": "5.5.0",
+            "shebang-command": "1.2.0",
+            "which": "1.3.1"
+          }
+        }
+      }
+    },
     "expand-brackets": {
       "version": "0.1.5",
       "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
@@ -2147,6 +2829,14 @@
       "dev": true,
       "requires": {
         "is-extglob": "1.0.0"
+      },
+      "dependencies": {
+        "is-extglob": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+          "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+          "dev": true
+        }
       }
     },
     "fancy-log": {
@@ -2214,6 +2904,17 @@
         "randomatic": "3.0.0",
         "repeat-element": "1.1.2",
         "repeat-string": "1.6.1"
+      },
+      "dependencies": {
+        "isobject": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+          "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+          "dev": true,
+          "requires": {
+            "isarray": "1.0.0"
+          }
+        }
       }
     },
     "find-up": {
@@ -2238,12 +2939,6 @@
         "resolve-dir": "1.0.1"
       },
       "dependencies": {
-        "arr-diff": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
-          "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
-          "dev": true
-        },
         "array-unique": {
           "version": "0.3.2",
           "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
@@ -2468,12 +3163,6 @@
             "kind-of": "6.0.2"
           }
         },
-        "is-extglob": {
-          "version": "2.1.1",
-          "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-          "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
-          "dev": true
-        },
         "is-glob": {
           "version": "3.1.0",
           "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
@@ -2503,12 +3192,6 @@
             }
           }
         },
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
-        },
         "kind-of": {
           "version": "6.0.2",
           "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
@@ -2637,6 +3320,12 @@
       "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=",
       "dev": true
     },
+    "fork-stream": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/fork-stream/-/fork-stream-0.0.4.tgz",
+      "integrity": "sha1-24Sfznf2cIpfjzhq5TOgkHtUrnA=",
+      "dev": true
+    },
     "fragment-cache": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
@@ -3209,12 +3898,30 @@
       "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
       "dev": true
     },
+    "get-assigned-identifiers": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz",
+      "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==",
+      "dev": true
+    },
     "get-caller-file": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz",
       "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=",
       "dev": true
     },
+    "get-func-name": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
+      "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
+      "dev": true
+    },
+    "get-stream": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
+      "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
+      "dev": true
+    },
     "get-value": {
       "version": "2.0.6",
       "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
@@ -3243,19 +3950,42 @@
       "requires": {
         "glob-parent": "2.0.0",
         "is-glob": "2.0.1"
+      },
+      "dependencies": {
+        "glob-parent": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
+          "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+          "dev": true,
+          "requires": {
+            "is-glob": "2.0.1"
+          }
+        }
       }
     },
     "glob-parent": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
-      "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+      "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
       "dev": true,
       "requires": {
-        "is-glob": "2.0.1"
-      }
-    },
-    "glob-stream": {
-      "version": "6.1.0",
+        "is-glob": "3.1.0",
+        "path-dirname": "1.0.2"
+      },
+      "dependencies": {
+        "is-glob": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+          "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+          "dev": true,
+          "requires": {
+            "is-extglob": "2.1.1"
+          }
+        }
+      }
+    },
+    "glob-stream": {
+      "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz",
       "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=",
       "dev": true,
@@ -3270,33 +4000,6 @@
         "remove-trailing-separator": "1.1.0",
         "to-absolute-glob": "2.0.2",
         "unique-stream": "2.2.1"
-      },
-      "dependencies": {
-        "glob-parent": {
-          "version": "3.1.0",
-          "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
-          "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
-          "dev": true,
-          "requires": {
-            "is-glob": "3.1.0",
-            "path-dirname": "1.0.2"
-          }
-        },
-        "is-extglob": {
-          "version": "2.1.1",
-          "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-          "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
-          "dev": true
-        },
-        "is-glob": {
-          "version": "3.1.0",
-          "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
-          "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
-          "dev": true,
-          "requires": {
-            "is-extglob": "2.1.1"
-          }
-        }
       }
     },
     "glob-watcher": {
@@ -3341,27 +4044,6 @@
       "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==",
       "dev": true
     },
-    "globby": {
-      "version": "6.1.0",
-      "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
-      "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
-      "dev": true,
-      "requires": {
-        "array-union": "1.0.2",
-        "glob": "7.1.2",
-        "object-assign": "4.1.1",
-        "pify": "2.3.0",
-        "pinkie-promise": "2.0.1"
-      },
-      "dependencies": {
-        "pify": {
-          "version": "2.3.0",
-          "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
-          "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
-          "dev": true
-        }
-      }
-    },
     "glogg": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.1.tgz",
@@ -3377,6 +4059,12 @@
       "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
       "dev": true
     },
+    "growl": {
+      "version": "1.10.5",
+      "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+      "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
+      "dev": true
+    },
     "gulp": {
       "version": "github:gulpjs/gulp#71c094a51c7972d26f557899ddecab0210ef3776",
       "dev": true,
@@ -3412,12 +4100,6 @@
             "v8flags": "3.1.1",
             "yargs": "7.1.0"
           }
-        },
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
         }
       }
     },
@@ -3475,6 +4157,17 @@
         "plugin-error": "1.0.1"
       }
     },
+    "gulp-if": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/gulp-if/-/gulp-if-2.0.2.tgz",
+      "integrity": "sha1-pJe351cwBQQcqivIt92jyARE1ik=",
+      "dev": true,
+      "requires": {
+        "gulp-match": "1.0.3",
+        "ternary-stream": "2.0.1",
+        "through2": "2.0.3"
+      }
+    },
     "gulp-insert": {
       "version": "0.5.0",
       "resolved": "https://registry.npmjs.org/gulp-insert/-/gulp-insert-0.5.0.tgz",
@@ -3511,6 +4204,30 @@
         }
       }
     },
+    "gulp-match": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/gulp-match/-/gulp-match-1.0.3.tgz",
+      "integrity": "sha1-kcfA1/Kb7NZgbVfYCn+Hdqh6uo4=",
+      "dev": true,
+      "requires": {
+        "minimatch": "3.0.4"
+      }
+    },
+    "gulp-mocha": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/gulp-mocha/-/gulp-mocha-6.0.0.tgz",
+      "integrity": "sha512-FfBldW5ttnDpKf4Sg6/BLOOKCCbr5mbixDGK1t02/8oSrTCwNhgN/mdszG3cuQuYNzuouUdw4EH/mlYtgUscPg==",
+      "dev": true,
+      "requires": {
+        "dargs": "5.1.0",
+        "execa": "0.10.0",
+        "mocha": "5.2.0",
+        "npm-run-path": "2.0.2",
+        "plugin-error": "1.0.1",
+        "supports-color": "5.4.0",
+        "through2": "2.0.3"
+      }
+    },
     "gulp-ng-annotate": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/gulp-ng-annotate/-/gulp-ng-annotate-2.1.0.tgz",
@@ -3578,9 +4295,9 @@
       }
     },
     "gulp-rename": {
-      "version": "1.2.3",
-      "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-1.2.3.tgz",
-      "integrity": "sha512-CmdPM0BjJ105QCX1fk+j7NGhiN/1rCl9HLGss+KllBS/tdYadpjTxqdKyh/5fNV+M3yjT1MFz5z93bXdrTyzAw==",
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-1.3.0.tgz",
+      "integrity": "sha512-nEuZB7/9i0IZ8AXORTizl2QLP9tcC9uWc/s329zElBLJw1CfOhmMXBxwVlCRKjDyrWuhVP0uBKl61KeQ32TiCg==",
       "dev": true
     },
     "gulp-sourcemaps": {
@@ -3591,7 +4308,7 @@
       "requires": {
         "@gulp-sourcemaps/identity-map": "1.0.1",
         "@gulp-sourcemaps/map-sources": "1.0.0",
-        "acorn": "5.6.1",
+        "acorn": "5.7.1",
         "convert-source-map": "1.5.1",
         "css": "2.2.3",
         "debug-fabulous": "1.1.0",
@@ -3600,14 +4317,17 @@
         "source-map": "0.6.1",
         "strip-bom-string": "1.0.0",
         "through2": "2.0.3"
-      },
-      "dependencies": {
-        "source-map": {
-          "version": "0.6.1",
-          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-          "dev": true
-        }
+      }
+    },
+    "gulp-terser": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/gulp-terser/-/gulp-terser-1.0.1.tgz",
+      "integrity": "sha512-UwesC0Lma+rk17pS/rFjVWN4zan2v/vRKDnLbZMIZJqP236yVlbzAPvD1VWDi5u3pCYKbLGoI3ynlgVat+7u9A==",
+      "dev": true,
+      "requires": {
+        "plugin-error": "1.0.1",
+        "terser": "3.7.6",
+        "through2": "2.0.3"
       }
     },
     "gulp-tslint": {
@@ -3624,15 +4344,6 @@
         "through": "2.3.8"
       },
       "dependencies": {
-        "ansi-styles": {
-          "version": "3.2.1",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-          "dev": true,
-          "requires": {
-            "color-convert": "1.9.1"
-          }
-        },
         "chalk": {
           "version": "2.3.1",
           "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz",
@@ -3643,15 +4354,6 @@
             "escape-string-regexp": "1.0.5",
             "supports-color": "5.4.0"
           }
-        },
-        "supports-color": {
-          "version": "5.4.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
-          "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
-          "dev": true,
-          "requires": {
-            "has-flag": "3.0.0"
-          }
         }
       }
     },
@@ -3718,12 +4420,6 @@
             "arr-union": "2.1.0",
             "extend-shallow": "1.1.4"
           }
-        },
-        "source-map": {
-          "version": "0.6.1",
-          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-          "dev": true
         }
       }
     },
@@ -3738,10 +4434,133 @@
         "lodash": "4.17.10",
         "make-error-cause": "1.2.2",
         "through2": "2.0.3",
-        "uglify-js": "3.4.0",
+        "uglify-js": "3.4.1",
         "vinyl-sourcemaps-apply": "0.2.1"
       }
     },
+    "gulp-uglifyes": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/gulp-uglifyes/-/gulp-uglifyes-0.1.3.tgz",
+      "integrity": "sha512-p6EzPCBbPo+joHp85NxNcT/aPuodfb+UIYTsEKB8PuxAPwbiqhr3HBrpARqvLyO5VYYjREynnT6Tr++0YLRSUQ==",
+      "dev": true,
+      "requires": {
+        "gulp-util": "3.0.8",
+        "through2": "2.0.3",
+        "uglify-es": "3.3.9"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.13.0",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz",
+          "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==",
+          "dev": true
+        },
+        "uglify-es": {
+          "version": "3.3.9",
+          "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz",
+          "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==",
+          "dev": true,
+          "requires": {
+            "commander": "2.13.0",
+            "source-map": "0.6.1"
+          }
+        }
+      }
+    },
+    "gulp-util": {
+      "version": "3.0.8",
+      "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz",
+      "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=",
+      "dev": true,
+      "requires": {
+        "array-differ": "1.0.0",
+        "array-uniq": "1.0.3",
+        "beeper": "1.1.1",
+        "chalk": "1.1.3",
+        "dateformat": "2.2.0",
+        "fancy-log": "1.3.2",
+        "gulplog": "1.0.0",
+        "has-gulplog": "0.1.0",
+        "lodash._reescape": "3.0.0",
+        "lodash._reevaluate": "3.0.0",
+        "lodash._reinterpolate": "3.0.0",
+        "lodash.template": "3.6.2",
+        "minimist": "1.2.0",
+        "multipipe": "0.1.2",
+        "object-assign": "3.0.0",
+        "replace-ext": "0.0.1",
+        "through2": "2.0.3",
+        "vinyl": "0.5.3"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "2.2.1",
+            "escape-string-regexp": "1.0.5",
+            "has-ansi": "2.0.0",
+            "strip-ansi": "3.0.1",
+            "supports-color": "2.0.0"
+          }
+        },
+        "clone": {
+          "version": "1.0.4",
+          "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+          "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
+          "dev": true
+        },
+        "clone-stats": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz",
+          "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=",
+          "dev": true
+        },
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        },
+        "object-assign": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz",
+          "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=",
+          "dev": true
+        },
+        "replace-ext": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz",
+          "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true
+        },
+        "vinyl": {
+          "version": "0.5.3",
+          "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz",
+          "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=",
+          "dev": true,
+          "requires": {
+            "clone": "1.0.4",
+            "clone-stats": "0.0.1",
+            "replace-ext": "0.0.1"
+          }
+        }
+      }
+    },
     "gulplog": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz",
@@ -3751,6 +4570,15 @@
         "glogg": "1.0.1"
       }
     },
+    "has": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+      "dev": true,
+      "requires": {
+        "function-bind": "1.1.1"
+      }
+    },
     "has-ansi": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
@@ -3790,14 +4618,6 @@
         "get-value": "2.0.6",
         "has-values": "1.0.0",
         "isobject": "3.0.1"
-      },
-      "dependencies": {
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
-        }
       }
     },
     "has-values": {
@@ -3841,6 +4661,43 @@
         }
       }
     },
+    "hash-base": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
+      "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3",
+        "safe-buffer": "5.1.2"
+      }
+    },
+    "hash.js": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.4.tgz",
+      "integrity": "sha512-A6RlQvvZEtFS5fLU43IDu0QUmBy+fDO9VMdTXvufKwIkt/rFfvICAViCax5fbDO4zdNzaC3/27ZhKUok5bAJyw==",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3",
+        "minimalistic-assert": "1.0.1"
+      }
+    },
+    "he": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
+      "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
+      "dev": true
+    },
+    "hmac-drbg": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+      "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
+      "dev": true,
+      "requires": {
+        "hash.js": "1.1.4",
+        "minimalistic-assert": "1.0.1",
+        "minimalistic-crypto-utils": "1.0.1"
+      }
+    },
     "home-or-tmp": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz",
@@ -3866,10 +4723,17 @@
       "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==",
       "dev": true
     },
-    "html5shiv": {
-      "version": "3.7.3",
-      "resolved": "https://registry.npmjs.org/html5shiv/-/html5shiv-3.7.3.tgz",
-      "integrity": "sha1-14qEo2e8uacQEA1XgCw4ewhGMdI="
+    "htmlescape": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz",
+      "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=",
+      "dev": true
+    },
+    "https-browserify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
+      "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
+      "dev": true
     },
     "iconv-lite": {
       "version": "0.4.23",
@@ -3880,6 +4744,12 @@
         "safer-buffer": "2.1.2"
       }
     },
+    "ieee754": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz",
+      "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==",
+      "dev": true
+    },
     "ignore": {
       "version": "3.3.8",
       "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.8.tgz",
@@ -3914,6 +4784,23 @@
       "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
       "dev": true
     },
+    "inline-source-map": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz",
+      "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=",
+      "dev": true,
+      "requires": {
+        "source-map": "0.5.7"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.5.7",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+          "dev": true
+        }
+      }
+    },
     "inquirer": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz",
@@ -3942,26 +4829,6 @@
           "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
           "dev": true
         },
-        "ansi-styles": {
-          "version": "3.2.1",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-          "dev": true,
-          "requires": {
-            "color-convert": "1.9.1"
-          }
-        },
-        "chalk": {
-          "version": "2.4.1",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
-          "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
-          "dev": true,
-          "requires": {
-            "ansi-styles": "3.2.1",
-            "escape-string-regexp": "1.0.5",
-            "supports-color": "5.4.0"
-          }
-        },
         "is-fullwidth-code-point": {
           "version": "2.0.0",
           "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
@@ -3986,18 +4853,27 @@
           "requires": {
             "ansi-regex": "3.0.0"
           }
-        },
-        "supports-color": {
-          "version": "5.4.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
-          "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
-          "dev": true,
-          "requires": {
-            "has-flag": "3.0.0"
-          }
         }
       }
     },
+    "insert-module-globals": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.0.tgz",
+      "integrity": "sha512-VE6NlW+WGn2/AeOMd496AHFYmE7eLKkUY6Ty31k4og5vmA3Fjuwe9v6ifH6Xx/Hz27QvdoMoviw1/pqWRB09Sw==",
+      "dev": true,
+      "requires": {
+        "JSONStream": "1.3.3",
+        "acorn-node": "1.5.2",
+        "combine-source-map": "0.8.0",
+        "concat-stream": "1.6.2",
+        "is-buffer": "1.1.6",
+        "path-is-absolute": "1.0.1",
+        "process": "0.11.10",
+        "through2": "2.0.3",
+        "undeclared-identifiers": "1.1.2",
+        "xtend": "4.0.1"
+      }
+    },
     "interpret": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
@@ -4118,9 +4994,9 @@
       "dev": true
     },
     "is-extglob": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
-      "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
       "dev": true
     },
     "is-finite": {
@@ -4148,6 +5024,14 @@
       "dev": true,
       "requires": {
         "is-extglob": "1.0.0"
+      },
+      "dependencies": {
+        "is-extglob": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+          "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+          "dev": true
+        }
       }
     },
     "is-negated-glob": {
@@ -4213,14 +5097,6 @@
       "dev": true,
       "requires": {
         "isobject": "3.0.1"
-      },
-      "dependencies": {
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
-        }
       }
     },
     "is-posix-bracket": {
@@ -4256,6 +5132,12 @@
       "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==",
       "dev": true
     },
+    "is-stream": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+      "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
+      "dev": true
+    },
     "is-unc-path": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz",
@@ -4296,13 +5178,10 @@
       "dev": true
     },
     "isobject": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
-      "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
-      "dev": true,
-      "requires": {
-        "isarray": "1.0.0"
-      }
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+      "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+      "dev": true
     },
     "jquery": {
       "version": "3.3.1",
@@ -4315,9 +5194,9 @@
       "integrity": "sha1-XAgV08xvkP9fqvWyaKbiO0ypBPo="
     },
     "jquery.fancytree": {
-      "version": "2.28.1",
-      "resolved": "https://registry.npmjs.org/jquery.fancytree/-/jquery.fancytree-2.28.1.tgz",
-      "integrity": "sha512-Vs5ka+Zn0ldZtmtjHtzE/8bzN3RqFb2nbE+nS6ZUy4ISW6KOBGk2QYtLFA8HbPsgkqVnjoDIXvU7VI6CSQ/FmA==",
+      "version": "2.29.0",
+      "resolved": "https://registry.npmjs.org/jquery.fancytree/-/jquery.fancytree-2.29.0.tgz",
+      "integrity": "sha512-n08g4KxmXt1JNUK2D2kRxD7t1r2Nke5OhrMviqfBh5qdcJB1DWXYAZmHYEyVr/r3cpuyU6vy4gMDWHnh30v4vA==",
       "requires": {
         "jquery": "3.3.1"
       }
@@ -4351,9 +5230,9 @@
       "dev": true
     },
     "json-stable-stringify": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
-      "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=",
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz",
+      "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=",
       "dev": true,
       "requires": {
         "jsonify": "0.0.0"
@@ -4377,6 +5256,12 @@
       "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=",
       "dev": true
     },
+    "jsonparse": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
+      "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=",
+      "dev": true
+    },
     "just-debounce": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz",
@@ -4392,6 +5277,25 @@
         "is-buffer": "1.1.6"
       }
     },
+    "labeled-stream-splicer": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.1.tgz",
+      "integrity": "sha512-MC94mHZRvJ3LfykJlTUipBqenZz1pacOZEMhhQ8dMGcDHs0SBE5GbsavUXV7YtP3icBW17W0Zy1I0lfASmo9Pg==",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3",
+        "isarray": "2.0.4",
+        "stream-splicer": "2.0.0"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "2.0.4",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.4.tgz",
+          "integrity": "sha512-GMxXOiUirWg1xTKRipM0Ek07rX+ubx4nNVElTJdNLYmNO/2YrDkgJGw9CljXn+r4EWiDQg/8lsRdHyg2PJuUaA==",
+          "dev": true
+        }
+      }
+    },
     "last-run": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz",
@@ -4452,7 +5356,7 @@
         "is-plain-object": "2.0.4",
         "object.map": "1.0.1",
         "rechoir": "0.6.2",
-        "resolve": "1.7.1"
+        "resolve": "1.8.1"
       }
     },
     "load-json-file": {
@@ -4482,6 +5386,131 @@
       "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==",
       "dev": true
     },
+    "lodash._basecopy": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz",
+      "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=",
+      "dev": true
+    },
+    "lodash._basetostring": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz",
+      "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=",
+      "dev": true
+    },
+    "lodash._basevalues": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz",
+      "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=",
+      "dev": true
+    },
+    "lodash._getnative": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz",
+      "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=",
+      "dev": true
+    },
+    "lodash._isiterateecall": {
+      "version": "3.0.9",
+      "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz",
+      "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=",
+      "dev": true
+    },
+    "lodash._reescape": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz",
+      "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=",
+      "dev": true
+    },
+    "lodash._reevaluate": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz",
+      "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=",
+      "dev": true
+    },
+    "lodash._reinterpolate": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
+      "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=",
+      "dev": true
+    },
+    "lodash._root": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz",
+      "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=",
+      "dev": true
+    },
+    "lodash.escape": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz",
+      "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=",
+      "dev": true,
+      "requires": {
+        "lodash._root": "3.0.1"
+      }
+    },
+    "lodash.isarguments": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
+      "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=",
+      "dev": true
+    },
+    "lodash.isarray": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz",
+      "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=",
+      "dev": true
+    },
+    "lodash.keys": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
+      "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=",
+      "dev": true,
+      "requires": {
+        "lodash._getnative": "3.9.1",
+        "lodash.isarguments": "3.1.0",
+        "lodash.isarray": "3.0.4"
+      }
+    },
+    "lodash.memoize": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz",
+      "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=",
+      "dev": true
+    },
+    "lodash.restparam": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz",
+      "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=",
+      "dev": true
+    },
+    "lodash.template": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz",
+      "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=",
+      "dev": true,
+      "requires": {
+        "lodash._basecopy": "3.0.1",
+        "lodash._basetostring": "3.0.1",
+        "lodash._basevalues": "3.0.0",
+        "lodash._isiterateecall": "3.0.9",
+        "lodash._reinterpolate": "3.0.0",
+        "lodash.escape": "3.2.0",
+        "lodash.keys": "3.1.2",
+        "lodash.restparam": "3.6.1",
+        "lodash.templatesettings": "3.1.1"
+      }
+    },
+    "lodash.templatesettings": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz",
+      "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=",
+      "dev": true,
+      "requires": {
+        "lodash._reinterpolate": "3.0.0",
+        "lodash.escape": "3.2.0"
+      }
+    },
     "loose-envify": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
@@ -4571,16 +5600,10 @@
       "requires": {
         "findup-sync": "2.0.0",
         "micromatch": "3.1.10",
-        "resolve": "1.7.1",
+        "resolve": "1.8.1",
         "stack-trace": "0.0.10"
       },
       "dependencies": {
-        "arr-diff": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
-          "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
-          "dev": true
-        },
         "array-unique": {
           "version": "0.3.2",
           "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
@@ -4825,12 +5848,6 @@
             }
           }
         },
-        "isobject": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
-          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
-          "dev": true
-        },
         "kind-of": {
           "version": "6.0.2",
           "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
@@ -4866,6 +5883,16 @@
       "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=",
       "dev": true
     },
+    "md5.js": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz",
+      "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=",
+      "dev": true,
+      "requires": {
+        "hash-base": "3.0.4",
+        "inherits": "2.0.3"
+      }
+    },
     "memoizee": {
       "version": "0.4.12",
       "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.12.tgz",
@@ -4888,6 +5915,15 @@
       "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=",
       "dev": true
     },
+    "merge-stream": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz",
+      "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=",
+      "dev": true,
+      "requires": {
+        "readable-stream": "2.3.6"
+      }
+    },
     "micromatch": {
       "version": "2.3.11",
       "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
@@ -4907,6 +5943,33 @@
         "object.omit": "2.0.1",
         "parse-glob": "3.0.4",
         "regex-cache": "0.4.4"
+      },
+      "dependencies": {
+        "arr-diff": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
+          "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
+          "dev": true,
+          "requires": {
+            "arr-flatten": "1.1.0"
+          }
+        },
+        "is-extglob": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+          "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+          "dev": true
+        }
+      }
+    },
+    "miller-rabin": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
+      "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
+      "dev": true,
+      "requires": {
+        "bn.js": "4.11.8",
+        "brorand": "1.1.0"
       }
     },
     "mimic-fn": {
@@ -4915,6 +5978,18 @@
       "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
       "dev": true
     },
+    "minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+      "dev": true
+    },
+    "minimalistic-crypto-utils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
+      "dev": true
+    },
     "minimatch": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@@ -4960,11 +6035,97 @@
         "minimist": "0.0.8"
       }
     },
+    "mocha": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz",
+      "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==",
+      "dev": true,
+      "requires": {
+        "browser-stdout": "1.3.1",
+        "commander": "2.15.1",
+        "debug": "3.1.0",
+        "diff": "3.5.0",
+        "escape-string-regexp": "1.0.5",
+        "glob": "7.1.2",
+        "growl": "1.10.5",
+        "he": "1.1.1",
+        "minimatch": "3.0.4",
+        "mkdirp": "0.5.1",
+        "supports-color": "5.4.0"
+      }
+    },
+    "module-deps": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.1.0.tgz",
+      "integrity": "sha512-NPs5N511VD1rrVJihSso/LiBShRbJALYBKzDW91uZYy7BpjnO4bGnZL3HjZ9yKcFdZUWwaYjDz9zxbuP7vKMuQ==",
+  

<TRUNCATED>

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


[5/8] qpid-dispatch git commit: DISPATCH-1049 Add console tests

Posted by ea...@apache.org.
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/qdrChartService.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/qdrChartService.js b/console/stand-alone/plugin/js/qdrChartService.js
index 574ea07..fc53520 100644
--- a/console/stand-alone/plugin/js/qdrChartService.js
+++ b/console/stand-alone/plugin/js/qdrChartService.js
@@ -16,844 +16,825 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-'use strict';
 /* global angular d3 c3 */
 
-/**
- * @module QDR
- */
-var QDR = (function(QDR) {
-
-  // The QDR chart service handles periodic gathering data for charts and displaying the charts
-  QDR.module.factory('QDRChartService', ['QDRService',
-    function(QDRService) {
-
-      let instance = 0; // counter for chart instances
-      let bases = [];
-      var findBase = function(name, attr, request) {
-        for (let i = 0; i < bases.length; ++i) {
-          let base = bases[i];
-          if (base.equals(name, attr, request))
-            return base;
-        }
-        return null;
-      };
-
-      function ChartBase(name, attr, request) {
-        // the base chart attributes
-        this.name = name; // the record's "name" field
-        this.attr = attr; // the record's attr field to chart
-        this.request = request; // the associated request that fetches the data
-
-        // copy the savable properties to an object
-        this.copyProps = function(o) {
-          o.name = this.name;
-          o.attr = this.attr;
-          this.request.copyProps(o);
-        };
-
-        this.equals = function(name, attr, request) {
-          return (this.name == name && this.attr == attr && this.request.equals(request));
-        };
+import { QDRLogger } from './qdrGlobals.js';
+
+let instance = 0,
+  bases = [];
+
+class ChartBase {
+  constructor(name, attr, request) {
+    // the base chart attributes
+    this.name = name; // the record's "name" field
+    this.attr = attr; // the record's attr field to chart
+    this.request = request; // the associated request that fetches the data
+    // copy the savable properties to an object
+    this.copyProps = function (o) {
+      o.name = this.name;
+      o.attr = this.attr;
+      this.request.copyProps(o);
+    };
+    this.equals = function (name, attr, request) {
+      return (this.name == name && this.attr == attr && this.request.equals(request));
+    };
+  }
+}
+
+// Object that represents a visible chart
+// There can be multiple of these per ChartBase (eg. one rate  and one value chart)
+class Chart {
+  constructor(opts, request, QDRService, QDRChartService) {
+    var findBase = function (name, attr, request) {
+      for (let i = 0; i < bases.length; ++i) {
+        let base = bases[i];
+        if (base.equals(name, attr, request))
+          return base;
       }
-
-      // Object that represents a visible chart
-      // There can be multiple of these per ChartBase (eg. one rate  and one value chart)
-      function Chart(opts, request) { //name, attr, cinstance, request) {
-
-        let base = findBase(opts.name, opts.attr, request);
-        if (!base) {
-          base = new ChartBase(opts.name, opts.attr, request);
-          bases.push(base);
+      return null;
+    };
+    let base = findBase(opts.name, opts.attr, request);
+    if (!base) {
+      base = new ChartBase(opts.name, opts.attr, request);
+      bases.push(base);
+    }
+    this.base = base;
+    this.QDRService = QDRService;
+    this.QDRChartService = QDRChartService;
+    this.instance = angular.isDefined(opts.instance) ? opts.instance : ++instance;
+    this.dashboard = false; // is this chart on the dashboard page
+    this.hdash = false; // is this chart on the hawtio dashboard page
+    this.hreq = false; // has this hdash chart been requested
+    this.type = opts.type ? opts.type : 'value'; // value or rate
+    this.rateWindow = opts.rateWindow ? opts.rateWindow : 1000; // calculate the rate of change over this time interval. higher == smother graph
+    this.areaColor = '#32b9f3'; // the chart's area color when not an empty string
+    this.lineColor = '#058dc7'; // the chart's line color when not an empty string
+    this.visibleDuration = opts.visibleDuration ? opts.visibleDuration : opts.type === 'rate' ? 0.25 : 1; // number of minutes of data to show (<= base.duration)
+    this.userTitle = null; // user title overrides title()
+    this.hideLabel = opts.hideLabel;
+    this.hideLegend = opts.hideLegend;
+    // generate a unique id for this chart
+    this.id = function () {
+      let name = this.name();
+      let nameparts = name.split('/');
+      if (nameparts.length == 2)
+        name = nameparts[1];
+      let key = this.QDRService.utilities.nameFromId(this.request().nodeId) + this.request().entity + name + this.attr() + '_' + this.instance + '_' + (this.request().aggregate ? '1' : '0');
+      // remove all characters except letters,numbers, and _
+      return key.replace(/[^\w]/gi, '');
+    };
+    // copy the savable properties to an object
+    this.copyProps = function (o) {
+      o.type = this.type;
+      o.rateWindow = this.rateWindow;
+      o.areaColor = this.areaColor;
+      o.lineColor = this.lineColor;
+      o.visibleDuration = this.visibleDuration;
+      o.userTitle = this.userTitle;
+      o.dashboard = this.dashboard;
+      o.hdash = this.hdash;
+      o.instance = this.instance;
+      this.base.copyProps(o);
+    };
+    this.name = function (_) {
+      if (!arguments.length)
+        return this.base.name;
+      this.base.name = _;
+      return this;
+    };
+    this.attr = function (_) {
+      if (!arguments.length)
+        return this.base.attr;
+      this.base.attr = _;
+      return this;
+    };
+    this.nodeId = function (_) {
+      if (!arguments.length)
+        return this.base.request.nodeId;
+      this.base.request.nodeId = _;
+      return this;
+    };
+    this.entity = function (_) {
+      if (!arguments.length)
+        return this.base.request.entity;
+      this.base.request.entity = _;
+      return this;
+    };
+    this.aggregate = function (_) {
+      if (!arguments.length)
+        return this.base.request.aggregate;
+      this.base.request.aggregate = _;
+      return this;
+    };
+    this.request = function (_) {
+      if (!arguments.length)
+        return this.base.request;
+      this.base.request = _;
+      return this;
+    };
+    this.data = function () {
+      return this.base.request.data(this.base.name, this.base.attr); // refernce to chart's data array
+    };
+    this.interval = function (_) {
+      if (!arguments.length)
+        return this.base.request.interval;
+      this.base.request.interval = _;
+      return this;
+    };
+    this.duration = function (_) {
+      if (!arguments.length)
+        return this.base.request.duration;
+      this.base.request.duration = _;
+      return this;
+    };
+    this.router = function () {
+      return this.QDRService.utilities.nameFromId(this.nodeId());
+    };
+    this.title = function (_) {
+      let name = this.request().aggregate ? 'Aggregate' : this.QDRService.utilities.nameFromId(this.nodeId());
+      let computed = name +
+              ' ' + this.QDRService.utilities.humanify(this.attr()) +
+              ' - ' + this.name();
+      if (!arguments.length)
+        return this.userTitle || computed;
+      // don't store computed title in userTitle
+      if (_ === computed)
+        _ = null;
+      this.userTitle = _;
+      return this;
+    };
+    this.title_short = function () {
+      if (!arguments.length)
+        return this.userTitle || this.name();
+      return this;
+    };
+    this.copy = function () {
+      let chart = this.QDRChartService.registerChart({
+        nodeId: this.nodeId(),
+        entity: this.entity(),
+        name: this.name(),
+        attr: this.attr(),
+        interval: this.interval(),
+        forceCreate: true,
+        aggregate: this.aggregate(),
+        hdash: this.hdash
+      });
+      chart.type = this.type;
+      chart.areaColor = this.areaColor;
+      chart.lineColor = this.lineColor;
+      chart.rateWindow = this.rateWindow;
+      chart.visibleDuration = this.visibleDuration;
+      chart.userTitle = this.userTitle;
+      return chart;
+    };
+    // compare to a chart
+    this.equals = function (c) {
+      return (c.instance == this.instance &&
+              c.base.equals(this.base.name, this.base.attr, this.base.request) &&
+              c.type == this.type &&
+              c.rateWindow == this.rateWindow &&
+              c.areaColor == this.areaColor &&
+              c.lineColor == this.lineColor);
+    };
+  }
+}
+
+
+// Object that represents the management request to fetch and store data for multiple charts
+class ChartRequest {
+  constructor(opts) {
+    this.duration = opts.duration || 10; // number of minutes to keep the data
+    this.nodeId = opts.nodeId; // eg amqp:/_topo/0/QDR.A/$management
+    this.entity = opts.entity; // eg .router.address
+    // sorted since the responses will always be sorted
+    this.aggregate = opts.aggregate; // list of nodeIds for aggregate charts
+    this.datum = {}; // object containing array of arrays for each attr
+    // like {attr1: [[date,value],[date,value]...], attr2: [[date,value]...]}
+    this.interval = opts.interval || 1000; // number of milliseconds between updates to data
+    this.setTimeoutHandle = null; // used to cancel the next request
+    // allow override of normal request's management call to get data
+    this.override = opts.override; // call this instead of internal function to retreive data
+    this.overrideAttrs = opts.overrideAttrs;
+    this.data = function (name, attr) {
+      if (this.datum[name] && this.datum[name][attr])
+        return this.datum[name][attr];
+      return null;
+    };
+    this.addAttrName = function (name, attr) {
+      if (Object.keys(this.datum).indexOf(name) == -1) {
+        this.datum[name] = {};
+      }
+      if (Object.keys(this.datum[name]).indexOf(attr) == -1) {
+        this.datum[name][attr] = [];
+      }
+    };
+    this.addAttrName(opts.name, opts.attr);
+    this.copyProps = function (o) {
+      o.nodeId = this.nodeId;
+      o.entity = this.entity;
+      o.interval = this.interval;
+      o.aggregate = this.aggregate;
+      o.duration = this.duration;
+    };
+    this.removeAttr = function (name, attr) {
+      if (this.datum[name]) {
+        if (this.datum[name][attr]) {
+          delete this.datum[name][attr];
         }
-        this.base = base;
-        this.instance = angular.isDefined(opts.instance) ? opts.instance : ++instance;
-        this.dashboard = false; // is this chart on the dashboard page
-        this.hdash = false; // is this chart on the hawtio dashboard page
-        this.hreq = false; // has this hdash chart been requested
-        this.type = opts.type ? opts.type : 'value'; // value or rate
-        this.rateWindow = opts.rateWindow ? opts.rateWindow : 1000; // calculate the rate of change over this time interval. higher == smother graph
-        this.areaColor = '#32b9f3'; // the chart's area color when not an empty string
-        this.lineColor = '#058dc7'; // the chart's line color when not an empty string
-        this.visibleDuration = opts.visibleDuration ? opts.visibleDuration : opts.type === 'rate' ? 0.25 : 1; // number of minutes of data to show (<= base.duration)
-        this.userTitle = null; // user title overrides title()
-        this.hideLabel = opts.hideLabel;
-        this.hideLegend = opts.hideLegend;
-
-        // generate a unique id for this chart
-        this.id = function() {
-          let name = this.name();
-          let nameparts = name.split('/');
-          if (nameparts.length == 2)
-            name = nameparts[1];
-          let key = QDRService.management.topology.nameFromId(this.request().nodeId) + this.request().entity + name + this.attr() + '_' + this.instance + '_' + (this.request().aggregate ? '1' : '0');
-          // remove all characters except letters,numbers, and _
-          return key.replace(/[^\w]/gi, '');
-        };
-        // copy the savable properties to an object
-        this.copyProps = function(o) {
-          o.type = this.type;
-          o.rateWindow = this.rateWindow;
-          o.areaColor = this.areaColor;
-          o.lineColor = this.lineColor;
-          o.visibleDuration = this.visibleDuration;
-          o.userTitle = this.userTitle;
-          o.dashboard = this.dashboard;
-          o.hdash = this.hdash;
-          o.instance = this.instance;
-          this.base.copyProps(o);
-        };
-        this.name = function(_) {
-          if (!arguments.length) return this.base.name;
-          this.base.name = _;
-          return this;
-        };
-        this.attr = function(_) {
-          if (!arguments.length) return this.base.attr;
-          this.base.attr = _;
-          return this;
-        };
-        this.nodeId = function(_) {
-          if (!arguments.length) return this.base.request.nodeId;
-          this.base.request.nodeId = _;
-          return this;
-        };
-        this.entity = function(_) {
-          if (!arguments.length) return this.base.request.entity;
-          this.base.request.entity = _;
-          return this;
-        };
-        this.aggregate = function(_) {
-          if (!arguments.length) return this.base.request.aggregate;
-          this.base.request.aggregate = _;
-          return this;
-        };
-        this.request = function(_) {
-          if (!arguments.length) return this.base.request;
-          this.base.request = _;
-          return this;
-        };
-        this.data = function() {
-          return this.base.request.data(this.base.name, this.base.attr); // refernce to chart's data array
-        };
-        this.interval = function(_) {
-          if (!arguments.length) return this.base.request.interval;
-          this.base.request.interval = _;
-          return this;
-        };
-        this.duration = function(_) {
-          if (!arguments.length) return this.base.request.duration;
-          this.base.request.duration = _;
-          return this;
-        };
-        this.router = function () {
-          return QDRService.management.topology.nameFromId(this.nodeId());
-        };
-        this.title = function(_) {
-          let name = this.request().aggregate ? 'Aggregate' : QDRService.management.topology.nameFromId(this.nodeId());
-          let computed = name +
-            ' ' + QDRService.utilities.humanify(this.attr()) +
-            ' - ' + this.name();
-          if (!arguments.length) return this.userTitle || computed;
-
-          // don't store computed title in userTitle
-          if (_ === computed)
-            _ = null;
-          this.userTitle = _;
-          return this;
-        };
-        this.title_short = function() {
-          if (!arguments.length) return this.userTitle || this.name();
-          return this;
-        };
-        this.copy = function() {
-          let chart = self.registerChart({
-            nodeId: this.nodeId(),
-            entity: this.entity(),
-            name: this.name(),
-            attr: this.attr(),
-            interval: this.interval(),
-            forceCreate: true,
-            aggregate: this.aggregate(),
-            hdash: this.hdash
-          });
-          chart.type = this.type;
-          chart.areaColor = this.areaColor;
-          chart.lineColor = this.lineColor;
-          chart.rateWindow = this.rateWindow;
-          chart.visibleDuration = this.visibleDuration;
-          chart.userTitle = this.userTitle;
-          return chart;
-        };
-        // compare to a chart
-        this.equals = function(c) {
-          return (c.instance == this.instance &&
-            c.base.equals(this.base.name, this.base.attr, this.base.request) &&
-            c.type == this.type &&
-            c.rateWindow == this.rateWindow &&
-            c.areaColor == this.areaColor &&
-            c.lineColor == this.lineColor);
-        };
       }
-
-      // Object that represents the management request to fetch and store data for multiple charts
-      function ChartRequest(opts) { //nodeId, entity, name, attr, interval, aggregate) {
-        this.duration = opts.duration || 10; // number of minutes to keep the data
-        this.nodeId = opts.nodeId; // eg amqp:/_topo/0/QDR.A/$management
-        this.entity = opts.entity; // eg .router.address
-        // sorted since the responses will always be sorted
-        this.aggregate = opts.aggregate; // list of nodeIds for aggregate charts
-        this.datum = {}; // object containing array of arrays for each attr
-        // like {attr1: [[date,value],[date,value]...], attr2: [[date,value]...]}
-
-        this.interval = opts.interval || 1000; // number of milliseconds between updates to data
-        this.setTimeoutHandle = null; // used to cancel the next request
-
-        // allow override of normal request's management call to get data
-        this.override = opts.override; // call this instead of internal function to retreive data
-        this.overrideAttrs = opts.overrideAttrs;
-
-        this.data = function(name, attr) {
-          if (this.datum[name] && this.datum[name][attr])
-            return this.datum[name][attr];
-          return null;
-        };
-        this.addAttrName = function(name, attr) {
-          if (Object.keys(this.datum).indexOf(name) == -1) {
-            this.datum[name] = {};
-          }
-          if (Object.keys(this.datum[name]).indexOf(attr) == -1) {
-            this.datum[name][attr] = [];
-          }
-        };
-        this.addAttrName(opts.name, opts.attr);
-
-        this.copyProps = function(o) {
-          o.nodeId = this.nodeId;
-          o.entity = this.entity;
-          o.interval = this.interval;
-          o.aggregate = this.aggregate;
-          o.duration = this.duration;
-        };
-
-        this.removeAttr = function(name, attr) {
-          if (this.datum[name]) {
-            if (this.datum[name][attr]) {
-              delete this.datum[name][attr];
-            }
-          }
-          return this.attrs().length;
-        };
-
-        this.equals = function(r, entity, aggregate) {
-          if (arguments.length == 3) {
-            let o = {
-              nodeId: r,
-              entity: entity,
-              aggregate: aggregate
-            };
-            r = o;
-          }
-          return (this.nodeId === r.nodeId && this.entity === r.entity && this.aggregate == r.aggregate);
-        };
-        this.names = function() {
-          return Object.keys(this.datum);
-        };
-        this.attrs = function() {
-          let attrs = {};
-          Object.keys(this.datum).forEach(function(name) {
-            Object.keys(this.datum[name]).forEach(function(attr) {
-              attrs[attr] = 1;
-            });
-          }, this);
-          return Object.keys(attrs);
-        };
+      return this.attrs().length;
+    };
+    this.equals = function (r, entity, aggregate) {
+      if (arguments.length == 3) {
+        let o = {
+          nodeId: r,
+          entity: entity,
+          aggregate: aggregate
+        };
+        r = o;
       }
-
-      // Below here are the properties and methods available on QDRChartService
-      let self = {
-        charts: [], // list of charts to gather data for
-        chartRequests: [], // the management request info (multiple charts can be driven off of a single request
-
-        init: function() {
-          self.loadCharts();
-          QDRService.management.connection.addDisconnectAction(function() {
-            self.charts.forEach(function(chart) {
-              self.unRegisterChart(chart, true);
-            });
-            QDRService.management.connection.addConnectAction(self.init);
-          });
-        },
-
-        findChartRequest: function(nodeId, entity, aggregate) {
-          let ret = null;
-          self.chartRequests.some(function(request) {
-            if (request.equals(nodeId, entity, aggregate)) {
-              ret = request;
-              return true;
-            }
-          });
-          return ret;
-        },
-
-        findCharts: function(opts) { //name, attr, nodeId, entity, hdash) {
-          if (!opts.hdash)
-            opts.hdash = false; // rather than undefined
-          return self.charts.filter(function(chart) {
-            return (chart.name() == opts.name &&
-              chart.attr() == opts.attr &&
-              chart.nodeId() == opts.nodeId &&
-              chart.entity() == opts.entity &&
-              chart.hdash == opts.hdash);
-          });
-        },
-
-        delChartRequest: function(request) {
-          for (let i = 0; i < self.chartRequests.length; ++i) {
-            let r = self.chartRequests[i];
-            if (request.equals(r)) {
-              QDR.log.debug('removed request: ' + request.nodeId + ' ' + request.entity);
-              self.chartRequests.splice(i, 1);
-              self.stopCollecting(request);
-              return;
-            }
-          }
-        },
-
-        delChart: function(chart, skipSave) {
-          let foundBases = 0;
-          for (let i = 0; i < self.charts.length; ++i) {
-            let c = self.charts[i];
-            if (c.base === chart.base)
-              ++foundBases;
-            if (c.equals(chart)) {
-              self.charts.splice(i, 1);
-              if (chart.dashboard && !skipSave)
-                self.saveCharts();
-            }
-          }
-          if (foundBases == 1) {
-            let baseIndex = bases.indexOf(chart.base);
-            bases.splice(baseIndex, 1);
-          }
-        },
-
-        createChart: function (opts, request) {
-          return new Chart(opts, request);
-        },
-        createChartRequest: function (opts) {
-          let request = new ChartRequest(opts); //nodeId, entity, name, attr, interval, aggregate);
-          request.creationTimestamp = opts.now;
-          self.chartRequests.push(request);
-          self.startCollecting(request);
-          self.sendChartRequest(request, true);
-          return request;
-        },
-        destroyChartRequest: function (request) {
-          self.stopCollecting(request);
-          self.delChartRequest(request);
-        },
-
-        registerChart: function(opts) { //nodeId, entity, name, attr, interval, instance, forceCreate, aggregate, hdash) {
-          let request = self.findChartRequest(opts.nodeId, opts.entity, opts.aggregate);
-          if (request) {
-            // add any new attr or name to the list
-            request.addAttrName(opts.name, opts.attr);
-          } else {
-            // the nodeId/entity did not already exist, so add a new request and chart
-            QDR.log.debug('added new request: ' + opts.nodeId + ' ' + opts.entity);
-            request = self.createChartRequest(opts);
-          }
-          let charts = self.findCharts(opts); //name, attr, nodeId, entity, hdash);
-          let chart;
-          if (charts.length == 0 || opts.forceCreate) {
-            if (!opts.use_instance && opts.instance)
-              delete opts.instance;
-            chart = new Chart(opts, request); //opts.name, opts.attr, opts.instance, request);
-            self.charts.push(chart);
-          } else {
-            chart = charts[0];
-          }
-          return chart;
-        },
-
-        // remove the chart for name/attr
-        // if all attrs are gone for this request, remove the request
-        unRegisterChart: function(chart, skipSave) {
-          // remove the chart
-
-          // TODO: how do we remove charts that were added to the hawtio dashboard but then removed?
-          // We don't get a notification that they were removed. Instead, we could just stop sending
-          // the request in the background and only send the request when the chart's tick() event is triggered
-          //if (chart.hdash) {
-          //  chart.dashboard = false;
-          //  self.saveCharts();
-          //    return;
-          //}
-
-          for (let i = 0; i < self.charts.length; ++i) {
-            let c = self.charts[i];
-            if (chart.equals(c)) {
-              let request = chart.request();
-              self.delChart(chart, skipSave);
-              if (request) {
-                // see if any other charts use this attr
-                for (let j = 0; j < self.charts.length; ++j) {
-                  let ch = self.charts[j];
-                  if (ch.attr() == chart.attr() && ch.request().equals(chart.request()))
-                    return;
-                }
-                // no other charts use this attr, so remove it
-                if (request.removeAttr(chart.name(), chart.attr()) == 0) {
-                  self.destroyChartRequest(request);
-                }
-              }
+      return (this.nodeId === r.nodeId && this.entity === r.entity && this.aggregate == r.aggregate);
+    };
+    this.names = function () {
+      return Object.keys(this.datum);
+    };
+    this.attrs = function () {
+      let attrs = {};
+      Object.keys(this.datum).forEach(function (name) {
+        Object.keys(this.datum[name]).forEach(function (attr) {
+          attrs[attr] = 1;
+        });
+      }, this);
+      return Object.keys(attrs);
+    };
+  }
+}
+
+class AreaChart {
+  constructor(chart, chartId, defer, width, QDRService) {
+    if (!chart)
+      return;
+    this.QDRService = QDRService;
+    // reference to underlying chart
+    this.chart = chart;
+    // if this is an aggregate chart, show it stacked
+    this.stacked = chart.request().aggregate;
+    // the id of the html element that is bound to the chart. The svg will be a child of this
+    this.htmlId = chartId;
+    this.colorMap = {};
+    if (!defer)
+      this.generate(width);
+  }
+
+  // create the svg and bind it to the given div.id
+  generate (width) {
+    let chart = this.chart;  // for access during chart callbacks
+    let self = this;
+
+    // an array of 10 colors
+    let colors = d3.scale.category10().range();
+    // list of router names. used to get the color index
+    let nameList = this.QDRService.management.topology.nodeNameList();
+    for (let i=0; i<nameList.length; i++) {
+      this.colorMap[nameList[i]] = colors[i % 10];
+    }
+    let nodeName = this.QDRService.utilities.nameFromId(this.chart.base.request.nodeId);
+    this.colorMap[nodeName] = this.chart.areaColor;
+
+    let c3ChartDefaults = $().c3ChartDefaults();
+    let singleAreaChartConfig = c3ChartDefaults.getDefaultSingleAreaConfig();
+    singleAreaChartConfig.bindto = '#' + this.htmlId;
+    singleAreaChartConfig.size = {
+      width: width || 400,
+      height: 200
+    };
+    singleAreaChartConfig.data = {
+      x: 'x',           // x-axis is named x
+      columns: [[]],
+      type: 'area-spline'
+    };
+    singleAreaChartConfig.axis = {
+      x: {
+        type: 'timeseries',
+        tick: {
+          format: (function (d) {
+            let data = this.singleAreaChart.data.shown();
+            let first = data[0]['values'][0].x;
+
+            if (d - first == 0) {
+              return d3.timeFormat('%I:%M:%S')(d);
             }
-          }
-          if (!skipSave)
-            self.saveCharts();
-        },
-
-        stopCollecting: function(request) {
-          if (request.setTimeoutHandle) {
-            clearInterval(request.setTimeoutHandle);
-            request.setTimeoutHandle = null;
-          }
-        },
-
-        startCollecting: function(request) {
-          request.setTimeoutHandle = setInterval(self.sendChartRequest, request.interval, request);
-        },
-        shouldRequest: function() {
-          // see if any of the charts associated with this request have either dialog, dashboard, or hreq
-          return self.charts.some(function(chart) {
-            return (chart.dashboard || chart.hreq) || (!chart.dashboard && !chart.hdash);
-          });
-        },
-        // send the request
-        sendChartRequest: function(request) {
-          if (request.busy)
-            return;
-          if (self.charts.length > 0 && !self.shouldRequest(request)) {
-            return;
-          }
-          // ensure the response has the name field so we can associate the response values with the correct chart
-          let attrs = request.attrs();
-          if (attrs.indexOf('name') == -1)
-            attrs.push('name');
-
-          // this is called when the response is received
-          var saveResponse = function(nodeId, entity, response) {
-            request.busy = false;
-            if (!response || !response.attributeNames)
-              return;
-            //QDR.log.debug("got chart results for " + nodeId + " " + entity);
-            // records is an array that has data for all names
-            let records = response.results;
-            if (!records)
-              return;
+            return d3.timeFormat('%M:%S')(d);
+          }).bind(this),
+          culling: {max: 4}
+        }
+      },
+      y: {
+        tick: {
+          format: function (d) { return d<1 ? d3.format('.2f')(d) : d3.format('.2s')(d); },
+          count: 5
+        }
+      }
+    };
 
-            let now = new Date();
-            let cutOff = new Date(now.getTime() - request.duration * 60 * 1000);
-            // index of the "name" attr in the response
-            let nameIndex = response.attributeNames.indexOf('name');
-            if (nameIndex < 0)
-              return;
+    if (!chart.hideLabel) {
+      singleAreaChartConfig.axis.x.label = {
+        text: chart.name(),
+        position: 'outer-right'
+      };
 
-            let names = request.names();
-            // for each record returned, find the name/attr for this request and save the data with this timestamp
-            for (let i = 0; i < records.length; ++i) {
-              let name = records[i][nameIndex];
-              // if we want to store the values for some attrs for this name
-              if (names.indexOf(name) > -1) {
-                attrs.forEach(function(attr) {
-                  let attrIndex = response.attributeNames.indexOf(attr);
-                  if (records[i][attrIndex] !== undefined) {
-                    let data = request.data(name, attr); // get a reference to the data array
-                    if (data) {
-
-                      if (request.aggregate) {
-                        data.push([now, response.aggregates[i][attrIndex].sum, response.aggregates[i][attrIndex].detail]);
-                      } else {
-                        data.push([now, records[i][attrIndex]]);
-                      }
-                      // expire the old data
-                      while (data[0][0] < cutOff) {
-                        data.shift();
-                      }
-                    }
-                  }
-                });
-              }
-            }
-          };
-          request.busy = true;
-          // check for override of request
-          if (request.override) {
-            request.override(request, saveResponse);
-          } else {
-            // send the appropriate request
-            if (request.aggregate) {
-              let nodeList = QDRService.management.topology.nodeIdList();
-              QDRService.management.topology.getMultipleNodeInfo(nodeList, request.entity, attrs, saveResponse, request.nodeId);
-            } else {
-              QDRService.management.topology.fetchEntity(request.nodeId, request.entity, attrs, saveResponse);
-            }
-          }
-        },
-
-        numCharts: function() {
-          return self.charts.filter(function(chart) {
-            return chart.dashboard;
-          }).length;
-          //return self.charts.length;
-        },
-
-        isAttrCharted: function(nodeId, entity, name, attr, aggregate) {
-          let charts = self.findCharts({
-            name: name,
-            attr: attr,
-            nodeId: nodeId,
-            entity: entity
-          });
-          // if any of the matching charts are on the dashboard page, return true
-          return charts.some(function(chart) {
-            return (chart.dashboard && (aggregate ? chart.aggregate() : !chart.aggregate()));
-          });
-        },
-
-        addHDash: function(chart) {
-          chart.hdash = true;
-          self.saveCharts();
-        },
-        delHDash: function(chart) {
-          chart.hdash = false;
-          self.saveCharts();
-        },
-        addDashboard: function(chart) {
-          chart.dashboard = true;
-          self.saveCharts();
-        },
-        delDashboard: function(chart) {
-          chart.dashboard = false;
-          self.saveCharts();
-        },
-        // save the charts to local storage
-        saveCharts: function() {
-          let minCharts = [];
-
-          self.charts.forEach(function(chart) {
-            let minChart = {};
-            // don't save chart unless it is on the dashboard
-            if (chart.dashboard || chart.hdash) {
-              chart.copyProps(minChart);
-              minCharts.push(minChart);
-            }
-          });
-          localStorage['QDRCharts'] = angular.toJson(minCharts);
-        },
-        loadCharts: function() {
-          let charts = angular.fromJson(localStorage['QDRCharts']);
-          if (charts) {
-            // get array of known ids
-            let nodeList = QDRService.management.topology.nodeIdList();
-            charts.forEach(function(chart) {
-              // if this chart is not in the current list of nodes, skip
-              if (nodeList.indexOf(chart.nodeId) >= 0) {
-                if (!angular.isDefined(chart.instance)) {
-                  chart.instance = ++instance;
-                }
-                if (chart.instance >= instance)
-                  instance = chart.instance + 1;
-                if (!chart.duration)
-                  chart.duration = 1;
-                if (chart.nodeList)
-                  chart.aggregate = true;
-                if (!chart.hdash)
-                  chart.hdash = false;
-                if (!chart.dashboard)
-                  chart.dashboard = false;
-                if (!chart.hdash && !chart.dashboard)
-                  chart.dashboard = true;
-                if (chart.hdash && chart.dashboard)
-                  chart.dashboard = false;
-                chart.forceCreate = true;
-                chart.use_instance = true;
-                let newChart = self.registerChart(chart); //chart.nodeId, chart.entity, chart.name, chart.attr, chart.interval, true, chart.aggregate);
-                newChart.dashboard = chart.dashboard;
-                newChart.hdash = chart.hdash;
-                newChart.hreq = false;
-                newChart.type = chart.type;
-                newChart.rateWindow = chart.rateWindow;
-                newChart.areaColor = chart.areaColor ? chart.areaColor : '#32b9f3';
-                newChart.lineColor = chart.lineColor ? chart.lineColor : '#058dc7';
-                newChart.duration(chart.duration);
-                newChart.visibleDuration = chart.visibleDuration ? chart.visibleDuration : newChart.type === 'rate' ? 0.25 : 1;
-                if (chart.userTitle)
-                  newChart.title(chart.userTitle);
-              }
-            });
+    }
+    singleAreaChartConfig.transition = {
+      duration: 0
+    };
+
+    singleAreaChartConfig.area = {
+      zerobased: false
+    };
+
+    singleAreaChartConfig.tooltip = {
+      contents: function (d) {
+        let d3f = ',';
+        if (chart.type === 'rate')
+          d3f = ',.2f';
+        let zPre = function (i) {
+          if (i < 10) {
+            i = '0' + i;
           }
-        },
-
-        // constructor for a c3 area chart
-        pfAreaChart: function (chart, chartId, defer, width) {
-          if (!chart)
-            return;
-
-          // reference to underlying chart
-          this.chart = chart;
-
-          // if this is an aggregate chart, show it stacked
-          this.stacked = chart.request().aggregate;
-
-          // the id of the html element that is bound to the chart. The svg will be a child of this
-          this.htmlId = chartId;
-
-          // an array of 20 colors
-          this.colors = d3.scale.category10().range();
-
-          if (!defer)
-            this.generate(width);
-        },
+          return i;
+        };
+        let h = zPre(d[0].x.getHours());
+        let m = zPre(d[0].x.getMinutes());
+        let s = zPre(d[0].x.getSeconds());
+        let table = '<table class=\'dispatch-c3-tooltip\'>  <tr><th colspan=\'2\' class=\'text-center\'><strong>'+h+':'+m+':'+s+'</strong></th></tr> <tbody>';
+        for (let i=0; i<d.length; i++) {
+          let c = self.colorMap[d[i].id];
+          let span = `<span class='chart-tip-legend' style='background-color: ${c};'> </span>` + d[i].id;
+          table += ('<tr><td>'+span+'<td>'+d3.format(d3f)(d[i].value)+'</td></tr>');
+        }
+        table += '</tbody></table>';
+        return table;
+      }
+    };
 
-        // aggregate chart is based on pfAreaChart
-        pfAggChart: function (chart, chartId, defer) {
-          // inherit pfChart's properties, but force a defer
-          self.pfAreaChart.call(this, chart, chartId, true);
+    singleAreaChartConfig.title = {
+      text: this.QDRService.utilities.humanify(this.chart.attr())
+    };
 
-          // the request is for aggregate data, but the chart is for the sum and not the detail
-          // Explanation: When the chart.request is aggregate, each data point is composed of 3 parts:
-          //  1. the datetime stamp
-          //  2. the sum of the value for all routers
-          //  3. an object with each router's name and value for this data point
-          // Normally, an aggregate chart shows lines for each of the routers and ignores the sum
-          // For this chart, we want to chart the sum (the 2nd value), so we set stacked to false
-          this.stacked = false;
+    singleAreaChartConfig.data.color = (color, d) => {
+      let c = this.colorMap[d.id];
+      return c ? c : color;
+    };
+    singleAreaChartConfig.color.pattern[0] = this.chart.areaColor;
 
-          // let chart legends and tooltips show 'Total' instead of a router name
-          this.aggregate = true;
+    //singleAreaChartConfig.data.colors = {};
+    //nameList.forEach( (name, i) => singleAreaChartConfig.data.colors[name] = this.colorMap[name] );
+    singleAreaChartConfig.data.colors = this.colormap;
 
-          if (!defer)
-            this.generate();
-        }
+    if (!chart.hideLegend) {
+      singleAreaChartConfig.legend = {
+        show: true,
       };
-      // allow pfAggChart to inherit prototyped methods
-      self.pfAggChart.prototype = Object.create(self.pfAreaChart.prototype);
-      // except for the constructor
-      self.pfAggChart.prototype.constructor = self.pfAggChart;
-
-      // create the svg and bind it to the given div.id
-      self.pfAreaChart.prototype.generate = function (width) {
-        let chart = this.chart;  // for access during chart callbacks
-        let self = this;
-
-        // list of router names. used to get the color index
-        let nameList = QDRService.management.topology.nodeNameList();
-
-        let c3ChartDefaults = $().c3ChartDefaults();
-        let singleAreaChartConfig = c3ChartDefaults.getDefaultSingleAreaConfig();
-        singleAreaChartConfig.bindto = '#' + this.htmlId;
-        singleAreaChartConfig.size = {
-          width: width || 400,
-          height: 200
-        };
-        singleAreaChartConfig.data = {
-          x: 'x',           // x-axis is named x
-          columns: [[]],
-          type: 'area-spline'
-        };
-        singleAreaChartConfig.axis = {
-          x: {
-            type: 'timeseries',
-            tick: {
-              format: (function (d) {
-                let data = this.singleAreaChart.data.shown();
-                let first = data[0]['values'][0].x;
-
-                if (d - first == 0) {
-                  return d3.timeFormat('%I:%M:%S')(d);
-                }
-                return d3.timeFormat('%M:%S')(d);
-              }).bind(this),
-              culling: {max: 4}
-            }
-          },
-          y: {
-            tick: {
-              format: function (d) { return d<1 ? d3.format('.2f')(d) : d3.format('.2s')(d); },
-              count: 5
-            }
-          }
-        };
+    }
 
-        if (!chart.hideLabel) {
-          singleAreaChartConfig.axis.x.label = {
-            text: chart.name(),
-            position: 'outer-right'
-          };
+    if (this.stacked) {
+      // create a stacked area chart
+      singleAreaChartConfig.data.groups = [this.QDRService.management.topology.nodeNameList()];
+      singleAreaChartConfig.data.order = function (t1, t2) { return t1.id < t2.id; };
+    }
 
+    this.singleAreaChart = c3.generate(singleAreaChartConfig);
+  }
+
+  chartData() {
+    let data = this.chart.data();
+    let nodeList = this.QDRService.management.topology.nodeNameList();
+
+    // oldest data point that should be visible
+    let now = new Date();
+    let visibleDate = new Date(now.getTime() - this.chart.visibleDuration * 60 * 1000);
+
+    let accessorSingle = function (d, d1, elapsed) {
+      return this.chart.type === 'rate' ? (d1[1] - d[1]) / elapsed : d[1];
+    };
+    let accessorStacked = function (d, d1, elapsed, i) {
+      return this.chart.type === 'rate' ? (d1[2][i].val - d[2][i].val) / elapsed : d[2][i].val;
+    };
+    let accessor = this.stacked ? accessorStacked : accessorSingle;
+
+    let dx = ['x'];
+    let dlines = [];
+    if (this.stacked) {
+      // for stacked, there is a line per router
+      nodeList.forEach( function (node) {
+        dlines.push([node]);
+      });
+    } else {
+      // for non-stacked, there is only one line
+      dlines.push([this.aggregate ? 'Total' : this.chart.router()]);
+    }
+    for (let i=0; i<data.length; i++) {
+      let d = data[i], elapsed = 1, d1;
+      if (d[0] >= visibleDate) {
+        if (this.chart.type === 'rate' && i < data.length-1) {
+          d1 = data[i+1];
+          elapsed = Math.max((d1[0] - d[0]) / 1000, 0.001); // number of seconds that elapsed
         }
-        singleAreaChartConfig.transition = {
-          duration: 0
-        };
-
-        singleAreaChartConfig.area = {
-          zerobased: false
-        };
-
-        singleAreaChartConfig.tooltip = {
-          contents: function (d) {
-            let d3f = ',';
-            if (chart.type === 'rate')
-              d3f = ',.2f';
-            let zPre = function (i) {
-              if (i < 10) {
-                i = '0' + i;
-              }
-              return i;
-            };
-            let h = zPre(d[0].x.getHours());
-            let m = zPre(d[0].x.getMinutes());
-            let s = zPre(d[0].x.getSeconds());
-            let table = '<table class=\'dispatch-c3-tooltip\'>  <tr><th colspan=\'2\' class=\'text-center\'><strong>'+h+':'+m+':'+s+'</strong></th></tr> <tbody>';
-            for (let i=0; i<d.length; i++) {
-              let colorIndex = nameList.indexOf(d[i].id) % 10;
-              let span = '<span class=\'chart-tip-legend\' style=\'background-color: '+self.colors[colorIndex]+';\'> </span>' + d[i].id;
-              table += ('<tr><td>'+span+'<td>'+d3.format(d3f)(d[i].value)+'</td></tr>');
+        // don't push the last data point for a rate chart
+        if (this.chart.type !== 'rate' || i < data.length-1) {
+          dx.push(d[0]);
+          if (this.stacked) {
+            for (let nodeIndex=0; nodeIndex<nodeList.length; nodeIndex++) {
+              dlines[nodeIndex].push(accessor.call(this, d, d1, elapsed, nodeIndex));
             }
-            table += '</tbody></table>';
-            return table;
+          } else {
+            dlines[0].push(accessor.call(this, d, d1, elapsed));
           }
-        };
-
-        singleAreaChartConfig.title = {
-          text: QDRService.utilities.humanify(this.chart.attr())
-        };
-
-        singleAreaChartConfig.data.colors = {};
-        nameList.forEach( (function (r, i) {
-          singleAreaChartConfig.data.colors[r] = this.colors[i % 10];
-        }).bind(this));
-
-        singleAreaChartConfig.data.color = (function (color, d) {
-          let i = nameList.indexOf(d);
-          return i >= 0 ? this.colors[i % 10] : color;
-        }).bind(this);
-
-        if (!chart.hideLegend) {
-          singleAreaChartConfig.legend = {
-            show: true,
-          };
         }
-
-        if (this.stacked) {
-          // create a stacked area chart
-          singleAreaChartConfig.data.groups = [QDRService.management.topology.nodeNameList()];
-          singleAreaChartConfig.data.order = function (t1, t2) { return t1.id < t2.id; };
-        }
-
-        this.singleAreaChart = c3.generate(singleAreaChartConfig);
-      };
-
-      // filter/modify the chart.data into data points for the svg
-      /* the collected data looks like:
-         [[date, val, [v1,v2,...]], [date, val, [v1,v2,...]],...]
-         with date being the timestamp of the sample
-              val being the total value
-              and the [v1,v2,...] array being the component values for each router for stacked charts
-
-         for stacked charts, the returned data looks like:
-         [['x', date, date,...},
-          ['R1', v1, v1,...},
-          ['R2', v2, v2,...],
-          ...]
-
-         for non-stacked charts, the returned data looks like:
-         ['x', date, date,...],
-         ['R1', val, val,...]]
-
-         for rate charts, all the values returned are the change per second between adjacent values
-      */
-      self.pfAreaChart.prototype.chartData = function() {
-        let data = this.chart.data();
-        let nodeList = QDRService.management.topology.nodeNameList();
-
-        // oldest data point that should be visible
-        let now = new Date();
-        let visibleDate = new Date(now.getTime() - this.chart.visibleDuration * 60 * 1000);
-
-        let accessorSingle = function (d, d1, elapsed) {
-          return this.chart.type === 'rate' ? (d1[1] - d[1]) / elapsed : d[1];
-        };
-        let accessorStacked = function (d, d1, elapsed, i) {
-          return this.chart.type === 'rate' ? (d1[2][i].val - d[2][i].val) / elapsed : d[2][i].val;
-        };
-        let accessor = this.stacked ? accessorStacked : accessorSingle;
-
-        let dx = ['x'];
-        let dlines = [];
-        if (this.stacked) {
-          // for stacked, there is a line per router
-          nodeList.forEach( function (node) {
-            dlines.push([node]);
-          });
-        } else {
-          // for non-stacked, there is only one line
-          dlines.push([this.aggregate ? 'Total' : this.chart.router()]);
+      }
+    }
+    let columns = [dx];
+    dlines.forEach( function (line) {
+      columns.push(line);
+    });
+    return columns;
+  }
+
+  // get the data for the chart and update it
+  tick () {
+    // can't draw charts that don't have data yet
+    if (!this.chart.data() || this.chart.data().length == 0 || !this.singleAreaChart) {
+      return;
+    }
+    let nodeName = this.QDRService.utilities.nameFromId(this.chart.base.request.nodeId);
+    this.colorMap[nodeName] = this.chart.areaColor;
+    this.singleAreaChart.data.colors(this.colorMap);
+
+    // update the chart title
+    // since there is no c3 api to get or set the chart title, we change the title directly using d3
+    let rate = '';
+    if (this.chart.type === 'rate')
+      rate = ' per second';
+    d3.select('#'+this.htmlId+' svg text.c3-title').text(this.QDRService.utilities.humanify(this.chart.attr()) + rate);
+
+    let d = this.chartData();
+    // load the new data
+    // using the c3.flow api causes the x-axis labels to jump around
+    this.singleAreaChart.load({
+      columns: d
+    });
+  }
+
+}
+
+// aggregate chart is based on pfAreaChart
+class AggChart extends AreaChart {
+  constructor(chart, chartId, defer, QDRService) {
+    // inherit pfChart's properties, but force a defer
+    super(chart, chartId, true, undefined, QDRService);
+    // the request is for aggregate data, but the chart is for the sum and not the detail
+    // Explanation: When the chart.request is aggregate, each data point is composed of 3 parts:
+    //  1. the datetime stamp
+    //  2. the sum of the value for all routers
+    //  3. an object with each router's name and value for this data point
+    // Normally, an aggregate chart shows lines for each of the routers and ignores the sum
+    // For this chart, we want to chart the sum (the 2nd value), so we set stacked to false
+    this.stacked = false;
+    // let chart legends and tooltips show 'Total' instead of a router name
+    this.aggregate = true;
+    if (!defer)
+      this.generate();
+  }
+}
+
+
+export class QDRChartService {
+  constructor(QDRService, $log) {
+    this.charts = [];
+    this.chartRequests = [];
+    this.QDRService = QDRService;
+    this.QDRLog = new QDRLogger($log, 'QDRChartService');
+  }
+  
+  // Example service function
+  init () {
+    let self = this;
+    this.loadCharts();
+    this.QDRService.management.connection.addDisconnectAction(function() {
+      self.charts.forEach(function(chart) {
+        self.unRegisterChart(chart, true);
+      });
+      self.QDRService.management.connection.addConnectAction(self.init);
+    });
+  }
+  findChartRequest (nodeId, entity, aggregate) {
+    let ret = null;
+    this.chartRequests.some(function(request) {
+      if (request.equals(nodeId, entity, aggregate)) {
+        ret = request;
+        return true;
+      }
+    });
+    return ret;
+  }
+  findCharts (opts) { //name, attr, nodeId, entity, hdash) {
+    if (!opts.hdash)
+      opts.hdash = false; // rather than undefined
+    return this.charts.filter(function(chart) {
+      return (chart.name() == opts.name &&
+        chart.attr() == opts.attr &&
+        chart.nodeId() == opts.nodeId &&
+        chart.entity() == opts.entity &&
+        chart.hdash == opts.hdash);
+    });
+  }
+
+  delChartRequest (request) {
+    for (let i = 0; i < this.chartRequests.length; ++i) {
+      let r = this.chartRequests[i];
+      if (request.equals(r)) {
+        this.QDRLog.debug('removed request: ' + request.nodeId + ' ' + request.entity);
+        this.chartRequests.splice(i, 1);
+        this.stopCollecting(request);
+        return;
+      }
+    }
+  }
+
+  delChart (chart, skipSave) {
+    let foundBases = 0;
+    for (let i = 0; i < this.charts.length; ++i) {
+      let c = this.charts[i];
+      if (c.base === chart.base)
+        ++foundBases;
+      if (c.equals(chart)) {
+        this.charts.splice(i, 1);
+        if (chart.dashboard && !skipSave)
+          this.saveCharts();
+      }
+    }
+    if (foundBases == 1) {
+      let baseIndex = bases.indexOf(chart.base);
+      bases.splice(baseIndex, 1);
+    }
+  }
+
+  createChart (opts, request) {
+    return new Chart(opts, request, this.QDRService, this);
+  }
+  createChartRequest (opts) {
+    let request = new ChartRequest(opts); //nodeId, entity, name, attr, interval, aggregate);
+    request.creationTimestamp = opts.now;
+    this.chartRequests.push(request);
+    this.startCollecting(request);
+    this.sendChartRequest(request);
+    return request;
+  }
+  destroyChartRequest (request) {
+    this.stopCollecting(request);
+    this.delChartRequest(request);
+  }
+
+  registerChart (opts) { //nodeId, entity, name, attr, interval, instance, forceCreate, aggregate, hdash) {
+    let request = this.findChartRequest(opts.nodeId, opts.entity, opts.aggregate);
+    if (request) {
+      // add any new attr or name to the list
+      request.addAttrName(opts.name, opts.attr);
+    } else {
+      // the nodeId/entity did not already exist, so add a new request and chart
+      this.QDRLog.debug('added new request: ' + opts.nodeId + ' ' + opts.entity);
+      request = this.createChartRequest(opts);
+    }
+    let charts = this.findCharts(opts); //name, attr, nodeId, entity, hdash);
+    let chart;
+    if (charts.length == 0 || opts.forceCreate) {
+      if (!opts.use_instance && opts.instance)
+        delete opts.instance;
+      chart = new Chart(opts, request, this.QDRService, this); //opts.name, opts.attr, opts.instance, request);
+      this.charts.push(chart);
+    } else {
+      chart = charts[0];
+    }
+    return chart;
+  }
+
+  // remove the chart for name/attr
+  // if all attrs are gone for this request, remove the request
+  unRegisterChart (chart, skipSave) {
+    // remove the chart
+    for (let i = 0; i < this.charts.length; ++i) {
+      let c = this.charts[i];
+      if (chart.equals(c)) {
+        let request = chart.request();
+        this.delChart(chart, skipSave);
+        if (request) {
+          // see if any other charts use this attr
+          for (let j = 0; j < this.charts.length; ++j) {
+            let ch = this.charts[j];
+            if (ch.attr() == chart.attr() && ch.request().equals(chart.request()))
+              return;
+          }
+          // no other charts use this attr, so remove it
+          if (request.removeAttr(chart.name(), chart.attr()) == 0) {
+            this.destroyChartRequest(request);
+          }
         }
-        for (let i=0; i<data.length; i++) {
-          let d = data[i], elapsed = 1, d1;
-          if (d[0] >= visibleDate) {
-            if (this.chart.type === 'rate' && i < data.length-1) {
-              d1 = data[i+1];
-              elapsed = Math.max((d1[0] - d[0]) / 1000, 0.001); // number of seconds that elapsed
-            }
-            // don't push the last data point for a rate chart
-            if (this.chart.type !== 'rate' || i < data.length-1) {
-              dx.push(d[0]);
-              if (this.stacked) {
-                for (let nodeIndex=0; nodeIndex<nodeList.length; nodeIndex++) {
-                  dlines[nodeIndex].push(accessor.call(this, d, d1, elapsed, nodeIndex));
+      }
+    }
+    if (!skipSave)
+      this.saveCharts();
+  }
+
+  stopCollecting (request) {
+    if (request.setTimeoutHandle) {
+      clearInterval(request.setTimeoutHandle);
+      request.setTimeoutHandle = null;
+    }
+  }
+
+  startCollecting (request) {
+    request.setTimeoutHandle = setInterval(this.sendChartRequest.bind(this), request.interval, request);
+  }
+  shouldRequest () {
+    // see if any of the charts associated with this request have either dialog, dashboard, or hreq
+    return this.charts.some(function(chart) {
+      return (chart.dashboard || chart.hreq) || (!chart.dashboard && !chart.hdash);
+    });
+  }
+  // send the request
+  sendChartRequest (request) {
+    if (request.busy)
+      return;
+    if (this.charts.length > 0 && !this.shouldRequest(request)) {
+      return;
+    }
+    // ensure the response has the name field so we can associate the response values with the correct chart
+    let attrs = request.attrs();
+    if (attrs.indexOf('name') == -1)
+      attrs.push('name');
+
+    // this is called when the response is received
+    var saveResponse = function(nodeId, entity, response) {
+      request.busy = false;
+      if (!response || !response.attributeNames)
+        return;
+      // records is an array that has data for all names
+      let records = response.results;
+      if (!records)
+        return;
+
+      let now = new Date();
+      let cutOff = new Date(now.getTime() - request.duration * 60 * 1000);
+      // index of the "name" attr in the response
+      let nameIndex = response.attributeNames.indexOf('name');
+      if (nameIndex < 0)
+        return;
+
+      let names = request.names();
+      // for each record returned, find the name/attr for this request and save the data with this timestamp
+      for (let i = 0; i < records.length; ++i) {
+        let name = records[i][nameIndex];
+        // if we want to store the values for some attrs for this name
+        if (names.indexOf(name) > -1) {
+          attrs.forEach(function(attr) {
+            let attrIndex = response.attributeNames.indexOf(attr);
+            if (records[i][attrIndex] !== undefined) {
+              let data = request.data(name, attr); // get a reference to the data array
+              if (data) {
+
+                if (request.aggregate) {
+                  data.push([now, response.aggregates[i][attrIndex].sum, response.aggregates[i][attrIndex].detail]);
+                } else {
+                  data.push([now, records[i][attrIndex]]);
+                }
+                // expire the old data
+                while (data[0][0] < cutOff) {
+                  data.shift();
                 }
-              } else {
-                dlines[0].push(accessor.call(this, d, d1, elapsed));
               }
             }
-          }
+          });
         }
-        let columns = [dx];
-        dlines.forEach( function (line) {
-          columns.push(line);
-        });
-        return columns;
-      };
-
-      // get the data for the chart and update it
-      self.pfAreaChart.prototype.tick = function() {
-        // can't draw charts that don't have data yet
-        if (!this.chart.data() || this.chart.data().length == 0 || !this.singleAreaChart) {
-          return;
+      }
+    };
+    request.busy = true;
+    // check for override of request
+    if (request.override) {
+      request.override(request, saveResponse);
+    } else {
+      // send the appropriate request
+      if (request.aggregate) {
+        let nodeList = this.QDRService.management.topology.nodeIdList();
+        this.QDRService.management.topology.getMultipleNodeInfo(nodeList, request.entity, attrs, saveResponse, request.nodeId);
+      } else {
+        this.QDRService.management.topology.fetchEntity(request.nodeId, request.entity, attrs, saveResponse);
+      }
+    }
+  }
+
+  numCharts () {
+    return this.charts.filter(function(chart) {
+      return chart.dashboard;
+    }).length;
+    //return this.charts.length;
+  }
+
+  isAttrCharted (nodeId, entity, name, attr, aggregate) {
+    let charts = this.findCharts({
+      name: name,
+      attr: attr,
+      nodeId: nodeId,
+      entity: entity
+    });
+    // if any of the matching charts are on the dashboard page, return true
+    return charts.some(function(chart) {
+      return (chart.dashboard && (aggregate ? chart.aggregate() : !chart.aggregate()));
+    });
+  }
+
+  addHDash (chart) {
+    chart.hdash = true;
+    this.saveCharts();
+  }
+  delHDash (chart) {
+    chart.hdash = false;
+    this.saveCharts();
+  }
+  addDashboard (chart) {
+    chart.dashboard = true;
+    this.saveCharts();
+  }
+  delDashboard (chart) {
+    chart.dashboard = false;
+    this.saveCharts();
+  }
+  // save the charts to local storage
+  saveCharts () {
+    let minCharts = [];
+
+    this.charts.forEach(function(chart) {
+      let minChart = {};
+      // don't save chart unless it is on the dashboard
+      if (chart.dashboard || chart.hdash) {
+        chart.copyProps(minChart);
+        minCharts.push(minChart);
+      }
+    });
+    localStorage['QDRCharts'] = angular.toJson(minCharts);
+  }
+  loadCharts () {
+    let charts = angular.fromJson(localStorage['QDRCharts']);
+    if (charts) {
+      let self = this;
+      // get array of known ids
+      let nodeList = this.QDRService.management.topology.nodeIdList();
+      charts.forEach(function(chart) {
+        // if this chart is not in the current list of nodes, skip
+        if (nodeList.indexOf(chart.nodeId) >= 0) {
+          if (!angular.isDefined(chart.instance)) {
+            chart.instance = ++instance;
+          }
+          if (chart.instance >= instance)
+            instance = chart.instance + 1;
+          if (!chart.duration)
+            chart.duration = 1;
+          if (chart.nodeList)
+            chart.aggregate = true;
+          if (!chart.hdash)
+            chart.hdash = false;
+          if (!chart.dashboard)
+            chart.dashboard = false;
+          if (!chart.hdash && !chart.dashboard)
+            chart.dashboard = true;
+          if (chart.hdash && chart.dashboard)
+            chart.dashboard = false;
+          chart.forceCreate = true;
+          chart.use_instance = true;
+          let newChart = self.registerChart(chart); //chart.nodeId, chart.entity, chart.name, chart.attr, chart.interval, true, chart.aggregate);
+          newChart.dashboard = chart.dashboard;
+          newChart.hdash = chart.hdash;
+          newChart.hreq = false;
+          newChart.type = chart.type;
+          newChart.rateWindow = chart.rateWindow;
+          newChart.areaColor = chart.areaColor ? chart.areaColor : '#32b9f3';
+          newChart.lineColor = chart.lineColor ? chart.lineColor : '#058dc7';
+          newChart.duration(chart.duration);
+          newChart.visibleDuration = chart.visibleDuration ? chart.visibleDuration : newChart.type === 'rate' ? 0.25 : 1;
+          if (chart.userTitle)
+            newChart.title(chart.userTitle);
         }
-
-        // update the chart title
-        // since there is no c3 api to get or set the chart title, we change the title directly using d3
-        let rate = '';
-        if (this.chart.type === 'rate')
-          rate = ' per second';
-        d3.select('#'+this.htmlId+' svg text.c3-title').text(QDRService.utilities.humanify(this.chart.attr()) + rate);
-
-        let d = this.chartData();
-        // load the new data
-        // using the c3.flow api causes the x-axis labels to jump around
-        this.singleAreaChart.load({
-          columns: d
-        });
-      };
-
-      return self;
+      });
     }
-  ]);
-
-  return QDR;
-}(QDR || {}));
+  }
+  // constructor for a c3 area chart
+  pfAreaChart (chart, chartId, defer, width) {
+    return new AreaChart(chart, chartId, defer, width, this.QDRService);
+  }
+
+  // aggregate chart is based on pfAreaChart
+  pfAggChart (chart, chartId, defer) {
+    return new AggChart(chart, chartId, defer, this.QDRService);
+  }
+}
+
+QDRChartService.$inject = ['QDRService', '$log'];
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/qdrCharts.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/qdrCharts.js b/console/stand-alone/plugin/js/qdrCharts.js
index b296de7..0cffe4e 100644
--- a/console/stand-alone/plugin/js/qdrCharts.js
+++ b/console/stand-alone/plugin/js/qdrCharts.js
@@ -16,34 +16,27 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-'use strict';
 
 /* global angular */
 
-/**
- * @module QDR
- */
-var QDR = (function (QDR) {
+import { QDRTemplatePath, QDRRedirectWhenConnected } from './qdrGlobals.js';
 
-  /**
-   * @method ChartsController
-   *
-   * Controller that handles the QDR charts page
-   */
-  QDR.module.controller('QDR.ChartsController', function($scope, QDRService, QDRChartService, $uibModal, $location, $routeParams, $timeout) {
+export class ChartsController {
+  constructor(QDRService, QDRChartService, $scope, $location, $timeout, $routeParams, $uibModal) {
+    this.controllerName = 'QDR.ChartsController';
 
     let updateTimer = null;
 
     if (!QDRService.management.connection.is_connected()) {
       // we are not connected. we probably got here from a bookmark or manual page reload
-      QDR.redirectWhenConnected($location, 'charts');
+      QDRRedirectWhenConnected($location, 'charts');
       return;
     }
 
     $scope.svgCharts = [];
     // create an svg object for each chart
     QDRChartService.charts.filter(function (chart) {return chart.dashboard;}).forEach(function (chart) {
-      let svgChart = new QDRChartService.pfAreaChart(chart, chart.id(), true);
+      let svgChart = QDRChartService.pfAreaChart(chart, chart.id(), true);
       svgChart.zoomed = false;
       $scope.svgCharts.push(svgChart);
     });
@@ -108,7 +101,7 @@ var QDR = (function (QDR) {
     // called from dialog when we want to clone the dialog chart
     // the chart argument here is a QDRChartService chart
     $scope.addChart = function (chart) {
-      let nchart = new QDRChartService.pfAreaChart(chart, chart.id(), true);
+      let nchart = QDRChartService.pfAreaChart(chart, chart.id(), true);
       $scope.svgCharts.push(nchart);
       $timeout( function () {
         nchart.generate();
@@ -132,7 +125,7 @@ var QDR = (function (QDR) {
         backdrop: true,
         keyboard: true,
         backdropClick: true,
-        templateUrl: QDR.templatePath + template,
+        templateUrl: QDRTemplatePath + template,
         controller: 'QDR.ChartDialogController',
         resolve: {
           chart: function() {
@@ -150,11 +143,7 @@ var QDR = (function (QDR) {
         }
       });
     }
-
-  });
-
-
-  return QDR;
-
-}(QDR || {}));
+  }
+}
+ChartsController.$inject = ['QDRService', 'QDRChartService', '$scope', '$location', '$timeout', '$routeParams', '$uibModal'];
 

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/qdrGlobals.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/qdrGlobals.js b/console/stand-alone/plugin/js/qdrGlobals.js
index af06c55..6fadf14 100644
--- a/console/stand-alone/plugin/js/qdrGlobals.js
+++ b/console/stand-alone/plugin/js/qdrGlobals.js
@@ -16,31 +16,43 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-'use strict';
 
-var QDR = (function(QDR) {
+export var QDRFolder = (function () {
+  function Folder(title) {
+    this.title = title;
+    this.children = [];
+    this.folder = true;
+  }
+  return Folder;
+})();
+export var QDRLeaf = (function () {
+  function Leaf(title) {
+    this.title = title;
+  }
+  return Leaf;
+})();
 
-  QDR.Folder = (function () {
-    function Folder(title) {
-      this.title = title;
-      this.children = [];
-      this.folder = true;
-    }
-    return Folder;
-  })();
-  QDR.Leaf = (function () {
-    function Leaf(title) {
-      this.title = title;
-    }
-    return Leaf;
-  })();
+export var QDRCore = {
+  notification: function (severity, msg) {
+    $.notify(msg, severity);
+  }
+};
 
-  QDR.Core = {
-    notification: function (severity, msg) {
-      $.notify(msg, severity);
-    }
-  };
+export class QDRLogger {
+  constructor($log, source) {
+    this.log = function (msg) { $log.log(`QDR-${source}: ${msg}`); };
+    this.debug = function (msg) { $log.debug(`QDR-${source}: ${msg}`); };
+    this.error = function (msg) { $log.error(`QDR-${source}: ${msg}`); };
+    this.info = function (msg) { $log.info(`QDR-${source}: ${msg}`); };
+    this.warn = function (msg) { $log.warn(`QDR-${source}: ${msg}`); };
+  }
+}
 
-  return QDR;
+export const QDRTemplatePath = 'html/';
+export const QDR_SETTINGS_KEY = 'QDRSettings';
+export const QDR_LAST_LOCATION = 'QDRLastLocation';
 
-} (QDR || {}));
\ No newline at end of file
+export var QDRRedirectWhenConnected = function ($location, org) {
+  $location.path('/connect');
+  $location.search('org', org);
+};


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


[3/8] qpid-dispatch git commit: DISPATCH-1049 Add console tests

Posted by ea...@apache.org.
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/qdrOverview.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/qdrOverview.js b/console/stand-alone/plugin/js/qdrOverview.js
index 2f784e7..536d93a 100644
--- a/console/stand-alone/plugin/js/qdrOverview.js
+++ b/console/stand-alone/plugin/js/qdrOverview.js
@@ -16,21 +16,15 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-'use strict';
 /* global angular d3 */
+import { QDRFolder, QDRLeaf, QDRCore, QDRLogger, QDRTemplatePath, QDRRedirectWhenConnected } from './qdrGlobals.js';
 
-/**
- * @module QDR
- */
-var QDR = (function (QDR) {
+export class OverviewController {
+  constructor(QDRService, $scope, $log, $location, $timeout, $uibModal) {
+    this.controllerName = 'QDR.OverviewController';
 
-  /**
-   *
-   * Controller that handles the QDR overview page
-   */
-  QDR.module.controller('QDR.OverviewController', ['$scope', 'QDRService', '$location', '$timeout', '$uibModal', 'uiGridConstants', function($scope, QDRService, $location, $timeout, $uibModal) {
-
-    QDR.log.debug('QDR.OverviewControll started with location of ' + $location.path() + ' and connection of  ' + QDRService.management.connection.is_connected());
+    let QDRLog = new QDRLogger($log, 'OverviewController');
+    QDRLog.debug('QDR.OverviewControll started with location of ' + $location.path() + ' and connection of  ' + QDRService.management.connection.is_connected());
     let updateIntervalHandle = undefined;
     const updateInterval = 5000;
 
@@ -46,14 +40,14 @@ var QDR = (function (QDR) {
       content: '<i class="icon-list"></i> Attributes',
       title: 'View the attribute values on your selection',
       isValid: function () { return true; },
-      href: function () { return '#/' + QDR.pluginName + '/attributes'; },
+      href: function () { return '#/attributes'; },
       index: 0
     },
     {
       content: '<i class="icon-leaf"></i> Operations',
       title: 'Execute operations on your selection',
       isValid: function () { return true; },
-      href: function () { return '#/' + QDR.pluginName + '/operations'; },
+      href: function () { return '#/operations'; },
       index: 1
     }];
     $scope.activeTab = $scope.subLevelTabs[0];
@@ -72,7 +66,7 @@ var QDR = (function (QDR) {
       {title: 'Overview', name: 'Overview', right: false}
     ];
 
-    $scope.tmplOverviewTree = QDR.templatePath + 'tmplOverviewTree.html';
+    $scope.tmplOverviewTree = QDRTemplatePath + 'tmplOverviewTree.html';
     $scope.templates = [
       { name: 'Charts', url: 'overviewCharts.html'},
       { name: 'Routers', url: 'routers.html'},
@@ -188,7 +182,7 @@ var QDR = (function (QDR) {
             if (nodes[node]['connection'].results[i][0] === 'inter-router')
               ++connections;
           }
-          let routerRow = {connections: connections, nodeId: node, id: QDRService.management.topology.nameFromId(node)};
+          let routerRow = {connections: connections, nodeId: node, id: QDRService.utilities.nameFromId(node)};
           nodes[node]['router'].attributeNames.forEach( function (routerAttr, i) {
             if (routerAttr !== 'routerId' && routerAttr !== 'id')
               routerRow[routerAttr] = nodes[node]['router'].results[0][i];
@@ -441,7 +435,7 @@ var QDR = (function (QDR) {
         }
         return include;
       });
-      QDR.log.info('setting linkFields in updateLinkGrid');
+      QDRLog.info('setting linkFields in updateLinkGrid');
       $scope.linkFields = filteredLinks;
       expandGridToContent('Links', $scope.linkFields.length);
       getLinkPagedData($scope.linkPagingOptions.pageSize, $scope.linkPagingOptions.currentPage);
@@ -594,7 +588,7 @@ var QDR = (function (QDR) {
 
     var getAllLinkFields = function (completionCallbacks, selectionCallback) {
       if (!$scope.linkFields) {
-        QDR.log.debug('$scope.linkFields was not defined!');
+        QDRLog.debug('$scope.linkFields was not defined!');
         return;
       }
       let nodeIds = QDRService.management.topology.nodeIdList();
@@ -611,10 +605,10 @@ var QDR = (function (QDR) {
           if (elapsed < 0)
             return 0;
           let delivered = QDRService.utilities.valFor(response.attributeNames, result, 'deliveryCount') - oldname[0].rawDeliveryCount;
-          //QDR.log.debug("elapsed " + elapsed + " delivered " + delivered)
+          //QDRLog.debug("elapsed " + elapsed + " delivered " + delivered)
           return elapsed > 0 ? parseFloat(Math.round((delivered/elapsed) * 100) / 100).toFixed(2) : 0;
         } else {
-          //QDR.log.debug("unable to find old linkName")
+          //QDRLog.debug("unable to find old linkName")
           return 0;
         }
       };
@@ -637,7 +631,7 @@ var QDR = (function (QDR) {
               return QDRService.utilities.pretty(und + uns);
             };
             var getLinkName = function () {
-              let namestr = QDRService.management.topology.nameFromId(nodeName);
+              let namestr = QDRService.utilities.nameFromId(nodeName);
               return namestr + ':' + QDRService.utilities.valFor(response.attributeNames, result, 'identity');
             };
             var fixAddress = function () {
@@ -1050,8 +1044,8 @@ var QDR = (function (QDR) {
         .then( function (results, context) {
           let statusCode = context.message.application_properties.statusCode;
           if (statusCode < 200 || statusCode >= 300) {
-            QDR.Core.notification('error', context.message.statusDescription);
-            QDR.log.error('Error ' + context.message.statusDescription);
+            QDRCore.notification('error', context.message.statusDescription);
+            QDRLog.error('Error ' + context.message.statusDescription);
           }
         });
     };
@@ -1220,9 +1214,9 @@ var QDR = (function (QDR) {
         }
       });
       d.result.then(function () {
-        QDR.log.debug('d.open().then');
+        QDRLog.debug('d.open().then');
       }, function () {
-        QDR.log.debug('Modal dismissed at: ' + new Date());
+        QDRLog.debug('Modal dismissed at: ' + new Date());
       });
     }
 
@@ -1365,7 +1359,7 @@ var QDR = (function (QDR) {
           let entry = allLogEntries[n];
           entry.forEach( function (module) {
             if (module.name === node.key) {
-              module.nodeName = QDRService.management.topology.nameFromId(n);
+              module.nodeName = QDRService.utilities.nameFromId(n);
               module.nodeId = n;
               module.enable = logInfo.enable;
               $scope.logModuleData.push(module);
@@ -1441,7 +1435,7 @@ var QDR = (function (QDR) {
     };
 
     if (!QDRService.management.connection.is_connected()) {
-      QDR.redirectWhenConnected($location, 'overview');
+      QDRRedirectWhenConnected($location, 'overview');
       return;
     }
     $scope.template = $scope.templates[0];
@@ -1530,7 +1524,7 @@ var QDR = (function (QDR) {
     var showCharts = function () {
 
     };
-    let charts = new QDR.Folder('Charts');
+    let charts = new QDRFolder('Charts');
     charts.info = {fn: showCharts};
     charts.type = 'Charts';  // for the charts template
     charts.key = 'Charts';
@@ -1538,7 +1532,7 @@ var QDR = (function (QDR) {
     topLevelChildren.push(charts);
 
     // create a routers tree branch
-    let routers = new QDR.Folder('Routers');
+    let routers = new QDRFolder('Routers');
     routers.type = 'Routers';
     routers.info = {fn: allRouterInfo};
     routers.expanded = (expandedNodeList.indexOf('Routers') > -1);
@@ -1549,8 +1543,8 @@ var QDR = (function (QDR) {
     // called when the list of routers changes
     var updateRouterTree = function (nodes) {
       var worker = function (node) {
-        let name = QDRService.management.topology.nameFromId(node);
-        let router = new QDR.Leaf(name);
+        let name = QDRService.utilities.nameFromId(node);
+        let router = new QDRLeaf(name);
         router.type = 'Router';
         router.info = {fn: routerInfo};
         router.nodeId = node;
@@ -1563,7 +1557,7 @@ var QDR = (function (QDR) {
     };
 
     // create an addresses tree branch
-    let addresses = new QDR.Folder('Addresses');
+    let addresses = new QDRFolder('Addresses');
     addresses.type = 'Addresses';
     addresses.info = {fn: allAddressInfo};
     addresses.expanded = (expandedNodeList.indexOf('Addresses') > -1);
@@ -1573,7 +1567,7 @@ var QDR = (function (QDR) {
     topLevelChildren.push(addresses);
     var updateAddressTree = function (addressFields) {
       var worker = function (address) {
-        let a = new QDR.Leaf(address.title);
+        let a = new QDRLeaf(address.title);
         a.info = {fn: addressInfo};
         a.key = address.uid;
         a.fields = address;
@@ -1601,7 +1595,7 @@ var QDR = (function (QDR) {
     };
 
     $scope.filter = angular.fromJson(localStorage[FILTERKEY]) || {endpointsOnly: 'true', hideConsoles: true};
-    let links = new QDR.Folder('Links');
+    let links = new QDRFolder('Links');
     links.type = 'Links';
     links.info = {fn: allLinkInfo};
     links.expanded = (expandedNodeList.indexOf('Links') > -1);
@@ -1613,7 +1607,7 @@ var QDR = (function (QDR) {
     // called both before the tree is created and whenever a background update is done
     var updateLinkTree = function (linkFields) {
       var worker = function (link) {
-        let l = new QDR.Leaf(link.title);
+        let l = new QDRLeaf(link.title);
         let isConsole = QDRService.utilities.isConsole(QDRService.management.topology.getConnForLink(link));
         l.info = {fn: linkInfo};
         l.key = link.uid;
@@ -1630,7 +1624,7 @@ var QDR = (function (QDR) {
       updateLeaves(linkFields, 'Links', worker);
     };
 
-    let connections = new QDR.Folder('Connections');
+    let connections = new QDRFolder('Connections');
     connections.type = 'Connections';
     connections.info = {fn: allConnectionInfo};
     connections.expanded = (expandedNodeList.indexOf('Connections') > -1);
@@ -1645,7 +1639,7 @@ var QDR = (function (QDR) {
         if (connection.name === 'connection/' && connection.role === 'inter-router' && connection.host === '')
           host = connection.container + ':' + connection.identity;
 
-        let c = new QDR.Leaf(host);
+        let c = new QDRLeaf(host);
         let isConsole = QDRService.utilities.isAConsole (connection.properties, connection.identity, connection.role, connection.routerId);
         c.type = 'Connection';
         c.info = {fn: connectionInfo};
@@ -1664,7 +1658,7 @@ var QDR = (function (QDR) {
 
     var updateLogTree = function (logFields) {
       var worker = function (log) {
-        let l = new QDR.Leaf(log.name);
+        let l = new QDRLeaf(log.name);
         l.type = 'Log';
         l.info = {fn: logInfo};
         l.key = log.name;
@@ -1678,7 +1672,7 @@ var QDR = (function (QDR) {
     let htmlReady = false;
     let dataReady = false;
     $scope.largeNetwork = QDRService.management.topology.isLargeNetwork();
-    let logs = new QDR.Folder('Logs');
+    let logs = new QDRFolder('Logs');
     logs.type = 'Logs';
     logs.info = {fn: allLogInfo};
     logs.expanded = (expandedNodeList.indexOf('Logs') > -1);
@@ -1723,13 +1717,13 @@ var QDR = (function (QDR) {
     // add placeholders for the top level tree nodes
     let topLevelTreeNodes = [routers, addresses, links, connections, logs];
     topLevelTreeNodes.forEach( function (parent) {
-      let placeHolder = new QDR.Leaf('loading...');
+      let placeHolder = new QDRLeaf('loading...');
       placeHolder.extraClasses = 'loading';
       parent.children = [placeHolder];
     });
 
     var updateExpanded = function () {
-      QDR.log.debug('updateExpandedEntities');
+      QDRLog.debug('updateExpandedEntities');
       clearTimeout(updateIntervalHandle);
 
       let tree = $('#overtree').fancytree('getTree');
@@ -1742,7 +1736,7 @@ var QDR = (function (QDR) {
         });
         q.await( function (error) {
           if (error)
-            QDR.log.error(error.message);
+            QDRLog.error(error.message);
 
           // if there are no active nodes, activate the saved one
           let tree = $('#overtree').fancytree('getTree');
@@ -1771,8 +1765,7 @@ var QDR = (function (QDR) {
       clearTimeout(updateIntervalHandle);
       $(window).off('resize', resizer);
     });
-  }]);
-
-  return QDR;
 
-}(QDR || {}));
+  }
+}
+OverviewController.$inject = ['QDRService', '$scope', '$log', '$location', '$timeout', '$uibModal'];

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/qdrOverviewChartsController.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/qdrOverviewChartsController.js b/console/stand-alone/plugin/js/qdrOverviewChartsController.js
index e47e597..a65d74c 100644
--- a/console/stand-alone/plugin/js/qdrOverviewChartsController.js
+++ b/console/stand-alone/plugin/js/qdrOverviewChartsController.js
@@ -16,14 +16,11 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-'use strict';
 /* global angular */
-/**
- * @module QDR
- */
-var QDR = (function(QDR) {
 
-  QDR.module.controller('QDR.OverviewChartsController', function ($scope, QDRService, QDRChartService, $timeout) {
+export class OverviewChartsController {
+  constructor(QDRService, QDRChartService, $scope, $timeout) {
+    this.controllerName = 'QDR.OverviewChartsController';
 
     $scope.overviewCharts = [];
     let updateTimer;
@@ -90,7 +87,7 @@ var QDR = (function(QDR) {
     ];
     $scope.overviewCharts = charts.map( function (chart) {
       let c = QDRChartService.registerChart(chart);
-      return new QDRChartService.pfAreaChart(c, c.id(), true);
+      return QDRChartService.pfAreaChart(c, c.id(), true);
     });
 
 
@@ -128,7 +125,6 @@ var QDR = (function(QDR) {
         QDRChartService.unRegisterChart(svg.chart);
       });
     });
-  });
-  return QDR;
-
-} (QDR || {}));
+  }
+}
+OverviewChartsController.$inject = ['QDRService', 'QDRChartService', '$scope', '$timeout'];

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/qdrOverviewLogsController.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/qdrOverviewLogsController.js b/console/stand-alone/plugin/js/qdrOverviewLogsController.js
index 1e541a5..316605e 100644
--- a/console/stand-alone/plugin/js/qdrOverviewLogsController.js
+++ b/console/stand-alone/plugin/js/qdrOverviewLogsController.js
@@ -16,19 +16,19 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-'use strict';
-/**
- * @module QDR
- */
-var QDR = (function(QDR) {
 
-  QDR.module.controller('QDR.OverviewLogsController', function ($scope, $uibModalInstance, QDRService, $timeout, nodeName, nodeId, module, level) {
+import { QDRCore, QDRLogger } from './qdrGlobals.js';
 
+export class OverviewLogsController {
+  constructor(QDRService, $scope, $log, $uibModalInstance, $timeout, nodeName, nodeId, module, level) {
+    this.controllerName = 'QDR.OverviewLogsController';
+
+    let QDRLog = new QDRLogger($log, 'OverviewLogsController');
     var gotLogInfo = function (nodeId, response, context) {
       let statusCode = context.message.application_properties.statusCode;
       if (statusCode < 200 || statusCode >= 300) {
-        QDR.Core.notification('error', context.message.statusDescription);
-        QDR.log.info('Error ' + context.message.statusDescription);
+        QDRCore.notification('error', context.message.statusDescription);
+        QDRLog.info('Error ' + context.message.statusDescription);
       } else {
         let levelLogs = response.filter( function (result) {
           if (result[1] == null)
@@ -37,7 +37,7 @@ var QDR = (function(QDR) {
         });
         let logFields = levelLogs.map( function (result) {
           return {
-            nodeId: QDRService.management.topology.nameFromId(nodeId),
+            nodeId: QDRService.utilities.nameFromId(nodeId),
             name: result[0],
             type: result[1],
             message: result[2],
@@ -64,7 +64,6 @@ var QDR = (function(QDR) {
       $uibModalInstance.close(true);
     };
 
-  });
-  return QDR;
-
-} (QDR || {}));
+  }
+}
+OverviewLogsController.$inject = ['QDRService', '$scope', '$log', '$uibModalInstance', '$timeout', 'nodeName', 'nodeId', 'module', 'level'];

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/qdrSchema.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/qdrSchema.js b/console/stand-alone/plugin/js/qdrSchema.js
index 69569ea..4421636 100644
--- a/console/stand-alone/plugin/js/qdrSchema.js
+++ b/console/stand-alone/plugin/js/qdrSchema.js
@@ -16,19 +16,18 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-'use strict';
-/**
- * @module QDR
- */
-var QDR = (function (QDR) {
+import { QDRRedirectWhenConnected} from './qdrGlobals.js';
+
+export class SchemaController {
+  constructor(QDRService, $scope, $location, $timeout) {
+    this.controllerName = 'QDR.SchemaController';
 
-  QDR.module.controller('QDR.SchemaController', ['$scope', '$location', '$timeout', 'QDRService', function($scope, $location, $timeout, QDRService) {
     if (!QDRService.management.connection.is_connected()) {
-      QDR.redirectWhenConnected($location, 'schema');
+      QDRRedirectWhenConnected($location, 'schema');
       return;
     }
     var onDisconnect = function () {
-      $timeout( function () {QDR.redirectWhenConnected('schema');});
+      $timeout( function () {QDRRedirectWhenConnected('schema');});
     };
     // we are currently connected. setup a handler to get notified if we are ever disconnected
     QDRService.management.connection.addDisconnectAction( onDisconnect );
@@ -77,7 +76,6 @@ var QDR = (function (QDR) {
       QDRService.management.connection.delDisconnectAction( onDisconnect );
     });
 
-  }]);
-
-  return QDR;
-}(QDR || {}));
+  }
+}
+SchemaController.$inject = ['QDRService', '$scope', '$location', '$timeout'];

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/qdrService.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/qdrService.js b/console/stand-alone/plugin/js/qdrService.js
index f4effdd..8f13192 100644
--- a/console/stand-alone/plugin/js/qdrService.js
+++ b/console/stand-alone/plugin/js/qdrService.js
@@ -16,254 +16,95 @@ Licensed to the Apache Software Foundation (ASF) under one
   specific language governing permissions and limitations
   under the License.
 */
-'use strict';
 /* global Promise */
-/**
- * @module QDR
- */
-var QDR = (function(QDR) {
-
-  // The QDR service handles the connection to the router
-  QDR.module.factory('QDRService', ['$timeout', '$location', function($timeout, $location) {
-    let dm = require('dispatch-management');
-    let self = {
-      management: new dm.Management($location.protocol()),
-      utilities: dm.Utilities,
-
-      onReconnect: function () {
-        self.management.connection.on('disconnected', self.onDisconnect);
-        let org = localStorage[QDR.LAST_LOCATION] || '/overview';
-        $timeout ( function () {
-          $location.path(org);
-          $location.search('org', null);
-          $location.replace();
-        });
-      },
-      onDisconnect: function () {
-        self.management.connection.on('connected', self.onReconnect);
-        $timeout( function () {
-          $location.path('/connect');
-          let curPath = $location.path();
-          let parts = curPath.split('/');
-          let org = parts[parts.length-1];
-          if (org && org.length > 0 && org !== 'connect') {
-            $location.search('org', org);
-          } else {
-            $location.search('org', null);
-          }
-          $location.replace();
-        });
-      },
-
-      connect: function (connectOptions) {
-        return new Promise ( function (resolve, reject) {
-          self.management.connection.connect(connectOptions)
-            .then( function (r) {
-              // if we are ever disconnected, show the connect page and wait for a reconnect
-              self.management.connection.on('disconnected', self.onDisconnect);
-
-              self.management.getSchema()
+import { Management as dm } from '../../modules/management.js';
+import { utils } from '../../modules/utilities.js';
+
+import { QDR_LAST_LOCATION, QDRLogger} from './qdrGlobals.js';
+
+export class QDRService {
+  constructor($log, $timeout, $location) {
+    this.$timeout = $timeout;
+    this.$location = $location;
+    this.management = new dm($location.protocol());
+    this.utilities = utils;
+    this.QDRLog = new QDRLogger($log, 'QDRService');
+  }
+  
+  // Example service function
+  onReconnect () {
+    this.management.connection.on('disconnected', this.onDisconnect);
+    let org = localStorage[QDR_LAST_LOCATION] || '/overview';
+    this.$timeout ( function () {
+      this.$location.path(org);
+      this.$location.search('org', null);
+      this.$location.replace();
+    });
+  }
+  onDisconnect () {
+    this.management.connection.on('connected', this.onReconnect);
+    this.$timeout( function () {
+      this.$location.path('/connect');
+      let curPath = this.$location.path();
+      let parts = curPath.split('/');
+      let org = parts[parts.length-1];
+      if (org && org.length > 0 && org !== 'connect') {
+        this.$location.search('org', org);
+      } else {
+        this.$location.search('org', null);
+      }
+      this.$location.replace();
+    });
+  }
+  connect (connectOptions) {
+    let self = this;
+    return new Promise ( function (resolve, reject) {
+      self.management.connection.connect(connectOptions)
+        .then( function (r) {
+          // if we are ever disconnected, show the connect page and wait for a reconnect
+          self.management.connection.on('disconnected', self.onDisconnect);
+
+          self.management.getSchema()
+            .then( function () {
+              self.QDRLog.info('got schema after connection');
+              self.management.topology.setUpdateEntities([]);
+              self.QDRLog.info('requesting a topology');
+              self.management.topology.get() // gets the list of routers
                 .then( function () {
-                  QDR.log.info('got schema after connection');
-                  self.management.topology.setUpdateEntities([]);
-                  QDR.log.info('requesting a topology');
-                  self.management.topology.get() // gets the list of routers
-                    .then( function () {
-                      QDR.log.info('got initial topology');
-                      let curPath = $location.path();
-                      let parts = curPath.split('/');
-                      let org = parts[parts.length-1];
-                      if (org === '' || org === 'connect') {
-                        org = localStorage[QDR.LAST_LOCATION] || QDR.pluginRoot + '/overview';
-                      }
-                      $timeout ( function () {
-                        $location.path(org);
-                        $location.search('org', null);
-                        $location.replace();
-                      });
-                    });
+                  self.QDRLog.info('got initial topology');
+                  let curPath = self.$location.path();
+                  let parts = curPath.split('/');
+                  let org = parts[parts.length-1];
+                  if (org === '' || org === 'connect') {
+                    org = localStorage[QDR_LAST_LOCATION] || '/overview';
+                  }
+                  self.$timeout ( function () {
+                    self.$location.path(org);
+                    self.$location.search('org', null);
+                    self.$location.replace();
+                  });
                 });
-              resolve(r);
-            }, function (e) {
-              reject(e);
             });
+          resolve(r);
+        }, function (e) {
+          reject(e);
         });
-      },
-      disconnect: function () {
-        self.management.connection.disconnect();
-        delete self.management;
-        self.management = new dm.Management($location.protocol());
-      }
-
-
-    };
-
-    return self;
-  }]);
-
-  return QDR;
+    });
+  }
+  disconnect () {
+    this.management.connection.disconnect();
+    delete this.management;
+    this.management = new dm(this.$location.protocol());
+  }
+}
 
-}(QDR || {}));
+QDRService.$inject = ['$log', '$timeout', '$location'];
 
 (function() {
   console.dump = function(o) {
     if (window.JSON && window.JSON.stringify)
-      QDR.log.info(JSON.stringify(o, undefined, 2));
+      console.log(JSON.stringify(o, undefined, 2));
     else
       console.log(o);
   };
-})();
-
-if (!String.prototype.startsWith) {
-  String.prototype.startsWith = function (searchString, position) {
-    return this.substr(position || 0, searchString.length) === searchString;
-  };
-}
-
-if (!String.prototype.endsWith) {
-  String.prototype.endsWith = function(searchString, position) {
-    let subjectString = this.toString();
-    if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) {
-      position = subjectString.length;
-    }
-    position -= searchString.length;
-    let lastIndex = subjectString.lastIndexOf(searchString, position);
-    return lastIndex !== -1 && lastIndex === position;
-  };
-}
-
-// https://tc39.github.io/ecma262/#sec-array.prototype.findIndex
-if (!Array.prototype.findIndex) {
-  Object.defineProperty(Array.prototype, 'findIndex', {
-    value: function(predicate) {
-      // 1. Let O be ? ToObject(this value).
-      if (this == null) {
-        throw new TypeError('"this" is null or not defined');
-      }
-
-      let o = Object(this);
-
-      // 2. Let len be ? ToLength(? Get(O, "length")).
-      let len = o.length >>> 0;
-
-      // 3. If IsCallable(predicate) is false, throw a TypeError exception.
-      if (typeof predicate !== 'function') {
-        throw new TypeError('predicate must be a function');
-      }
-
-      // 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
-      let thisArg = arguments[1];
-
-      // 5. Let k be 0.
-      let k = 0;
-
-      // 6. Repeat, while k < len
-      while (k < len) {
-        // a. Let Pk be ! ToString(k).
-        // b. Let kValue be ? Get(O, Pk).
-        // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
-        // d. If testResult is true, return k.
-        let kValue = o[k];
-        if (predicate.call(thisArg, kValue, k, o)) {
-          return k;
-        }
-        // e. Increase k by 1.
-        k++;
-      }
-
-      // 7. Return -1.
-      return -1;
-    }
-  });
-}
-
-// https://tc39.github.io/ecma262/#sec-array.prototype.find
-if (!Array.prototype.find) {
-  Object.defineProperty(Array.prototype, 'find', {
-    value: function(predicate) {
-      // 1. Let O be ? ToObject(this value).
-      if (this == null) {
-        throw new TypeError('"this" is null or not defined');
-      }
-
-      let o = Object(this);
-
-      // 2. Let len be ? ToLength(? Get(O, "length")).
-      let len = o.length >>> 0;
-
-      // 3. If IsCallable(predicate) is false, throw a TypeError exception.
-      if (typeof predicate !== 'function') {
-        throw new TypeError('predicate must be a function');
-      }
-
-      // 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
-      let thisArg = arguments[1];
-
-      // 5. Let k be 0.
-      let k = 0;
-
-      // 6. Repeat, while k < len
-      while (k < len) {
-        // a. Let Pk be ! ToString(k).
-        // b. Let kValue be ? Get(O, Pk).
-        // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
-        // d. If testResult is true, return kValue.
-        let kValue = o[k];
-        if (predicate.call(thisArg, kValue, k, o)) {
-          return kValue;
-        }
-        // e. Increase k by 1.
-        k++;
-      }
-
-      // 7. Return undefined.
-      return undefined;
-    }
-  });
-}
-
-// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill
-if (!Array.prototype.fill) {
-  Object.defineProperty(Array.prototype, 'fill', {
-    value: function(value) {
-
-      // Steps 1-2.
-      if (this == null) {
-        throw new TypeError('this is null or not defined');
-      }
-
-      var O = Object(this);
-
-      // Steps 3-5.
-      var len = O.length >>> 0;
-
-      // Steps 6-7.
-      var start = arguments[1];
-      var relativeStart = start >> 0;
-
-      // Step 8.
-      var k = relativeStart < 0 ?
-        Math.max(len + relativeStart, 0) :
-        Math.min(relativeStart, len);
-
-      // Steps 9-10.
-      var end = arguments[2];
-      var relativeEnd = end === undefined ?
-        len : end >> 0;
-
-      // Step 11.
-      var final = relativeEnd < 0 ?
-        Math.max(len + relativeEnd, 0) :
-        Math.min(relativeEnd, len);
-
-      // Step 12.
-      while (k < final) {
-        O[k] = value;
-        k++;
-      }
-
-      // Step 13.
-      return O;
-    }
-  });
-}
\ No newline at end of file
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/qdrSettings.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/qdrSettings.js b/console/stand-alone/plugin/js/qdrSettings.js
index e73d13b..2d0e01b 100644
--- a/console/stand-alone/plugin/js/qdrSettings.js
+++ b/console/stand-alone/plugin/js/qdrSettings.js
@@ -16,29 +16,20 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-'use strict';
 /* global angular */
-/**
- * @module QDR
- */
-var QDR = (function(QDR) {
+import { QDR_SETTINGS_KEY, QDRLogger} from './qdrGlobals.js';
 
-  /**
-   * @method SettingsController
-   * @param $scope
-   * @param QDRServer
-   *
-   * Controller that handles the QDR settings page
-   */
-
-  QDR.module.controller('QDR.SettingsController', ['$scope', 'QDRService', 'QDRChartService', '$timeout', function($scope, QDRService, QDRChartService, $timeout) {
+export class SettingsController {
+  constructor(QDRService, QDRChartService, $scope, $log, $timeout) {
+    this.controllerName = 'QDR.SettingsController';
 
+    let QDRLog = new QDRLogger($log, 'SettingsController');
     $scope.connecting = false;
     $scope.connectionError = false;
     $scope.connectionErrorText = undefined;
     $scope.forms = {};
 
-    $scope.formEntity = angular.fromJson(localStorage[QDR.SETTINGS_KEY]) || {
+    $scope.formEntity = angular.fromJson(localStorage[QDR_SETTINGS_KEY]) || {
       address: '',
       port: '',
       username: '',
@@ -51,7 +42,7 @@ var QDR = (function(QDR) {
       if (newValue !== oldValue) {
         let pass = newValue.password;
         newValue.password = '';
-        localStorage[QDR.SETTINGS_KEY] = angular.toJson(newValue);
+        localStorage[QDR_SETTINGS_KEY] = angular.toJson(newValue);
         newValue.password = pass;
       }
     }, true);
@@ -84,7 +75,7 @@ var QDR = (function(QDR) {
     };
 
     var doConnect = function() {
-      QDR.log.info('doConnect called on connect page');
+      QDRLog.info('doConnect called on connect page');
       if (!$scope.formEntity.address)
         $scope.formEntity.address = 'localhost';
       if (!$scope.formEntity.port)
@@ -116,65 +107,7 @@ var QDR = (function(QDR) {
           failed(e);
         });
     };
-  }]);
-
-
-  QDR.module.directive('posint', function() {
-    return {
-      require: 'ngModel',
-
-      link: function(scope, elem, attr, ctrl) {
-        // input type number allows + and - but we don't want them so filter them out
-        elem.bind('keypress', function(event) {
-          let nkey = !event.charCode ? event.which : event.charCode;
-          let skey = String.fromCharCode(nkey);
-          let nono = '-+.,';
-          if (nono.indexOf(skey) >= 0) {
-            event.preventDefault();
-            return false;
-          }
-          // firefox doesn't filter out non-numeric input. it just sets the ctrl to invalid
-          if (/[!@#$%^&*()]/.test(skey) && event.shiftKey || // prevent shift numbers
-            !( // prevent all but the following
-              nkey <= 0 || // arrows
-              nkey == 8 || // delete|backspace
-              nkey == 13 || // enter
-              (nkey >= 37 && nkey <= 40) || // arrows
-              event.ctrlKey || event.altKey || // ctrl-v, etc.
-              /[0-9]/.test(skey)) // numbers
-          ) {
-            event.preventDefault();
-            return false;
-          }
-        });
-        // check the current value of input
-        var _isPortInvalid = function(value) {
-          let port = value + '';
-          let isErrRange = false;
-          // empty string is valid
-          if (port.length !== 0) {
-            let n = ~~Number(port);
-            if (n < 1 || n > 65535) {
-              isErrRange = true;
-            }
-          }
-          ctrl.$setValidity('range', !isErrRange);
-          return isErrRange;
-        };
-
-        //For DOM -> model validation
-        ctrl.$parsers.unshift(function(value) {
-          return _isPortInvalid(value) ? undefined : value;
-        });
-
-        //For model -> DOM validation
-        ctrl.$formatters.unshift(function(value) {
-          _isPortInvalid(value);
-          return value;
-        });
-      }
-    };
-  });
+  }
+}
+SettingsController.$inject = ['QDRService', 'QDRChartService', '$scope', '$log', '$timeout'];
 
-  return QDR;
-}(QDR || {}));

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/qdrTopAddressesController.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/qdrTopAddressesController.js b/console/stand-alone/plugin/js/qdrTopAddressesController.js
index 2c3d3a3..2c27dbc 100644
--- a/console/stand-alone/plugin/js/qdrTopAddressesController.js
+++ b/console/stand-alone/plugin/js/qdrTopAddressesController.js
@@ -16,14 +16,11 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-'use strict';
 /* global angular */
-/**
- * @module QDR
- */
-var QDR = (function(QDR) {
 
-  QDR.module.controller('QDR.TopAddressesController', function ($scope, QDRService, $timeout) {
+export class TopAddressesController {
+  constructor(QDRService, $scope, $timeout) {
+    this.controllerName = 'QDR.TopAddressesController';
 
     $scope.addressesData = [];
     $scope.topAddressesGrid = {
@@ -214,7 +211,6 @@ var QDR = (function(QDR) {
         clearInterval(timer);
     });
 
-  });
-  return QDR;
-
-} (QDR || {}));
+  }
+}
+TopAddressesController.$inject = ['QDRService', '$scope', '$timeout'];

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/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
new file mode 100644
index 0000000..d7f4110
--- /dev/null
+++ b/console/stand-alone/plugin/js/topology/links.js
@@ -0,0 +1,216 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+
+class Link {
+  constructor(source, target, dir, cls, uid) {
+    this.source = source;
+    this.target = target;
+    this.left = dir != 'out';
+    this.right = (dir == 'out' || dir == 'both');
+    this.cls = cls;
+    this.uid = uid;
+  }
+}
+
+export class Links {
+  constructor(QDRService, logger) {
+    this.links = [];
+    this.QDRService = QDRService;
+    this.logger = logger;
+  }
+  getLinkSource (nodesIndex) {
+    for (let i=0; i<this.links.length; ++i) {
+      if (this.links[i].target === nodesIndex)
+        return i;
+    }
+    return -1;
+  }
+  getLink(_source, _target, dir, cls, uid) {
+    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') {
+        s = s.id;
+        t = t.id;
+      }
+      if (s == _source && t == _target) {
+        return i;
+      }
+      // same link, just reversed
+      if (s == _target && t == _source) {
+        return -i;
+      }
+    }
+    //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;
+    return this.links.push(new Link(_source, _target, dir, cls, uid)) - 1;
+  }
+  linkFor (source, target) {
+    for (let i = 0; i < this.links.length; ++i) {
+      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))
+        return this.links[i];
+    }
+    // the selected node was a client/broker
+    return null;
+  }
+
+
+  initializeLinks (nodeInfo, nodes, unknowns, localStorage, height) {
+    let animate = false;
+    let source = 0;
+    let client = 1.0;
+    for (let id in nodeInfo) {
+      let onode = nodeInfo[id];
+      if (!onode['connection'])
+        continue;
+      let conns = onode['connection'].results;
+      let attrs = onode['connection'].attributeNames;
+      //QDRLog.debug("external client parent is " + parent);
+      let normalsParent = {}; // 1st normal node for this parent
+
+      for (let j = 0; j < conns.length; j++) {
+        let connection = this.QDRService.utilities.flatten(attrs, conns[j]);
+        let role = connection.role;
+        let properties = connection.properties || {};
+        let dir = connection.dir;
+        if (role == 'inter-router') {
+          let connId = connection.container;
+          let target = getContainerIndex(connId, nodeInfo, this.QDRService);
+          if (target >= 0) {
+            this.getLink(source, target, dir, '', source + '-' + target);
+          }
+        } /* else if (role == "normal" || role == "on-demand" || role === "route-container")*/ {
+          // not an connection between routers, but an external connection
+          let name = this.QDRService.utilities.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
+            };
+            //QDRLog.debug("new client pos (" + position.x + ", " + position.y + ")")
+          }// else QDRLog.debug("using previous location")
+          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 = this.QDRService.utilities.isAConsole(properties, connection.identity, role, node.key) ? 'console' : 'client';
+          let cdir = getLinkDir(id, connection, onode, this.QDRService);
+          if (existingNodeIndex >= 0) {
+            // make a link between the current router (source) and the existing node
+            this.getLink(source, existingNodeIndex, dir, 'small', connection.name);
+          } 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 (cdir === 'unknown')
+                cdir = dir;
+              node.cdir = cdir;
+              nodes.add(node);
+              // create link from original node to the new node
+              this.getLink(this.links[normalSource].source, nodes.getLength()-1, cdir, 'small', connection.name);
+              // create link from this router to the new node
+              this.getLink(source, nodes.getLength()-1, cdir, 'small', connection.name);
+              // remove the old node from the normals list
+              nodes.get(normalInfo.nodesIndex).normals.splice(normalInfo.normalsIndex, 1);
+            }
+          } else if (role === 'normal') {
+          // normal nodes can be collapsed into a single node if they are all the same dir
+            if (cdir !== 'unknown') {
+              node.user = connection.user;
+              node.isEncrypted = connection.isEncrypted;
+              node.host = connection.host;
+              node.connectionId = connection.identity;
+              node.cdir = cdir;
+              // determine arrow direction by using the link directions
+              if (!normalsParent[nodeType+cdir]) {
+                normalsParent[nodeType+cdir] = node;
+                nodes.add(node);
+                node.normals = [node];
+                // now add a link
+                this.getLink(source, nodes.getLength() - 1, cdir, 'small', connection.name);
+                client++;
+              } else {
+                normalsParent[nodeType+cdir].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', connection.name);
+            client++;
+          }
+        }
+      }
+      source++;
+    }
+    return animate;
+  }
+  clearHighlighted () {
+    for (let i = 0; i < this.links.length; ++i) {
+      this.links[i].highlighted = false;
+    }
+  }
+}
+
+var getContainerIndex = function (_id, nodeInfo, QDRService) {
+  let nodeIndex = 0;
+  for (let id in nodeInfo) {
+    if (QDRService.utilities.nameFromId(id) === _id)
+      return nodeIndex;
+    ++nodeIndex;
+  }
+  return -1;
+};
+
+var getLinkDir = function (id, connection, onode, QDRService) {
+  let links = onode['router.link'];
+  if (!links) {
+    return 'unknown';
+  }
+  let inCount = 0, outCount = 0;
+  links.results.forEach( function (linkResult) {
+    let link = QDRService.utilities.flatten(links.attributeNames, linkResult);
+    if (link.linkType === 'endpoint' && link.connectionId === connection.identity)
+      if (link.linkDir === 'in')
+        ++inCount;
+      else
+        ++outCount;
+  });
+  if (inCount > 0 && outCount > 0)
+    return 'both';
+  if (inCount > 0)
+    return 'in';
+  if (outCount > 0)
+    return 'out';
+  return 'unknown';
+};
+

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/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
new file mode 100644
index 0000000..45ef23f
--- /dev/null
+++ b/console/stand-alone/plugin/js/topology/nodes.js
@@ -0,0 +1,162 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+
+export class Node {
+  constructor(id, name, nodeType, properties, routerId, x, y, nodeIndex, resultIndex, fixed, connectionContainer) {
+    this.key = id;
+    this.name = name;
+    this.nodeType = nodeType;
+    this.properties = properties;
+    this.routerId = routerId;
+    this.x = x;
+    this.y = y;
+    this.id = nodeIndex;
+    this.resultIndex = resultIndex;
+    this.fixed = !!+fixed;
+    this.cls = '';
+    this.container = connectionContainer;
+  }
+}
+
+export class Nodes {
+  constructor(QDRService, logger) {
+    this.nodes = [];
+    this.QDRService = QDRService;
+    this.logger = logger;
+  }
+  getLength () {
+    return this.nodes.length;
+  }
+  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`);
+    return undefined;
+  }
+  setNodesFixed (name, b) {
+    this.nodes.some(function (n) {
+      if (n.name === name) {
+        n.fixed = b;
+        return true;
+      }
+    });
+  }
+  nodeFor (name) {
+    for (let i = 0; i < this.nodes.length; ++i) {
+      if (this.nodes[i].name == name)
+        return this.nodes[i];
+    }
+    return null;
+  }
+  nodeExists (connectionContainer) {
+    return this.nodes.findIndex( function (node) {
+      return node.container === connectionContainer;
+    });
+  }
+  normalExists (connectionContainer) {
+    let normalInfo = {};
+    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;
+        }))
+          break;
+      }
+    }
+    return normalInfo;
+  }
+  savePositions () {
+    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,
+      });
+    });
+  }
+  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;
+        return this.nodes[i];
+      }
+    }
+    return undefined;
+  }
+  getOrCreateNode (id, name, nodeType, nodeInfo, nodeIndex, x, y, 
+    connectionContainer, resultIndex, fixed, properties) {
+    properties = properties || {};
+    let gotNode = this.find(connectionContainer, properties, name);
+    if (gotNode) {
+      return gotNode;
+    }
+    let routerId = this.QDRService.utilities.nameFromId(id);
+    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, nodeInfo, nodeIndex, x, y, 
+    connectContainer, resultIndex, fixed, properties) {
+    let obj = this.getOrCreateNode(id, name, nodeType, nodeInfo, nodeIndex, x, y, 
+      connectContainer, resultIndex, fixed, properties);
+    this.nodes.push(obj);
+    return obj;
+  }
+  clearHighlighted () {
+    for (let i = 0; i<this.nodes.length; ++i) {
+      this.nodes[i].highlighted = false;
+    }
+  }
+  initialize (nodeInfo, localStorage, width, height) {
+    let nodeCount = Object.keys(nodeInfo).length;
+    let yInit = 50;
+    let animate = false;
+    for (let id in nodeInfo) {
+      let name = this.QDRService.utilities.nameFromId(id);
+      // if we have any new nodes, animate the force graph to position them
+      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,
+        };
+      }
+      if (position.y > height) {
+        position.y = 200 - yInit;
+        yInit *= -1;
+      }
+      this.addUsing(id, name, 'inter-router', nodeInfo, this.nodes.length, position.x, position.y, name, undefined, position.fixed, {});
+    }
+    return animate;
+  }
+
+}
+


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


[8/8] qpid-dispatch git commit: DISPATCH-1049 Add console tests

Posted by ea...@apache.org.
DISPATCH-1049 Add console tests


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

Branch: refs/heads/master
Commit: b5deb03579a7dedd81a56f32baa3e5f4b5b57136
Parents: af99754
Author: Ernest Allen <ea...@redhat.com>
Authored: Mon Jun 25 17:25:50 2018 -0400
Committer: Ernest Allen <ea...@redhat.com>
Committed: Mon Jun 25 17:25:50 2018 -0400

----------------------------------------------------------------------
 .gitignore                                      |    2 +-
 console/CMakeLists.txt                          |  207 +-
 console/stand-alone/.babelrc                    |   21 +
 console/stand-alone/gulpfile.js                 |  101 +-
 console/stand-alone/index.html                  |   11 +-
 console/stand-alone/main.js                     |  254 ++
 console/stand-alone/modules/connection.js       |  347 +++
 console/stand-alone/modules/correlator.js       |   50 +
 console/stand-alone/modules/management.js       |   63 +
 console/stand-alone/modules/topology.js         |  403 +++
 console/stand-alone/modules/utilities.js        |  115 +
 console/stand-alone/package-lock.json           | 2519 ++++++++++++++----
 console/stand-alone/package.json                |   24 +-
 console/stand-alone/plugin/css/dispatch.css     |    4 +-
 console/stand-alone/plugin/html/qdrList.html    |   53 +-
 .../plugin/html/tmplChartConfig.html            |   12 +-
 console/stand-alone/plugin/js/chord/data.js     |  234 +-
 console/stand-alone/plugin/js/chord/filters.js  |   52 +-
 .../plugin/js/chord/layout/layout.js            |    6 +-
 console/stand-alone/plugin/js/chord/matrix.js   |    3 +-
 console/stand-alone/plugin/js/chord/qdrChord.js |   24 +-
 .../plugin/js/chord/ribbon/ribbon.js            |    4 +-
 console/stand-alone/plugin/js/dispatchPlugin.js |  264 --
 .../stand-alone/plugin/js/dlgChartController.js |   31 +-
 console/stand-alone/plugin/js/navbar.js         |  140 +-
 .../stand-alone/plugin/js/posintDirective.js    |   75 +
 .../stand-alone/plugin/js/qdrChartService.js    | 1607 ++++++-----
 console/stand-alone/plugin/js/qdrCharts.js      |   33 +-
 console/stand-alone/plugin/js/qdrGlobals.js     |   58 +-
 console/stand-alone/plugin/js/qdrList.js        | 1732 ++++++------
 console/stand-alone/plugin/js/qdrListChart.js   |  146 -
 console/stand-alone/plugin/js/qdrOverview.js    |   85 +-
 .../plugin/js/qdrOverviewChartsController.js    |   18 +-
 .../plugin/js/qdrOverviewLogsController.js      |   25 +-
 console/stand-alone/plugin/js/qdrSchema.js      |   22 +-
 console/stand-alone/plugin/js/qdrService.js     |  317 +--
 console/stand-alone/plugin/js/qdrSettings.js    |   89 +-
 .../plugin/js/qdrTopAddressesController.js      |   16 +-
 console/stand-alone/plugin/js/topology/links.js |  216 ++
 console/stand-alone/plugin/js/topology/nodes.js |  162 ++
 .../plugin/js/topology/qdrTopology.js           | 2512 +++++++----------
 .../stand-alone/plugin/js/topology/topoUtils.js |  225 ++
 .../stand-alone/plugin/js/topology/traffic.js   |  445 ++++
 .../stand-alone/plugin/js/topology/traffic.ts   |  443 ---
 console/stand-alone/test/filter.js              |   73 +
 console/stand-alone/test/links.js               |   82 +
 console/stand-alone/test/matrix.js              |   51 +
 console/stand-alone/test/nodes.json             |    1 +
 console/stand-alone/test/utilities.js           |  192 ++
 console/stand-alone/tsconfig.json               |   23 +-
 console/stand-alone/vendor-js.txt               |   12 +-
 51 files changed, 8155 insertions(+), 5449 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/.gitignore
----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
index 49a9c44..d7c32e7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,7 +12,7 @@ tests/policy-1/policy-*.json
 .metadata
 .settings
 console/test/topolgies/config-*
-.history
+.history/
 .tox
 .vscode
 console/stand-alone/node_modules/

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/CMakeLists.txt
----------------------------------------------------------------------
diff --git a/console/CMakeLists.txt b/console/CMakeLists.txt
index bfd9cd8..a55b572 100644
--- a/console/CMakeLists.txt
+++ b/console/CMakeLists.txt
@@ -20,101 +20,122 @@
 ##
 ## Add cmake option to choose whether to install stand-alone console
 ##
-option(CONSOLE_INSTALL "Build and install console (requires npm)" ON)
+option(CONSOLE_INSTALL "Build and install console (requires npm 5.2+)" ON)
 
 if(CONSOLE_INSTALL)
-  find_program(NPX_EXE npx DOC "Location of the npx task runner")
-    if (NPX_EXE)
-
-      set(CONSOLE_SOURCE_DIR "${CMAKE_SOURCE_DIR}/console/stand-alone")
-      set(CONSOLE_BUILD_DIR "${CMAKE_BINARY_DIR}/console")
-
-      ## Files needed to create the ${CONSOLE_ARTIFACTS}
-      file (GLOB_RECURSE CONSOLE_JS_SOURCES ${CONSOLE_SOURCE_DIR}/plugin/js/*.js)
-      file (GLOB_RECURSE CONSOLE_TS_SOURCES ${CONSOLE_SOURCE_DIR}/plugin/js/*.ts)
-      set(CONSOLE_CSS_SOURCE ${CONSOLE_SOURCE_DIR}/plugin/css/dispatch.css)
-      set(ALL_CONSOLE_SOURCES ${CONSOLE_JS_SOURCES} ${CONSOLE_TS_SOURCES} ${CONSOLE_CSS_SOURCE})
-
-      ## Files created during the console build
-      set(CONSOLE_ARTIFACTS
-        ${CONSOLE_BUILD_DIR}/dist/js/dispatch.min.js
-        ${CONSOLE_BUILD_DIR}/dist/js/vendor.min.js
-        ${CONSOLE_BUILD_DIR}/dist/css/dispatch.min.css
-        ${CONSOLE_BUILD_DIR}/dist/css/vendor.min.css
-      )
-
-      ## copy the build config files
-      configure_file( ${CONSOLE_SOURCE_DIR}/package.json ${CONSOLE_BUILD_DIR}/ COPYONLY)
-      configure_file( ${CONSOLE_SOURCE_DIR}/package-lock.json ${CONSOLE_BUILD_DIR}/ COPYONLY)
-      configure_file( ${CONSOLE_SOURCE_DIR}/tslint.json ${CONSOLE_BUILD_DIR}/ COPYONLY)
-      configure_file( ${CONSOLE_SOURCE_DIR}/gulpfile.js ${CONSOLE_BUILD_DIR}/ COPYONLY)
-      configure_file( ${CONSOLE_SOURCE_DIR}/vendor-js.txt ${CONSOLE_BUILD_DIR}/ COPYONLY)
-      configure_file( ${CONSOLE_SOURCE_DIR}/vendor-css.txt ${CONSOLE_BUILD_DIR}/ COPYONLY)
-
-      ## Tell cmake how and when to build ${CONSOLE_ARTIFACTS}
-      add_custom_command (
-        OUTPUT ${CONSOLE_ARTIFACTS}
-        COMMENT "Running console build"
-        COMMAND npm install --loglevel=error
-        COMMAND ${NPX_EXE} gulp --src ${CONSOLE_SOURCE_DIR}
-        DEPENDS ${ALL_CONSOLE_SOURCES}
-        WORKING_DIRECTORY ${CONSOLE_BUILD_DIR}/
-        )
-
-      ## Ensure ${CONSOLE_ARTIFACTS} is built on a make when needed
-      add_custom_target(console ALL
-        DEPENDS ${CONSOLE_ARTIFACTS}
-      )
-
-      ##
-      ## Install the static and built console files
-      ##
-
-      ## Files copied to the root of the console's install dir
-      set(BASE_FILES
-        ${CONSOLE_SOURCE_DIR}/index.html
-        ${CONSOLE_SOURCE_DIR}/favicon-32x32.png
-      )
-      ## Files copied to the css/ dir
-      set(CSS_FONTS
-        ${CONSOLE_SOURCE_DIR}/plugin/css/brokers.ttf
-        ${CONSOLE_BUILD_DIR}/node_modules/angular-ui-grid/ui-grid.woff
-        ${CONSOLE_BUILD_DIR}/node_modules/angular-ui-grid/ui-grid.ttf
-      )
-      ## Files copied to the fonts/ dir
-      set(VENDOR_FONTS
-        ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-Regular-webfont.woff2
-        ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-Bold-webfont.woff2
-        ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-Light-webfont.woff2
-        ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-Semibold-webfont.woff2
-        ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-BoldItalic-webfont.woff2
-        ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-Italic-webfont.woff2
-        ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/fontawesome-webfont.woff2
-        ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/fontawesome-webfont.eot
-        ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/PatternFlyIcons-webfont.ttf
-        ${CONSOLE_BUILD_DIR}/node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2
-      )
-
-      install(DIRECTORY ${CONSOLE_BUILD_DIR}/dist/
-        DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR}
-        PATTERN "*.map" EXCLUDE
-      )
-      install(DIRECTORY ${CONSOLE_SOURCE_DIR}/plugin/html/
-        DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR}/html
-        FILES_MATCHING PATTERN "*.html"
-      )
-      install(FILES ${BASE_FILES}
-        DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR}
-      )
-      install(FILES ${CSS_FONTS}
-        DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR}/css/
-      )
-      install(FILES ${VENDOR_FONTS}
-        DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR}/fonts/
-      )
-    else(NPX_EXE)
-      message(STATUS "Cannot build console, npm not found")
-    endif(NPX_EXE)
+  find_program (NPM_EXECUTABLE npm DOC "Location of npm package manager")
+
+  if (NPM_EXECUTABLE)
+    execute_process(COMMAND ${NPM_EXECUTABLE} --version
+        OUTPUT_VARIABLE NPM_VERSION)
+    if(${NPM_VERSION} VERSION_EQUAL "5.2.0" OR ${NPM_VERSION} VERSION_GREATER "5.2.0")
+
+      find_program(NPX_EXE npx DOC "Location of the npx task runner")
+        if (NPX_EXE)
+
+          set(CONSOLE_SOURCE_DIR "${CMAKE_SOURCE_DIR}/console/stand-alone")
+          set(CONSOLE_BUILD_DIR "${CMAKE_BINARY_DIR}/console")
+
+          ## Files needed to create the ${CONSOLE_ARTIFACTS}
+          file (GLOB_RECURSE CONSOLE_JS_SOURCES ${CONSOLE_SOURCE_DIR}/plugin/js/*.js)
+          file (GLOB_RECURSE CONSOLE_TS_SOURCES ${CONSOLE_SOURCE_DIR}/plugin/js/*.ts)
+          file (GLOB_RECURSE CONSOLE_MODULE_SOURCES ${CONSOLE_SOURCE_DIR}/modules/*.js)
+          set(CONSOLE_CSS_SOURCE ${CONSOLE_SOURCE_DIR}/plugin/css/dispatch.css)
+          set(CONSOLE_MAIN ${CONSOLE_SOURCE_DIR}/main.js)
+          set(ALL_CONSOLE_SOURCES ${CONSOLE_MAIN} ${CONSOLE_MODULE_SOURCES} ${CONSOLE_JS_SOURCES} ${CONSOLE_TS_SOURCES} ${CONSOLE_CSS_SOURCE})
+
+          ## Files created during the console build
+          set(CONSOLE_ARTIFACTS
+            ${CONSOLE_BUILD_DIR}/dist/js/main.min.js
+            ${CONSOLE_BUILD_DIR}/dist/js/vendor.min.js
+            ${CONSOLE_BUILD_DIR}/dist/css/dispatch.min.css
+            ${CONSOLE_BUILD_DIR}/dist/css/vendor.min.css
+          )
+
+          ## copy the build config files
+          configure_file( ${CONSOLE_SOURCE_DIR}/package.json ${CONSOLE_BUILD_DIR}/ COPYONLY)
+          configure_file( ${CONSOLE_SOURCE_DIR}/package-lock.json ${CONSOLE_BUILD_DIR}/ COPYONLY)
+          configure_file( ${CONSOLE_SOURCE_DIR}/tslint.json ${CONSOLE_BUILD_DIR}/ COPYONLY)
+          configure_file( ${CONSOLE_SOURCE_DIR}/gulpfile.js ${CONSOLE_BUILD_DIR}/ COPYONLY)
+          configure_file( ${CONSOLE_SOURCE_DIR}/vendor-js.txt ${CONSOLE_BUILD_DIR}/ COPYONLY)
+          configure_file( ${CONSOLE_SOURCE_DIR}/vendor-css.txt ${CONSOLE_BUILD_DIR}/ COPYONLY)
+
+          ## Tell cmake how and when to build ${CONSOLE_ARTIFACTS}
+          add_custom_command (
+            OUTPUT ${CONSOLE_ARTIFACTS}
+            COMMENT "Running console build"
+            COMMAND npm install --loglevel=error
+            COMMAND ${NPX_EXE} gulp --src ${CONSOLE_SOURCE_DIR} --build "production"
+            DEPENDS ${ALL_CONSOLE_SOURCES}
+            WORKING_DIRECTORY ${CONSOLE_BUILD_DIR}/
+            )
+
+          ## Ensure ${CONSOLE_ARTIFACTS} is built on a make when needed
+          add_custom_target(console ALL
+            DEPENDS ${CONSOLE_ARTIFACTS}
+          )
+
+          ##
+          ## Install the static and built console files
+          ##
+
+          ## Files copied to the root of the console's install dir
+          set(BASE_FILES
+            ${CONSOLE_SOURCE_DIR}/index.html
+            ${CONSOLE_SOURCE_DIR}/favicon-32x32.png
+          )
+          ## Files copied to the css/ dir
+          set(CSS_FONTS
+            ${CONSOLE_SOURCE_DIR}/plugin/css/brokers.ttf
+          )
+          ## Files copied to the css/fonts/ dir
+          set(CSSFONTS_FONTS
+            ${CONSOLE_BUILD_DIR}/node_modules/angular-ui-grid/fonts/ui-grid.woff
+            ${CONSOLE_BUILD_DIR}/node_modules/angular-ui-grid/fonts/ui-grid.ttf
+          )
+          ## Files copied to the fonts/ dir
+          set(VENDOR_FONTS
+            ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-Regular-webfont.woff2
+            ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-Bold-webfont.woff2
+            ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-Light-webfont.woff2
+            ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-Semibold-webfont.woff2
+            ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-SemiboldItalic-webfont.woff2
+            ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-BoldItalic-webfont.woff2
+            ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-Italic-webfont.woff2
+            ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/fontawesome-webfont.woff2
+            ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/fontawesome-webfont.eot
+            ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/PatternFlyIcons-webfont.ttf
+            ${CONSOLE_BUILD_DIR}/node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2
+          )
+
+          install(DIRECTORY ${CONSOLE_BUILD_DIR}/dist/
+            DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR}
+            PATTERN "*.map" EXCLUDE
+          )
+          install(DIRECTORY ${CONSOLE_SOURCE_DIR}/plugin/html/
+            DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR}/html
+            FILES_MATCHING PATTERN "*.html"
+          )
+          install(FILES ${BASE_FILES}
+            DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR}
+          )
+          install(FILES ${CSS_FONTS}
+            DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR}/css/
+          )
+          install(FILES ${CSSFONTS_FONTS}
+            DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR}/css/fonts/
+          )
+          install(FILES ${VENDOR_FONTS}
+            DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR}/fonts/
+          )
+        else(NPX_EXE)
+          message(STATUS "Cannot build console, npx not found.")
+        endif(NPX_EXE)
+    else(${NPM_VERSION} VERSION_EQUAL "5.2.0" OR ${NPM_VERSION} VERSION_GREATER "5.2.0")
+      message(STATUS "Cannot build console. npm version 5.2 or greater is required.")
+    endif(${NPM_VERSION} VERSION_EQUAL "5.2.0" OR ${NPM_VERSION} VERSION_GREATER "5.2.0")
+  endif(NPM_EXECUTABLE)
+
 endif(CONSOLE_INSTALL)
 
 ##

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/.babelrc
----------------------------------------------------------------------
diff --git a/console/stand-alone/.babelrc b/console/stand-alone/.babelrc
new file mode 100644
index 0000000..d67a300
--- /dev/null
+++ b/console/stand-alone/.babelrc
@@ -0,0 +1,21 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+{
+  "presets": ["es2015"]
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/gulpfile.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/gulpfile.js b/console/stand-alone/gulpfile.js
index 46531c3..0ddaf10 100644
--- a/console/stand-alone/gulpfile.js
+++ b/console/stand-alone/gulpfile.js
@@ -19,20 +19,26 @@ under the License.
 `;
 
 const gulp = require('gulp'),
-  babel = require('gulp-babel'),
+  mocha = require('gulp-mocha'),
+  gulpif = require('gulp-if'),
+  rollup = require('rollup-stream'),
+  source = require('vinyl-source-stream'),
+  buffer = require('vinyl-buffer'),
   concat = require('gulp-concat'),
   uglify = require('gulp-uglify'),
+  terser = require('gulp-terser'),
+  babel = require('gulp-babel'),
   ngAnnotate = require('gulp-ng-annotate'),
-  rename = require('gulp-rename'),
   cleanCSS = require('gulp-clean-css'),
   del = require('del'),
   eslint = require('gulp-eslint'),
   maps = require('gulp-sourcemaps'),
   insert = require('gulp-insert'),
+  rename = require('gulp-rename'),
   fs = require('fs'),
   tsc = require('gulp-typescript'),
-  tslint = require('gulp-tslint');
-  //tsProject = tsc.createProject('tsconfig.json');
+  tslint = require('gulp-tslint'),
+  through = require('through2');
 
   // temp directory for converted typescript files
 const built_ts = 'built_ts';
@@ -59,6 +65,7 @@ const arg = (argList => {
 })(process.argv);
 
 var src = arg.src ? arg.src + '/' : '';
+var production = (arg.build === 'production');
 
 const paths = {
   typescript: {
@@ -70,10 +77,14 @@ const paths = {
     dest: 'dist/css/'
   },
   scripts: {
-    src: [src + 'plugin/js/**/*.js', built_ts + '/**/*.js'],
+    src: [src + 'plugin/js/**/*.js'],
     dest: 'dist/js/'
   }
 };
+var touch = through.obj(function(file, enc, done) {
+  var now = new Date;
+  fs.utimes(file.path, now, now, done);
+});
 
 function clean() {
   return del(['dist',built_ts ]);
@@ -110,20 +121,6 @@ function vendor_styles() {
     .pipe(gulp.dest(paths.styles.dest));
 }
 
-function scripts() {
-  return gulp.src(paths.scripts.src, { sourcemaps: true })
-    .pipe(babel({
-      presets: [require.resolve('babel-preset-env')]
-    }))
-    .pipe(ngAnnotate())
-    .pipe(maps.init())
-    .pipe(uglify())
-    .pipe(concat('dispatch.min.js'))
-    .pipe(insert.prepend(license))
-    .pipe(maps.write('./'))
-    .pipe(gulp.dest(paths.scripts.dest));
-}
-
 function vendor_scripts() {
   var vendor_lines = fs.readFileSync('vendor-js.txt').toString().split('\n');
   var vendor_files = vendor_lines.filter( function (line) {
@@ -134,7 +131,8 @@ function vendor_scripts() {
     .pipe(uglify())
     .pipe(concat('vendor.min.js'))
     .pipe(maps.write('./'))
-    .pipe(gulp.dest(paths.scripts.dest));
+    .pipe(gulp.dest(paths.scripts.dest))
+    .pipe(touch);
 }
 function watch() {
   gulp.watch(paths.scripts.src, scripts);
@@ -167,10 +165,68 @@ function ts_lint() {
     .pipe(tslint.report());
 }
 
+function scripts() {
+  return rollup({
+    input: src + './main.js',
+    sourcemap: true,
+    format: 'es'
+  }).on('error', e => {
+    console.error(`${e.stack}`);
+  })
+  
+  // point to the entry file and gives the name of the output file.
+    .pipe(source('main.min.js', src))
+  
+  // buffer the output. most gulp plugins, including gulp-sourcemaps, don't support streams.
+    .pipe(buffer())
+  
+  // tell gulp-sourcemaps to load the inline sourcemap produced by rollup-stream.
+    .pipe(maps.init({loadMaps: true}))
+  // transform the code further here.
+  /*
+    .pipe(babel(
+      {presets: [
+        ['env', {
+          targets: {
+            'browsers': [
+              'Chrome >= 52',
+              'FireFox >= 44',
+              'Safari >= 7',
+              'Explorer 11',
+              'last 4 Edge versions'
+            ]
+          },
+          useBuiltIns: true,
+          //debug: true
+        }],
+        'es2015'
+      ],
+      'ignore': [
+        'node_modules'
+      ]
+      }
+    ))
+    */
+    .pipe(ngAnnotate())
+    //.pipe(gulpif(production, uglify()))
+    .pipe(gulpif(production, terser()))
+    .pipe(gulpif(production, insert.prepend(license)))
+  // write the sourcemap alongside the output file.
+    .pipe(maps.write('.'))
+  
+  // and output to ./dist/main.js as normal.
+    .pipe(gulp.dest(paths.scripts.dest));
+}
+
+function test () {
+  return gulp.src(['test/**/*.js'], {read: false})
+    .pipe(mocha({require: ['babel-core/register'], exit: true}))
+    .on('error', console.error);
+}
+
 var build = gulp.series(
   clean,                          // removes the dist/ dir
-  gulp.parallel(lint, ts_lint),   // lints the .js, .ts files
-  typescript,                     // converts .ts to .js
+  lint,                           // lints the .js
   gulp.parallel(vendor_styles, vendor_scripts, styles, scripts), // uglify and concat
   cleanup                         // remove .js that were converted from .ts
 );
@@ -186,5 +242,6 @@ exports.tsc = typescript;
 exports.scripts = scripts;
 exports.styles = styles;
 exports.vendor = vendor;
+exports.test = test;
 
 gulp.task('default', build);

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/index.html
----------------------------------------------------------------------
diff --git a/console/stand-alone/index.html b/console/stand-alone/index.html
index 68070a3..1f08594 100644
--- a/console/stand-alone/index.html
+++ b/console/stand-alone/index.html
@@ -20,7 +20,6 @@ under the License.
 <html xmlns:ng="https://angularjs.org">
 
 <head>
-
     <meta charset="utf-8"/>
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
@@ -76,9 +75,15 @@ under the License.
     </div>
 </div>
 
+<!-- <script type="module" src="js/main.min.js"></script> -->
 <script type="text/javascript" src="js/vendor.min.js"></script>
-<script type="text/javascript" src="js/dispatch.min.js"></script>
-
+<script type="text/javascript" src="js/main.min.js"></script>
+<script defer nomodule>
+    var installError = document.getElementById('installError');
+    if (installError) {
+        installError.innerHTML = 'This browser is not supported because it does not support es-2015 modules. <a href="https://www.ecma-international.org/ecma-262/6.0/">https://www.ecma-international.org/ecma-262/6.0/</a><br/>Please use a different browser.';
+    }
+</script>
 
 <script>
     // If angular hasn't loaded a page after 1 second, display the error message

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/main.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/main.js b/console/stand-alone/main.js
new file mode 100644
index 0000000..ca69709
--- /dev/null
+++ b/console/stand-alone/main.js
@@ -0,0 +1,254 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+/* global angular d3 */
+
+/**
+ * @module QDR
+ * @main QDR
+ *
+ * The main entry point for the QDR module
+ *
+ */
+
+//import angular from 'angular';
+import { QDRLogger, QDRTemplatePath, QDR_LAST_LOCATION } from './plugin/js/qdrGlobals.js';
+import { QDRService } from './plugin/js/qdrService.js';
+import { QDRChartService } from './plugin/js/qdrChartService.js';
+import { NavBarController } from './plugin/js/navbar.js';
+import { OverviewController } from './plugin/js/qdrOverview.js';
+import { OverviewChartsController } from './plugin/js/qdrOverviewChartsController.js';
+import { OverviewLogsController } from './plugin/js/qdrOverviewLogsController.js';
+import { TopologyController } from './plugin/js/topology/qdrTopology.js';
+import { ChordController } from './plugin/js/chord/qdrChord.js';
+import { ListController } from './plugin/js/qdrList.js';
+import { TopAddressesController } from './plugin/js/qdrTopAddressesController.js';
+import { ChartDialogController } from './plugin/js/dlgChartController.js';
+import { SettingsController } from './plugin/js/qdrSettings.js';
+import { SchemaController } from './plugin/js/qdrSchema.js';
+import { ChartsController } from './plugin/js/qdrCharts.js';
+import { posint } from './plugin/js/posintDirective.js';
+
+(function(QDR) {
+
+  /**
+   * This plugin's angularjs module instance
+   */
+  QDR.module = angular.module('QDR', ['ngRoute', 'ngSanitize', 'ngResource', 'ui.bootstrap',
+    'ui.grid', 'ui.grid.selection', 'ui.grid.autoResize', 'ui.grid.resizeColumns', 'ui.grid.saveState',
+    'ui.slider', 'ui.checkbox']);
+
+  // set up the routing for this plugin
+  QDR.module.config(function($routeProvider) {
+    $routeProvider
+      .when('/', {
+        templateUrl: QDRTemplatePath + 'qdrOverview.html'
+      })
+      .when('/overview', {
+        templateUrl: QDRTemplatePath + 'qdrOverview.html'
+      })
+      .when('/topology', {
+        templateUrl: QDRTemplatePath + 'qdrTopology.html'
+      })
+      .when('/list', {
+        templateUrl: QDRTemplatePath + 'qdrList.html'
+      })
+      .when('/schema', {
+        templateUrl: QDRTemplatePath + 'qdrSchema.html'
+      })
+      .when('/charts', {
+        templateUrl: QDRTemplatePath + 'qdrCharts.html'
+      })
+      .when('/chord', {
+        templateUrl: QDRTemplatePath + 'qdrChord.html'
+      })
+      .when('/connect', {
+        templateUrl: QDRTemplatePath + 'qdrConnect.html'
+      });
+  });
+
+  QDR.module.config(function ($compileProvider) {
+    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|chrome-extension|file|blob):/);
+    $compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|mailto|chrome-extension):/);
+  });
+
+  QDR.module.filter('to_trusted', ['$sce', function($sce){
+    return function(text) {
+      return $sce.trustAsHtml(text);
+    };
+  }]);
+
+  QDR.module.filter('humanify', ['QDRService', function (QDRService) {
+    return function (input) {
+      return QDRService.utilities.humanify(input);
+    };
+  }]);
+
+  QDR.module.filter('Pascalcase', function () {
+    return function (str) {
+      if (!str)
+        return '';
+      return str.replace(/(\w)(\w*)/g,
+        function(g0,g1,g2){return g1.toUpperCase() + g2.toLowerCase();});
+    };
+  });
+
+  QDR.module.filter('safePlural', function () {
+    return function (str) {
+      var es = ['x', 'ch', 'ss', 'sh'];
+      for (var i=0; i<es.length; ++i) {
+        if (str.endsWith(es[i]))
+          return str + 'es';
+      }
+      if (str.endsWith('y'))
+        return str.substr(0, str.length-2) + 'ies';
+      if (str.endsWith('s'))
+        return str;
+      return str + 's';
+    };
+  });
+
+  QDR.module.filter('pretty', function () {
+    return function (str) {
+      var formatComma = d3.format(',');
+      if (!isNaN(parseFloat(str)) && isFinite(str))
+        return formatComma(str);
+      return str;
+    };
+  });
+
+  // one-time initialization happens in the run function
+  // of our module
+  QDR.module.run( ['$rootScope', '$route', '$timeout', '$location', '$log', 'QDRService', 'QDRChartService',  function ($rootScope, $route, $timeout, $location, $log, QDRService, QDRChartService) {
+    let QDRLog = new QDRLogger($log, 'main');
+    QDRLog.info('************* creating Dispatch Console ************');
+
+    var curPath = $location.path();
+    var org = curPath.substr(1);
+    if (org && org.length > 0 && org !== 'connect') {
+      $location.search('org', org);
+    } else {
+      $location.search('org', null);
+    }
+    QDR.queue = d3.queue;
+
+    if (!QDRService.management.connection.is_connected()) {
+      // attempt to connect to the host:port that served this page
+      var host = $location.host();
+      var port = $location.port();
+      var search = $location.search();
+      if (search.org) {
+        if (search.org === 'connect')
+          $location.search('org', 'overview');
+      }
+      var connectOptions = {address: host, port: port};
+      QDRLog.info('Attempting AMQP over websockets connection using address:port of browser ('+host+':'+port+')');
+      QDRService.management.connection.testConnect(connectOptions)
+        .then( function () {
+          // We didn't connect with reconnect: true flag.
+          // The reason being that if we used reconnect:true and the connection failed, rhea would keep trying. There
+          // doesn't appear to be a way to tell it to stop trying to reconnect.
+          QDRService.disconnect();
+          QDRLog.info('Connect succeeded. Using address:port of browser');
+          connectOptions.reconnect = true;
+          // complete the connection (create the sender/receiver)
+          QDRService.connect(connectOptions)
+            .then( function () {
+            // register a callback for when the node list is available (needed for loading saved charts)
+              QDRService.management.topology.addUpdatedAction('initChartService', function() {
+                QDRService.management.topology.delUpdatedAction('initChartService');
+                QDRChartService.init(); // initialize charting service after we are connected
+              });
+              // get the list of nodes
+              QDRService.management.topology.startUpdating(false);
+            });
+        }, function () {
+          QDRLog.info('failed to auto-connect to ' + host + ':' + port);
+          QDRLog.info('redirecting to connect page');
+          $timeout(function () {
+            $location.path('/connect');
+            $location.search('org', org);
+            $location.replace();
+          });
+        });
+    }
+
+    $rootScope.$on('$routeChangeSuccess', function() {
+      var path = $location.path();
+      if (path !== '/connect') {
+        localStorage[QDR_LAST_LOCATION] = path;
+      }
+    });
+  }]);
+
+  QDR.module.controller ('QDR.MainController', ['$scope', '$log', '$location', function ($scope, $log, $location) {
+    let QDRLog = new QDRLogger($log, 'MainController');
+    QDRLog.debug('started QDR.MainController with location.url: ' + $location.url());
+    QDRLog.debug('started QDR.MainController with window.location.pathname : ' + window.location.pathname);
+    $scope.topLevelTabs = [];
+    $scope.topLevelTabs.push({
+      id: 'qdr',
+      content: 'Qpid Dispatch Router Console',
+      title: 'Dispatch Router Console',
+      isValid: function() { return true; },
+      href: function() { return '#connect'; },
+      isActive: function() { return true; }
+    });
+  }]);
+
+  QDR.module.controller ('QDR.Core', function ($scope, $rootScope) {
+    $scope.alerts = [];
+    $scope.breadcrumb = {};
+    $scope.closeAlert = function(index) {
+      $scope.alerts.splice(index, 1);
+    };
+    $scope.$on('setCrumb', function(event, data) {
+      $scope.breadcrumb = data;
+    });
+    $scope.$on('newAlert', function(event, data) {
+      $scope.alerts.push(data);
+      $scope.$apply();
+    });
+    $scope.$on('clearAlerts', function () {
+      $scope.alerts = [];
+      $scope.$apply();
+    });
+    $scope.pageMenuClicked = function () {
+      $rootScope.$broadcast('pageMenuClicked');
+    };
+  });
+
+  QDR.module.controller('QDR.NavBarController', NavBarController);
+  QDR.module.controller('QDR.OverviewController', OverviewController);
+  QDR.module.controller('QDR.OverviewChartsController', OverviewChartsController);
+  QDR.module.controller('QDR.OverviewLogsController', OverviewLogsController);
+  QDR.module.controller('QDR.TopAddressesController', TopAddressesController);
+  QDR.module.controller('QDR.ChartDialogController', ChartDialogController);
+  QDR.module.controller('QDR.SettingsController', SettingsController);
+  QDR.module.controller('QDR.TopologyController', TopologyController);
+  QDR.module.controller('QDR.ChordController', ChordController);
+  QDR.module.controller('QDR.ListController', ListController);
+  QDR.module.controller('QDR.SchemaController', SchemaController);
+  QDR.module.controller('QDR.ChartsController', ChartsController);
+  
+  QDR.module.service('QDRService', QDRService);
+  QDR.module.service('QDRChartService', QDRChartService);
+  QDR.module.directive('posint', posint);
+  //  .directive('exampleDirective', () => new ExampleDirective);
+}({}));
+

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/modules/connection.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/modules/connection.js b/console/stand-alone/modules/connection.js
new file mode 100644
index 0000000..db21e01
--- /dev/null
+++ b/console/stand-alone/modules/connection.js
@@ -0,0 +1,347 @@
+/*
+ * Copyright 2017 Red Hat Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* global Promise */
+
+const rhea = require('rhea');
+//import { on, websocket_connect, removeListener, once, connect } from 'rhea';
+import Correlator from './correlator.js';
+
+class ConnectionManager {
+  constructor(protocol) {
+    this.sender = undefined;
+    this.receiver = undefined;
+    this.connection = undefined;
+    this.version = undefined;
+    this.errorText = undefined;
+    this.protocol = protocol;
+    this.schema = undefined;
+    this.connectActions = [];
+    this.disconnectActions = [];
+    this.correlator = new Correlator();
+    this.on_message = (function (context) {
+      this.correlator.resolve(context);
+    }).bind(this);
+    this.on_disconnected = (function () {
+      this.errorText = 'Disconnected';
+      this.executeDisconnectActions(this.errorText);
+    }).bind(this);
+    this.on_connection_open = (function () {
+      this.executeConnectActions();
+    }).bind(this);
+  }
+  versionCheck(minVer) {
+    var verparts = this.version.split('.');
+    var minparts = minVer.split('.');
+    try {
+      for (var i = 0; i < minparts.length; ++i) {
+        if (parseInt(minVer[i] > parseInt(verparts[i])))
+          return false;
+      }
+    }
+    catch (e) {
+      return false;
+    }
+    return true;
+  }
+  addConnectAction(action) {
+    if (typeof action === 'function') {
+      this.delConnectAction(action);
+      this.connectActions.push(action);
+    }
+  }
+  addDisconnectAction(action) {
+    if (typeof action === 'function') {
+      this.delDisconnectAction(action);
+      this.disconnectActions.push(action);
+    }
+  }
+  delConnectAction(action) {
+    if (typeof action === 'function') {
+      var index = this.connectActions.indexOf(action);
+      if (index >= 0)
+        this.connectActions.splice(index, 1);
+    }
+  }
+  delDisconnectAction(action) {
+    if (typeof action === 'function') {
+      var index = this.disconnectActions.indexOf(action);
+      if (index >= 0)
+        this.disconnectActions.splice(index, 1);
+    }
+  }
+  executeConnectActions() {
+    this.connectActions.forEach(function (action) {
+      try {
+        action();
+      }
+      catch (e) {
+        // in case the page that registered the handler has been unloaded
+      }
+    });
+    this.connectActions = [];
+  }
+  executeDisconnectActions(message) {
+    this.disconnectActions.forEach(function (action) {
+      try {
+        action(message);
+      }
+      catch (e) {
+        // in case the page that registered the handler has been unloaded
+      }
+    });
+    this.disconnectActions = [];
+  }
+  on(eventType, fn) {
+    if (eventType === 'connected') {
+      this.addConnectAction(fn);
+    }
+    else if (eventType === 'disconnected') {
+      this.addDisconnectAction(fn);
+    }
+    else {
+      console.log('unknown event type ' + eventType);
+    }
+  }
+  setSchema(schema) {
+    this.schema = schema;
+  }
+  is_connected() {
+    return this.connection &&
+      this.sender &&
+      this.receiver &&
+      this.receiver.remote &&
+      this.receiver.remote.attach &&
+      this.receiver.remote.attach.source &&
+      this.receiver.remote.attach.source.address &&
+      this.connection.is_open();
+  }
+  disconnect() {
+    if (this.sender)
+      this.sender.close();
+    if (this.receiver)
+      this.receiver.close();
+    if (this.connection)
+      this.connection.close();
+  }
+  createSenderReceiver(options) {
+    return new Promise((function (resolve, reject) {
+      var timeout = options.timeout || 10000;
+      // set a timer in case the setup takes too long
+      var giveUp = (function () {
+        this.connection.removeListener('receiver_open', receiver_open);
+        this.connection.removeListener('sendable', sendable);
+        this.errorText = 'timed out creating senders and receivers';
+        reject(Error(this.errorText));
+      }).bind(this);
+      var timer = setTimeout(giveUp, timeout);
+      // register an event hander for when the setup is complete
+      var sendable = (function (context) {
+        clearTimeout(timer);
+        this.version = this.connection.properties ? this.connection.properties.version : '0.1.0';
+        // in case this connection dies
+        rhea.on('disconnected', this.on_disconnected);
+        // in case this connection dies and is then reconnected automatically
+        rhea.on('connection_open', this.on_connection_open);
+        // receive messages here
+        this.connection.on('message', this.on_message);
+        resolve(context);
+      }).bind(this);
+      this.connection.once('sendable', sendable);
+      // Now actually createt the sender and receiver.
+      // register an event handler for when the receiver opens
+      var receiver_open = (function () {
+        // once the receiver is open, create the sender
+        if (options.sender_address)
+          this.sender = this.connection.open_sender(options.sender_address);
+        else
+          this.sender = this.connection.open_sender();
+      }).bind(this);
+      this.connection.once('receiver_open', receiver_open);
+      // create a dynamic receiver
+      this.receiver = this.connection.open_receiver({ source: { dynamic: true } });
+    }).bind(this));
+  }
+  connect(options) {
+    return new Promise((function (resolve, reject) {
+      var finishConnecting = function () {
+        this.createSenderReceiver(options)
+          .then(function (results) {
+            resolve(results);
+          }, function (error) {
+            reject(error);
+          });
+      };
+      if (!this.connection) {
+        options.test = false; // if you didn't want a connection, you should have called testConnect() and not connect()
+        this.testConnect(options)
+          .then((function () {
+            finishConnecting.call(this);
+          }).bind(this), (function () {
+            // connect failed or timed out
+            this.errorText = 'Unable to connect';
+            this.executeDisconnectActions(this.errorText);
+            reject(Error(this.errorText));
+          }).bind(this));
+      }
+      else {
+        finishConnecting.call(this);
+      }
+    }).bind(this));
+  }
+  getReceiverAddress() {
+    return this.receiver.remote.attach.source.address;
+  }
+  // Try to connect using the options.
+  // if options.test === true -> close the connection if it succeeded and resolve the promise
+  // if the connection attempt fails or times out, reject the promise regardless of options.test
+  testConnect(options, callback) {
+    return new Promise((function (resolve, reject) {
+      var timeout = options.timeout || 10000;
+      var reconnect = options.reconnect || false; // in case options.reconnect is undefined
+      var baseAddress = options.address + ':' + options.port;
+      if (options.linkRouteAddress) {
+        baseAddress += ('/' + options.linkRouteAddress);
+      }
+      var wsprotocol = location.protocol === 'https:' ? 'wss' : 'ws';
+      if (this.connection) {
+        delete this.connection;
+        this.connection = null;
+      }
+      var ws = rhea.websocket_connect(WebSocket);
+      var c = {
+        connection_details: new ws(wsprotocol + '://' + baseAddress, ['binary']),
+        reconnect: reconnect,
+        properties: options.properties || { console_identifier: 'Dispatch console' }
+      };
+      if (options.hostname)
+        c.hostname = options.hostname;
+      if (options.username && options.username !== '') {
+        c.username = options.username;
+      }
+      if (options.password && options.password !== '') {
+        c.password = options.password;
+      }
+      // set a timeout
+      var disconnected = (function () {
+        clearTimeout(timer);
+        rhea.removeListener('disconnected', disconnected);
+        rhea.removeListener('connection_open', connection_open);
+        this.connection = null;
+        var rej = 'failed to connect';
+        if (callback)
+          callback({ error: rej });
+        reject(Error(rej));
+      }).bind(this);
+      var timer = setTimeout(disconnected, timeout);
+      // the event handler for when the connection opens
+      var connection_open = (function (context) {
+        clearTimeout(timer);
+        // prevent future disconnects from calling reject
+        rhea.removeListener('disconnected', disconnected);
+        // we were just checking. we don't really want a connection
+        if (options.test) {
+          context.connection.close();
+          this.connection = null;
+        }
+        else
+          this.on_connection_open();
+        var res = { context: context };
+        if (callback)
+          callback(res);
+        resolve(res);
+      }).bind(this);
+      // register an event handler for when the connection opens
+      rhea.once('connection_open', connection_open);
+      // register an event handler for if the connection fails to open
+      rhea.once('disconnected', disconnected);
+      // attempt the connection
+      this.connection = rhea.connect(c);
+    }).bind(this));
+  }
+  sendMgmtQuery(operation) {
+    return this.send([], '/$management', operation);
+  }
+  sendQuery(toAddr, entity, attrs, operation) {
+    operation = operation || 'QUERY';
+    var fullAddr = this._fullAddr(toAddr);
+    var body = { attributeNames: attrs || [] };
+    return this.send(body, fullAddr, operation, this.schema.entityTypes[entity].fullyQualifiedType);
+  }
+  send(body, to, operation, entityType) {
+    var application_properties = {
+      operation: operation,
+      type: 'org.amqp.management',
+      name: 'self'
+    };
+    if (entityType)
+      application_properties.entityType = entityType;
+    return this._send(body, to, application_properties);
+  }
+  sendMethod(toAddr, entity, attrs, operation, props) {
+    var fullAddr = this._fullAddr(toAddr);
+    var application_properties = {
+      operation: operation,
+    };
+    if (entity) {
+      application_properties.type = this.schema.entityTypes[entity].fullyQualifiedType;
+    }
+    if (attrs.name)
+      application_properties.name = attrs.name;
+    else if (attrs.identity)
+      application_properties.identity = attrs.identity;
+    if (props) {
+      for (var attrname in props) {
+        application_properties[attrname] = props[attrname];
+      }
+    }
+    return this._send(attrs, fullAddr, application_properties);
+  }
+  _send(body, to, application_properties) {
+    var _correlationId = this.correlator.corr();
+    var self = this;
+    return new Promise(function (resolve, reject) {
+      self.correlator.register(_correlationId, resolve, reject);
+      self.sender.send({
+        body: body,
+        to: to,
+        reply_to: self.receiver.remote.attach.source.address,
+        correlation_id: _correlationId,
+        application_properties: application_properties
+      });
+    });
+  }
+  _fullAddr(toAddr) {
+    var toAddrParts = toAddr.split('/');
+    toAddrParts.shift();
+    var fullAddr = toAddrParts.join('/');
+    return fullAddr;
+  }
+  availableQeueuDepth() {
+    return this.correlator.depth();
+  }
+}
+
+class ConnectionException {
+  constructor(message) {
+    this.message = message;
+    this.name = 'ConnectionException';
+  }
+}
+
+const _ConnectionManager = ConnectionManager;
+export { _ConnectionManager as ConnectionManager };
+const _ConnectionException = ConnectionException;
+export { _ConnectionException as ConnectionException };

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/modules/correlator.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/modules/correlator.js b/console/stand-alone/modules/correlator.js
new file mode 100644
index 0000000..bf34f93
--- /dev/null
+++ b/console/stand-alone/modules/correlator.js
@@ -0,0 +1,50 @@
+/*
+  * Copyright 2017 Red Hat Inc.
+  *
+  * Licensed under the Apache License, Version 2.0 (the "License");
+  * you may not use this file except in compliance with the License.
+  * You may obtain a copy of the License at
+  *
+  *     http://www.apache.org/licenses/LICENSE-2.0
+  *
+  * Unless required by applicable law or agreed to in writing, software
+  * distributed under the License is distributed on an "AS IS" BASIS,
+  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  * See the License for the specific language governing permissions and
+  * limitations under the License.
+  */
+
+import { utils } from './utilities.js';
+
+class Correlator {
+  constructor() {
+    this._objects = {};
+    this._correlationID = 0;
+    this.maxCorrelatorDepth = 10;
+  }
+  corr() {
+    return ++(this._correlationID) + '';
+  }
+  // Associate this correlation id with the promise's resolve and reject methods
+  register(id, resolve, reject) {
+    this._objects[id] = { resolver: resolve, rejector: reject };
+  }
+  // Call the promise's resolve method.
+  // This is called by rhea's receiver.on('message') function
+  resolve(context) {
+    var correlationID = context.message.correlation_id;
+    // call the promise's resolve function with a copy of the rhea response (so we don't keep any references to internal rhea data)
+    this._objects[correlationID].resolver({ response: utils.copy(context.message.body), context: context });
+    delete this._objects[correlationID];
+  }
+  reject(id, error) {
+    this._objects[id].rejector(error);
+    delete this._objects[id];
+  }
+  // Return the number of requests that can be sent before we start queuing requests
+  depth() {
+    return Math.max(1, this.maxCorrelatorDepth - Object.keys(this._objects).length);
+  }
+}
+
+export default Correlator;

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/modules/management.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/modules/management.js b/console/stand-alone/modules/management.js
new file mode 100644
index 0000000..4b3bb32
--- /dev/null
+++ b/console/stand-alone/modules/management.js
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2015 Red Hat Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* global Promise */
+
+import { ConnectionManager } from './connection.js';
+import Topology from './topology.js';
+
+export class Management {
+  constructor(protocol) {
+    this.connection = new ConnectionManager(protocol);
+    this.topology = new Topology(this.connection);
+  }
+  getSchema(callback) {
+    var self = this;
+    return new Promise(function (resolve, reject) {
+      self.connection.sendMgmtQuery('GET-SCHEMA')
+        .then(function (responseAndContext) {
+          var response = responseAndContext.response;
+          for (var entityName in response.entityTypes) {
+            var entity = response.entityTypes[entityName];
+            if (entity.deprecated) {
+              // deprecated entity
+              delete response.entityTypes[entityName];
+            }
+            else {
+              for (var attributeName in entity.attributes) {
+                var attribute = entity.attributes[attributeName];
+                if (attribute.deprecated) {
+                  // deprecated attribute
+                  delete response.entityTypes[entityName].attributes[attributeName];
+                }
+              }
+            }
+          }
+          self.connection.setSchema(response);
+          if (callback)
+            callback(response);
+          resolve(response);
+        }, function (error) {
+          if (callback)
+            callback(error);
+          reject(error);
+        });
+    });
+  }
+  schema() {
+    return this.connection.schema;
+  }
+}

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/modules/topology.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/modules/topology.js b/console/stand-alone/modules/topology.js
new file mode 100644
index 0000000..e208a6f
--- /dev/null
+++ b/console/stand-alone/modules/topology.js
@@ -0,0 +1,403 @@
+/*
+ * Copyright 2015 Red Hat Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* global Promise d3 */
+
+import { utils } from './utilities.js';
+
+class Topology {
+  constructor(connectionManager) {
+    this.connection = connectionManager;
+    this.updatedActions = {};
+    this.entities = []; // which entities to request each topology update
+    this.entityAttribs = {};
+    this._nodeInfo = {}; // info about all known nodes and entities
+    this.filtering = false; // filter out nodes that don't have connection info
+    this.timeout = 5000;
+    this.updateInterval = 5000;
+    this._getTimer = null;
+    this.updating = false;
+  }
+  addUpdatedAction(key, action) {
+    if (typeof action === 'function') {
+      this.updatedActions[key] = action;
+    }
+  }
+  delUpdatedAction(key) {
+    if (key in this.updatedActions)
+      delete this.updatedActions[key];
+  }
+  executeUpdatedActions(error) {
+    for (var action in this.updatedActions) {
+      this.updatedActions[action].apply(this, [error]);
+    }
+  }
+  setUpdateEntities(entities) {
+    this.entities = entities;
+    for (var i = 0; i < entities.length; i++) {
+      this.entityAttribs[entities[i]] = [];
+    }
+  }
+  addUpdateEntities(entityAttribs) {
+    if (Object.prototype.toString.call(entityAttribs) !== '[object Array]') {
+      entityAttribs = [entityAttribs];
+    }
+    for (var i = 0; i < entityAttribs.length; i++) {
+      var entity = entityAttribs[i].entity;
+      this.entityAttribs[entity] = entityAttribs[i].attrs || [];
+    }
+  }
+  on(eventName, fn, key) {
+    if (eventName === 'updated')
+      this.addUpdatedAction(key, fn);
+  }
+  unregister(eventName, key) {
+    if (eventName === 'updated')
+      this.delUpdatedAction(key);
+  }
+  nodeInfo() {
+    return this._nodeInfo;
+  }
+  get() {
+    return new Promise((function (resolve, reject) {
+      this.connection.sendMgmtQuery('GET-MGMT-NODES')
+        .then((function (response) {
+          response = response.response;
+          if (Object.prototype.toString.call(response) === '[object Array]') {
+            var workInfo = {};
+            // if there is only one node, it will not be returned
+            if (response.length === 0) {
+              var parts = this.connection.getReceiverAddress().split('/');
+              parts[parts.length - 1] = '$management';
+              response.push(parts.join('/'));
+            }
+            for (var i = 0; i < response.length; ++i) {
+              workInfo[response[i]] = {};
+            }
+            var gotResponse = function (nodeName, entity, response) {
+              workInfo[nodeName][entity] = response;
+            };
+            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);
+              }
+            }
+            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];
+                  }
+                }
+              }
+              this._nodeInfo = utils.copy(workInfo);
+              this.onDone(this._nodeInfo);
+              resolve(this._nodeInfo);
+            }).bind(this));
+          }
+        }).bind(this), function (error) {
+          reject(error);
+        });
+    }).bind(this));
+  }
+  onDone(result) {
+    clearTimeout(this._getTimer);
+    if (this.updating)
+      this._getTimer = setTimeout((this.get).bind(this), this.updateInterval);
+    this.executeUpdatedActions(result);
+  }
+  startUpdating(filter) {
+    this.stopUpdating();
+    this.updating = true;
+    this.filtering = filter;
+    this.get();
+  }
+  stopUpdating() {
+    this.updating = false;
+    if (this._getTimer) {
+      clearTimeout(this._getTimer);
+      this._getTimer = null;
+    }
+  }
+  fetchEntity(node, entity, attrs, callback) {
+    var results = {};
+    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 () {
+      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) {
+      heartbeat(nodeName, dotentity, response);
+      callback(null);
+    });
+  }
+  // get all the requested entities/attributes for a single router
+  fetchEntities(node, entityAttribs, doneCallback, resultCallback) {
+    var q = d3.queue(this.connection.availableQeueuDepth());
+    var results = {};
+    if (!resultCallback) {
+      resultCallback = function (nodeName, dotentity, response) {
+        if (!results[nodeName])
+          results[nodeName] = {};
+        results[nodeName][dotentity] = response;
+      };
+    }
+    var gotAResponse = function (nodeName, dotentity, response) {
+      resultCallback(nodeName, dotentity, response);
+    };
+    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.await(function () {
+      doneCallback(results);
+    });
+  }
+  // get all the requested entities for all known routers
+  fetchAllEntities(entityAttribs, doneCallback, resultCallback) {
+    var q = d3.queue(this.connection.availableQeueuDepth());
+    var results = {};
+    if (!resultCallback) {
+      resultCallback = function (nodeName, dotentity, response) {
+        if (!results[nodeName])
+          results[nodeName] = {};
+        results[nodeName][dotentity] = response;
+      };
+    }
+    var gotAResponse = function (nodeName, dotentity, response) {
+      resultCallback(nodeName, dotentity, response);
+    };
+    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.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);
+  }
+  // ensure these nodes have all these entities. don't fetch unless forced to
+  ensureEntities(nodes, entityAttribs, callback, extra) {
+    if (Object.prototype.toString.call(entityAttribs) !== '[object Array]') {
+      entityAttribs = [entityAttribs];
+    }
+    if (Object.prototype.toString.call(nodes) !== '[object Array]') {
+      nodes = [nodes];
+    }
+    this.addUpdateEntities(entityAttribs);
+    var q = d3.queue(this.connection.availableQeueuDepth());
+    for (var n = 0; n < nodes.length; ++n) {
+      for (var i = 0; i < entityAttribs.length; ++i) {
+        var ea = entityAttribs[i];
+        // if we don'e already have the entity or we want to force a refresh
+        if (!this._nodeInfo[nodes[n]][ea.entity] || ea.force)
+          q.defer((this.q_ensureNodeInfo).bind(this), nodes[n], ea.entity, ea.attrs || [], q);
+      }
+    }
+    q.await(function () {
+      callback(extra);
+    });
+  }
+  addNodeInfo(id, entity, values) {
+    // save the results in the nodeInfo object
+    if (id) {
+      if (!(id in this._nodeInfo)) {
+        this._nodeInfo[id] = {};
+      }
+      // copy the values to allow garbage collection
+      this._nodeInfo[id][entity] = values;
+    }
+  }
+  isLargeNetwork() {
+    return Object.keys(this._nodeInfo).length >= 12;
+  }
+  getConnForLink(link) {
+    // find the connection for this link
+    var conns = this._nodeInfo[link.nodeId].connection;
+    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]);
+  }
+  nodeNameList() {
+    var nl = [];
+    for (var id in this._nodeInfo) {
+      nl.push(utils.nameFromId(id));
+    }
+    return nl.sort();
+  }
+  nodeIdList() {
+    var nl = [];
+    for (var id in this._nodeInfo) {
+      //if (this._nodeInfo['connection'])
+      nl.push(id);
+    }
+    return nl.sort();
+  }
+  nodeList() {
+    var nl = [];
+    for (var id in this._nodeInfo) {
+      nl.push({
+        name: utils.nameFromId(id),
+        id: id
+      });
+    }
+    return nl;
+  }
+  // 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));
+    return {
+      abort: function () {
+        delete this._nodeInfo[nodeId];
+      }
+    };
+  }
+  getNodeInfo(nodeName, entity, attrs, q, callback) {
+    var timedOut = function (q) {
+      q.abort();
+    };
+    var atimer = setTimeout(timedOut, this.timeout, q);
+    this.connection.sendQuery(nodeName, entity, attrs)
+      .then(function (response) {
+        clearTimeout(atimer);
+        callback(nodeName, entity, response.response);
+      }, function () {
+        q.abort();
+      });
+  }
+  getMultipleNodeInfo(nodeNames, entity, attrs, callback, selectedNodeId, aggregate) {
+    var self = this;
+    if (typeof aggregate === 'undefined')
+      aggregate = true;
+    var responses = {};
+    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);
+    });
+    q.await(function () {
+      if (aggregate)
+        self.aggregateNodeInfo(nodeNames, entity, selectedNodeId, responses, callback);
+      else {
+        callback(nodeNames, entity, responses);
+      }
+    });
+  }
+  quiesceLink(nodeId, name) {
+    var attributes = {
+      adminStatus: 'disabled',
+      name: name
+    };
+    return this.connection.sendMethod(nodeId, 'router.link', attributes, 'UPDATE');
+  }
+  aggregateNodeInfo(nodeNames, entity, selectedNodeId, responses, callback) {
+    // aggregate the responses
+    var self = this;
+    var newResponse = {};
+    var thisNode = responses[selectedNodeId];
+    newResponse.attributeNames = thisNode.attributeNames;
+    newResponse.results = thisNode.results;
+    newResponse.aggregates = [];
+    // initialize the aggregates
+    for (var i = 0; i < thisNode.results.length; ++i) {
+      // there is a result for each unique entity found (ie addresses, links, etc.)
+      var result = thisNode.results[i];
+      var vals = [];
+      // there is a val for each attribute in this entity
+      result.forEach(function (val) {
+        vals.push({
+          sum: val,
+          detail: []
+        });
+      });
+      newResponse.aggregates.push(vals);
+    }
+    var nameIndex = thisNode.attributeNames.indexOf('name');
+    var ent = self.connection.schema.entityTypes[entity];
+    var ids = Object.keys(responses);
+    ids.sort();
+    ids.forEach(function (id) {
+      var response = responses[id];
+      var results = response.results;
+      results.forEach(function (result) {
+        // find the matching result in the aggregates
+        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) {
+              if (ent.attributes[key] && ent.attributes[key].graph) {
+                if (id != selectedNodeId)
+                  aggregate[i].sum += result[i];
+              }
+              aggregate[i].detail.push({
+                node: utils.nameFromId(id) + ':',
+                val: result[i]
+              });
+            });
+            return true; // stop looping
+          }
+          return false; // continute looking for the aggregate record
+        });
+        if (!found) {
+          // this attribute was not found in the aggregates yet
+          // because it was not in the selectedNodeId's results
+          var vals = [];
+          result.forEach(function (val) {
+            vals.push({
+              sum: val,
+              detail: [{
+                node: utils.nameFromId(id),
+                val: val
+              }]
+            });
+          });
+          newResponse.aggregates.push(vals);
+        }
+      });
+    });
+    callback(nodeNames, entity, newResponse);
+  }
+}
+
+export default Topology;
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/modules/utilities.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/modules/utilities.js b/console/stand-alone/modules/utilities.js
new file mode 100644
index 0000000..328da38
--- /dev/null
+++ b/console/stand-alone/modules/utilities.js
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2015 Red Hat Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* global d3 */
+var ddd = typeof window === 'undefined' ? require ('d3') : d3;
+
+var utils = {
+  isAConsole: function (properties, connectionId, nodeType, key) {
+    return this.isConsole({
+      properties: properties,
+      connectionId: connectionId,
+      nodeType: nodeType,
+      key: key
+    });
+  },
+  isConsole: function (d) {
+    return (d && d.properties && d.properties.console_identifier === 'Dispatch console');
+  },
+  isArtemis: function (d) {
+    return (d.nodeType === 'route-container' || d.nodeType === 'on-demand') && (d.properties && d.properties.product === 'apache-activemq-artemis');
+  },
+
+  isQpid: function (d) {
+    return (d.nodeType === 'route-container' || d.nodeType === 'on-demand') && (d.properties && d.properties.product === 'qpid-cpp');
+  },
+  flatten: function (attributes, result) {
+    if (!attributes || !result)
+      return {};
+    var flat = {};
+    attributes.forEach(function(attr, i) {
+      if (result && result.length > i)
+        flat[attr] = result[i];
+    });
+    return flat;
+  },
+  copy: function (obj) {
+    if (obj)
+      return JSON.parse(JSON.stringify(obj));
+  },
+  identity_clean: function (identity) {
+    if (!identity)
+      return '-';
+    var pos = identity.indexOf('/');
+    if (pos >= 0)
+      return identity.substring(pos + 1);
+    return identity;
+  },
+  addr_text: function (addr) {
+    if (!addr)
+      return '-';
+    if (addr[0] == 'M')
+      return addr.substring(2);
+    else
+      return addr.substring(1);
+  },
+  addr_class: function (addr) {
+    if (!addr) return '-';
+    if (addr[0] == 'M') return 'mobile';
+    if (addr[0] == 'R') return 'router';
+    if (addr[0] == 'A') return 'area';
+    if (addr[0] == 'L') return 'local';
+    if (addr[0] == 'C') return 'link-incoming';
+    if (addr[0] == 'E') return 'link-incoming';
+    if (addr[0] == 'D') return 'link-outgoing';
+    if (addr[0] == 'F') return 'link-outgoing';
+    if (addr[0] == 'T') return 'topo';
+    return 'unknown: ' + addr[0];
+  },
+  humanify: function (s) {
+    if (!s || s.length === 0)
+      return s;
+    var t = s.charAt(0).toUpperCase() + s.substr(1).replace(/[A-Z]/g, ' $&');
+    return t.replace('.', ' ');
+  },
+  pretty: function (v) {
+    var formatComma = ddd.format(',');
+    if (!isNaN(parseFloat(v)) && isFinite(v))
+      return formatComma(v);
+    return v;
+  },
+  isMSIE: function () {
+    return (document.documentMode || /Edge/.test(navigator.userAgent));
+  },
+  valFor: function (aAr, vAr, key) {
+    var idx = aAr.indexOf(key);
+    if ((idx > -1) && (idx < vAr.length)) {
+      return vAr[idx];
+    }
+    return null;
+  },
+  // extract the name of the router from the router id
+  nameFromId: function (id) {
+    // the router id looks like 'amqp:/topo/0/routerName/$managemrnt'
+    var parts = id.split('/');
+    // handle cases where the router name contains a /
+    parts.splice(0, 3); // remove amqp, topo, 0
+    parts.pop(); // remove $management
+    return parts.join('/');
+  }
+  
+};
+export { utils };
\ 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


[6/8] qpid-dispatch git commit: DISPATCH-1049 Add console tests

Posted by ea...@apache.org.
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/package.json
----------------------------------------------------------------------
diff --git a/console/stand-alone/package.json b/console/stand-alone/package.json
index aa59f68..aad443e 100644
--- a/console/stand-alone/package.json
+++ b/console/stand-alone/package.json
@@ -4,7 +4,8 @@
   "description": "Qpid Dispatch Router stand-alone console",
   "main": "index.html",
   "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1"
+    "test": "mocha --require babel-core/register",
+    "tsc": "tsc"
   },
   "repository": {
     "type": "git",
@@ -31,15 +32,13 @@
     "angular-ui-bootstrap": "^2.5.6",
     "angular-ui-grid": "^4.0.8",
     "angular-ui-slider": "^0.4.0",
-    "bluebird": "^3.5.1",
+    "babel-polyfill": "^6.26.0",
     "bootstrap": "^3.3.7",
     "c3": "^0.4.18",
     "d3": "^3.5.14",
     "d3-path": "^1.0.5",
     "d3-queue": "^3.0.7",
     "d3-time-format": "^2.1.1",
-    "dispatch-management": "~0.1.21",
-    "html5shiv": "^3.7.3",
     "jquery": "^3.2.1",
     "jquery-ui-dist": "^1.12.1",
     "jquery.fancytree": "^2.26.0",
@@ -48,23 +47,38 @@
     "rhea": "^0.2.13"
   },
   "devDependencies": {
+    "@types/mocha": "^5.2.2",
+    "@types/node": "^10.3.3",
     "babel-core": "^6.26.3",
     "babel-preset-env": "^1.7.0",
+    "babel-preset-es2015": "^6.24.1",
+    "browserify": "^16.2.2",
+    "chai": "^4.1.2",
     "del": "^3.0.0",
     "fs": "0.0.1-security",
+    "glob": "^7.1.2",
     "gulp": "github:gulpjs/gulp#4.0",
     "gulp-babel": "^7.0.1",
     "gulp-clean-css": "^3.9.4",
     "gulp-concat": "^2.6.1",
     "gulp-eslint": "^4.0.2",
+    "gulp-if": "^2.0.2",
     "gulp-insert": "^0.5.0",
+    "gulp-mocha": "^6.0.0",
     "gulp-ng-annotate": "^2.1.0",
     "gulp-rename": "^1.2.3",
     "gulp-sourcemaps": "^2.6.4",
+    "gulp-terser": "^1.0.1",
     "gulp-tslint": "^8.1.3",
     "gulp-typescript": "^4.0.2",
     "gulp-uglify": "^3.0.0",
+    "gulp-uglifyes": "^0.1.3",
+    "mocha": "^5.2.0",
+    "rollup-stream": "^1.24.1",
+    "through2": "^2.0.3",
     "tslint": "^5.10.0",
-    "typescript": "^2.9.1"
+    "typescript": "^2.9.1",
+    "vinyl-buffer": "^1.0.1",
+    "vinyl-source-stream": "^2.0.0"
   }
 }

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/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 38495ad..f76d421 100644
--- a/console/stand-alone/plugin/css/dispatch.css
+++ b/console/stand-alone/plugin/css/dispatch.css
@@ -975,10 +975,10 @@ i.red {
 }
 
 .qdrListActions div.delete {
-    width: 20em;
+    width: 30em;
     margin: auto;
     border: 1px solid #eaeaea;
-    height: 5em;
+    /* height: 5em; */
     padding: 4em;
     background-color: #fcfcfc;
 }

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/html/qdrList.html
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/html/qdrList.html b/console/stand-alone/plugin/html/qdrList.html
index 64aa091..06faa43 100644
--- a/console/stand-alone/plugin/html/qdrList.html
+++ b/console/stand-alone/plugin/html/qdrList.html
@@ -18,16 +18,8 @@ under the License.
 -->
 
 <style>
-@media (min-width: 768px) {
-  .list-grid {
+.list-grid {
     padding-left: 300px;
-  }
-}
-@media (max-width: 768px) {
-  .list-grid {
-      padding-left: 0;
-  }
-
 }
 
 span.fancytree-icon {
@@ -51,11 +43,21 @@ span.fancytree-icon {
   height: 25px;
   width: 300px;
   background-color: #333333;
+  border: 0;
 }
 
 #list-controller div.list-grid select {
     background-color: white;
 }
+
+.list-grid {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: calc(100% - 40px);
+    margin-left: 20px;
+}
+
 </style>
 
 <div id="list-controller" ng-controller="QDR.ListController">
@@ -71,23 +73,23 @@ span.fancytree-icon {
         </div>
     </div>
 
-    <div class="list-grid col-xs-12">
+    <div class="list-grid">
         <div class="row-fluid qdrListActions">
             <ul class="nav nav-tabs">
                 <li ng-repeat="mode in modes" ng-show="isValid(mode)" ng-click="selectMode(mode)" ng-class="{active : isModeSelected(mode)}" title="{{mode.title}}" ng-bind-html="mode.content"> </li>
             </ul>
-            <h4>{{selectedRecordName}}</h4>
+            <h4>{{selectedRecordName | to_trusted}}</h4>
             <div ng-show="currentMode.id === 'attributes'" class="selectedItems">
-                <div ng-show="selectedRecordName === selectedEntity" class="no-content">There are no {{selectedEntity | safePlural}}</div>
+                <div ng-show="selectedRecordName === selectedEntity" class="no-content">There are no {{selectedEntity | safePlural | to_trusted}}</div>
                 <div id='details-grid' ng-hide="selectedRecordName === selectedEntity" ui-grid="details" ui-grid-resize-columns ui-grid-save-state
                 ui-grid-auto-resize ng-style="getTableHeight()"></div>
             </div>
             <div ng-show="currentMode.id === 'delete'">
                 <div class="delete" ng-show="selectedRecordName !== selectedEntity">
-                    <button class="btn btn-primary" ng-click="remove()">Delete</button> {{selectedRecordName}}
+                    <button class="btn btn-primary" ng-click="remove()">Delete</button> {{selectedRecordName | to_trusted}}
                 </div>
                 <div ng-hide="selectedRecordName !== selectedEntity">
-                    There are no {{selectedEntity | safePlural}}
+                    There are no {{selectedEntity | safePlural | to_trusted}}
                 </div>
             </div>
             <div class="operations" ng-show="currentMode.id === 'operations'">
@@ -98,7 +100,7 @@ span.fancytree-icon {
                             <th>Value</th>
                         </tr>
                     <tr title="{{attribute.title}}" ng-repeat="attribute in detailFields">
-                        <td><label for="{{attribute.name}}">{{attribute.name | humanify}}</label></td>
+                        <td><label for="{{attribute.name}}">{{attribute.name | humanify | to_trusted}}</label></td>
                         <!-- we can't do <input type="{angular expression}"> because... jquery throws an exception because... -->
                         <td>
                         <div ng-if="attribute.input == 'input'">
@@ -106,7 +108,7 @@ span.fancytree-icon {
                             <div ng-if="attribute.type == 'number'"><input type="number" name="{{attribute.name}}" id="{{attribute.name}}" ng-model="attribute.rawValue" ng-required="attribute.required" ng-class="{required: attribute.required, unique: attribute.unique}" class="ui-widget-content ui-corner-all"/><span ng-if="attribute.required" title="required" class="required-indicator"></span><span ng-if="attribute.unique" title="unique" class="unique-indicator"></span></div>
                             <div ng-if="attribute.type == 'text'"><input type="text" name="{{attribute.name}}" id="{{attribute.name}}" ng-model="attribute.attributeValue" ng-required="attribute.required" ng-class="{required: attribute.required, unique: attribute.unique}" class="ui-widget-content ui-corner-all"/><span ng-if="attribute.required" title="required" class="required-indicator"></span><span ng-if="attribute.unique" title="unique" class="unique-indicator"></span></div>
                             <div ng-if="attribute.type == 'textarea'"><textarea name="{{attribute.name}}" id="{{attribute.name}}" ng-model="attribute.attributeValue" ng-required="attribute.required" ng-class="{required: attribute.required, unique: attribute.unique}" class="ui-widget-content ui-corner-all"></textarea><span ng-if="attribute.required" title="required" class="required-indicator"></span><span ng-if="attribute.unique" title="unique" class="unique-indicator"></span></div>
-                            <span ng-if="attribute.type == 'disabled'" >{{getAttributeValue(attribute)}}</span>
+                            <span ng-if="attribute.type == 'disabled'" >{{getAttributeValue(attribute) | to_trusted}}</span>
                         </div>
                         <div ng-if="attribute.input == 'select'">
                             <select id="{{attribute.name}}" ng-model="attribute.selected" ng-required="attribute.required" ng-class="{required: attribute.required, unique: attribute.unique}" ng-options="item for item in attribute.rawtype track by item"></select>
@@ -118,7 +120,7 @@ span.fancytree-icon {
                         </div>
                         </td>
                     </tr>
-                    <tr><td></td><td><button class="btn btn-primary" type="button" ng-click="ok()">{{operation | Pascalcase}}</button></td></tr>
+                    <tr><td></td><td><button class="btn btn-primary" type="button" ng-click="ok()">{{operation | Pascalcase | to_trusted}}</button></td></tr>
                     </table>
                 </fieldset>
             </div>
@@ -139,10 +141,23 @@ span.fancytree-icon {
                         </tr>
                     </table>
                 </div>
-                <div ng-if="logResults.length == 0 && !fetchingLog">No log entries for {{selectedRecordName}}</div>
-                <div ng-if="fetchingLog">Fetching logs for {{selectedRecordName}}</div>
+                <div ng-if="logResults.length == 0 && !fetchingLog">No log entries for {{selectedRecordName | to_trusted}}</div>
+                <div ng-if="fetchingLog">Fetching logs for {{selectedRecordName | to_trusted}}</div>
             </div>
         </div>
     </div>
 </div>
 
+<style>
+    @media (min-width: 768px) {
+      .list-grid {
+        padding-left: 300px;
+      }
+    }
+    @media (max-width: 768px) {
+      .list-grid {
+          padding-left: 0;
+      }
+    
+    }
+</style>

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/html/tmplChartConfig.html
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/html/tmplChartConfig.html b/console/stand-alone/plugin/html/tmplChartConfig.html
index 4c9c727..efd474e 100644
--- a/console/stand-alone/plugin/html/tmplChartConfig.html
+++ b/console/stand-alone/plugin/html/tmplChartConfig.html
@@ -31,7 +31,7 @@
         <uib-tabset>
             <uib-tab heading="Type">
                 <legend>Chart type</legend>
-                <div>
+                <div class="clearfix">
                     <label><input type="radio" ng-model="dialogChart.type" value="value" /> Value Chart</label>
                     <label><input type="radio" ng-model="dialogChart.type" value="rate" /> Rate Chart</label>
 <!--
@@ -41,19 +41,15 @@
                     </div>
 -->
                 </div>
-                <div style="clear:both;"> </div>
             </uib-tab>
-<!--
-            <uib-tab ng-hide="$parent.chart.aggregate()" heading="Colors">
+            <uib-tab ng-hide="chart.aggregate()" heading="Colors">
                 <legend>Chart colors</legend>
-                <div>
+                <div class="clearfix">
                     <div class="colorPicker">
-                        <label>Area: <input id="areaColor" name="areaColor" type="color" /></label>
+                        <label>Area: <input id="areaColor" name="areaColor" type="color" ng-model="dialogChart.areaColor"/></label>
                     </div>
                 </div>
-                <div style="clear:both;"> </div>
             </uib-tab>
--->
             <uib-tab heading="Duration">
                 <legend>Chart duration</legend>
                 <div class="clearfix">

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/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 7f28a9c..45e68a7 100644
--- a/console/stand-alone/plugin/js/chord/data.js
+++ b/console/stand-alone/plugin/js/chord/data.js
@@ -17,136 +17,132 @@ specific language governing permissions and limitations
 under the License.
 */
 
-'use strict';
-/* global angular Promise MIN_CHORD_THRESHOLD */
+/* global angular Promise */
+import { MIN_CHORD_THRESHOLD } from './matrix.js';
 
 const SAMPLES = 3;  // number of snapshots to use for rate calculations
 
-function ChordData (QDRService, isRate, converter) {
-  this.QDRService = QDRService;
-  this.last_matrix = undefined;
-  this.last_values = {values: undefined, timestamp: undefined};
-  this.rateValues = undefined;
-  this.snapshots = [];  // last N values used for calculating rate
-  this.isRate = isRate;
-  // fn to convert raw data to matrix
-  this.converter = converter;
-  // object that determines which addresses are excluded
-  this.filter = [];
-}
-ChordData.prototype.setRate = function (isRate) {
-  this.isRate = isRate;
-};
-ChordData.prototype.setConverter = function (converter) {
-  this.converter = converter;
-};
-ChordData.prototype.setFilter = function (filter) {
-  this.filter = filter;
-};
-ChordData.prototype.getAddresses = function () {
-  let addresses = {};
-  let outer = this.snapshots;
-  if (outer.length === 0)
-    outer = outer = [this.last_values];
-  outer.forEach( function (snap) {
-    snap.values.forEach( function (lv) {
-      if (!(lv.address in addresses)) {
-        addresses[lv.address] = this.filter.indexOf(lv.address) < 0;
-      }
+class ChordData { // eslint-disable-line no-unused-vars
+  constructor(QDRService, isRate, converter) {
+    this.QDRService = QDRService;
+    this.last_matrix = undefined;
+    this.last_values = { values: undefined, timestamp: undefined };
+    this.rateValues = undefined;
+    this.snapshots = []; // last N values used for calculating rate
+    this.isRate = isRate;
+    // fn to convert raw data to matrix
+    this.converter = converter;
+    // object that determines which addresses are excluded
+    this.filter = [];
+  }
+  setRate(isRate) {
+    this.isRate = isRate;
+  }
+  setConverter(converter) {
+    this.converter = converter;
+  }
+  setFilter(filter) {
+    this.filter = filter;
+  }
+  getAddresses() {
+    let addresses = {};
+    let outer = this.snapshots;
+    if (outer.length === 0)
+      outer = outer = [this.last_values];
+    outer.forEach(function (snap) {
+      snap.values.forEach(function (lv) {
+        if (!(lv.address in addresses)) {
+          addresses[lv.address] = this.filter.indexOf(lv.address) < 0;
+        }
+      }, this);
     }, this);
-  }, this);
-  return addresses;
-};
-ChordData.prototype.getRouters = function () {
-  let routers = {};
-  let outer = this.snapshots;
-  if (outer.length === 0)
-    outer = [this.last_values];
-  outer.forEach( function (snap) {
-    snap.values.forEach( function (lv) {
-      routers[lv.egress] = true;
-      routers[lv.ingress] = true;
+    return addresses;
+  }
+  getRouters() {
+    let routers = {};
+    let outer = this.snapshots;
+    if (outer.length === 0)
+      outer = [this.last_values];
+    outer.forEach(function (snap) {
+      snap.values.forEach(function (lv) {
+        routers[lv.egress] = true;
+        routers[lv.ingress] = true;
+      });
     });
-  });
-  return Object.keys(routers).sort();
-};
-
-ChordData.prototype.applyFilter = function (filter) {
-  if (filter)
-    this.setFilter(filter);
-
-  return new Promise( (function (resolve) {
-    resolve(convert(this, this.last_values));
-  }));
-};
-
-// construct a square matrix of the number of messages each router has egressed from each router
-ChordData.prototype.getMatrix = function () {
-  let self = this;
-  return new Promise( (function (resolve, reject) {
-    // get the router.node and router.link info
-    self.QDRService.management.topology.fetchAllEntities([
-      {entity: 'router.node', attrs: ['id', 'index']},
-      {entity: 'router.link', attrs: ['linkType', 'linkDir', 'owningAddr', 'ingressHistogram']}], 
-    function(results) {
-      if (!results) {
-        reject(Error('unable to fetch entities'));
-        return;
-      }
-      // the raw data received from the rouers
-      let values = [];
-
-      // for each router in the network
-      for (let nodeId in results) {
-        // get a map of router ids to index into ingressHistogram for the links for this router.
-        // each routers has a different order for the routers
-        let ingressRouters = [];
-        let routerNode = results[nodeId]['router.node'];
-        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++) {
-          ingressRouters.push(routerNode.results[i][idIndex]);
+    return Object.keys(routers).sort();
+  }
+  applyFilter(filter) {
+    if (filter)
+      this.setFilter(filter);
+    return new Promise((function (resolve) {
+      resolve(convert(this, this.last_values));
+    }));
+  }
+  // construct a square matrix of the number of messages each router has egressed from each router
+  getMatrix() {
+    let self = this;
+    return new Promise((function (resolve, reject) {
+      // get the router.node and router.link info
+      self.QDRService.management.topology.fetchAllEntities([
+        { entity: 'router.node', attrs: ['id', 'index'] },
+        { entity: 'router.link', attrs: ['linkType', 'linkDir', 'owningAddr', 'ingressHistogram'] }
+      ], function (results) {
+        if (!results) {
+          reject(Error('unable to fetch entities'));
+          return;
         }
-
-        // the name of the router we are working on
-        let egressRouter = self.QDRService.management.topology.nameFromId(nodeId);
-
-        // loop through the router links for this router looking for out/endpoint/non-console links
-        let routerLinks = results[nodeId]['router.link'];
-        for (let i=0; i<routerLinks.results.length; i++) {
-          let link = self.QDRService.utilities.flatten(routerLinks.attributeNames, routerLinks.results[i]);
-          // if the link is an outbound/enpoint/non console
-          if (link.linkType === 'endpoint' && link.linkDir === 'out' && !link.owningAddr.startsWith('Ltemp.')) {
-            // keep track of the raw egress values as well as their ingress and egress routers and the address
-            for (let j=0; j<ingressRouters.length; j++) {
-              let messages = link.ingressHistogram[j];
-              if (messages) {
-                values.push({ingress: ingressRouters[j], 
-                  egress:  egressRouter, 
-                  address: self.QDRService.utilities.addr_text(link.owningAddr), 
-                  messages: messages});
+        // the raw data received from the rouers
+        let values = [];
+        // for each router in the network
+        for (let nodeId in results) {
+          // get a map of router ids to index into ingressHistogram for the links for this router.
+          // each routers has a different order for the routers
+          let ingressRouters = [];
+          let routerNode = results[nodeId]['router.node'];
+          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++) {
+            ingressRouters.push(routerNode.results[i][idIndex]);
+          }
+          // the name of the router we are working on
+          let egressRouter = self.QDRService.utilities.nameFromId(nodeId);
+          // loop through the router links for this router looking for out/endpoint/non-console links
+          let routerLinks = results[nodeId]['router.link'];
+          for (let i = 0; i < routerLinks.results.length; i++) {
+            let link = self.QDRService.utilities.flatten(routerLinks.attributeNames, routerLinks.results[i]);
+            // if the link is an outbound/enpoint/non console
+            if (link.linkType === 'endpoint' && link.linkDir === 'out' && !link.owningAddr.startsWith('Ltemp.')) {
+              // keep track of the raw egress values as well as their ingress and egress routers and the address
+              for (let j = 0; j < ingressRouters.length; j++) {
+                let messages = link.ingressHistogram[j];
+                if (messages) {
+                  values.push({
+                    ingress: ingressRouters[j],
+                    egress: egressRouter,
+                    address: self.QDRService.utilities.addr_text(link.owningAddr),
+                    messages: messages
+                  });
+                }
               }
             }
           }
         }
-      }
-      // values is an array of objects like [{ingress: 'xxx', egress: 'xxx', address: 'xxx', messages: ###}, ....]
-
-      // convert the raw values array into a matrix object
-      let matrix = convert(self, values);
+        // values is an array of objects like [{ingress: 'xxx', egress: 'xxx', address: 'xxx', messages: ###}, ....]
+        // convert the raw values array into a matrix object
+        let matrix = convert(self, values);
+        // resolve the promise
+        resolve(matrix);
+      });
+    }));
+  }
+  convertUsing(converter) {
+    let values = this.isRate ? this.rateValues : this.last_values.values;
+    // convert the values to a matrix using the requested converter and the current filter
+    return converter(values, this.filter);
+  }
+}
 
-      // resolve the promise
-      resolve(matrix);
-    });
-  }));
-};
-ChordData.prototype.convertUsing = function (converter) {
-  let values = this.isRate ? this.rateValues : this.last_values.values;
 
-  // convert the values to a matrix using the requested converter and the current filter
-  return converter(values, this.filter);
-};
 
 // Private functions
 
@@ -226,3 +222,5 @@ let convert = function (self, values) {
 
   return matrix;
 };
+
+export { ChordData };

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/chord/filters.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/chord/filters.js b/console/stand-alone/plugin/js/chord/filters.js
index 7bb68cc..d184221 100644
--- a/console/stand-alone/plugin/js/chord/filters.js
+++ b/console/stand-alone/plugin/js/chord/filters.js
@@ -16,11 +16,10 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-'use strict';
-/* global valuesMatrix */
 
+import { valuesMatrix } from './matrix.js';
 // this filter will show an arc per router with the addresses aggregated
-var aggregateAddresses = function (values, filter) { // eslint-disable-line no-unused-vars
+export const aggregateAddresses = function (values, filter) {
   let m = new valuesMatrix(true);
   values.forEach (function (value) {
     if (filter.indexOf(value.address) < 0) {
@@ -40,53 +39,8 @@ var aggregateAddresses = function (values, filter) { // eslint-disable-line no-u
   return m.sorted();
 };
 
-// this filter will show an arc per router-address
-var _separateAddresses = function (values, filter) { // eslint-disable-line no-unused-vars
-  let m = new valuesMatrix(false);
-  values = values.filter( function (v) { return filter.indexOf(v.address) < 0;});
-  if (values.length === 0)
-    return m;
-
-  let addresses = {}, routers = {};
-  // get the list of routers and addresses in the data
-  values.forEach( function (value) {
-    addresses[value.address] = true;
-    routers[value.ingress] = true;
-    routers[value.egress] = true;
-  });
-  let saddresses = Object.keys(addresses).sort();
-  let srouters = Object.keys(routers).sort();
-  let alen = saddresses.length;
-  // sanity check
-  if (alen === 0)
-    return m;
-
-  /* Convert the data to a matrix */
-
-  // initialize the matrix to have the correct ingress, egress, and address in each row and col
-  m.zeroInit(saddresses.length * srouters.length);
-  m.rows.forEach( function (row, r) {
-    let egress = srouters[Math.floor(r/alen)];
-    row.cols.forEach( function (col, c) {
-      let ingress = srouters[Math.floor(c/alen)];
-      let address = saddresses[c % alen];
-      m.setRowCol(r, c, ingress, egress, address, 0);
-    });
-  });
-  // set the values at each cell in the matrix
-  for (let i=0, alen=saddresses.length, vlen=values.length; i<vlen; i++) {
-    let value = values[i];
-    let egressIndex = srouters.indexOf(value.egress);
-    let ingressIndex = srouters.indexOf(value.ingress);
-    let addressIndex = saddresses.indexOf(value.address);
-    let row = egressIndex * alen + addressIndex;
-    let col = ingressIndex * alen + addressIndex;
-    m.setColMessages(row, col, value.messages);
-  }
-  return m;
-};
 
-let separateAddresses = function (values, filter) { // eslint-disable-line no-unused-vars
+export const separateAddresses = function (values, filter) {
   let m = new valuesMatrix(false);
   values.forEach( function (value) {
     if (filter.indexOf(value.address) < 0) {

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/chord/layout/layout.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/chord/layout/layout.js b/console/stand-alone/plugin/js/chord/layout/layout.js
index e3d223f..907e9a8 100644
--- a/console/stand-alone/plugin/js/chord/layout/layout.js
+++ b/console/stand-alone/plugin/js/chord/layout/layout.js
@@ -16,11 +16,9 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-'use strict';
 /* global d3 */
 
-
-var qrdlayoutChord = function() { // eslint-disable-line no-unused-vars
+var qdrlayoutChord = function() { // eslint-disable-line no-unused-vars
   var chord = {}, chords, groups, matrix, n, padding = 0, Ï„ = Math.PI*2, groupBy;
   function relayout() {
     groupBy = groupBy || d3.range(n);
@@ -145,3 +143,5 @@ let unique = function (arr) {
   }
   return Object.keys(counts).length;
 };
+
+export { qdrlayoutChord };
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/chord/matrix.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/chord/matrix.js b/console/stand-alone/plugin/js/chord/matrix.js
index 3f10ddc..156deb3 100644
--- a/console/stand-alone/plugin/js/chord/matrix.js
+++ b/console/stand-alone/plugin/js/chord/matrix.js
@@ -16,7 +16,6 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-'use strict';
 /* global d3 */
 
 const MIN_CHORD_THRESHOLD = 0.01;
@@ -212,3 +211,5 @@ let emptyMatrix = function (size) {
   }
   return matrix;
 };
+
+export { MIN_CHORD_THRESHOLD, valuesMatrix };

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/chord/qdrChord.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/chord/qdrChord.js b/console/stand-alone/plugin/js/chord/qdrChord.js
index 91ee96a..edb1b3a 100644
--- a/console/stand-alone/plugin/js/chord/qdrChord.js
+++ b/console/stand-alone/plugin/js/chord/qdrChord.js
@@ -16,16 +16,21 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-'use strict';
-/* global angular d3 separateAddresses aggregateAddresses ChordData qdrRibbon qrdlayoutChord */
+/* global angular d3 */
 
-var QDR = (function (QDR) {
-  QDR.module.controller('QDR.ChordController', ['$scope', 'QDRService', '$location', '$timeout', '$sce', function($scope, QDRService, $location, $timeout, $sce) {
+import { QDRRedirectWhenConnected } from '../qdrGlobals.js';
+import { separateAddresses, aggregateAddresses } from './filters.js';
+import { ChordData } from './data.js';
+import { qdrRibbon } from './ribbon/ribbon.js';
+import { qdrlayoutChord } from './layout/layout.js';
 
+export class ChordController {
+  constructor(QDRService, $scope, $location, $timeout, $sce) {
+    this.controllerName = 'QDR.ChordController';
     // if we get here and there is no connection, redirect to the connect page and then 
     // return here once we are connected
     if (!QDRService.management.connection.is_connected()) {
-      QDR.redirectWhenConnected($location, 'chord');
+      QDRRedirectWhenConnected($location, 'chord');
       return;
     }
 
@@ -441,7 +446,7 @@ var QDR = (function (QDR) {
 
       // create a new chord layout so we can animate between the last one and this one
       let groupBy = matrix.getGroupBy();
-      let rechord = qrdlayoutChord().padding(ARCPADDING).groupBy(groupBy).matrix(matrixMessages);
+      let rechord = qdrlayoutChord().padding(ARCPADDING).groupBy(groupBy).matrix(matrixMessages);
 
       // The chord layout has a function named .groups() that returns the
       // data for the arcs. We decorate this data with a unique key.
@@ -831,8 +836,7 @@ var QDR = (function (QDR) {
       });
     }
     let interval = setInterval(doUpdate, transitionDuration);
-  
-  }]);
-  return QDR;
 
-} (QDR || {}));
+  }
+}
+ChordController.$inject = ['QDRService', '$scope', '$location', '$timeout', '$sce'];

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/chord/ribbon/ribbon.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/chord/ribbon/ribbon.js b/console/stand-alone/plugin/js/chord/ribbon/ribbon.js
index 604fa1e..fb6996f 100644
--- a/console/stand-alone/plugin/js/chord/ribbon/ribbon.js
+++ b/console/stand-alone/plugin/js/chord/ribbon/ribbon.js
@@ -16,9 +16,7 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-'use strict';
 /* global d3 */
-
 const halfPI = Math.PI / 2.0;
 const twoPI = Math.PI * 2.0;
 
@@ -163,3 +161,5 @@ let cpRatio = function (gap, x, y) {
   let distScale = d3.scale.linear().domain([0, top/8, top/2, top]).range([0, .3, .4, .5]);
   return distScale(dist);
 };
+
+export { qdrRibbon };
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/dispatchPlugin.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/dispatchPlugin.js b/console/stand-alone/plugin/js/dispatchPlugin.js
deleted file mode 100644
index 47c8a60..0000000
--- a/console/stand-alone/plugin/js/dispatchPlugin.js
+++ /dev/null
@@ -1,264 +0,0 @@
-/*
-Licensed to the Apache Software Foundation (ASF) under one
-or more contributor license agreements.  See the NOTICE file
-distributed with this work for additional information
-regarding copyright ownership.  The ASF licenses this file
-to you under the Apache License, Version 2.0 (the
-"License"); you may not use this file except in compliance
-with the License.  You may obtain a copy of the License at
-
-  http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing,
-software distributed under the License is distributed on an
-"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-KIND, either express or implied.  See the License for the
-specific language governing permissions and limitations
-under the License.
-*/
-'use strict';
-/* global angular d3 */
-
-/**
- * @module QDR
- * @main QDR
- *
- * The main entry point for the QDR module
- *
- */
-var QDR = (function(QDR) {
-
-  /**
-   * @property pluginName
-   * @type {string}
-   *
-   * The name of this plugin
-   */
-  QDR.pluginName = 'QDR';
-  QDR.pluginRoot = '';
-  QDR.isStandalone = true;
-
-  /**
-   * @property templatePath
-   * @type {string}
-   *
-   * The top level path to this plugin's partials
-   */
-  QDR.templatePath = 'html/';
-  /**
-   * @property SETTINGS_KEY
-   * @type {string}
-   *
-   * The key used to fetch our settings from local storage
-   */
-  QDR.SETTINGS_KEY = 'QDRSettings';
-  QDR.LAST_LOCATION = 'QDRLastLocation';
-
-  QDR.redirectWhenConnected = function ($location, org) {
-    $location.path(QDR.pluginRoot + '/connect');
-    $location.search('org', org);
-  };
-
-  /**
-   * @property module
-   * @type {object}
-   *
-   * This plugin's angularjs module instance
-   */
-  QDR.module = angular.module(QDR.pluginName, ['ngRoute', 'ngSanitize', 'ngResource', 'ui.bootstrap',
-    'ui.grid', 'ui.grid.selection', 'ui.grid.autoResize', 'ui.grid.resizeColumns', 'ui.grid.saveState', 'ui.slider', 'ui.checkbox']);
-
-  // set up the routing for this plugin
-  QDR.module.config(function($routeProvider) {
-    $routeProvider
-      .when('/', {
-        templateUrl: QDR.templatePath + 'qdrOverview.html'
-      })
-      .when('/overview', {
-        templateUrl: QDR.templatePath + 'qdrOverview.html'
-      })
-      .when('/topology', {
-        templateUrl: QDR.templatePath + 'qdrTopology.html'
-      })
-      .when('/list', {
-        templateUrl: QDR.templatePath + 'qdrList.html'
-      })
-      .when('/schema', {
-        templateUrl: QDR.templatePath + 'qdrSchema.html'
-      })
-      .when('/charts', {
-        templateUrl: QDR.templatePath + 'qdrCharts.html'
-      })
-      .when('/chord', {
-        templateUrl: QDR.templatePath + 'qdrChord.html'
-      })
-      .when('/connect', {
-        templateUrl: QDR.templatePath + 'qdrConnect.html'
-      });
-  });
-
-  QDR.module.config(function ($compileProvider) {
-    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|chrome-extension|file|blob):/);
-    $compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|mailto|chrome-extension):/);
-    /*    var cur = $compileProvider.urlSanitizationWhitelist();
-    $compileProvider.urlSanitizationWhitelist(/^\s*(https?|ftp|mailto|file|blob):/);
-    cur = $compileProvider.urlSanitizationWhitelist();
-*/
-  });
-
-  QDR.module.filter('to_trusted', ['$sce', function($sce){
-    return function(text) {
-      return $sce.trustAsHtml(text);
-    };
-  }]);
-
-  QDR.module.filter('humanify', function (QDRService) {
-    return function (input) {
-      return QDRService.utilities.humanify(input);
-    };
-  });
-
-  QDR.module.filter('Pascalcase', function () {
-    return function (str) {
-      if (!str)
-        return '';
-      return str.replace(/(\w)(\w*)/g,
-        function(g0,g1,g2){return g1.toUpperCase() + g2.toLowerCase();});
-    };
-  });
-
-  QDR.module.filter('safePlural', function () {
-    return function (str) {
-      var es = ['x', 'ch', 'ss', 'sh'];
-      for (var i=0; i<es.length; ++i) {
-        if (str.endsWith(es[i]))
-          return str + 'es';
-      }
-      if (str.endsWith('y'))
-        return str.substr(0, str.length-2) + 'ies';
-      if (str.endsWith('s'))
-        return str;
-      return str + 's';
-    };
-  });
-
-  QDR.module.filter('pretty', function () {
-    return function (str) {
-      var formatComma = d3.format(',');
-      if (!isNaN(parseFloat(str)) && isFinite(str))
-        return formatComma(str);
-      return str;
-    };
-  });
-
-  QDR.logger = function ($log) {
-    var log = $log;
-
-    this.debug = function (msg) { msg = 'QDR: ' + msg; log.debug(msg);};
-    this.error = function (msg) {msg = 'QDR: ' + msg; log.error(msg);};
-    this.info = function (msg) {msg = 'QDR: ' + msg; log.info(msg);};
-    this.warn = function (msg) {msg = 'QDR: ' + msg; log.warn(msg);};
-
-    return this;
-  };
-  // one-time initialization happens in the run function
-  // of our module
-  QDR.module.run( ['$rootScope', '$route', '$timeout', '$location', '$log', 'QDRService', 'QDRChartService',  function ($rootScope, $route, $timeout, $location, $log, QDRService, QDRChartService) {
-    QDR.log = new QDR.logger($log);
-    QDR.log.info('************* creating Dispatch Console ************');
-    var curPath = $location.path();
-    var org = curPath.substr(1);
-    if (org && org.length > 0 && org !== 'connect') {
-      $location.search('org', org);
-    } else {
-      $location.search('org', null);
-    }
-    QDR.queue = d3.queue;
-
-    if (!QDRService.management.connection.is_connected()) {
-      // attempt to connect to the host:port that served this page
-      var host = $location.host();
-      var port = $location.port();
-      var search = $location.search();
-      if (search.org) {
-        if (search.org === 'connect')
-          $location.search('org', 'overview');
-      }
-      var connectOptions = {address: host, port: port};
-      QDR.log.info('Attempting AMQP over websockets connection using address:port of browser ('+host+':'+port+')');
-      QDRService.management.connection.testConnect(connectOptions)
-        .then( function () {
-          // We didn't connect with reconnect: true flag.
-          // The reason being that if we used reconnect:true and the connection failed, rhea would keep trying. There
-          // doesn't appear to be a way to tell it to stop trying to reconnect.
-          QDRService.disconnect();
-          QDR.log.info('Connect succeeded. Using address:port of browser');
-          connectOptions.reconnect = true;
-          // complete the connection (create the sender/receiver)
-          QDRService.connect(connectOptions)
-            .then( function () {
-              // register a callback for when the node list is available (needed for loading saved charts)
-              QDRService.management.topology.addUpdatedAction('initChartService', function() {
-                QDRService.management.topology.delUpdatedAction('initChartService');
-                QDRChartService.init(); // initialize charting service after we are connected
-              });
-              // get the list of nodes
-              QDRService.management.topology.startUpdating(false);
-            });
-        }, function () {
-          QDR.log.info('failed to auto-connect to ' + host + ':' + port);
-          QDR.log.info('redirecting to connect page');
-          $timeout(function () {
-            $location.path('/connect');
-            $location.search('org', org);
-            $location.replace();
-          });
-        });
-    }
-
-    $rootScope.$on('$routeChangeSuccess', function() {
-      var path = $location.path();
-      if (path !== '/connect') {
-        localStorage[QDR.LAST_LOCATION] = path;
-      }
-    });
-  }]);
-
-  QDR.module.controller ('QDR.MainController', ['$scope', '$location', function ($scope, $location) {
-    QDR.log.debug('started QDR.MainController with location.url: ' + $location.url());
-    QDR.log.debug('started QDR.MainController with window.location.pathname : ' + window.location.pathname);
-    $scope.topLevelTabs = [];
-    $scope.topLevelTabs.push({
-      id: 'qdr',
-      content: 'Qpid Dispatch Router Console',
-      title: 'Dispatch Router Console',
-      isValid: function() { return true; },
-      href: function() { return '#connect'; },
-      isActive: function() { return true; }
-    });
-  }]);
-
-  QDR.module.controller ('QDR.Core', function ($scope, $rootScope) {
-    $scope.alerts = [];
-    $scope.breadcrumb = {};
-    $scope.closeAlert = function(index) {
-      $scope.alerts.splice(index, 1);
-    };
-    $scope.$on('setCrumb', function(event, data) {
-      $scope.breadcrumb = data;
-    });
-    $scope.$on('newAlert', function(event, data) {
-      $scope.alerts.push(data);
-      $scope.$apply();
-    });
-    $scope.$on('clearAlerts', function () {
-      $scope.alerts = [];
-      $scope.$apply();
-    });
-    $scope.pageMenuClicked = function () {
-      $rootScope.$broadcast('pageMenuClicked');
-    };
-  });
-
-  return QDR;
-}(QDR || {}));

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/dlgChartController.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/dlgChartController.js b/console/stand-alone/plugin/js/dlgChartController.js
index e8c240f..271baa2 100644
--- a/console/stand-alone/plugin/js/dlgChartController.js
+++ b/console/stand-alone/plugin/js/dlgChartController.js
@@ -16,15 +16,11 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-'use strict';
 /* global angular */
-/**
- * @module QDR
- */
-var QDR = (function(QDR) {
 
-  // controller for the edit/configure chart dialog
-  QDR.module.controller('QDR.ChartDialogController', function($scope, QDRChartService, $location, $uibModalInstance, chart, updateTick, dashboard, adding) {
+export class ChartDialogController {
+  constructor(QDRChartService, $scope, $location, $uibModalInstance, chart, updateTick, dashboard, adding) {
+    this.controllerName = 'QDR.ChartDialogController';
     let dialogSvgChart = null;
     $scope.svgDivId = 'dialogEditChart';    // the div id for the svg chart
 
@@ -41,8 +37,10 @@ var QDR = (function(QDR) {
     });
     $scope.$watch('dialogChart.areaColor', function (newValue, oldValue) {
       if (newValue !== oldValue) {
-        if (dialogSvgChart)
+        if (dialogSvgChart) {
+          dialogSvgChart.chart.areaColor = newValue;
           dialogSvgChart.tick($scope.svgDivId);
+        }
       }
     });
     $scope.$watch('dialogChart.lineColor', function (newValue, oldValue) {
@@ -73,7 +71,7 @@ var QDR = (function(QDR) {
     $scope.showChartsPage = function () {
       cleanup();
       $uibModalInstance.close(true);
-      $location.path(QDR.pluginRoot + '/charts');
+      $location.path('/charts');
     };
 
     var cleanup = function () {
@@ -185,20 +183,13 @@ var QDR = (function(QDR) {
         setTimeout(showChart, 100);
         return;
       }
-      dialogSvgChart = new QDRChartService.pfAreaChart($scope.dialogChart, $scope.svgDivId);
-      /*
-      $('input[name=areaColor]').val($scope.dialogChart.areaColor);
-      $('input[name=areaColor]').on('input', function (e) {
-        $scope.dialogChart.areaColor = $(this).val();
-        updateDialogChart()
-      })
-*/
+      dialogSvgChart = QDRChartService.pfAreaChart($scope.dialogChart, $scope.svgDivId, false, 550);
       if (updateTimer)
         clearTimeout(updateTimer);
       updateDialogChart();
     };
     showChart();
-  });
-  return QDR;
 
-} (QDR || {}));
+  }
+}
+ChartDialogController.$inject = ['QDRChartService', '$scope', '$location', '$uibModalInstance', 'chart', 'updateTick', 'dashboard', 'adding'];

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/navbar.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/navbar.js b/console/stand-alone/plugin/js/navbar.js
index c17cd77..64d41c0 100644
--- a/console/stand-alone/plugin/js/navbar.js
+++ b/console/stand-alone/plugin/js/navbar.js
@@ -16,105 +16,92 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-'use strict';
 /* global angular */
 
-/**
- * @module QDR
- */
-var QDR = (function (QDR) {
+export class NavBarController {
+  constructor(QDRService, QDRChartService, $scope, $routeParams, $location) {
+    this.controllerName = 'QDR.NavBarController';
 
-  QDR.breadcrumbs = [
-    {
-      content: '<i class="icon-power"></i> Connect',
-      title: 'Connect to a router',
-      isValid: function () { return true; },
-      href: '#/connect',
-      name: 'Connect'
-    },
-    {
-      content: '<i class="pficon-home"></i> Overview',
-      title: 'View router overview',
-      isValid: function (QDRService) {return QDRService.management.connection.is_connected(); },
-      href: '#/overview',
-      name: 'Overview'
-    },
-    {
-      content: '<i class="icon-list "></i> Entities',
-      title: 'View the attributes of the router entities',
-      isValid: function (QDRService) { return QDRService.management.connection.is_connected(); },
-      href: '#/list',
-      name: 'Entities'
-    },
-    {
-      content: '<i class="code-branch"></i> Topology',
-      title: 'View router network topology',
-      isValid: function (QDRService) { return QDRService.management.connection.is_connected(); },
-      href: '#/topology',
-      name: 'Topology'
-    },
-    {
-      content: '<i class="icon-bar-chart"></i> Charts',
-      title: 'View charts',
-      isValid: function (QDRService) { return QDRService.management.connection.is_connected(); },
-      href: '#/charts',
-      name: 'Charts'
-    },
-    {
-      content: '<i class="chord-diagram"></i> Message Flow',
-      title: 'Chord chart',
-      isValid: function (QDRService) { return QDRService.management.connection.is_connected(); },
-      href: '#/chord',
-      name: 'Message Flow'
-    },
-    {
-      content: '<i class="icon-schema"></i> Schema',
-      title: 'View dispatch schema',
-      isValid: function (QDRService) { return QDRService.management.connection.is_connected(); },
-      href: '#/schema',
-      name: 'Schema'
-    }
-  ];
-  /**
-   * @function NavBarController
-   *
-   * @param $scope
-   * @param workspace
-   *
-   * The controller for this plugin's navigation bar
-   *
-   */
-  QDR.module.controller('QDR.NavBarController', ['$rootScope', '$scope', 'QDRService', 'QDRChartService', '$routeParams', '$location', function($rootScope, $scope, QDRService, QDRChartService, $routeParams, $location) {
-    $scope.breadcrumbs = QDR.breadcrumbs;
+    $scope.breadcrumbs = [
+      {
+        content: '<i class="icon-power"></i> Connect',
+        title: 'Connect to a router',
+        isValid: function () { return true; },
+        href: '#/connect',
+        name: 'Connect'
+      },
+      {
+        content: '<i class="pficon-home"></i> Overview',
+        title: 'View router overview',
+        isValid: function (QDRService) {return QDRService.management.connection.is_connected(); },
+        href: '#/overview',
+        name: 'Overview'
+      },
+      {
+        content: '<i class="icon-list "></i> Entities',
+        title: 'View the attributes of the router entities',
+        isValid: function (QDRService) { return QDRService.management.connection.is_connected(); },
+        href: '#/list',
+        name: 'Entities'
+      },
+      {
+        content: '<i class="code-branch"></i> Topology',
+        title: 'View router network topology',
+        isValid: function (QDRService) { return QDRService.management.connection.is_connected(); },
+        href: '#/topology',
+        name: 'Topology'
+      },
+      {
+        content: '<i class="icon-bar-chart"></i> Charts',
+        title: 'View charts',
+        isValid: function (QDRService) { return QDRService.management.connection.is_connected(); },
+        href: '#/charts',
+        name: 'Charts'
+      },
+      {
+        content: '<i class="chord-diagram"></i> Message Flow',
+        title: 'Chord chart',
+        isValid: function (QDRService) { return QDRService.management.connection.is_connected(); },
+        href: '#/chord',
+        name: 'Message Flow'
+      },
+      {
+        content: '<i class="icon-schema"></i> Schema',
+        title: 'View dispatch schema',
+        isValid: function (QDRService) { return QDRService.management.connection.is_connected(); },
+        href: '#/schema',
+        name: 'Schema'
+      }
+    ];
     $scope.isValid = function(link) {
       return link.isValid(QDRService, $location);
     };
-
+  
     $scope.isActive = function(href) {
       return href.split('#')[1] === $location.path();
     };
-
+  
     $scope.isRight = function (link) {
       return angular.isDefined(link.right);
     };
-
+  
     $scope.hasChart = function (link) {
       if (link.href == '#/charts') {
         return QDRChartService.charts.some(function (c) { return c.dashboard; });
       }
     };
-
+  
     $scope.isDashboardable = function () {
       return  ($location.path().indexOf('schema') < 0 && $location.path().indexOf('connect') < 0);
     };
-
+  
     $scope.addToDashboardLink = function () {
       var href = '#' + $location.path();
       var size = angular.toJson({
         size_x: 2,
         size_y: 2
       });
-
+  
       var routeParams = angular.toJson($routeParams);
       var title = 'Dispatch Router';
       return '/hawtio/#/dashboard/add?tab=dashboard' +
@@ -123,9 +110,8 @@ var QDR = (function (QDR) {
             '&title=' + encodeURIComponent(title) +
             '&size=' + encodeURIComponent(size);
     };
+  }
 
-  }]);
-
-  return QDR;
+}
+NavBarController.$inject = ['QDRService', 'QDRChartService', '$scope', '$routeParams', '$location'];
 
-} (QDR || {}));

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/plugin/js/posintDirective.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/posintDirective.js b/console/stand-alone/plugin/js/posintDirective.js
new file mode 100644
index 0000000..66b1aa0
--- /dev/null
+++ b/console/stand-alone/plugin/js/posintDirective.js
@@ -0,0 +1,75 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+export let posint = function() {
+  return {
+    require: 'ngModel',
+
+    link: function(scope, elem, attr, ctrl) {
+      // input type number allows + and - but we don't want them so filter them out
+      elem.bind('keypress', function(event) {
+        let nkey = !event.charCode ? event.which : event.charCode;
+        let skey = String.fromCharCode(nkey);
+        let nono = '-+.,';
+        if (nono.indexOf(skey) >= 0) {
+          event.preventDefault();
+          return false;
+        }
+        // firefox doesn't filter out non-numeric input. it just sets the ctrl to invalid
+        if (/[!@#$%^&*()]/.test(skey) && event.shiftKey || // prevent shift numbers
+          !( // prevent all but the following
+            nkey <= 0 || // arrows
+            nkey == 8 || // delete|backspace
+            nkey == 13 || // enter
+            (nkey >= 37 && nkey <= 40) || // arrows
+            event.ctrlKey || event.altKey || // ctrl-v, etc.
+            /[0-9]/.test(skey)) // numbers
+        ) {
+          event.preventDefault();
+          return false;
+        }
+      });
+      // check the current value of input
+      var _isPortInvalid = function(value) {
+        let port = value + '';
+        let isErrRange = false;
+        // empty string is valid
+        if (port.length !== 0) {
+          let n = ~~Number(port);
+          if (n < 1 || n > 65535) {
+            isErrRange = true;
+          }
+        }
+        ctrl.$setValidity('range', !isErrRange);
+        return isErrRange;
+      };
+
+      //For DOM -> model validation
+      ctrl.$parsers.unshift(function(value) {
+        return _isPortInvalid(value) ? undefined : value;
+      });
+
+      //For model -> DOM validation
+      ctrl.$formatters.unshift(function(value) {
+        _isPortInvalid(value);
+        return value;
+      });
+    }
+  };
+};
+


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


[2/8] qpid-dispatch git commit: DISPATCH-1049 Add console tests

Posted by ea...@apache.org.
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/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 15e73c4..9a48ba2 100644
--- a/console/stand-alone/plugin/js/topology/qdrTopology.js
+++ b/console/stand-alone/plugin/js/topology/qdrTopology.js
@@ -16,1593 +16,1091 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-'use strict';
 
-/* global angular d3 separateAddresses Traffic */
+/* global angular d3 */
 /**
  * @module QDR
  */
-var QDR = (function(QDR) {
-
-  /**
-   * @method TopologyController
-   *
-   * Controller that handles the QDR topology page
-   */
-
-  QDR.module.controller('QDR.TopologyController', ['$scope', '$rootScope', 'QDRService', '$location', '$timeout', '$uibModal', '$sce',
-    function($scope, $rootScope, QDRService, $location, $timeout, $uibModal, $sce) {
-
-      const TOPOOPTIONSKEY = 'topoOptions';
-      const radius = 25;
-      const radiusNormal = 15;
-
-      //  - nodes is an array of router/client info. these are the circles
-      //  - links is an array of connections between the routers. these are the lines with arrows
-      let nodes = [];
-      let links = [];
-      let forceData = {nodes: nodes, links: links};
-      let urlPrefix = $location.absUrl();
-      urlPrefix = urlPrefix.split('#')[0];
-      QDR.log.debug('started QDR.TopologyController with urlPrefix: ' + urlPrefix);
-
-      $scope.legendOptions = angular.fromJson(localStorage[TOPOOPTIONSKEY]) || {showTraffic: false, trafficType: 'dots'};
-      if (!$scope.legendOptions.trafficType)
-        $scope.legendOptions.trafficType = 'dots';
-      $scope.legend = {status: {legendOpen: true, optionsOpen: true}};
-      $scope.legend.status.optionsOpen = $scope.legendOptions.showTraffic;
-      let traffic = new Traffic($scope, $timeout, QDRService, separateAddresses, 
-        radius, forceData, nextHop, $scope.legendOptions.trafficType, urlPrefix);
-
-      // the showTraaffic checkbox was just toggled (or initialized)
-      $scope.$watch('legend.status.optionsOpen', function () {
-        $scope.legendOptions.showTraffic = $scope.legend.status.optionsOpen;
-        localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions);
-        if ($scope.legend.status.optionsOpen) {
-          traffic.start();
-        } else {
-          traffic.stop();
-          traffic.remove();
-          restart();
-        }
-      });
-      $scope.$watch('legendOptions.trafficType', function () {
-        localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions);
-        if ($scope.legendOptions.showTraffic) {
-          restart();
-          traffic.setAnimationType($scope.legendOptions.trafficType, separateAddresses, radius);
-          traffic.start();
-        }
-      });
-
-      // mouse event vars
-      let selected_node = null,
-        selected_link = null,
-        mousedown_link = null,
-        mousedown_node = null,
-        mouseover_node = null,
-        mouseup_node = null,
-        initial_mouse_down_position = null;
-
-      $scope.schema = 'Not connected';
-
-      $scope.contextNode = null; // node that is associated with the current context menu
-      $scope.isRight = function(mode) {
-        return mode.right;
-      };
-
-      var setNodesFixed = function (name, b) {
-        nodes.some(function (n) {
-          if (n.name === name) {
-            n.fixed = b;
-            return true;
-          }
-        });
-      };
-      $scope.setFixed = function(b) {
-        if ($scope.contextNode) {
-          $scope.contextNode.fixed = b;
-          setNodesFixed($scope.contextNode.name, b);
-          savePositions();
-        }
+import { QDRLogger, QDRRedirectWhenConnected } from '../qdrGlobals.js';
+import { Traffic } from './traffic.js';
+import { separateAddresses } from '../chord/filters.js';
+import { Nodes } from './nodes.js';
+import { Links } from './links.js';
+import { nextHop, connectionPopupHTML } from './topoUtils.js';
+/**
+ * @module QDR
+ */
+export class TopologyController {
+  constructor(QDRService, $scope, $log, $rootScope, $location, $timeout, $uibModal, $sce) {
+    this.controllerName = 'QDR.TopologyController';
+
+    let QDRLog = new QDRLogger($log, 'TopologyController');
+    const TOPOOPTIONSKEY = 'topoOptions';
+    const radius = 25;
+    const radiusNormal = 15;
+
+    //  - nodes is an array of router/client info. these are the circles
+    //  - links is an array of connections between the routers. these are the lines with arrows
+    let nodes = new Nodes(QDRService, QDRLog);
+    let links = new Links(QDRService, QDRLog);
+    let forceData = {nodes: nodes, links: links};
+    // urlPrefix is used when referring to svg:defs
+    let urlPrefix = $location.absUrl();
+    urlPrefix = urlPrefix.split('#')[0];
+
+    $scope.legendOptions = angular.fromJson(localStorage[TOPOOPTIONSKEY]) || {showTraffic: false, trafficType: 'dots'};
+    if (!$scope.legendOptions.trafficType)
+      $scope.legendOptions.trafficType = 'dots';
+    $scope.legend = {status: {legendOpen: true, optionsOpen: true}};
+    $scope.legend.status.optionsOpen = $scope.legendOptions.showTraffic;
+    let traffic = new Traffic($scope, $timeout, QDRService, separateAddresses, 
+      radius, forceData, $scope.legendOptions.trafficType, urlPrefix);
+
+    // the showTraaffic checkbox was just toggled (or initialized)
+    $scope.$watch('legend.status.optionsOpen', function () {
+      $scope.legendOptions.showTraffic = $scope.legend.status.optionsOpen;
+      localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions);
+      if ($scope.legend.status.optionsOpen) {
+        traffic.start();
+      } else {
+        traffic.stop();
+        traffic.remove();
         restart();
+      }
+    });
+    $scope.$watch('legendOptions.trafficType', function () {
+      localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions);
+      if ($scope.legendOptions.showTraffic) {
+        restart();
+        traffic.setAnimationType($scope.legendOptions.trafficType, separateAddresses, radius);
+        traffic.start();
+      }
+    });
+
+    // mouse event vars
+    let selected_node = null,
+      selected_link = null,
+      mousedown_link = null,
+      mousedown_node = null,
+      mouseover_node = null,
+      mouseup_node = null,
+      initial_mouse_down_position = null;
+
+    $scope.schema = 'Not connected';
+
+    $scope.contextNode = null; // node that is associated with the current context menu
+    $scope.isRight = function(mode) {
+      return mode.right;
+    };
+
+    $scope.setFixed = function(b) {
+      if ($scope.contextNode) {
+        $scope.contextNode.fixed = b;
+        nodes.setNodesFixed($scope.contextNode.name, b);
+        nodes.savePositions();
+      }
+      restart();
+    };
+    $scope.isFixed = function() {
+      if (!$scope.contextNode)
+        return false;
+      return ($scope.contextNode.fixed & 1);
+    };
+
+    let mouseX, mouseY;
+    var relativeMouse = function () {
+      let offset = $('#main_container').offset();
+      return {left: (mouseX + $(document).scrollLeft()) - 1,
+        top: (mouseY  + $(document).scrollTop()) - 1,
+        offset: offset
       };
-      $scope.isFixed = function() {
-        if (!$scope.contextNode)
-          return false;
-        return ($scope.contextNode.fixed & 1);
-      };
-
-      let mouseX, mouseY;
-      var relativeMouse = function () {
-        let offset = $('#main_container').offset();
-        return {left: (mouseX + $(document).scrollLeft()) - 1,
-          top: (mouseY  + $(document).scrollTop()) - 1,
-          offset: offset
-        };
-      };
-      // event handlers for popup context menu
-      $(document).mousemove(function(e) {
-        mouseX = e.clientX;
-        mouseY = e.clientY;
-      });
-      $(document).mousemove();
-      $(document).click(function() {
-        $scope.contextNode = null;
-        $('.contextMenu').fadeOut(200);
-      });
-
-      const radii = {
-        'inter-router': 25,
-        'normal': 15,
-        'on-demand': 15,
-        'route-container': 15,
-      };
-      let svg, lsvg;
-      let force;
-      let animate = false; // should the force graph organize itself when it is displayed
-      let path, circle;
-      let savedKeys = {};
-      let width = 0;
-      let height = 0;
-
-      var getSizes = function() {
-        const gap = 5;
-        let legendWidth = 194;
-        let topoWidth = $('#topology').width();
-        if (topoWidth < 768)
-          legendWidth = 0;
-        let width = $('#topology').width() - gap - legendWidth;
-        let top = $('#topology').offset().top;
-        let height = window.innerHeight - top - gap;
-        if (width < 10) {
-          QDR.log.info('page width and height are abnormal w:' + width + ' height:' + height);
-          return [0, 0];
-        }
-        return [width, height];
-      };
-      var resize = function() {
-        if (!svg)
-          return;
-        let sizes = getSizes();
-        width = sizes[0];
-        height = sizes[1];
-        if (width > 0) {
-          // set attrs and 'resume' force
-          svg.attr('width', width);
-          svg.attr('height', height);
-          force.size(sizes).resume();
-        }
-        $timeout(createLegend);
-      };
-
-      // the window is narrow and the page menu icon was clicked.
-      // Re-create the legend
-      $scope.$on('pageMenuClicked', function () {
-        $timeout(createLegend);
-      });
-
-      window.addEventListener('resize', resize);
+    };
+    // event handlers for popup context menu
+    $(document).mousemove(e => {
+      mouseX = e.clientX;
+      mouseY = e.clientY;
+    });
+    $(document).mousemove();
+    $(document).click(function() {
+      $scope.contextNode = null;
+      $('.contextMenu').fadeOut(200);
+    });
+
+    const radii = {
+      'inter-router': 25,
+      'normal': 15,
+      'on-demand': 15,
+      'route-container': 15,
+    };
+    let svg, lsvg;  // main svg and legend svg
+    let force;
+    let animate = false; // should the force graph organize itself when it is displayed
+    let path, circle;
+    let savedKeys = {};
+    let width = 0;
+    let height = 0;
+
+    var getSizes = function() {
+      const gap = 5;
+      let legendWidth = 194;
+      let topoWidth = $('#topology').width();
+      if (topoWidth < 768)
+        legendWidth = 0;
+      let width = $('#topology').width() - gap - legendWidth;
+      let top = $('#topology').offset().top;
+      let height = window.innerHeight - top - gap;
+      if (width < 10) {
+        QDRLog.info(`page width and height are abnormal w: ${width} h: ${height}`);
+        return [0, 0];
+      }
+      return [width, height];
+    };
+    var resize = function() {
+      if (!svg)
+        return;
       let sizes = getSizes();
       width = sizes[0];
       height = sizes[1];
-      if (width <= 0 || height <= 0)
-        return;
+      if (width > 0) {
+        // set attrs and 'resume' force
+        svg.attr('width', width);
+        svg.attr('height', height);
+        force.size(sizes).resume();
+      }
+      $timeout(createLegend);
+    };
+
+    // the window is narrow and the page menu icon was clicked.
+    // Re-create the legend
+    $scope.$on('pageMenuClicked', function () {
+      $timeout(createLegend);
+    });
+
+    window.addEventListener('resize', resize);
+    let sizes = getSizes();
+    width = sizes[0];
+    height = sizes[1];
+    if (width <= 0 || height <= 0)
+      return;
+
+    // vary the following force graph attributes based on nodeCount
+    // <= 6 routers returns min, >= 80 routers returns max, interpolate linearly
+    var forceScale = function(nodeCount, min, max) {
+      let count = Math.max(Math.min(nodeCount, 80), 6);
+      let x = d3.scale.linear()
+        .domain([6,80])
+        .range([min, max]);
+      //QDRLog.debug("forceScale(" + nodeCount + ", " + min + ", " + max + "  returns " + x(count) + " " + x(nodeCount))
+      return x(count);
+    };
+    var linkDistance = function (d, nodeCount) {
+      if (d.target.nodeType === 'inter-router')
+        return forceScale(nodeCount, 150, 70);
+      return forceScale(nodeCount, 75, 40);
+    };
+    var charge = function (d, nodeCount) {
+      if (d.nodeType === 'inter-router')
+        return forceScale(nodeCount, -1800, -900);
+      return -900;
+    };
+    var gravity = function (d, nodeCount) {
+      return forceScale(nodeCount, 0.0001, 0.1);
+    };
+    // initialize the nodes and links array from the QDRService.topology._nodeInfo object
+    var initForceGraph = function() {
+      forceData.nodes = nodes = new Nodes(QDRService, QDRLog);
+      forceData.links = links = new Links(QDRService, QDRLog);
+      let nodeInfo = QDRService.management.topology.nodeInfo();
+      let nodeCount = Object.keys(nodeInfo).length;
+
+      let oldSelectedNode = selected_node;
+      let oldMouseoverNode = mouseover_node;
+      mouseover_node = null;
+      selected_node = null;
+      selected_link = null;
+
+      nodes.savePositions();
+      d3.select('#SVG_ID').remove();
+      svg = d3.select('#topology')
+        .append('svg')
+        .attr('id', 'SVG_ID')
+        .attr('width', width)
+        .attr('height', height);
+
+      // 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( ${(radii['inter-router'] + 2)},${(radii['inter-router'] + 2)})`)
+        .selectAll('g');
 
-      var nodeExists = function (connectionContainer) {
-        return nodes.findIndex( function (node) {
-          return node.container === connectionContainer;
-        });
-      };
-      var normalExists = function (connectionContainer) {
-        let normalInfo = {};
-        for (let i=0; i<nodes.length; ++i) {
-          if (nodes[i].normals) {
-            if (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;
-      };
-      var getLinkSource = function (nodesIndex) {
-        for (let i=0; i<links.length; ++i) {
-          if (links[i].target === nodesIndex)
-            return i;
-        }
-        return -1;
-      };
-      var aNode = function(id, name, nodeType, nodeInfo, nodeIndex, x, y, connectionContainer, resultIndex, fixed, properties) {
-        properties = properties || {};
-        for (let i=0; i<nodes.length; ++i) {
-          if (nodes[i].name === name || nodes[i].container === connectionContainer) {
-            if (properties.product)
-              nodes[i].properties = properties;
-            return nodes[i];
+      // mouse event vars
+      mousedown_link = null;
+      mousedown_node = null;
+      mouseup_node = null;
+
+      // initialize the list of nodes
+      forceData.nodes = nodes = new Nodes(QDRService, QDRLog);
+      animate = nodes.initialize(nodeInfo, localStorage, width, height);
+      nodes.savePositions();
+
+      // initialize the list of links
+      let unknowns = [];
+      forceData.links = links = new Links(QDRService, QDRLog);
+      if (links.initializeLinks(nodeInfo, nodes, unknowns, localStorage, height)) {
+        animate = true;
+      }
+      $scope.schema = QDRService.management.schema();
+      // init D3 force layout
+      force = d3.layout.force()
+        .nodes(nodes.nodes)
+        .links(links.links)
+        .size([width, height])
+        .linkDistance(function(d) { return linkDistance(d, nodeCount); })
+        .charge(function(d) { return charge(d, nodeCount); })
+        .friction(.10)
+        .gravity(function(d) { return gravity(d, nodeCount); })
+        .on('tick', tick)
+        .on('end', function () {nodes.savePositions();})
+        .start();
+
+      // This section adds in the arrows
+      svg.append('svg:defs').attr('class', 'marker-defs').selectAll('marker')
+        .data(['end-arrow', 'end-arrow-selected', 'end-arrow-small', 'end-arrow-highlighted', 
+          'start-arrow', 'start-arrow-selected', 'start-arrow-small', 'start-arrow-highlighted'])
+        .enter().append('svg:marker') 
+        .attr('id', function (d) { return d; })
+        .attr('viewBox', '0 -5 10 10')
+        .attr('refX', function (d) { 
+          if (d.substr(0, 3) === 'end') {
+            return 24;
           }
-        }
-        let routerId = QDRService.management.topology.nameFromId(id);
-        return {
-          key: id,
-          name: name,
-          nodeType: nodeType,
-          properties: properties,
-          routerId: routerId,
-          x: x,
-          y: y,
-          id: nodeIndex,
-          resultIndex: resultIndex,
-          fixed: !!+fixed,
-          cls: '',
-          container: connectionContainer
-        };
-      };
-
-      var getLinkDir = function (id, connection, onode) {
-        let links = onode['router.link'];
-        if (!links) {
-          return 'unknown';
-        }
-        let inCount = 0, outCount = 0;
-        links.results.forEach( function (linkResult) {
-          let link = QDRService.utilities.flatten(links.attributeNames, linkResult);
-          if (link.linkType === 'endpoint' && link.connectionId === connection.identity)
-            if (link.linkDir === 'in')
-              ++inCount;
-            else
-              ++outCount;
-        });
-        if (inCount > 0 && outCount > 0)
-          return 'both';
-        if (inCount > 0)
-          return 'in';
-        if (outCount > 0)
-          return 'out';
-        return 'unknown';
-      };
-
-      var savePositions = function () {
-        nodes.forEach( function (d) {
-          localStorage[d.name] = angular.toJson({
-            x: Math.round(d.x),
-            y: Math.round(d.y),
-            fixed: (d.fixed & 1) ? 1 : 0,
-          });
+          return d !== 'start-arrow-small' ? -14 : -24;})
+        .attr('markerWidth', 4)
+        .attr('markerHeight', 4)
+        .attr('orient', 'auto')
+        .classed('small', function (d) {return d.indexOf('small') > -1;})
+        .append('svg:path')
+        .attr('d', function (d) {
+          return d.substr(0, 3) === 'end' ? 'M 0 -5 L 10 0 L 0 5 z' : 'M 10 -5 L 0 0 L 10 5 z';
         });
-      };
-
-      var initializeNodes = function (nodeInfo) {
-        let nodeCount = Object.keys(nodeInfo).length;
-        let yInit = 50;
-        forceData.nodes = nodes = [];
-        for (let id in nodeInfo) {
-          let name = QDRService.management.topology.nameFromId(id);
-          // if we have any new nodes, animate the force graph to position them
-          let position = angular.fromJson(localStorage[name]);
-          if (!angular.isDefined(position)) {
-            animate = true;
-            position = {
-              x: Math.round(width / 4 + ((width / 2) / nodeCount) * nodes.length),
-              y: Math.round(height / 2 + Math.sin(nodes.length / (Math.PI*2.0)) * height / 4),
-              fixed: false,
-            };
-            //QDR.log.debug("new node pos (" + position.x + ", " + position.y + ")")
-          }
-          if (position.y > height) {
-            position.y = 200 - yInit;
-            yInit *= -1;
-          }
-          nodes.push(aNode(id, name, 'inter-router', nodeInfo, nodes.length, position.x, position.y, name, undefined, position.fixed));
-        }
-      };
 
-      var initializeLinks = function (nodeInfo, unknowns) {
-        forceData.links = links = [];
-        let source = 0;
-        let client = 1.0;
-        for (let id in nodeInfo) {
-          let onode = nodeInfo[id];
-          if (!onode['connection'])
-            continue;
-          let conns = onode['connection'].results;
-          let attrs = onode['connection'].attributeNames;
-          //QDR.log.debug("external client parent is " + parent);
-          let normalsParent = {}; // 1st normal node for this parent
-
-          for (let j = 0; j < conns.length; j++) {
-            let connection = QDRService.utilities.flatten(attrs, conns[j]);
-            let role = connection.role;
-            let properties = connection.properties || {};
-            let dir = connection.dir;
-            if (role == 'inter-router') {
-              let connId = connection.container;
-              let target = getContainerIndex(connId, nodeInfo);
-              if (target >= 0) {
-                getLink(source, target, dir, '', source + '-' + target);
-              }
-            } /* else if (role == "normal" || role == "on-demand" || role === "route-container")*/ {
-              // not an connection between routers, but an external connection
-              let name = QDRService.management.topology.nameFromId(id) + '.' + connection.identity;
-
-              // if we have any new clients, animate the force graph to position them
-              let position = angular.fromJson(localStorage[name]);
-              if (!angular.isDefined(position)) {
-                animate = true;
-                position = {
-                  x: Math.round(nodes[source].x + 40 * Math.sin(client / (Math.PI * 2.0))),
-                  y: Math.round(nodes[source].y + 40 * Math.cos(client / (Math.PI * 2.0))),
-                  fixed: false
-                };
-                //QDR.log.debug("new client pos (" + position.x + ", " + position.y + ")")
-              }// else QDR.log.debug("using previous location")
-              if (position.y > height) {
-                position.y = Math.round(nodes[source].y + 40 + Math.cos(client / (Math.PI * 2.0)));
-              }
-              let existingNodeIndex = nodeExists(connection.container);
-              let normalInfo = normalExists(connection.container);
-              let node = aNode(id, name, role, nodeInfo, nodes.length, position.x, position.y, connection.container, j, position.fixed, properties);
-              let nodeType = QDRService.utilities.isAConsole(properties, connection.identity, role, node.key) ? 'console' : 'client';
-              let cdir = getLinkDir(id, connection, onode);
-              if (existingNodeIndex >= 0) {
-                // make a link between the current router (source) and the existing node
-                getLink(source, existingNodeIndex, dir, 'small', connection.name);
-              } else if (normalInfo.nodesIndex) {
-                // get node index of node that contained this connection in its normals array
-                let normalSource = getLinkSource(normalInfo.nodesIndex);
-                if (normalSource >= 0) {
-                  if (cdir === 'unknown')
-                    cdir = dir;
-                  node.cdir = cdir;
-                  nodes.push(node);
-                  // create link from original node to the new node
-                  getLink(links[normalSource].source, nodes.length-1, cdir, 'small', connection.name);
-                  // create link from this router to the new node
-                  getLink(source, nodes.length-1, cdir, 'small', connection.name);
-                  // remove the old node from the normals list
-                  nodes[normalInfo.nodesIndex].normals.splice(normalInfo.normalsIndex, 1);
-                }
-              } else if (role === 'normal') {
-              // normal nodes can be collapsed into a single node if they are all the same dir
-                if (cdir !== 'unknown') {
-                  node.user = connection.user;
-                  node.isEncrypted = connection.isEncrypted;
-                  node.host = connection.host;
-                  node.connectionId = connection.identity;
-                  node.cdir = cdir;
-                  // determine arrow direction by using the link directions
-                  if (!normalsParent[nodeType+cdir]) {
-                    normalsParent[nodeType+cdir] = node;
-                    nodes.push(node);
-                    node.normals = [node];
-                    // now add a link
-                    getLink(source, nodes.length - 1, cdir, 'small', connection.name);
-                    client++;
-                  } else {
-                    normalsParent[nodeType+cdir].normals.push(node);
-                  }
-                } else {
-                  node.id = nodes.length - 1 + unknowns.length;
-                  unknowns.push(node);
-                }
-              } else {
-                nodes.push(node);
-                // now add a link
-                getLink(source, nodes.length - 1, dir, 'small', connection.name);
-                client++;
-              }
-            }
-          }
-          source++;
-        }
-      };
-
-      // vary the following force graph attributes based on nodeCount
-      // <= 6 routers returns min, >= 80 routers returns max, interpolate linearly
-      var forceScale = function(nodeCount, min, max) {
-        let count = nodeCount;
-        if (nodeCount < 6) count = 6;
-        if (nodeCount > 80) count = 80;
-        let x = d3.scale.linear()
-          .domain([6,80])
-          .range([min, max]);
-        //QDR.log.debug("forceScale(" + nodeCount + ", " + min + ", " + max + "  returns " + x(count) + " " + x(nodeCount))
-        return x(count);
-      };
-      var linkDistance = function (d, nodeCount) {
-        if (d.target.nodeType === 'inter-router')
-          return forceScale(nodeCount, 150, 70);
-        return forceScale(nodeCount, 75, 40);
-      };
-      var charge = function (d, nodeCount) {
-        if (d.nodeType === 'inter-router')
-          return forceScale(nodeCount, -1800, -900);
-        return -900;
-      };
-      var gravity = function (d, nodeCount) {
-        return forceScale(nodeCount, 0.0001, 0.1);
-      };
-      // initialize the nodes and links array from the QDRService.topology._nodeInfo object
-      var initForceGraph = function() {
-        forceData.nodes = nodes = [];
-        forceData.links = links = [];
-        let nodeInfo = QDRService.management.topology.nodeInfo();
-        let nodeCount = Object.keys(nodeInfo).length;
-
-        let oldSelectedNode = selected_node;
-        let oldMouseoverNode = mouseover_node;
-        mouseover_node = null;
-        selected_node = null;
-        selected_link = null;
-
-        savePositions();
-        d3.select('#SVG_ID').remove();
-        svg = d3.select('#topology')
-          .append('svg')
-          .attr('id', 'SVG_ID')
-          .attr('width', width)
-          .attr('height', height);
-
-        // 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(' + (radii['inter-router'] + 2) + ',' + (radii['inter-router'] + 2) + ')')
-          .selectAll('g');
-
-        // mouse event vars
-        mousedown_link = null;
-        mousedown_node = null;
-        mouseup_node = null;
-
-        // initialize the list of nodes
-        initializeNodes(nodeInfo);
-        savePositions();
-
-        // initialize the list of links
-        let unknowns = [];
-        initializeLinks(nodeInfo, unknowns);
-        $scope.schema = QDRService.management.schema();
-        // init D3 force layout
-        force = d3.layout.force()
-          .nodes(nodes)
-          .links(links)
-          .size([width, height])
-          .linkDistance(function(d) { return linkDistance(d, nodeCount); })
-          .charge(function(d) { return charge(d, nodeCount); })
-          .friction(.10)
-          .gravity(function(d) { return gravity(d, nodeCount); })
-          .on('tick', tick)
-          .on('end', function () {savePositions();})
-          .start();
-
-        // This section adds in the arrows
-        svg.append('svg:defs').attr('class', 'marker-defs').selectAll('marker')
-          .data(['end-arrow', 'end-arrow-selected', 'end-arrow-small', 'end-arrow-highlighted', 
-            'start-arrow', 'start-arrow-selected', 'start-arrow-small', 'start-arrow-highlighted'])
-          .enter().append('svg:marker') 
-          .attr('id', function (d) { return d; })
-          .attr('viewBox', '0 -5 10 10')
-          .attr('refX', function (d) { 
-            if (d.substr(0, 3) === 'end') {
-              return 24;
-            }
-            return d !== 'start-arrow-small' ? -14 : -24;})
-          .attr('markerWidth', 4)
-          .attr('markerHeight', 4)
-          .attr('orient', 'auto')
-          .classed('small', function (d) {return d.indexOf('small') > -1;})
-          .append('svg:path')
-          .attr('d', function (d) {
-            return d.substr(0, 3) === 'end' ? 'M 0 -5 L 10 0 L 0 5 z' : 'M 10 -5 L 0 0 L 10 5 z';
-          });
-
-        // gradient for sender/receiver client
-        let grad = svg.append('svg:defs').append('linearGradient')
-          .attr('id', 'half-circle')
-          .attr('x1', '0%')
-          .attr('x2', '0%')
-          .attr('y1', '100%')
-          .attr('y2', '0%');
-        grad.append('stop').attr('offset', '50%').style('stop-color', '#C0F0C0');
-        grad.append('stop').attr('offset', '50%').style('stop-color', '#F0F000');
-
-        // handles to link and node element groups
-        path = svg.append('svg:g').selectAll('path'),
-        circle = svg.append('svg:g').selectAll('g');
-
-        // app starts here
-        restart(false);
-        force.start();
-        if (oldSelectedNode) {
-          d3.selectAll('circle.inter-router').classed('selected', function (d) {
-            if (d.key === oldSelectedNode.key) {
-              selected_node = d;
-              return true;
-            }
-            return false;
-          });
-        }
-        if (oldMouseoverNode && selected_node) {
-          d3.selectAll('circle.inter-router').each(function (d) {
-            if (d.key === oldMouseoverNode.key) {
-              mouseover_node = d;
-              QDRService.management.topology.ensureAllEntities([{entity: 'router.node', attrs: ['id','nextHop']}], function () {
-                nextHop(selected_node, d);
-                restart();
-              });
-            }
-          });
-        }
-
-        // if any clients don't yet have link directions, get the links for those nodes and restart the graph
-        if (unknowns.length > 0)
-          setTimeout(resolveUnknowns, 10, nodeInfo, unknowns);
-
-        var continueForce = function (extra) {
-          if (extra > 0) {
-            --extra;
-            force.start();
-            setTimeout(continueForce, 100, extra);
+      // gradient for sender/receiver client
+      let grad = svg.append('svg:defs').append('linearGradient')
+        .attr('id', 'half-circle')
+        .attr('x1', '0%')
+        .attr('x2', '0%')
+        .attr('y1', '100%')
+        .attr('y2', '0%');
+      grad.append('stop').attr('offset', '50%').style('stop-color', '#C0F0C0');
+      grad.append('stop').attr('offset', '50%').style('stop-color', '#F0F000');
+
+      // handles to link and node element groups
+      path = svg.append('svg:g').selectAll('path'),
+      circle = svg.append('svg:g').selectAll('g');
+
+      // app starts here
+      restart(false);
+      force.start();
+      if (oldSelectedNode) {
+        d3.selectAll('circle.inter-router').classed('selected', function (d) {
+          if (d.key === oldSelectedNode.key) {
+            selected_node = d;
+            return true;
           }
-        };
-        continueForce(forceScale(nodeCount, 0, 200));  // give graph time to settle down
-      };
-
-      var resolveUnknowns = function (nodeInfo, unknowns) {
-        let unknownNodes = {};
-        // collapse the unknown node.keys using an object
-        for (let i=0; i<unknowns.length; ++i) {
-          unknownNodes[unknowns[i].key] = 1;
-        }
-        unknownNodes = Object.keys(unknownNodes);
-        //QDR.log.info("-- resolveUnknowns: ensuring .connection and .router.link are present for each node")
-        QDRService.management.topology.ensureEntities(unknownNodes, [{entity: 'connection', force: true}, 
-          {entity: 'router.link', attrs: ['linkType','connectionId','linkDir'], force: true}], function () {
-          nodeInfo = QDRService.management.topology.nodeInfo();
-          initializeLinks(nodeInfo, []);
-          // collapse any router-container nodes that are duplicates
-          animate = true;
-          force.nodes(nodes).links(links).start();
-          restart(false);
+          return false;
         });
-      };
-
-      function getContainerIndex(_id, nodeInfo) {
-        let nodeIndex = 0;
-        for (let id in nodeInfo) {
-          if (QDRService.management.topology.nameFromId(id) === _id)
-            return nodeIndex;
-          ++nodeIndex;
-        }
-        return -1;
-      }
-
-      function getLink(_source, _target, dir, cls, uid) {
-        for (let i = 0; i < links.length; i++) {
-          let s = links[i].source,
-            t = links[i].target;
-          if (typeof links[i].source == 'object') {
-            s = s.id;
-            t = t.id;
-          }
-          if (s == _source && t == _target) {
-            return i;
-          }
-          // same link, just reversed
-          if (s == _target && t == _source) {
-            return -i;
-          }
-        }
-        //QDR.log.debug("creating new link (" + (links.length) + ") between " + nodes[_source].name + " and " + nodes[_target].name);
-        if (links.some( function (l) { return l.uid === uid;}))
-          uid = uid + '.' + links.length;
-        let link = {
-          source: _source,
-          target: _target,
-          left: dir != 'out',
-          right: (dir == 'out' || dir == 'both'),
-          cls: cls,
-          uid: uid,
-        };
-        return links.push(link) - 1;
       }
-
-
-      function resetMouseVars() {
-        mousedown_node = null;
-        mouseover_node = null;
-        mouseup_node = null;
-        mousedown_link = null;
-      }
-
-      // update force layout (called automatically each iteration)
-      function tick() {
-        circle.attr('transform', function(d) {
-          let cradius;
-          if (d.nodeType == 'inter-router') {
-            cradius = d.left ? radius + 8 : radius;
-          } else {
-            cradius = d.left ? radiusNormal + 18 : radiusNormal;
+      if (oldMouseoverNode && selected_node) {
+        d3.selectAll('circle.inter-router').each(function (d) {
+          if (d.key === oldMouseoverNode.key) {
+            mouseover_node = d;
+            QDRService.management.topology.ensureAllEntities([{entity: 'router.node', attrs: ['id','nextHop']}], function () {
+              nextHopHighlight(selected_node, d);
+              restart();
+            });
           }
-          d.x = Math.max(d.x, radiusNormal * 2);
-          d.y = Math.max(d.y, radiusNormal * 2);
-          d.x = Math.max(0, Math.min(width - cradius, d.x));
-          d.y = Math.max(0, Math.min(height - cradius, d.y));
-          return 'translate(' + d.x + ',' + d.y + ')';
         });
+      }
 
-        // draw directed edges with proper padding from node centers
-        path.attr('d', function(d) {
-          let sourcePadding, targetPadding, r;
-
-          r = d.target.nodeType === 'inter-router' ? radius : radiusNormal - 18;
-          sourcePadding = targetPadding = 0;
-          let dtx = Math.max(targetPadding, Math.min(width - r, d.target.x)),
-            dty = Math.max(targetPadding, Math.min(height - r, d.target.y)),
-            dsx = Math.max(sourcePadding, Math.min(width - r, d.source.x)),
-            dsy = Math.max(sourcePadding, Math.min(height - r, d.source.y));
-
-          let deltaX = dtx - dsx,
-            deltaY = dty - dsy,
-            dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
-          if (dist == 0)
-            dist = 0.001;
-          let normX = deltaX / dist,
-            normY = deltaY / dist;
-          let sourceX = dsx + (sourcePadding * normX),
-            sourceY = dsy + (sourcePadding * normY),
-            targetX = dtx - (targetPadding * normX),
-            targetY = dty - (targetPadding * normY);
-          sourceX = Math.max(0, sourceX);
-          sourceY = Math.max(0, sourceY);
-          targetX = Math.max(0, targetX);
-          targetY = Math.max(0, targetY);
-
-          return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY;
-        })
-          .attr('id', function (d) {
-            return ['path', d.source.index, d.target.index].join('-');
-          });
+      // if any clients don't yet have link directions, get the links for those nodes and restart the graph
+      if (unknowns.length > 0)
+        setTimeout(resolveUnknowns, 10, nodeInfo, unknowns);
 
-        if (!animate) {
-          animate = true;
-          force.stop();
+      var continueForce = function (extra) {
+        if (extra > 0) {
+          --extra;
+          force.start();
+          setTimeout(continueForce, 100, extra);
         }
+      };
+      continueForce(forceScale(nodeCount, 0, 200));  // give large graphs time to settle down
+    };
+
+    // To start up quickly, we only get the connection info for each router.
+    // That means we don't have the router.link info when links.initialize() is first called.
+    // The router.link info is needed to determine which direction the arrows between routers should point.
+    // So, the first time through links.initialize() we keep track of the nodes for which we 
+    // need router.link info and fill in that info here.
+    var resolveUnknowns = function (nodeInfo, unknowns) {
+      let unknownNodes = {};
+      // collapse the unknown node.keys using an object
+      for (let i=0; i<unknowns.length; ++i) {
+        unknownNodes[unknowns[i].key] = 1;
       }
+      unknownNodes = Object.keys(unknownNodes);
+      //QDRLog.info("-- resolveUnknowns: ensuring .connection and .router.link are present for each node")
+      QDRService.management.topology.ensureEntities(unknownNodes, [{entity: 'connection', force: true}, 
+        {entity: 'router.link', attrs: ['linkType','connectionId','linkDir'], force: true}], function () {
+        nodeInfo = QDRService.management.topology.nodeInfo();
+        forceData.links = links = new Links(QDRService, QDRLog);
+        links.initializeLinks(nodeInfo, nodes, [], localStorage, height);
+        animate = true;
+        force.nodes(nodes.nodes).links(links.links).start();
+        restart(false);
+      });
+    };
 
-      // highlight the paths between the selected node and the hovered node
-      function findNextHopNode(from, d) {
-        // d is the node that the mouse is over
-        // from is the selected_node ....
-        if (!from)
-          return null;
-
-        if (from == d)
-          return selected_node;
-
-        //QDR.log.debug("finding nextHop from: " + from.name + " to " + d.name);
-        let sInfo = QDRService.management.topology.nodeInfo()[from.key];
+    function resetMouseVars() {
+      mousedown_node = null;
+      mouseover_node = null;
+      mouseup_node = null;
+      mousedown_link = null;
+    }
 
-        if (!sInfo) {
-          QDR.log.warn('unable to find topology node info for ' + from.key);
-          return null;
+    // update force layout (called automatically each iteration)
+    function tick() {
+      circle.attr('transform', function(d) {
+        let cradius;
+        if (d.nodeType == 'inter-router') {
+          cradius = d.left ? radius + 8 : radius;
+        } else {
+          cradius = d.left ? radiusNormal + 18 : radiusNormal;
         }
+        d.x = Math.max(d.x, radiusNormal * 2);
+        d.y = Math.max(d.y, radiusNormal * 2);
+        d.x = Math.max(0, Math.min(width - cradius, d.x));
+        d.y = Math.max(0, Math.min(height - cradius, d.y));
+        return `translate(${d.x},${d.y})`;
+      });
 
-        // find the hovered name in the selected name's .router.node results
-        if (!sInfo['router.node'])
-          return null;
-        let aAr = sInfo['router.node'].attributeNames;
-        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) {
-            //QDR.log.debug("found " + d.name + " at " + hIdx);
-            let nextHop = QDRService.utilities.valFor(aAr, vAr[hIdx], 'nextHop');
-            //QDR.log.debug("nextHop was " + nextHop);
-            return (nextHop == null) ? nodeFor(addrT) : nodeFor(nextHop);
-          }
-        }
-        return null;
-      }
+      // draw directed edges with proper padding from node centers
+      path.attr('d', function(d) {
+        let sourcePadding, targetPadding, r;
+
+        r = d.target.nodeType === 'inter-router' ? radius : radiusNormal - 18;
+        sourcePadding = targetPadding = 0;
+        let dtx = Math.max(targetPadding, Math.min(width - r, d.target.x)),
+          dty = Math.max(targetPadding, Math.min(height - r, d.target.y)),
+          dsx = Math.max(sourcePadding, Math.min(width - r, d.source.x)),
+          dsy = Math.max(sourcePadding, Math.min(height - r, d.source.y));
+
+        let deltaX = dtx - dsx,
+          deltaY = dty - dsy,
+          dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
+        if (dist == 0)
+          dist = 0.001;
+        let normX = deltaX / dist,
+          normY = deltaY / dist;
+        let sourceX = dsx + (sourcePadding * normX),
+          sourceY = dsy + (sourcePadding * normY),
+          targetX = dtx - (targetPadding * normX),
+          targetY = dty - (targetPadding * normY);
+        sourceX = Math.max(0, sourceX);
+        sourceY = Math.max(0, sourceY);
+        targetX = Math.max(0, targetX);
+        targetY = Math.max(0, targetY);
+
+        return `M${sourceX},${sourceY}L${targetX},${targetY}`;
+      })
+        .attr('id', function (d) {
+          return ['path', d.source.index, d.target.index].join('-');
+        });
 
-      function nodeFor(name) {
-        for (let i = 0; i < nodes.length; ++i) {
-          if (nodes[i].name == name)
-            return nodes[i];
-        }
-        return null;
+      if (!animate) {
+        animate = true;
+        force.stop();
       }
+    }
 
-      function linkFor(source, target) {
-        for (let i = 0; i < links.length; ++i) {
-          if ((links[i].source == source) && (links[i].target == target))
-            return links[i];
-          if ((links[i].source == target) && (links[i].target == source))
-            return links[i];
-        }
-        // the selected node was a client/broker
-        return null;
-      }
+    function nextHopHighlight(selected_node, d) {
+      nextHop(selected_node, d, nodes, links, QDRService, selected_node, function (hlLink, hnode) {
+        hlLink.highlighted = true;
+        hnode.highlighted = true;
+      });
+      let hnode = nodes.nodeFor(d.name);
+      hnode.highlighted = true;
+    }
 
-      function clearPopups() {
-        d3.select('#crosssection').style('display', 'none');
-        $('.hastip').empty();
-        d3.select('#multiple_details').style('display', 'none');
-        d3.select('#link_details').style('display', 'none');
-        d3.select('#node_context_menu').style('display', 'none');
+    function clearPopups() {
+      d3.select('#crosssection').style('display', 'none');
+      $('.hastip').empty();
+      d3.select('#multiple_details').style('display', 'none');
+      d3.select('#link_details').style('display', 'none');
+      d3.select('#node_context_menu').style('display', 'none');
 
-      }
+    }
 
-      function clerAllHighlights() {
-        for (let i = 0; i < links.length; ++i) {
-          links[i]['highlighted'] = false;
-        }
-        for (let i = 0; i<nodes.length; ++i) {
-          nodes[i]['highlighted'] = false;
-        }
-      }
-      // takes the nodes and links array of objects and adds svg elements for everything that hasn't already
-      // been added
-      function restart(start) {
-        if (!circle)
-          return;
-        circle.call(force.drag);
-
-        // path (link) group
-        path = path.data(links, function(d) {return d.uid;});
-
-        // update existing links
-        path.classed('selected', function(d) {
-          return d === selected_link;
-        })
-          .classed('highlighted', function(d) {
-            return d.highlighted;
-          });
-        if (!$scope.legend.status.optionsOpen || $scope.legendOptions.trafficType === 'dots') {
-          path
-            .attr('marker-start', function(d) {
-              let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
-              if (d.highlighted)
-                sel = '-highlighted';
-              return d.left ? 'url(' + urlPrefix + '#start-arrow' + sel + ')' : '';
-            })
-            .attr('marker-end', function(d) {
-              let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
-              if (d.highlighted)
-                sel = '-highlighted';
-              return d.right ? 'url(' + urlPrefix + '#end-arrow' + sel + ')' : '';
-            });
-        }
-        // add new links. if a link with a new uid is found in the data, add a new path
-        path.enter().append('svg:path')
-          .attr('class', 'link')
+    function clearAllHighlights() {
+      links.clearHighlighted();
+      nodes.clearHighlighted();
+    }
+    // takes the nodes and links array of objects and adds svg elements for everything that hasn't already
+    // been added
+    function restart(start) {
+      if (!circle)
+        return;
+      circle.call(force.drag);
+
+      // path (link) group
+      path = path.data(links.links, function(d) {return d.uid;});
+
+      // update existing links
+      path.classed('selected', function(d) {
+        return d === selected_link;
+      })
+        .classed('highlighted', function(d) {
+          return d.highlighted;
+        });
+      if (!$scope.legend.status.optionsOpen || $scope.legendOptions.trafficType === 'dots') {
+        path
           .attr('marker-start', function(d) {
             let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
-            return d.left ? 'url(' + urlPrefix + '#start-arrow' + sel + ')' : '';
+            if (d.highlighted)
+              sel = '-highlighted';
+            return d.left ? `url(${urlPrefix}#start-arrow${sel})` : '';
           })
           .attr('marker-end', function(d) {
             let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
-            return d.right ? 'url(' + urlPrefix + '#end-arrow' + sel + ')' : '';
-          })
-          .classed('small', function(d) {
-            return d.cls == 'small';
-          })
-          .on('mouseover', function(d) { // mouse over a path
-            let event = d3.event;
-            mousedown_link = d;
-            selected_link = mousedown_link;
-            let updateTooltip = function () {
-              $timeout(function () {
-                $scope.trustedpopoverContent = $sce.trustAsHtml(connectionPopupHTML(d));
-                if (selected_link)
-                  displayTooltip(event);
-              });
-            };
-            // update the contents of the popup tooltip each time the data is polled
-            QDRService.management.topology.addUpdatedAction('connectionPopupHTML', updateTooltip);
-            QDRService.management.topology.ensureAllEntities(
-              [{ entity: 'router.link', force: true},{entity: 'connection'}], function () {
-                updateTooltip();
-              });
-            // show the tooltip
-            updateTooltip();
-            restart();
-
-          })
-          .on('mouseout', function() { // mouse out of a path
-            QDRService.management.topology.delUpdatedAction('connectionPopupHTML');
-            d3.select('#popover-div')
-              .style('display', 'none');
-            selected_link = null;
-            restart();
-          })
-          // left click a path
-          .on('click', function () {
-            d3.event.stopPropagation();
-            clearPopups();
+            if (d.highlighted)
+              sel = '-highlighted';
+            return d.right ? `url(${urlPrefix}#end-arrow${sel})` : '';
           });
-        // remove old links
-        path.exit().remove();
+      }
+      // add new links. if a link with a new uid is found in the data, add a new path
+      path.enter().append('svg:path')
+        .attr('class', 'link')
+        .attr('marker-start', function(d) {
+          let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
+          return d.left ? `url(${urlPrefix}#start-arrow${sel})` : '';
+        })
+        .attr('marker-end', function(d) {
+          let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
+          return d.right ? `url(${urlPrefix}#end-arrow${sel})` : '';
+        })
+        .classed('small', function(d) {
+          return d.cls == 'small';
+        })
+        .on('mouseover', function(d) { // mouse over a path
+          let event = d3.event;
+          mousedown_link = d;
+          selected_link = mousedown_link;
+          let updateTooltip = function () {
+            $timeout(function () {
+              $scope.trustedpopoverContent = $sce.trustAsHtml(connectionPopupHTML(d, QDRService));
+              if (selected_link)
+                displayTooltip(event);
+            });
+          };
+          // update the contents of the popup tooltip each time the data is polled
+          QDRService.management.topology.addUpdatedAction('connectionPopupHTML', updateTooltip);
+          QDRService.management.topology.ensureAllEntities(
+            [{ entity: 'router.link', force: true},{entity: 'connection'}], function () {
+              updateTooltip();
+            });
+          // show the tooltip
+          updateTooltip();
+          restart();
+
+        })
+        .on('mouseout', function() { // mouse out of a path
+          QDRService.management.topology.delUpdatedAction('connectionPopupHTML');
+          d3.select('#popover-div')
+            .style('display', 'none');
+          selected_link = null;
+          restart();
+        })
+        // left click a path
+        .on('click', function () {
+          d3.event.stopPropagation();
+          clearPopups();
+        });
+      // remove old links
+      path.exit().remove();
 
 
-        // circle (node) group
-        // nodes are known by id
-        circle = circle.data(nodes, function(d) {
-          return d.name;
+      // circle (node) group
+      // nodes are known by id
+      circle = circle.data(nodes.nodes, function(d) {
+        return d.name;
+      });
+
+      // update existing nodes visual states
+      circle.selectAll('circle')
+        .classed('highlighted', function(d) {
+          return d.highlighted;
+        })
+        .classed('selected', function(d) {
+          return (d === selected_node);
+        })
+        .classed('fixed', function(d) {
+          return d.fixed & 1;
         });
 
-        // update existing nodes visual states
-        circle.selectAll('circle')
-          .classed('highlighted', function(d) {
-            return d.highlighted;
-          })
-          .classed('selected', function(d) {
-            return (d === selected_node);
-          })
-          .classed('fixed', function(d) {
-            return d.fixed & 1;
+      // add new circle nodes
+      let g = circle.enter().append('svg:g')
+        .classed('multiple', function(d) {
+          return (d.normals && d.normals.length > 1);
+        })
+        .attr('id', function (d) { return (d.nodeType !== 'normal' ? 'router' : 'client') + '-' + d.index; });
+
+      appendCircle(g)
+        .on('mouseover', function(d) {  // mouseover a circle
+          QDRService.management.topology.delUpdatedAction('connectionPopupHTML');
+          if (d.nodeType === 'normal') {
+            showClientTooltip(d, d3.event);
+          } else
+            showRouterTooltip(d, d3.event);
+          if (d === mousedown_node)
+            return;
+          // enlarge target node
+          d3.select(this).attr('transform', 'scale(1.1)');
+          if (!selected_node) {
+            return;
+          }
+          // highlight the next-hop route from the selected node to this node
+          clearAllHighlights();
+          // we need .router.node info to highlight hops
+          QDRService.management.topology.ensureAllEntities([{entity: 'router.node', attrs: ['id','nextHop']}], function () {
+            mouseover_node = d;  // save this node in case the topology changes so we can restore the highlights
+            nextHopHighlight(selected_node, d);
+            restart();
           });
-
-        // add new circle nodes
-        let g = circle.enter().append('svg:g')
-          .classed('multiple', function(d) {
-            return (d.normals && d.normals.length > 1);
-          })
-          .attr('id', function (d) { return (d.nodeType !== 'normal' ? 'router' : 'client') + '-' + d.index; });
-
-        appendCircle(g)
-          .on('mouseover', function(d) {  // mouseover a circle
-            QDRService.management.topology.delUpdatedAction('connectionPopupHTML');
-            if (d.nodeType === 'normal') {
-              showClientTooltip(d, d3.event);
-            } else
-              showRouterTooltip(d, d3.event);
-            if (d === mousedown_node)
-              return;
-            // enlarge target node
-            d3.select(this).attr('transform', 'scale(1.1)');
-            if (!selected_node) {
-              return;
-            }
-            // highlight the next-hop route from the selected node to this node
-            clerAllHighlights();
-            // we need .router.node info to highlight hops
-            QDRService.management.topology.ensureAllEntities([{entity: 'router.node', attrs: ['id','nextHop']}], function () {
-              mouseover_node = d;  // save this node in case the topology changes so we can restore the highlights
-              nextHop(selected_node, d);
-              restart();
-            });
-          })
-          .on('mouseout', function() { // mouse out for a circle
-            // unenlarge target node
-            d3.select('#popover-div')
-              .style('display', 'none');
-            d3.select(this).attr('transform', '');
-            clerAllHighlights();
-            mouseover_node = null;
+        })
+        .on('mouseout', function() { // mouse out for a circle
+          // unenlarge target node
+          d3.select('#popover-div')
+            .style('display', 'none');
+          d3.select(this).attr('transform', '');
+          clearAllHighlights();
+          mouseover_node = null;
+          restart();
+        })
+        .on('mousedown', function(d) { // mouse down for circle
+          if (d3.event.button !== 0) { // ignore all but left button
+            return;
+          }
+          mousedown_node = d;
+          // mouse position relative to svg
+          initial_mouse_down_position = d3.mouse(this.parentNode.parentNode.parentNode).slice();
+        })
+        .on('mouseup', function(d) {  // mouse up for circle
+          if (!mousedown_node)
+            return;
+
+          selected_link = null;
+          // unenlarge target node
+          d3.select(this).attr('transform', '');
+
+          // check for drag
+          mouseup_node = d;
+
+          let mySvg = this.parentNode.parentNode.parentNode;
+          // if we dragged the node, make it fixed
+          let cur_mouse = d3.mouse(mySvg);
+          if (cur_mouse[0] != initial_mouse_down_position[0] ||
+            cur_mouse[1] != initial_mouse_down_position[1]) {
+            d.fixed = true;
+            nodes.setNodesFixed(d.name, true);
+            resetMouseVars();
             restart();
-          })
-          .on('mousedown', function(d) { // mouse down for circle
-            if (d3.event.button !== 0) { // ignore all but left button
-              return;
-            }
-            mousedown_node = d;
-            // mouse position relative to svg
-            initial_mouse_down_position = d3.mouse(this.parentNode.parentNode.parentNode).slice();
-          })
-          .on('mouseup', function(d) {  // mouse up for circle
-            if (!mousedown_node)
-              return;
-
-            selected_link = null;
-            // unenlarge target node
-            d3.select(this).attr('transform', '');
-
-            // check for drag
-            mouseup_node = d;
-
-            let mySvg = this.parentNode.parentNode.parentNode;
-            // if we dragged the node, make it fixed
-            let cur_mouse = d3.mouse(mySvg);
-            if (cur_mouse[0] != initial_mouse_down_position[0] ||
-              cur_mouse[1] != initial_mouse_down_position[1]) {
-              d.fixed = true;
-              setNodesFixed(d.name, true);
-              resetMouseVars();
-              restart();
-              return;
-            }
+            return;
+          }
 
-            // if this node was selected, unselect it
-            if (mousedown_node === selected_node) {
-              selected_node = null;
-            } else {
-              if (d.nodeType !== 'normal' && d.nodeType !== 'on-demand')
-                selected_node = mousedown_node;
-            }
-            clerAllHighlights();
-            mousedown_node = null;
-            if (!$scope.$$phase) $scope.$apply();
-            restart(false);
+          // if this node was selected, unselect it
+          if (mousedown_node === selected_node) {
+            selected_node = null;
+          } else {
+            if (d.nodeType !== 'normal' && d.nodeType !== 'on-demand')
+              selected_node = mousedown_node;
+          }
+          clearAllHighlights();
+          mousedown_node = null;
+          if (!$scope.$$phase) $scope.$apply();
+          restart(false);
 
-          })
-          .on('dblclick', function(d) { // circle
-            if (d.fixed) {
-              d.fixed = false;
-              setNodesFixed(d.name, false);
-              restart(); // redraw the node without a dashed line
-              force.start(); // let the nodes move to a new position
-            }
-          })
-          .on('contextmenu', function(d) {  // circle
-            $(document).click();
-            d3.event.preventDefault();
-            let rm = relativeMouse();
-            d3.select('#node_context_menu')
-              .style({
-                display: 'block',
-                left: rm.left + 'px',
-                top: (rm.top - rm.offset.top) + 'px'
-              });
-            $timeout( function () {
-              $scope.contextNode = d;
+        })
+        .on('dblclick', function(d) { // circle
+          if (d.fixed) {
+            d.fixed = false;
+            nodes.setNodesFixed(d.name, false);
+            restart(); // redraw the node without a dashed line
+            force.start(); // let the nodes move to a new position
+          }
+        })
+        .on('contextmenu', function(d) {  // circle
+          $(document).click();
+          d3.event.preventDefault();
+          let rm = relativeMouse();
+          d3.select('#node_context_menu')
+            .style({
+              display: 'block',
+              left: rm.left + 'px',
+              top: (rm.top - rm.offset.top) + 'px'
             });
-          })
-          .on('click', function(d) {  // circle
-            if (!mouseup_node)
-              return;
-            // clicked on a circle
-            clearPopups();
-            if (!d.normals) {
-              // circle was a router or a broker
-              if (QDRService.utilities.isArtemis(d)) {
-                const artemisPath = '/jmx/attributes?tab=artemis&con=Artemis';
-                if (QDR.isStandalone)
-                  window.location = $location.protocol() + '://localhost:8161/hawtio' + artemisPath;
-                else
-                  $location.path(artemisPath);
-              }
-              return;
-            }
-            d3.event.stopPropagation();
+          $timeout( function () {
+            $scope.contextNode = d;
           });
-
-        appendContent(g);
-        //appendTitle(g);
-
-        // remove old nodes
-        circle.exit().remove();
-
-        // add text to client circles if there are any that represent multiple clients
-        svg.selectAll('.subtext').remove();
-        let multiples = svg.selectAll('.multiple');
-        multiples.each(function(d) {
-          let g = d3.select(this);
-          g.append('svg:text')
-            .attr('x', radiusNormal + 3)
-            .attr('y', Math.floor(radiusNormal / 2))
-            .attr('class', 'subtext')
-            .text('x ' + d.normals.length);
+        })
+        .on('click', function(d) {  // circle
+          if (!mouseup_node)
+            return;
+          // clicked on a circle
+          clearPopups();
+          if (!d.normals) {
+            // circle was a router or a broker
+            if (QDRService.utilities.isArtemis(d)) {
+              const artemisPath = '/jmx/attributes?tab=artemis&con=Artemis';
+              window.location = $location.protocol() + '://localhost:8161/hawtio' + artemisPath;
+            }
+            return;
+          }
+          d3.event.stopPropagation();
         });
-        // call createLegend in timeout because:
-        // If we create the legend right away, then it will be destroyed when the accordian
-        // gets initialized as the page loads.
-        $timeout(createLegend);
 
-        if (!mousedown_node || !selected_node)
-          return;
+      appendContent(g);
+      //appendTitle(g);
 
-        if (!start)
-          return;
-        // set the graph in motion
-        //QDR.log.debug("mousedown_node is " + mousedown_node);
-        force.start();
+      // remove old nodes
+      circle.exit().remove();
 
-      }
-      let createLegend = function () {
-        // dynamically create the legend based on which node types are present
-        // 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(' + (radii['inter-router'] + 2) + ',' + (radii['inter-router'] + 2) + ')')
-          .selectAll('g');
-        let legendNodes = [];
-        legendNodes.push(aNode('Router', '', 'inter-router', '', undefined, 0, 0, 0, 0, false, {}));
-
-        if (!svg.selectAll('circle.console').empty()) {
-          legendNodes.push(aNode('Console', '', 'normal', '', undefined, 1, 0, 0, 0, false, {
-            console_identifier: 'Dispatch console'
-          }));
-        }
-        if (!svg.selectAll('circle.client.in').empty()) {
-          let node = aNode('Sender', '', 'normal', '', undefined, 2, 0, 0, 0, false, {});
-          node.cdir = 'in';
-          legendNodes.push(node);
-        }
-        if (!svg.selectAll('circle.client.out').empty()) {
-          let node = aNode('Receiver', '', 'normal', '', undefined, 3, 0, 0, 0, false, {});
-          node.cdir = 'out';
-          legendNodes.push(node);
-        }
-        if (!svg.selectAll('circle.client.inout').empty()) {
-          let node = aNode('Sender/Receiver', '', 'normal', '', undefined, 4, 0, 0, 0, false, {});
-          node.cdir = 'both';
-          legendNodes.push(node);
-        }
-        if (!svg.selectAll('circle.qpid-cpp').empty()) {
-          legendNodes.push(aNode('Qpid broker', '', 'route-container', '', undefined, 5, 0, 0, 0, false, {
-            product: 'qpid-cpp'
-          }));
-        }
-        if (!svg.selectAll('circle.artemis').empty()) {
-          legendNodes.push(aNode('Artemis broker', '', 'route-container', '', undefined, 6, 0, 0, 0, false,
-            {product: 'apache-activemq-artemis'}));
-        }
-        if (!svg.selectAll('circle.route-container').empty()) {
-          legendNodes.push(aNode('Service', '', 'route-container', 'external-service', undefined, 7, 0, 0, 0, false,
-            {product: ' External Service'}));
-        }
-        lsvg = lsvg.data(legendNodes, function(d) {
-          return d.key;
-        });
-        let lg = lsvg.enter().append('svg:g')
-          .attr('transform', function(d, i) {
-            // 45px between lines and add 10px space after 1st line
-            return 'translate(0, ' + (45 * i + (i > 0 ? 10 : 0)) + ')';
-          });
-
-        appendCircle(lg);
-        appendContent(lg);
-        appendTitle(lg);
-        lg.append('svg:text')
-          .attr('x', 35)
-          .attr('y', 6)
-          .attr('class', 'label')
-          .text(function(d) {
-            return d.key;
-          });
-        lsvg.exit().remove();
-        let svgEl = document.getElementById('svglegend');
-        if (svgEl) {
-          let bb;
-          // firefox can throw an exception on getBBox on an svg element
-          try {
-            bb = svgEl.getBBox();
-          } catch (e) {
-            bb = {
-              y: 0,
-              height: 200,
-              x: 0,
-              width: 200
-            };
-          }
-          svgEl.style.height = (bb.y + bb.height) + 'px';
-          svgEl.style.width = (bb.x + bb.width) + 'px';
-        }
-      };
-      let appendCircle = function(g) {
-        // add new circles and set their attr/class/behavior
-        return g.append('svg:circle')
-          .attr('class', 'node')
-          .attr('r', function(d) {
-            return radii[d.nodeType];
-          })
-          .attr('fill', function (d) {
-            if (d.cdir === 'both' && !QDRService.utilities.isConsole(d)) {
-              return 'url(' + urlPrefix + '#half-circle)';
-            }
-            return null;
-          })
-          .classed('fixed', function(d) {
-            return d.fixed & 1;
-          })
-          .classed('normal', function(d) {
-            return d.nodeType == 'normal' || QDRService.utilities.isConsole(d);
-          })
-          .classed('in', function(d) {
-            return d.cdir == 'in';
-          })
-          .classed('out', function(d) {
-            return d.cdir == 'out';
-          })
-          .classed('inout', function(d) {
-            return d.cdir == 'both';
-          })
-          .classed('inter-router', function(d) {
-            return d.nodeType == 'inter-router';
-          })
-          .classed('on-demand', function(d) {
-            return d.nodeType == 'on-demand';
-          })
-          .classed('console', function(d) {
-            return QDRService.utilities.isConsole(d);
-          })
-          .classed('artemis', function(d) {
-            return QDRService.utilities.isArtemis(d);
-          })
-          .classed('qpid-cpp', function(d) {
-            return QDRService.utilities.isQpid(d);
-          })
-          .classed('route-container', function (d) {
-            return (!QDRService.utilities.isArtemis(d) && !QDRService.utilities.isQpid(d) && d.nodeType === 'route-container');
-          })
-          .classed('client', function(d) {
-            return d.nodeType === 'normal' && !d.properties.console_identifier;
-          });
-      };
-      let appendContent = function(g) {
-        // show node IDs
+      // add text to client circles if there are any that represent multiple clients
+      svg.selectAll('.subtext').remove();
+      let multiples = svg.selectAll('.multiple');
+      multiples.each(function(d) {
+        let g = d3.select(this);
         g.append('svg:text')
-          .attr('x', 0)
-          .attr('y', function(d) {
-            let y = 7;
-            if (QDRService.utilities.isArtemis(d))
-              y = 8;
-            else if (QDRService.utilities.isQpid(d))
-              y = 9;
-            else if (d.nodeType === 'inter-router')
-              y = 4;
-            else if (d.nodeType === 'route-container')
-              y = 5;
-            return y;
-          })
-          .attr('class', 'id')
-          .classed('console', function(d) {
-            return QDRService.utilities.isConsole(d);
-          })
-          .classed('normal', function(d) {
-            return d.nodeType === 'normal';
-          })
-          .classed('on-demand', function(d) {
-            return d.nodeType === 'on-demand';
-          })
-          .classed('artemis', function(d) {
-            return QDRService.utilities.isArtemis(d);
-          })
-          .classed('qpid-cpp', function(d) {
-            return QDRService.utilities.isQpid(d);
-          })
-          .text(function(d) {
-            if (QDRService.utilities.isConsole(d)) {
-              return '\uf108'; // icon-desktop for this console
-            } else if (QDRService.utilities.isArtemis(d)) {
-              return '\ue900';
-            } else if (QDRService.utilities.isQpid(d)) {
-              return '\ue901';
-            } else if (d.nodeType === 'route-container') {
-              return d.properties.product ? d.properties.product[0].toUpperCase() : 'S';
-            } else if (d.nodeType === 'normal')
-              return '\uf109'; // icon-laptop for clients
-            return d.name.length > 7 ? d.name.substr(0, 6) + '...' : d.name;
-          });
-      };
-      let appendTitle = function(g) {
-        g.append('svg:title').text(function(d) {
-          return generateTitle(d);
-        });
-      };
+          .attr('x', radiusNormal + 3)
+          .attr('y', Math.floor(radiusNormal / 2))
+          .attr('class', 'subtext')
+          .text('x ' + d.normals.length);
+      });
+      // call createLegend in timeout because:
+      // If we create the legend right away, then it will be destroyed when the accordian
+      // gets initialized as the page loads.
+      $timeout(createLegend);
 
-      let generateTitle = function (d) {
-        let x = '';
-        if (d.normals && d.normals.length > 1)
-          x = ' x ' + d.normals.length;
-        if (QDRService.utilities.isConsole(d))
-          return 'Dispatch console' + x;
-        else if (QDRService.utilities.isArtemis(d))
-          return 'Broker - Artemis' + x;
-        else if (d.properties.product == 'qpid-cpp')
-          return 'Broker - qpid-cpp' + x;
-        else if (d.cdir === 'in')
-          return 'Sender' + x;
-        else if (d.cdir === 'out')
-          return 'Receiver' + x;
-        else if (d.cdir === 'both')
-          return 'Sender/Receiver' + x;
-        else if (d.nodeType === 'normal')
-          return 'client' + x;
-        else if (d.nodeType === 'on-demand')
-          return 'broker';
-        else if (d.properties.product) {
-          return d.properties.product;
-        }
-        else {
-          return '';
-        }
-      };
+      if (!mousedown_node || !selected_node)
+        return;
 
-      let showClientTooltip = function (d, event) {
-        let type = generateTitle(d);
-        let title = '<table class="popupTable"><tr><td>Type</td><td>' + type + '</td></tr>';
-        if (!d.normals || d.normals.length < 2)
-          title += ('<tr><td>Host</td><td>' + d.host + '</td></tr>');
-        title += '</table>';
-        showToolTip(title, event);
-      };
+      if (!start)
+        return;
+      // set the graph in motion
+      //QDRLog.debug("mousedown_node is " + mousedown_node);
+      force.start();
 
-      let showRouterTooltip = function (d, event) {
-        QDRService.management.topology.ensureEntities(d.key, [
-          {entity: 'listener', attrs: ['role', 'port', 'http']},
-          {entity: 'router', attrs: ['name', 'version', 'hostName']}
-        ], function () {
-          // update all the router title text
-          let nodes = QDRService.management.topology.nodeInfo();
-          let node = nodes[d.key];
-          let listeners = node['listener'];
-          let router = node['router'];
-          let r = QDRService.utilities.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 = QDRService.utilities.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>';
-          showToolTip(title, event);
+    }
+    let createLegend = function () {
+      // dynamically create the legend based on which node types are present
+      // 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(${(radii['inter-router'] + 2)},${(radii['inter-router'] + 2)})`)
+        .selectAll('g');
+      let legendNodes = new Nodes(QDRService, QDRLog);
+      legendNodes.addUsing('Router', '', 'inter-router', '', undefined, 0, 0, 0, 0, false, {});
+
+      if (!svg.selectAll('circle.console').empty()) {
+        legendNodes.addUsing('Console', 'Console', 'normal', '', undefined, 0, 0, 1, 0, false, {
+          console_identifier: 'Dispatch console'
         });
-      };
-      let showToolTip = function (title, event) {
-        // show the tooltip
-        $timeout ( function () {
-          $scope.trustedpopoverContent = $sce.trustAsHtml(title);
-          displayTooltip(event);
+      }
+      if (!svg.selectAll('circle.client.in').empty()) {
+        legendNodes.addUsing('Sender', 'Sender', 'normal', '', undefined, 0, 0, 2, 0, false, {}).cdir = 'in';
+      }
+      if (!svg.selectAll('circle.client.out').empty()) {
+        legendNodes.addUsing('Receiver', 'Receiver', 'normal', '', undefined, 0, 0, 3, 0, false, {}).cdir = 'out';
+      }
+      if (!svg.selectAll('circle.client.inout').empty()) {
+        legendNodes.addUsing('Sender/Receiver', 'Sender/Receiver', 'normal', '', undefined, 0, 0, 4, 0, false, {}).cdir = 'both';
+      }
+      if (!svg.selectAll('circle.qpid-cpp').empty()) {
+        legendNodes.addUsing('Qpid broker', 'Qpid broker', 'route-container', '', undefined, 0, 0, 5, 0, false, {
+          product: 'qpid-cpp'
         });
-      };
-
-      let displayTooltip = function (event) {
-        $timeout( function () {
-          let top = $('#topology').offset().top - 5;
-          let width = $('#topology').width();
-          d3.select('#popover-div')
-            .style('visibility', 'hidden')
-            .style('display', 'block')
-            .style('left', (event.pageX+5)+'px')
-            .style('top', (event.pageY-top)+'px');
-          let pwidth = $('#popover-div').width();
-          d3.select('#popover-div')
-            .style('visibility', 'visible')
-            .style('left',(Math.min(width-pwidth, event.pageX+5) + 'px'));
+      }
+      if (!svg.selectAll('circle.artemis').empty()) {
+        legendNodes.addUsing('Artemis broker', 'Artemis broker', 'route-container', '', undefined, 0, 0, 6, 0, false,
+          {product: 'apache-activemq-artemis'});
+      }
+      if (!svg.selectAll('circle.route-container').empty()) {
+        legendNodes.addUsing('Service', 'Service', 'route-container', 'external-service', undefined, 0, 0, 7, 0, false,
+          {product: ' External Service'});
+      }
+      lsvg = lsvg.data(legendNodes.nodes, function(d) {
+        return d.key;
+      });
+      let lg = lsvg.enter().append('svg:g')
+        .attr('transform', function(d, i) {
+          // 45px between lines and add 10px space after 1st line
+          return 'translate(0, ' + (45 * i + (i > 0 ? 10 : 0)) + ')';
         });
-      };
 
-      function nextHop(thisNode, d, cb) {
-        if ((thisNode) && (thisNode != d)) {
-          let target = findNextHopNode(thisNode, d);
-          //QDR.log.debug("highlight link from node ");
-          //console.dump(nodeFor(selected_node.name));
-          //console.dump(target);
-          if (target) {
-            let hnode = nodeFor(thisNode.name);
-            let hlLink = linkFor(hnode, target);
-            //QDR.log.debug("need to highlight");
-            //console.dump(hlLink);
-            if (hlLink) {
-              if (cb) {
-                cb(hlLink, hnode, target);
-              } else {
-                hlLink['highlighted'] = true;
-                hnode['highlighted'] = true;
-              }
-            }
-            else
-              target = null;
-          }
-          nextHop(target, d, cb);
-        }
-        if (thisNode == d && !cb) {
-          let hnode = nodeFor(thisNode.name);
-          hnode['highlighted'] = true;
+      appendCircle(lg);
+      appendContent(lg);
+      appendTitle(lg);
+      lg.append('svg:text')
+        .attr('x', 35)
+        .attr('y', 6)
+        .attr('class', 'label')
+        .text(function(d) {
+          return d.key;
+        });
+      lsvg.exit().remove();
+      let svgEl = document.getElementById('svglegend');
+      if (svgEl) {
+        let bb;
+        // firefox can throw an exception on getBBox on an svg element
+        try {
+          bb = svgEl.getBBox();
+        } catch (e) {
+          bb = {
+            y: 0,
+            height: 200,
+            x: 0,
+            width: 200
+          };
         }
+        svgEl.style.height = (bb.y + bb.height) + 'px';
+        svgEl.style.width = (bb.x + bb.width) + 'px';
       }
-
-      function hasChanged() {
-        // Don't update the underlying topology diagram if we are adding a new node.
-        // Once adding is completed, the topology will update automatically if it has changed
-        let nodeInfo = QDRService.management.topology.nodeInfo();
-        // don't count the nodes without connection info
-        let cnodes = Object.keys(nodeInfo).filter ( function (node) {
-          return (nodeInfo[node]['connection']);
+    };
+    let appendCircle = function(g) {
+      // add new circles and set their attr/class/behavior
+      return g.append('svg:circle')
+        .attr('class', 'node')
+        .attr('r', function(d) {
+          return radii[d.nodeType];
+        })
+        .attr('fill', function (d) {
+          if (d.cdir === 'both' && !QDRService.utilities.isConsole(d)) {
+            return 'url(' + urlPrefix + '#half-circle)';
+          }
+          return null;
+        })
+        .classed('fixed', function(d) {
+          return d.fixed & 1;
+        })
+        .classed('normal', function(d) {
+          return d.nodeType == 'normal' || QDRService.utilities.isConsole(d);
+        })
+        .classed('in', function(d) {
+          return d.cdir == 'in';
+        })
+        .classed('out', function(d) {
+          return d.cdir == 'out';
+        })
+        .classed('inout', function(d) {
+          return d.cdir == 'both';
+        })
+        .classed('inter-router', function(d) {
+          return d.nodeType == 'inter-router';
+        })
+        .classed('on-demand', function(d) {
+          return d.nodeType == 'on-demand';
+        })
+        .classed('console', function(d) {
+          return QDRService.utilities.isConsole(d);
+        })
+        .classed('artemis', function(d) {
+          return QDRService.utilities.isArtemis(d);
+        })
+        .classed('qpid-cpp', function(d) {
+          return QDRService.utilities.isQpid(d);
+        })
+        .classed('route-container', function (d) {
+          return (!QDRService.utilities.isArtemis(d) && !QDRService.utilities.isQpid(d) && d.nodeType === 'route-container');
+        })
+        .classed('client', function(d) {
+          return d.nodeType === 'normal' && !d.properties.console_identifier;
         });
-        let routers = nodes.filter( function (node) {
-          return node.nodeType === 'inter-router';
+    };
+    let appendContent = function(g) {
+      // show node IDs
+      g.append('svg:text')
+        .attr('x', 0)
+        .attr('y', function(d) {
+          let y = 7;
+          if (QDRService.utilities.isArtemis(d))
+            y = 8;
+          else if (QDRService.utilities.isQpid(d))
+            y = 9;
+          else if (d.nodeType === 'inter-router')
+            y = 4;
+          else if (d.nodeType === 'route-container')
+            y = 5;
+          return y;
+        })
+        .attr('class', 'id')
+        .classed('console', function(d) {
+          return QDRService.utilities.isConsole(d);
+        })
+        .classed('normal', function(d) {
+          return d.nodeType === 'normal';
+        })
+        .classed('on-demand', function(d) {
+          return d.nodeType === 'on-demand';
+        })
+        .classed('artemis', function(d) {
+          return QDRService.utilities.isArtemis(d);
+        })
+        .classed('qpid-cpp', function(d) {
+          return QDRService.utilities.isQpid(d);
+        })
+        .text(function(d) {
+          if (QDRService.utilities.isConsole(d)) {
+            return '\uf108'; // icon-desktop for this console
+          } else if (QDRService.utilities.isArtemis(d)) {
+            return '\ue900';
+          } else if (QDRService.utilities.isQpid(d)) {
+            return '\ue901';
+          } else if (d.nodeType === 'route-container') {
+            return d.properties.product ? d.properties.product[0].toUpperCase() : 'S';
+          } else if (d.nodeType === 'normal')
+            return '\uf109'; // icon-laptop for clients
+          return d.name.length > 7 ? d.name.substr(0, 6) + '...' : d.name;
         });
-        if (routers.length > cnodes.length) {
-          return -1;
-        }
-
-
-        if (cnodes.length != Object.keys(savedKeys).length) {
-          return cnodes.length > Object.keys(savedKeys).length ? 1 : -1;
-        }
-        // we may have dropped a node and added a different node in the same update cycle
-        for (let i=0; i<cnodes.length; i++) {
-          let key = cnodes[i];
-          // if this node isn't in the saved node list
-          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;
+    };
+    let appendTitle = function(g) {
+      g.append('svg:title').text(function(d) {
+        return generateTitle(d);
+      });
+    };
+
+    let generateTitle = function (d) {
+      let x = '';
+      if (d.normals && d.normals.length > 1)
+        x = ' x ' + d.normals.length;
+      if (QDRService.utilities.isConsole(d))
+        return 'Dispatch console' + x;
+      else if (QDRService.utilities.isArtemis(d))
+        return 'Broker - Artemis' + x;
+      else if (d.properties.product == 'qpid-cpp')
+        return 'Broker - qpid-cpp' + x;
+      else if (d.cdir === 'in')
+        return 'Sender' + x;
+      else if (d.cdir === 'out')
+        return 'Receiver' + x;
+      else if (d.cdir === 'both')
+        return 'Sender/Receiver' + x;
+      else if (d.nodeType === 'normal')
+        return 'client' + x;
+      else if (d.nodeType === 'on-demand')
+        return 'broker';
+      else if (d.properties.product) {
+        return d.properties.product;
+      }
+      else {
+        return '';
+      }
+    };
+
+    let showClientTooltip = function (d, event) {
+      let type = generateTitle(d);
+      let title = `<table class="popupTable"><tr><td>Type</td><td>${type}</td></tr>`;
+      if (!d.normals || d.normals.length < 2)
+        title += ('<tr><td>Host</td><td>' + d.host + '</td></tr>');
+      title += '</table>';
+      showToolTip(title, event);
+    };
+
+    let showRouterTooltip = function (d, event) {
+      QDRService.management.topology.ensureEntities(d.key, [
+        {entity: 'listener', attrs: ['role', 'port', 'http']},
+        {entity: 'router', attrs: ['name', 'version', 'hostName']}
+      ], function () {
+        // update all the router title text
+        let nodes = QDRService.management.topology.nodeInfo();
+        let node = nodes[d.key];
+        let listeners = node['listener'];
+        let router = node['router'];
+        let r = QDRService.utilities.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 = QDRService.utilities.flatten(listeners.attributeNames, listeners.results[l]);
+          if (listener.role === 'normal') {
+            ports.push(listener.port+'');
           }
         }
-        return 0;
-      }
-
-      function saveChanged() {
-        savedKeys = {};
-        let nodeInfo = QDRService.management.topology.nodeInfo();
-        // save the number of connections per node
-        for (let key in nodeInfo) {
-          if (nodeInfo[key]['connection'])
-            savedKeys[key] = nodeInfo[key]['connection'].results.length;
+        if (ports.length > 0) {
+          title += ('<tr><td>Ports</td><td>' + ports.join(', ') + '</td></tr>');
         }
-      }
-      // we are about to leave the page, save the node positions
-      $rootScope.$on('$locationChangeStart', function() {
-        //QDR.log.debug("locationChangeStart");
-        savePositions();
+        title += '</table>';
+        showToolTip(title, event);
       });
-      // When the DOM element is removed from the page,
-      // AngularJS will trigger the $destroy event on
-      // the scope
-      $scope.$on('$destroy', function() {
-        //QDR.log.debug("scope on destroy");
-        savePositions();
-        QDRService.management.topology.setUpdateEntities([]);
-        QDRService.management.topology.stopUpdating();
-        QDRService.management.topology.delUpdatedAction('normalsStats');
-        QDRService.management.topology.delUpdatedAction('topology');
-        QDRService.management.topology.delUpdatedAction('connectionPopupHTML');
-
-        d3.select('#SVG_ID').remove();
-        window.removeEventListener('resize', resize);
-        traffic.stop();
+    };
+    let showToolTip = function (title, event) {
+      // show the tooltip
+      $timeout ( function () {
+        $scope.trustedpopoverContent = $sce.trustAsHtml(title);
+        displayTooltip(event);
       });
+    };
+
+    let displayTooltip = function (event) {
+      $timeout( function () {
+        let top = $('#topology').offset().top - 5;
+        let width = $('#topology').width();
+        d3.select('#popover-div')
+          .style('visibility', 'hidden')
+          .style('display', 'block')
+          .style('left', (event.pageX+5)+'px')
+          .style('top', (event.pageY-top)+'px');
+        let pwidth = $('#popover-div').width();
+        d3.select('#popover-div')
+          .style('visibility', 'visible')
+          .style('left',(Math.min(width-pwidth, event.pageX+5) + 'px'));
+      });
+    };
+
+    function hasChanged() {
+      // Don't update the underlying topology diagram if we are adding a new node.
+      // Once adding is completed, the topology will update automatically if it has changed
+      let nodeInfo = QDRService.management.topology.nodeInfo();
+      // don't count the nodes without connection info
+      let cnodes = Object.keys(nodeInfo).filter ( function (node) {
+        return (nodeInfo[node]['connection']);
+      });
+      let routers = nodes.nodes.filter( function (node) {
+        return node.nodeType === 'inter-router';
+      });
+      if (routers.length > cnodes.length) {
+        return -1;
+      }
 
-      function handleInitialUpdate() {
-        // we only need to update connections during steady-state
-        QDRService.management.topology.setUpdateEntities(['connection']);
-        // we currently have all entities available on all routers
-        save

<TRUNCATED>

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