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/05/17 16:09:26 UTC

[1/6] qpid-dispatch git commit: DISPATCH-1000 Restore the html/css/js files that were in the npm repository

Repository: qpid-dispatch
Updated Branches:
  refs/heads/master 94e55a812 -> dfd3cf7cb


http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/console/stand-alone/plugin/js/qdrTopology.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/qdrTopology.js b/console/stand-alone/plugin/js/qdrTopology.js
new file mode 100644
index 0000000..cd0cdf2
--- /dev/null
+++ b/console/stand-alone/plugin/js/qdrTopology.js
@@ -0,0 +1,2098 @@
+/*
+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
+ */
+var QDR = (function(QDR) {
+
+  QDR.module.controller('QDR.TopologyFormController', function($scope, $timeout) {
+
+    $scope.attributes = [];
+    $scope.attributesConnections = [];
+
+    $scope.form = 'Router';
+    $scope.$on('showEntityForm', function(event, args) {
+      let attributes = args.attributes;
+      // capitalize 1st letter
+      $scope.form = args.entity.charAt(0).toUpperCase() + args.entity.slice(1);
+
+      let H = '<div class="infoGrid">';
+      attributes.forEach( function (a, i) {
+        let even = (i % 2) ? 'even' : 'odd';
+        if (a.attributeName === 'Listening on')
+          even += ' listening-on';
+        H += ('<div class="'+ even +'"><span title="'+a.attributeName+'">' + a.attributeName + '</span><span title="'+a.attributeValue+'">' + a.attributeValue + '</span></div>');
+      });
+      H += '</div>';
+      $('#formInfo').html(H);
+
+      if (!$scope.$$phase) $scope.$apply();
+
+    });
+    $scope.infoStyle = function () {
+      return {
+        height: (Math.max($scope.attributes.length, 15) * 30 + 46) + 'px'
+      };
+    };
+
+    $scope.panelVisible = true;  // show/hide the panel on the left
+    $scope.hideLeftPane = function () {
+      d3.select('.page-menu')
+        .style('left' , '-360px')
+        .style('z-index', '1');
+
+      d3.select('.diagram')
+        .transition().duration(300).ease('sin-in')
+        .style('margin-left', '-360px')
+        .each('end', function () {
+          $timeout(function () {$scope.panelVisible = false;});
+          let div = d3.select(this);
+          div.style('margin-left', '0');
+          div.style('padding-left', 0);
+        });
+    };
+    $scope.showLeftPane = function () {
+      d3.select('.page-menu')
+        .style('left' , '0px');
+
+      $timeout(function () {$scope.panelVisible = true;});
+      d3.select('.diagram')
+        .style('margin-left', '0px')
+        .transition().duration(300).ease('sin-out')
+        .style('margin-left', '300px')
+        .each('end', function () {
+          let div = d3.select(this);
+          div.style('margin-left', '0');
+          div.style('padding-left', '300px');
+        });
+    };
+
+
+  });
+  /**
+   * @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) {
+
+      $scope.multiData = [];
+      $scope.quiesceState = {};
+      let dontHide = false;
+      $scope.crosshtml = $sce.trustAsHtml('');
+
+      $scope.quiesceConnection = function(row) {
+        let entity = row.entity;
+        let state = $scope.quiesceState[entity.connectionId].state;
+        if (state === 'enabled') {
+          // start quiescing all links
+          $scope.quiesceState[entity.connectionId].state = 'quiescing';
+        } else if (state === 'quiesced') {
+          // start reviving all links
+          $scope.quiesceState[entity.connectionId].state = 'reviving';
+        }
+        $scope.multiDetails.updateState(entity);
+        dontHide = true;
+        $scope.multiDetails.selectRow(row.rowIndex, true);
+        $scope.multiDetails.showLinksList(row);
+      };
+      $scope.quiesceDisabled = function(row) {
+        return $scope.quiesceState[row.entity.connectionId].buttonDisabled;
+      };
+      $scope.quiesceText = function(row) {
+        return $scope.quiesceState[row.entity.connectionId].buttonText;
+      };
+      $scope.quiesceClass = function(row) {
+        const stateClassMap = {
+          enabled: 'btn-primary',
+          quiescing: 'btn-warning',
+          reviving: 'btn-warning',
+          quiesced: 'btn-danger'
+        };
+        return stateClassMap[$scope.quiesceState[row.entity.connectionId].state];
+      };
+
+      // This is the grid that shows each connection when a client node that represents multiple connections is clicked
+      $scope.multiData = [];
+      $scope.multiDetails = {
+        data: 'multiData',
+        enableColumnResize: true,
+        enableHorizontalScrollbar: 0,
+        enableVerticalScrollbar: 0,
+        jqueryUIDraggable: true,
+        enablePaging: false,
+        multiSelect: false,
+        enableSelectAll: false,
+        enableSelectionBatchEvent: false,
+        enableRowHeaderSelection: false,
+        noUnselect: true,
+        onRegisterApi: function (gridApi) {
+          if (gridApi.selection) {
+            gridApi.selection.on.rowSelectionChanged($scope, function(row){
+              let detailsDiv = d3.select('#link_details');
+              let isVis = detailsDiv.style('display') === 'block';
+              if (!dontHide && isVis && $scope.connectionId === row.entity.connectionId) {
+                hideLinkDetails();
+                return;
+              }
+              dontHide = false;
+              $scope.multiDetails.showLinksList(row);
+            });
+          }
+        },
+        showLinksList: function(obj) {
+          $scope.linkData = obj.entity.linkData;
+          $scope.connectionId = obj.entity.connectionId;
+          let visibleLen = Math.min(obj.entity.linkData.length, 10);
+          //QDR.log.debug("visibleLen is " + visibleLen)
+          let left = parseInt(d3.select('#multiple_details').style('left'), 10);
+          let offset = $('#topology').offset();
+          let detailsDiv = d3.select('#link_details');
+          detailsDiv
+            .style({
+              display: 'block',
+              opacity: 1,
+              left: (left + 20) + 'px',
+              top: (mouseY - offset.top + 20 + $(document).scrollTop()) + 'px',
+              height: ((visibleLen + 1) * 30) + 40 + 'px', // +1 for the header row
+              'overflow-y': obj.entity.linkData > 10 ? 'scroll' : 'hidden'
+            });
+        },
+        updateState: function(entity) {
+          let state = $scope.quiesceState[entity.connectionId].state;
+
+          // count enabled and disabled links for this connection
+          let enabled = 0,
+            disabled = 0;
+          entity.linkData.forEach(function(link) {
+            if (link.adminStatus === 'enabled')
+              ++enabled;
+            if (link.adminStatus === 'disabled')
+              ++disabled;
+          });
+
+          let linkCount = entity.linkData.length;
+          // if state is quiescing and any links are enabled, button should say 'Quiescing' and be disabled
+          if (state === 'quiescing' && (enabled > 0)) {
+            $scope.quiesceState[entity.connectionId].buttonText = 'Quiescing';
+            $scope.quiesceState[entity.connectionId].buttonDisabled = true;
+          } else
+          // if state is enabled and all links are disabled, button should say Revive and be enabled. set state to quisced
+          // if state is quiescing and all links are disabled, button should say 'Revive' and be enabled. set state to quiesced
+          if ((state === 'quiescing' || state === 'enabled') && (disabled === linkCount)) {
+            $scope.quiesceState[entity.connectionId].buttonText = 'Revive';
+            $scope.quiesceState[entity.connectionId].buttonDisabled = false;
+            $scope.quiesceState[entity.connectionId].state = 'quiesced';
+          } else
+          // if state is reviving and any links are disabled, button should say 'Reviving' and be disabled
+          if (state === 'reviving' && (disabled > 0)) {
+            $scope.quiesceState[entity.connectionId].buttonText = 'Reviving';
+            $scope.quiesceState[entity.connectionId].buttonDisabled = true;
+          } else
+          // if state is reviving or quiesced and all links are enabled, button should say 'Quiesce' and be enabled. set state to enabled
+          if ((state === 'reviving' || state === 'quiesced') && (enabled === linkCount)) {
+            $scope.quiesceState[entity.connectionId].buttonText = 'Quiesce';
+            $scope.quiesceState[entity.connectionId].buttonDisabled = false;
+            $scope.quiesceState[entity.connectionId].state = 'enabled';
+          }
+        },
+        columnDefs: [{
+          field: 'host',
+          cellTemplate: 'titleCellTemplate.html',
+          //headerCellTemplate: 'titleHeaderCellTemplate.html',
+          displayName: 'Connection host'
+        }, {
+          field: 'user',
+          cellTemplate: 'titleCellTemplate.html',
+          //headerCellTemplate: 'titleHeaderCellTemplate.html',
+          displayName: 'User'
+        }, {
+          field: 'properties',
+          cellTemplate: 'titleCellTemplate.html',
+          //headerCellTemplate: 'titleHeaderCellTemplate.html',
+          displayName: 'Properties'
+        }
+          /*,
+                {
+                  cellClass: 'gridCellButton',
+                  cellTemplate: '<button title="{{quiesceText(row)}} the links" type="button" ng-class="quiesceClass(row)" class="btn" ng-click="$event.stopPropagation();quiesceConnection(row)" ng-disabled="quiesceDisabled(row)">{{quiesceText(row)}}</button>'
+                }*/
+        ]
+      };
+      $scope.quiesceLinkClass = function(row) {
+        const stateClassMap = {
+          enabled: 'btn-primary',
+          disabled: 'btn-danger'
+        };
+        return stateClassMap[row.entity.adminStatus];
+      };
+      $scope.quiesceLink = function(row) {
+        QDRService.management.topology.quiesceLink(row.entity.nodeId, row.entity.name)
+          .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.info('Error ' + context.message.statusDescription);
+            }
+          });
+      };
+      $scope.quiesceLinkDisabled = function(row) {
+        return (row.entity.operStatus !== 'up' && row.entity.operStatus !== 'down');
+      };
+      $scope.quiesceLinkText = function(row) {
+        return row.entity.operStatus === 'down' ? 'Revive' : 'Quiesce';
+      };
+      $scope.linkData = [];
+      $scope.linkDetails = {
+        data: 'linkData',
+        jqueryUIDraggable: true,
+        columnDefs: [{
+          field: 'adminStatus',
+          cellTemplate: 'titleCellTemplate.html',
+          headerCellTemplate: 'titleHeaderCellTemplate.html',
+          displayName: 'Admin state'
+        }, {
+          field: 'operStatus',
+          cellTemplate: 'titleCellTemplate.html',
+          headerCellTemplate: 'titleHeaderCellTemplate.html',
+          displayName: 'Oper state'
+        }, {
+          field: 'dir',
+          cellTemplate: 'titleCellTemplate.html',
+          headerCellTemplate: 'titleHeaderCellTemplate.html',
+          displayName: 'dir'
+        }, {
+          field: 'owningAddr',
+          cellTemplate: 'titleCellTemplate.html',
+          headerCellTemplate: 'titleHeaderCellTemplate.html',
+          displayName: 'Address'
+        }, {
+          field: 'deliveryCount',
+          displayName: 'Delivered',
+          headerCellTemplate: 'titleHeaderCellTemplate.html',
+          cellClass: 'grid-values'
+
+        }, {
+          field: 'uncounts',
+          displayName: 'Outstanding',
+          headerCellTemplate: 'titleHeaderCellTemplate.html',
+          cellClass: 'grid-values'
+        }
+          /*,
+                {
+                  cellClass: 'gridCellButton',
+                  cellTemplate: '<button title="{{quiesceLinkText(row)}} this link" type="button" ng-class="quiesceLinkClass(row)" class="btn" ng-click="quiesceLink(row)" ng-disabled="quiesceLinkDisabled(row)">{{quiesceLinkText(row)}}</button>'
+                }*/
+        ]
+      };
+
+      let urlPrefix = $location.absUrl();
+      urlPrefix = urlPrefix.split('#')[0];
+      QDR.log.debug('started QDR.TopologyController with urlPrefix: ' + urlPrefix);
+
+      // 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();
+        }
+        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
+        };
+      };
+      // 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 radius = 25;
+      let radiusNormal = 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() {
+        let legendWidth = 143;
+        let display = $('#svg_legend').css('display');
+        if (display === 'none')
+          legendWidth = 0;
+        const gap = 5;
+        let width = $('#topology').width() - gap - legendWidth;
+        let top = $('#topology').offset().top;
+        let tpformHeight = $('#topologyForm').height();
+        let height = Math.max(window.innerHeight, tpformHeight + top) - 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();
+        }
+      };
+
+      $scope.$on('panel-resized', function () {
+        resize();
+      });
+      window.addEventListener('resize', resize);
+      let sizes = getSizes();
+      width = sizes[0];
+      height = sizes[1];
+      if (width <= 0 || height <= 0)
+        return;
+
+      // set up initial nodes and links
+      //  - nodes are known by 'id', not by index in array.
+      //  - selected edges are indicated on the node (as a bold red circle).
+      //  - links are always source < target; edge directions are set by 'left' and 'right'.
+      let nodes = [];
+      let links = [];
+
+      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];
+          }
+        }
+        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,
+          });
+        });
+      };
+
+      var initializeNodes = function (nodeInfo) {
+        let nodeCount = Object.keys(nodeInfo).length;
+        let yInit = 50;
+        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) {
+        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() {
+        nodes = [];
+        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)
+          .on('click', function() {
+            removeCrosssection();
+          });
+
+        $(document).keyup(function(e) {
+          if (e.keyCode === 27) {
+            removeCrosssection();
+          }
+        });
+
+        // the legend
+        d3.select('#svg_legend svg').remove();
+        lsvg = d3.select('#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();
+
+        svg.append('svg:defs').selectAll('marker')
+          .data(['end-arrow', 'end-arrow-selected', 'end-arrow-small', 'end-arrow-highlighted']) // Different link/path types can be defined here
+          .enter().append('svg:marker') // This section adds in the arrows
+          .attr('id', String)
+          .attr('viewBox', '0 -5 10 10')
+          .attr('markerWidth', 4)
+          .attr('markerHeight', 4)
+          .attr('orient', 'auto')
+          .classed('small', function (d) {return d.indexOf('small') > -1;})
+          .append('svg:path')
+          .attr('d', 'M 0 -5 L 10 0 L 0 5 z');
+
+        svg.append('svg:defs').selectAll('marker')
+          .data(['start-arrow', 'start-arrow-selected', 'start-arrow-small', 'start-arrow-highlighted']) // Different link/path types can be defined here
+          .enter().append('svg:marker') // This section adds in the arrows
+          .attr('id', String)
+          .attr('viewBox', '0 -5 10 10')
+          .attr('refX', 5)
+          .attr('markerWidth', 4)
+          .attr('markerHeight', 4)
+          .attr('orient', 'auto')
+          .append('svg:path')
+          .attr('d', 'M 10 -5 L 0 0 L 10 5 z');
+
+        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();
+              });
+            }
+          });
+        }
+        setTimeout(function () {
+          updateForm(Object.keys(QDRService.management.topology.nodeInfo())[0], 'router', 0);
+        });
+
+        // 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);
+          }
+        };
+        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);
+        });
+      };
+
+      function updateForm(key, entity, resultIndex) {
+        if (!angular.isDefined(resultIndex))
+          return;
+        let nodeList = QDRService.management.topology.nodeIdList();
+        if (nodeList.indexOf(key) > -1) {
+          QDRService.management.topology.fetchEntities(key, [
+            {entity: entity},
+            {entity: 'listener', attrs: ['role', 'port']}], function (results) {
+            let onode = results[key];
+            if (!onode[entity]) {
+              console.log('requested ' + entity + ' but didn\'t get it');
+              return;
+            }
+            let nodeResults = onode[entity].results[resultIndex];
+            let nodeAttributes = onode[entity].attributeNames;
+            let attributes = nodeResults.map(function(row, i) {
+              return {
+                attributeName: nodeAttributes[i],
+                attributeValue: row
+              };
+            });
+            // sort by attributeName
+            attributes.sort(function(a, b) {
+              return a.attributeName.localeCompare(b.attributeName);
+            });
+
+            // move the Name first
+            let nameIndex = attributes.findIndex(function(attr) {
+              return attr.attributeName === 'name';
+            });
+            if (nameIndex >= 0)
+              attributes.splice(0, 0, attributes.splice(nameIndex, 1)[0]);
+
+            // get the list of ports this router is listening on
+            if (entity === 'router') {
+              let listeners = onode['listener'].results;
+              let listenerAttributes = onode['listener'].attributeNames;
+              let normals = listeners.filter(function(listener) {
+                return QDRService.utilities.valFor(listenerAttributes, listener, 'role') === 'normal';
+              });
+              let ports = [];
+              normals.forEach(function(normalListener) {
+                ports.push(QDRService.utilities.valFor(listenerAttributes, normalListener, 'port'));
+              });
+              // add as 2nd row
+              if (ports.length) {
+                attributes.splice(1, 0, {
+                  attributeName: 'Listening on',
+                  attributeValue: ports,
+                  description: 'The port(s) on which this router is listening for connections'
+                });
+              }
+            }
+            $rootScope.$broadcast('showEntityForm', {
+              entity: entity,
+              attributes: attributes
+            });
+            if (!$scope.$$phase) $scope.$apply();
+          });
+        }
+      }
+
+      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;
+          }
+          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;
+
+          if (d.target.nodeType == 'inter-router') {
+            r = radius;
+            //                       right arrow  left line start
+            sourcePadding = d.left ? radius + 8 : radius;
+            //                      left arrow      right line start
+            targetPadding = d.right ? radius + 16 : radius;
+          } else {
+            r = radiusNormal - 18;
+            sourcePadding = d.left ? radiusNormal + 18 : radiusNormal;
+            targetPadding = d.right ? radiusNormal + 16 : radiusNormal;
+          }
+          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;
+        });
+
+        if (!animate) {
+          animate = true;
+          force.stop();
+        }
+      }
+
+      // 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];
+
+        if (!sInfo) {
+          QDR.log.warn('unable to find topology node info for ' + from.key);
+          return null;
+        }
+
+        // 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;
+      }
+
+      function nodeFor(name) {
+        for (let i = 0; i < nodes.length; ++i) {
+          if (nodes[i].name == name)
+            return nodes[i];
+        }
+        return null;
+      }
+
+      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
+        //QDR.log.debug("failed to find a link between ");
+        //console.dump(source);
+        //QDR.log.debug(" and ");
+        //console.dump(target);
+        return null;
+      }
+
+      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 removeCrosssection() {
+        d3.select('#crosssection svg g').transition()
+          .duration(1000)
+          .attr('transform', 'scale(0)')
+          .style('opacity', 0)
+          .each('end', function () {
+            d3.select('#crosssection svg').remove();
+            d3.select('#crosssection').style('display','none');
+          });
+        d3.select('#multiple_details').transition()
+          .duration(500)
+          .style('opacity', 0)
+          .each('end', function() {
+            d3.select('#multiple_details').style('display', 'none');
+            stopUpdateConnectionsGrid();
+          });
+        hideLinkDetails();
+      }
+
+      function hideLinkDetails() {
+        d3.select('#link_details').transition()
+          .duration(500)
+          .style('opacity', 0)
+          .each('end', function() {
+            d3.select('#link_details').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) {
+        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;
+          })
+          .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')
+          .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 resultIndex = 0; // the connection to use
+            let left = d.left ? d.target : d.source;
+            // right is the node that the arrow points to, left is the other node
+            let right = d.left ? d.source : d.target;
+            let onode = QDRService.management.topology.nodeInfo()[left.key];
+            // loop through all the connections for left, and find the one for right
+            if (!onode || !onode['connection'])
+              return;
+            // update the info dialog for the link the mouse is over
+            if (!selected_node && !selected_link) {
+              for (resultIndex = 0; resultIndex < onode['connection'].results.length; ++resultIndex) {
+                let conn = onode['connection'].results[resultIndex];
+                /// find the connection whose container is the right's name
+                let name = QDRService.utilities.valFor(onode['connection'].attributeNames, conn, 'container');
+                if (name == right.routerId) {
+                  break;
+                }
+              }
+              // did not find connection. this is a connection to a non-interrouter node
+              if (resultIndex === onode['connection'].results.length) {
+                // use the non-interrouter node's connection info
+                left = d.target;
+                resultIndex = left.resultIndex;
+              }
+              updateForm(left.key, 'connection', resultIndex);
+            }
+
+            mousedown_link = d;
+            selected_link = mousedown_link;
+            restart();
+          })
+          .on('mousemove', function (d) {
+            let top = $('#topology').offset().top - 5;
+            $timeout(function () {
+              $scope.trustedpopoverContent = $sce.trustAsHtml(connectionPopupHTML(d));
+            });
+            d3.select('#popover-div')
+              .style('display', 'block')
+              .style('left', (d3.event.pageX+5)+'px')
+              .style('top', (d3.event.pageY-top)+'px');
+          })
+          .on('mouseout', function() { // mouse out of a path
+            d3.select('#popover-div')
+              .style('display', 'none');
+            selected_link = null;
+            restart();
+          })
+          // left click a path
+          .on('click', function (d) {
+            let clickPos = d3.mouse(this);
+            d3.event.stopPropagation();
+            clearPopups();
+            var showCrossSection = function() {
+              const diameter = 400;
+              let pack = d3.layout.pack()
+                .size([diameter - 4, diameter - 4])
+                .padding(-10)
+                .value(function(d) { return d.size; });
+
+              d3.select('#crosssection svg').remove();
+              let svg = d3.select('#crosssection').append('svg')
+                .attr('width', diameter)
+                .attr('height', diameter);
+
+              let rg = svg.append('svg:defs')
+                .append('radialGradient')
+                .attr('id', 'cross-gradient')
+                .attr('gradientTransform', 'scale(2.0) translate(-0.5,-0.5)');
+
+              rg
+                .append('stop')
+                .attr('offset', '0%')
+                .attr('stop-color', '#feffff');
+              rg
+                .append('stop')
+                .attr('offset', '40%')
+                .attr('stop-color', '#cfe2f3');
+
+              let svgg = svg.append('g')
+                .attr('transform', 'translate(2,2)');
+
+              svgg
+                .append('rect')
+                .attr('x', 0)
+                .attr('y', 0)
+                .attr('width', 200)
+                .attr('height', 200)
+                .attr('class', 'cross-rect')
+                .attr('fill', 'url('+urlPrefix+'#cross-gradient)');
+
+              svgg
+                .append('line')
+                .attr('class', 'cross-line')
+                .attr({x1: 2, y1: 0, x2: 200, y2: 0});
+              svgg
+                .append('line')
+                .attr('class', 'cross-line')
+                .attr({x1: 2, y1: 0, x2: 0, y2: 200});
+
+              /*
+              let simpleLine = d3.svg.line();
+              svgg
+                .append('path')
+                .attr({
+                  d: simpleLine([[0,0],[0,200]]),
+                  stroke: '#000',
+                  'stroke-width': '4px'
+                });
+              svgg
+                .append('path')
+                .attr({
+                  d: simpleLine([[0,0],[200,0]]),
+                  stroke: '#000',
+                  'stroke-width': '4px'
+                });
+*/
+              let root = {
+                name: ' Links between ' + d.source.name + ' and ' + d.target.name,
+                children: []
+              };
+              let nodeInfo = QDRService.management.topology.nodeInfo();
+              let connections = nodeInfo[d.source.key]['connection'];
+              let containerIndex = connections.attributeNames.indexOf('container');
+              connections.results.some ( function (connection) {
+                if (connection[containerIndex] == d.target.routerId) {
+                  root.attributeNames = connections.attributeNames;
+                  root.obj = connection;
+                  root.desc = 'Connection';
+                  return true;    // stop looping after 1 match
+                }
+                return false;
+              });
+
+              // find router.links where link.remoteContainer is d.source.name
+              let links = nodeInfo[d.source.key]['router.link'];
+              let identityIndex = connections.attributeNames.indexOf('identity');
+              let roleIndex = connections.attributeNames.indexOf('role');
+              let connectionIdIndex = links.attributeNames.indexOf('connectionId');
+              let linkTypeIndex = links.attributeNames.indexOf('linkType');
+              let nameIndex = links.attributeNames.indexOf('name');
+              let linkDirIndex = links.attributeNames.indexOf('linkDir');
+
+              if (roleIndex < 0 || identityIndex < 0 || connectionIdIndex < 0
+                || linkTypeIndex < 0 || nameIndex < 0 || linkDirIndex < 0)
+                return;
+              links.results.forEach ( function (link) {
+                if (root.obj && link[connectionIdIndex] == root.obj[identityIndex] && link[linkTypeIndex] == root.obj[roleIndex])
+                  root.children.push (
+                    { name: ' ' + link[linkDirIndex] + ' ',
+                      size: 100,
+                      obj: link,
+                      desc: 'Link',
+                      attributeNames: links.attributeNames
+                    });
+              });
+              if (root.children.length == 0)
+                return;
+              let node = svgg.datum(root).selectAll('.node')
+                .data(pack.nodes)
+                .enter().append('g')
+                .attr('class', function(d) { return d.children ? 'parent node hastip' : 'leaf node hastip'; })
+                .attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')' + (!d.children ? 'scale(0.9)' : ''); });
+              node.append('circle')
+                .attr('r', function(d) { return d.r; });
+
+              node.on('mouseenter', function (d) {
+                let title = '<h4>' + d.desc + '</h4><table class=\'tiptable\'><tbody>';
+                if (d.attributeNames)
+                  d.attributeNames.forEach( function (n, i) {
+                    title += '<tr><td>' + n + '</td><td>';
+                    title += d.obj[i] != null ? d.obj[i] : '';
+                    title += '</td></tr>';
+                  });
+                title += '</tbody></table>';
+                $timeout( (function () {
+                  $scope.crosshtml = $sce.trustAsHtml(title);
+                  $('#crosshtml').show();
+                  let parent = $('#crosssection');
+                  let ppos = parent.position();
+                  let mleft = ppos.left + parent.width();
+                  $('#crosshtml').css({left: mleft, top: ppos.top});
+                }).bind(this));
+              });
+              node.on('mouseout', function () {
+                $('#crosshtml').hide();
+              });
+
+              node.append('text')
+                .attr('dy', function (d) { return d.children ? '-10em' : '.5em';})
+                .style('text-anchor', 'middle')
+                .text(function(d) {
+                  return d.name.substring(0, d.r / 3);
+                });
+              svgg.attr('transform', 'translate(2,2) scale(0.01)');
+
+              let bounds = $('#topology').position();
+              d3.select('#crosssection')
+                .style('display', 'block')
+                .style('left', (clickPos[0] + bounds.left) + 'px')
+                .style('top', (clickPos[1] + bounds.top) + 'px');
+
+              svgg.transition()
+                .attr('transform', 'translate(2,2) scale(1)')
+                .each('end', function ()  {
+                  d3.selectAll('#crosssection g.leaf text').attr('dy', '.3em');
+                });
+            };
+            QDRService.management.topology.ensureEntities(d.source.key, {entity: 'router.link', force: true}, showCrossSection);
+          });
+        // remove old links
+        path.exit().remove();
+
+
+        // circle (node) group
+        // nodes are known by id
+        circle = circle.data(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;
+          });
+
+        // add new circle nodes. if nodes[] is longer than the existing paths, add a new path for each new element
+        let g = circle.enter().append('svg:g')
+          .classed('multiple', function(d) {
+            return (d.normals && d.normals.length > 1);
+          });
+
+        var 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;
+            });
+        };
+        appendCircle(g)
+          .on('mouseover', function(d) {  // mouseover a circle
+            if (!selected_node && !mousedown_node) {
+              if (d.nodeType === 'inter-router') {
+                //QDR.log.debug("showing general form");
+                updateForm(d.key, 'router', 0);
+              } else if (d.nodeType === 'normal' || d.nodeType === 'on-demand' || d.nodeType === 'route-container') {
+                //QDR.log.debug("showing connections form");
+                updateForm(d.key, 'connection', d.resultIndex);
+              }
+            }
+
+            if (d === mousedown_node)
+              return;
+            //if (d === selected_node)
+            //    return;
+            // enlarge target node
+            d3.select(this).attr('transform', 'scale(1.1)');
+            // highlight the next-hop route from the selected node to this node
+            //mousedown_node = null;
+
+            if (!selected_node) {
+              return;
+            }
+            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(this).attr('transform', '');
+            clerAllHighlights();
+            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;
+              setNodesFixed(d.name, true);
+              resetMouseVars();
+              restart();
+              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);
+
+          })
+          .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('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();
+            startUpdateConnectionsGrid(d);
+          });
+        //.attr("transform", function (d) {return "scale(" + (d.nodeType === 'normal' ? .5 : 1) + ")"})
+        //.transition().duration(function (d) {return d.nodeType === 'normal' ? 3000 : 0}).ease("elastic").attr("transform", "scale(1)")
+
+        var 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;
+            });
+        };
+
+        appendContent(g);
+
+        var appendTitle = function(g) {
+          g.append('svg:title').text(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.properties.product) {
+              return d.properties.product;
+            } 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;
+            return d.nodeType == 'normal' ? 'client' + x : (d.nodeType == 'on-demand' ? 'broker' : 'Router ' + d.name);
+          });
+        };
+        appendTitle(g);
+
+        // remove old nodes
+        circle.exit().remove();
+
+        // add subcircles
+        svg.selectAll('.subcircle').remove();
+        let multiples = svg.selectAll('.multiple');
+        multiples.each(function(d) {
+          d.normals.forEach(function(n, i) {
+            if (i < d.normals.length - 1 && i < 3) // only show a few shadow circles
+              this.insert('svg:circle', ':first-child')
+                .attr('class', 'subcircle node')
+                .attr('r', 15 - i)
+                .attr('transform', 'translate(' + 4 * (i + 1) + ', 0)');
+          }, d3.select(this));
+        });
+
+        // dynamically create the legend based on which node types are present
+        // the legend
+        d3.select('#svg_legend svg').remove();
+        lsvg = d3.select('#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';
+        }
+
+        if (!mousedown_node || !selected_node)
+          return;
+
+        if (!start)
+          return;
+        // set the graph in motion
+        //QDR.log.debug("mousedown_node is " + mousedown_node);
+        force.start();
+
+      }
+
+      var startUpdateConnectionsGrid = function(d) {
+        // called after each topology update
+        var extendConnections = function() {
+          // force a fetch of the links for this node
+          QDRService.management.topology.ensureEntities(d.key, {entity: 'router.link', force: true}, function () {
+            // the links for this node are now available
+            $scope.multiData = [];
+            let normals = d.normals;
+            // find updated normals for d
+            d3.selectAll('.normal')
+              .each(function(newd) {
+                if (newd.id == d.id && newd.name == d.name) {
+                  normals = newd.normals;
+                }
+              });
+            if (normals) {
+              normals.forEach(function(n) {
+                let nodeInfo = QDRService.management.topology.nodeInfo();
+                let links = nodeInfo[n.key]['router.link'];
+                let linkTypeIndex = links.attributeNames.indexOf('linkType');
+                let connectionIdIndex = links.attributeNames.indexOf('connectionId');
+                n.linkData = [];
+                links.results.forEach(function(link) {
+                  if (link[linkTypeIndex] === 'endpoint' && link[connectionIdIndex] === n.connectionId) {
+                    let l = {};
+                    let ll = QDRService.utilities.flatten(links.attributeNames, link);
+                    l.owningAddr = ll.owningAddr;
+                    l.dir = ll.linkDir;
+                    if (l.owningAddr && l.owningAddr.length > 2)
+                      if (l.owningAddr[0] === 'M')
+                        l.owningAddr = l.owningAddr.substr(2);
+                      else
+                        l.owningAddr = l.owningAddr.substr(1);
+
+                    l.deliveryCount = ll.deliveryCount;
+                    l.uncounts = QDRService.utilities.pretty(ll.undeliveredCount + ll.unsettledCount);
+                    l.adminStatus = ll.adminStatus;
+                    l.operStatus = ll.operStatus;
+                    l.identity = ll.identity;
+                    l.connectionId = ll.connectionId;
+                    l.nodeId = n.key;
+                    l.type = ll.type;
+                    l.name = ll.name;
+
+                    // TODO: remove this fake quiescing/reviving logic when the routers do the work
+                    initConnState(n.connectionId);
+                    if ($scope.quiesceState[n.connectionId].linkStates[l.identity])
+                      l.adminStatus = $scope.quiesceState[n.connectionId].linkStates[l.identity];
+                    if ($scope.quiesceState[n.connectionId].state == 'quiescing') {
+                      if (l.adminStatus === 'enabled') {
+                        // 25% chance of switching
+                        let chance = Math.floor(Math.random() * 2);
+                        if (chance == 1) {
+                          l.adminStatus = 'disabled';
+                          $scope.quiesceState[n.connectionId].linkStates[l.identity] = 'disabled';
+                        }
+                      }
+                    }
+                    if ($scope.quiesceState[n.connectionId].state == 'reviving') {
+                      if (l.adminStatus === 'disabled') {
+                        // 25% chance of switching
+                        let chance = Math.floor(Math.random() * 2);
+                        if (chance == 1) {
+                          l.adminStatus = 'enabled';
+                          $scope.quiesceState[n.connectionId].linkStates[l.identity] = 'enabled';
+                        }
+                      }
+                    }
+                    QDR.log.debug('pushing link state for ' + l.owningAddr + ' status: ' + l.adminStatus);
+
+                    n.linkData.push(l);
+                  }
+                });
+                $scope.multiData.push(n);
+                if (n.connectionId == $scope.connectionId)
+                  $scope.linkData = n.linkData;
+                initConnState(n.connectionId);
+                $scope.multiDetails.updateState(n);
+              });
+            }
+            $scope.$apply();
+
+            d3.select('#multiple_details')
+              .style({
+                height: ((normals.length + 1) * 30) + 40 + 'px',
+                'overflow-y': normals.length > 10 ? 'scroll' : 'hidden'
+              });
+          });
+        };
+        $scope.connectionsStyle = function () {
+          return {
+            height: ($scope.multiData.length * 30 + 40) + 'px'
+          };
+        };
+        $scope.linksStyle = function () {
+          return {
+            height: ($scope.linkData.length * 30 + 40) + 'px'
+          };
+        };
+        // register a notification function for when the topology is updated
+        QDRService.management.topology.addUpdatedAction('normalsStats', extendConnections);
+        // call the function that gets the links right now
+        extendConnections();
+        clearPopups();
+        let display = 'block';
+        if (d.normals.length === 1) {
+          display = 'none';
+          mouseY = mouseY - 20;
+        }
+        let rm = relativeMouse();
+        d3.select('#multiple_details')
+          .style({
+            display: display,
+            opacity: 1,
+            left: rm.left + 'px',
+            top: (rm.top - rm.offset.top) + 'px'
+          });
+        if (d.normals.length === 1) {
+          // simulate a click on the connection to popup the link details
+          QDRService.management.topology.ensureEntities(d.key, {entity: 'router.link', force: true}, function () {
+            $scope.multiDetails.showLinksList({
+              entity: d
+            });
+          });
+        }
+      };
+      var stopUpdateConnectionsGrid = function() {
+        QDRService.management.topology.delUpdatedAction('normalsStats');
+      };
+
+      var initConnState = function(id) {
+        if (!angular.isDefined($scope.quiesceState[id])) {
+          $scope.quiesceState[id] = {
+            state: 'enabled',
+            buttonText: 'Quiesce',
+            buttonDisabled: false,
+            linkStates: {}
+          };
+        }
+      };
+
+      function nextHop(thisNode, d) {
+        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) {
+              hlLink['highlighted'] = true;
+              hnode['highlighted'] = true;
+            }
+            else
+              target = null;
+          }
+          nextHop(target, d);
+        }
+        if (thisNode == d) {
+          let hnode = nodeFor(thisNode.name);
+          hnode['highlighted'] = true;
+        }
+      }
+
+      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.filter( function (node) {
+          return node.nodeType === 'inter-router';
+        });
+        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;
+          }
+        }
+        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;
+        }
+      }
+      // we are about to leave the page, save the node positions
+      $rootScope.$on('$locationChangeStart', function() {
+        //QDR.log.debug("locationChangeStart");
+        savePositions();
+      });
+      // 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');
+
+        d3.select('#SVG_ID').remove();
+        window.removeEventListener('resize', resize);
+      });
+
+      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
+        saveChanged();
+        initForceGraph();
+        // after the graph is displayed fetch all .router.node info. This is done so highlighting between nodes
+        // doesn't incur a delay
+        QDRService.management.topology.addUpdateEntities({entity: 'router.node', attrs: ['id','nextHop']});
+        // call this function every time a background update is done
+        QDRService.management.topology.addUpdatedAction('topology', function() {
+          let changed = hasChanged();
+          // there is a new node, we need to get all of it's entities before drawing the graph
+          if (changed > 0) {
+            QDRService.management.topology.delUpdatedAction('topology');
+            animate = true;
+            setupInitialUpdate();
+          } else if (changed === -1) {
+            // we lost a node (or a client), we can draw the new svg immediately
+            animate = false;
+            saveChanged();
+            let nodeInfo = QDRService.management.topology.nodeInfo();
+            initializeNodes(nodeInfo);
+
+            let unknowns = [];
+            initializeLinks(nodeInfo, unknowns);
+            if (unknowns.length > 0) {
+              resolveUnknowns(nodeInfo, unknowns);
+            }
+            else {
+              force.nodes(nodes).links(links).start();
+              restart();
+            }
+
+            //initForceGraph();
+          } else {
+            //QDR.log.debug("topology didn't change")
+          }
+
+        });
+      }
+
+      function setupInitialUpdate() {
+        // make sure all router nodes have .connection info. if not then fetch any missing info
+        QDRService.management.topology.ensureAllEntities(
+          //          [{entity: ".connection"}, {entity: ".router.lin.router.link", attrs: ["linkType","connectionId","linkDir"]}],
+          [{entity: 'connection'}],
+          //[{entity: ".connection"}],
+          handleInitialUpdate);
+      }
+      if (!QDRService.management.connection.is_connected()) {
+        // we are not connected. we probably got here from a bookmark or manual page reload
+        QDR.redirectWhenConnected($location, 'topology');
+        return;
+      }
+
+      let connectionPopupHTML = function (d) {
+        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.connectionId;
+        }
+        if (isNaN(rightIndex)) {
+          // we have a connection to a console
+          rightIndex = +right.connectionId;
+        }
+        let HTML = '';
+        if (rightIndex >= 0) {
+          let conn = onode['connection'].results[rightIndex];
+          conn = QDRService.utilities.flatten(onode['connection'].attributeNames, conn);
+          HTML += '<table class="popupTable">';
+          HTML += ('<tr><td>Security</td><td>' + connSecurity(conn) + '</td></tr>');
+          HTML += ('<tr><td>Authentication</td><td>' + connAuth(conn) + '</td></tr>');
+          HTML += ('<tr><td>Tenant</td><td>' + connTenant(conn) + '</td></tr>');
+          HTML += '</table>';
+        }
+        return HTML;
+      };
+
+      animate = true;
+      setupInitialUpdate();
+      QDRService.management.topology.startUpdating(false);
+
+    }
+  ]);
+
+  return QDR;
+
+}(QDR || {}));
\ 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


[5/6] qpid-dispatch git commit: DISPATCH-1000 Restore the html/css/js files that were in the npm repository

Posted by ea...@apache.org.
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/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
new file mode 100644
index 0000000..4c9c727
--- /dev/null
+++ b/console/stand-alone/plugin/html/tmplChartConfig.html
@@ -0,0 +1,85 @@
+<!--
+ 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
+-->
+
+<!--
+    This is the template for the graph dialog that is displayed. It uses the
+    dialogCtrl controller in qdrCharts.js.
+-->
+<div class="chartOptions">
+    <div class="modal-header">
+        <h3 class="modal-title">Chart {{chart.attr() | humanify}}</h3>
+    </div>
+    <div class="modal-body">
+        <div id="{{svgDivId}}" class="line-chart-pf"></div>
+
+        <uib-tabset>
+            <uib-tab heading="Type">
+                <legend>Chart type</legend>
+                <div>
+                    <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>
+<!--
+                    <div class="dlg-slider" ng-show="dialogChart.type=='rate'">
+                        <span>Rate Window: {{rateWindow}} second{{rateWindow > 1 ? "s" : ""}}</span>
+                        <div id="rateSlider"></div>
+                    </div>
+-->
+                </div>
+                <div style="clear:both;"> </div>
+            </uib-tab>
+<!--
+            <uib-tab ng-hide="$parent.chart.aggregate()" heading="Colors">
+                <legend>Chart colors</legend>
+                <div>
+                    <div class="colorPicker">
+                        <label>Area: <input id="areaColor" name="areaColor" type="color" /></label>
+                    </div>
+                </div>
+                <div style="clear:both;"> </div>
+            </uib-tab>
+-->
+            <uib-tab heading="Duration">
+                <legend>Chart duration</legend>
+                <div class="clearfix">
+                    <div class="dlg-slider duration">
+                        <span>Show data for past {{dialogChart.visibleDuration}} minute{{dialogChart.visibleDuration > 1 ? "s" : ""}}</span>
+                        <div id="durationSlider"></div>
+                    </div>
+                </div>
+            </uib-tab>
+        </uib-tabset>
+    </div>
+    <div class="modal-footer">
+        <div ng-hide="adding()">
+            <button class="btn btn-success" type="button" ng-click="apply()">Apply to existing chart</button>
+            <button class="btn btn-info" type="button" ng-click="copyToDashboard()">Create new chart</button>
+            <button class="btn btn-primary" type="button" ng-click="okClick()">Close</button>
+        </div>
+        <div ng-show="adding()">
+            <span ng-hide="isOnChartsPage()">
+                <button class="btn btn-success" type="button" ng-click="addChartsPage()"><i class="icon-bar-chart"></i> Add</button> this chart to the Charts page.
+            </span>
+            <span ng-show="isOnChartsPage()">
+                View the <button class="btn btn-success" type="button" ng-click="showChartsPage()"><i class="icon-bar-chart"></i> Charts</button> page.
+            </span>
+            <button class="btn btn-primary" type="button" ng-click="okClick()">Close</button>
+        </div>
+    </div>
+</div>
+

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/console/stand-alone/plugin/html/tmplListChart.html
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/html/tmplListChart.html b/console/stand-alone/plugin/html/tmplListChart.html
new file mode 100644
index 0000000..3214187
--- /dev/null
+++ b/console/stand-alone/plugin/html/tmplListChart.html
@@ -0,0 +1,40 @@
+<!--
+ 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
+-->
+
+<!--
+    This is the template for the graph dialog that is displayed.
+-->
+<div class="modal-header">
+    <h3 class="modal-title">Chart {{chart.attr() | humanify}}</h3>
+</div>
+<div class="modal-body">
+    <p class="newChart">
+        <button ng-click="editChart()" title="Configure"><span class="fa-edit"></span></button>
+    </p><div style="clear:both"></div>
+    <div id="pfDialogChart" class="line-chart-pf"></div>
+</div>
+<div class="modal-footer">
+        <span ng-hide="isOnChartsPage()">
+            <button class="btn btn-success" type="button" ng-click="addChartsPage()"><i class="icon-bar-chart"></i> Add</button> this chart to the Charts page.
+        </span>
+        <span ng-show="isOnChartsPage()">
+            <button class="btn btn-danger" type="button" ng-click="delChartsPage()">Remove</button> this chart from the <button class="btn btn-success" type="button" ng-click="showChartsPage()"><i class="icon-bar-chart"></i> Charts</button> page.
+        </span>
+    <button class="btn btn-primary" type="button" ng-click="ok()">Close</button>
+</div>

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/console/stand-alone/plugin/html/tmplListTree.html
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/html/tmplListTree.html b/console/stand-alone/plugin/html/tmplListTree.html
new file mode 100644
index 0000000..64797fa
--- /dev/null
+++ b/console/stand-alone/plugin/html/tmplListTree.html
@@ -0,0 +1,42 @@
+<!--
+ 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
+-->
+
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+    <ng-include src="'listGrid.html'"></ng-include>
+</div>
+<div class="qdr-attributes col-sm-3 col-md-2 col-sm-pull-9 col-md-pull-10 sidebar-pf sidebar-pf-left">
+    <div class="pane-wrapper">
+        <div class="pane-header-wrapper">
+            <div class="tree-header"><select ng-options="node as node.name for node in nodes" ng-model="currentNode" ng-change="selectNode(currentNode)"></select></div>
+            <div ng-hide="largeNetwork" class="expand-collapse">
+                <i class="icon-chevron-down clickable" title="Expand all nodes" ng-click="expandAll()"></i>
+                <i class="icon-chevron-up clickable" title="Unexpand all nodes" ng-click="contractAll()"></i>
+            </div>
+        </div>
+        <div class="pane-viewport">
+            <div class="pane-content">
+                <div class="treeContainer">
+                    <div id="entityTree" onSelect="onTreeSelected" onRoot="onRootReady" hideRoot="true"></div>
+                    <div ng-init="treeReady()"></div>
+                </div>
+            </div>
+        </div>
+        <div class="pane-bar"></div>
+    </div>
+</div>

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/console/stand-alone/plugin/html/tmplOverviewTree.html
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/html/tmplOverviewTree.html b/console/stand-alone/plugin/html/tmplOverviewTree.html
new file mode 100644
index 0000000..d3f6e80
--- /dev/null
+++ b/console/stand-alone/plugin/html/tmplOverviewTree.html
@@ -0,0 +1,48 @@
+<!--
+ 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
+-->
+<div class="col-xs-9">
+    <ng-include src="'overviewGrid.html'"></ng-include>
+</div>
+
+<button type='button' class='navbar-toggle tree-menu-button' data-toggle="collapse" data-target=".overview-tree">
+    <span class="sr-only">Toggle navigation</span>
+    <span class="icon-bar"></span>
+    <span class="icon-bar"></span>
+    <span class="icon-bar"></span>
+ </button>
+
+<div class="overview-tree navbar-collapse collapse col-xs-3">
+    <div class="pane-wrapper">
+        <div class="pane-header-wrapper">
+            <div ng-hide="largeNetwork" class="expand-collapse">
+                <i class="icon-chevron-down clickable" title="Expand all nodes" ng-click="expandAll()"></i>
+                <i class="icon-chevron-up clickable" title="Unexpand all nodes" ng-click="contractAll()"></i>
+            </div>
+        </div>
+        <div class="pane-viewport">
+            <div class="pane-content">
+                <div class="treeContainer ng-scope">
+                    <div id="overtree"></div>
+                </div>
+            </div>
+        </div>
+        <div class="pane-bar" ng-mousedown="startMoving($event)" ng-click="toggle()"></div>
+    </div>
+</div>
+<div ng-init="overviewLoaded()"></div>

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/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
new file mode 100644
index 0000000..e8c240f
--- /dev/null
+++ b/console/stand-alone/plugin/js/dlgChartController.js
@@ -0,0 +1,204 @@
+/*
+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) {
+
+  // controller for the edit/configure chart dialog
+  QDR.module.controller('QDR.ChartDialogController', function($scope, QDRChartService, $location, $uibModalInstance, chart, updateTick, dashboard, adding) {
+    let dialogSvgChart = null;
+    $scope.svgDivId = 'dialogEditChart';    // the div id for the svg chart
+
+    let updateTimer = null;
+    $scope.chart = chart;  // the underlying chart object from the dashboard
+    $scope.dialogChart = $scope.chart.copy(); // the chart object for this dialog
+    $scope.userTitle = $scope.chart.title();
+
+    $scope.$watch('userTitle', function(newValue, oldValue) {
+      if (newValue !== oldValue) {
+        $scope.dialogChart.title(newValue);
+        dialogSvgChart.tick($scope.svgDivId);
+      }
+    });
+    $scope.$watch('dialogChart.areaColor', function (newValue, oldValue) {
+      if (newValue !== oldValue) {
+        if (dialogSvgChart)
+          dialogSvgChart.tick($scope.svgDivId);
+      }
+    });
+    $scope.$watch('dialogChart.lineColor', function (newValue, oldValue) {
+      if (newValue !== oldValue) {
+        if (dialogSvgChart)
+          dialogSvgChart.tick($scope.svgDivId);
+      }
+    });
+    $scope.$watch('dialogChart.type', function (newValue, oldValue) {
+      if (newValue !== oldValue) {
+        if (dialogSvgChart) {
+          dialogSvgChart.chart.visibleDuration = newValue === 'rate' ? 0.25 : 1;
+          dialogSvgChart.tick($scope.svgDivId);
+        }
+      }
+    });
+
+    // the stored rateWindow is in milliseconds, but the slider is in seconds
+    $scope.rateWindow = $scope.chart.rateWindow / 1000;
+
+    $scope.addChartsPage = function () {
+      QDRChartService.addDashboard(dialogSvgChart.chart);
+    };
+    $scope.delChartsPage = function () {
+      QDRChartService.delDashboard($scope.chart);
+    };
+
+    $scope.showChartsPage = function () {
+      cleanup();
+      $uibModalInstance.close(true);
+      $location.path(QDR.pluginRoot + '/charts');
+    };
+
+    var cleanup = function () {
+      if (updateTimer) {
+        clearTimeout(updateTimer);
+        updateTimer = null;
+      }
+      if (!$scope.isOnChartsPage())
+        QDRChartService.unRegisterChart($scope.dialogChart);     // remove the chart
+    };
+    $scope.okClick = function () {
+      cleanup();
+      $uibModalInstance.close(true);
+    };
+
+    var initRateSlider = function () {
+      if (document.getElementById('rateSlider')) {
+        $( '#rateSlider' ).slider({
+          value: $scope.rateWindow,
+          min: 1,
+          max: 10,
+          step: 1,
+          slide: function( event, ui ) {
+            $scope.rateWindow = ui.value;
+            $scope.dialogChart.rateWindow = ui.value * 1000;
+            $scope.$apply();
+            if (dialogSvgChart)
+              dialogSvgChart.tick($scope.svgDivId);
+          }
+        });
+      } else {
+        setTimeout(initRateSlider, 100);
+      }
+    };
+    //initRateSlider();
+
+    var initDurationSlider = function () {
+      if (document.getElementById('durationSlider')) {
+        $( '#durationSlider' ).slider({
+          value: $scope.dialogChart.visibleDuration,
+          min: 0.25,
+          max: 10,
+          step: 0.25,
+          slide: function( event, ui ) {
+            $scope.visibleDuration = $scope.dialogChart.visibleDuration = ui.value;
+            $scope.$apply();
+            if (dialogSvgChart)
+              dialogSvgChart.tick($scope.svgDivId);
+          }
+        });
+      } else {
+        setTimeout(initDurationSlider, 100);
+      }
+    };
+    initDurationSlider();
+
+    $scope.adding = function () {
+      return adding;
+    };
+
+    $scope.isOnChartsPage = function () {
+      let chart = $scope.chart;
+      if (adding)
+        return QDRChartService.isAttrCharted(chart.nodeId(), chart.entity(), chart.name(), chart.attr(), chart.aggregate());
+      else
+        return $scope.chart.dashboard;
+    };
+
+    // handle the Apply button click
+    // update the dashboard chart's properties
+    $scope.apply = function () {
+      $scope.chart.areaColor = $scope.dialogChart.areaColor;
+      $scope.chart.lineColor = $scope.dialogChart.lineColor;
+      $scope.chart.type = $scope.dialogChart.type;
+      $scope.chart.rateWindow = $scope.rateWindow * 1000;
+      $scope.chart.title($scope.dialogChart.title());
+      $scope.chart.visibleDuration = $scope.dialogChart.visibleDuration;
+      QDRChartService.saveCharts();
+      if (typeof updateTick === 'function')
+        updateTick();
+    };
+
+    // add a new chart to the dashboard based on the current dialog settings
+    $scope.copyToDashboard = function () {
+      let chart = $scope.dialogChart.copy();
+      // set the new chart's dashboard state
+      QDRChartService.addDashboard(chart);
+      // notify the chart controller that it needs to display a new chart
+      dashboard.addChart(chart);
+    };
+
+    // update the chart on the popup dialog
+    var updateDialogChart = function () {
+      // draw the chart using the current data
+      if (dialogSvgChart)
+        dialogSvgChart.tick($scope.svgDivId);
+
+      // draw the chart again in 1 second
+      const updateRate = localStorage['updateRate'] ? localStorage['updateRate'] : 1000;
+      if (updateTimer)
+        clearTimeout(updateTimer);
+      updateTimer = setTimeout(updateDialogChart, updateRate);
+    };
+
+    var showChart = function () {
+      // ensure the div for our chart is loaded in the dom
+      let div = angular.element('#' + $scope.svgDivId);
+      if (!div.width()) {
+        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()
+      })
+*/
+      if (updateTimer)
+        clearTimeout(updateTimer);
+      updateDialogChart();
+    };
+    showChart();
+  });
+  return QDR;
+
+} (QDR || {}));

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/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
new file mode 100644
index 0000000..574ea07
--- /dev/null
+++ b/console/stand-alone/plugin/js/qdrChartService.js
@@ -0,0 +1,859 @@
+/*
+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 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));
+        };
+      }
+
+      // 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);
+        }
+        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);
+        };
+      }
+
+      // 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);
+                }
+              }
+            }
+          }
+          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;
+
+            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();
+                      }
+                    }
+                  }
+                });
+              }
+            }
+          };
+          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);
+              }
+            });
+          }
+        },
+
+        // 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);
+        },
+
+        // 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);
+
+          // 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();
+        }
+      };
+      // 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'
+          };
+
+        }
+        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>');
+            }
+            table += '</tbody></table>';
+            return table;
+          }
+        };
+
+        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()]);
+        }
+        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));
+                }
+              } 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;
+        }
+
+        // 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 || {}));

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/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
new file mode 100644
index 0000000..b296de7
--- /dev/null
+++ b/console/stand-alone/plugin/js/qdrCharts.js
@@ -0,0 +1,160 @@
+/*
+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) {
+
+  /**
+   * @method ChartsController
+   *
+   * Controller that handles the QDR charts page
+   */
+  QDR.module.controller('QDR.ChartsController', function($scope, QDRService, QDRChartService, $uibModal, $location, $routeParams, $timeout) {
+
+    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');
+      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);
+      svgChart.zoomed = false;
+      $scope.svgCharts.push(svgChart);
+    });
+
+
+    // redraw the chart every update period
+    var updateCharts = function () {
+      $scope.svgCharts.forEach(function (svgChart) {
+        svgChart.tick(svgChart.chart.id()); // on this page we are using the chart.id() as the div id in which to render the chart
+      });
+      const updateRate = localStorage['updateRate'] ?  localStorage['updateRate'] : 1000;
+      if (updateTimer) {
+        clearTimeout(updateTimer);
+      }
+      updateTimer = setTimeout(updateCharts, updateRate);
+    };
+
+    // called by ng-init in the html when the page is loaded
+    $scope.chartsLoaded = function () {
+      // ensure the div for our chart is loaded in the dom
+      let div = angular.element('.chartContainer');
+      if (!div.width()) {
+        setTimeout($scope.chartsLoaded, 100);
+        return;
+      }
+      // create an svg object for each chart
+      $scope.svgCharts.forEach ( function (c) {
+        c.generate(380);
+        QDRChartService.sendChartRequest(c.chart.request(), true);
+      });
+      if (updateTimer)
+        clearTimeout(updateTimer);
+      setTimeout(updateCharts);
+    };
+
+    $scope.zoomChart = function (chart) {
+      chart.zoomed = !chart.zoomed;
+      chart.zoom(chart.chart.id(), chart.zoomed);
+    };
+    $scope.showListPage = function () {
+      $location.path('/list');
+    };
+
+    $scope.hasCharts = function () {
+      return QDRChartService.numCharts() > 0;
+    };
+
+    $scope.editChart = function (chart) {
+      doDialog('tmplChartConfig.html', chart.chart);
+    };
+
+    $scope.delChart = function (chart) {
+      QDRChartService.unRegisterChart(chart.chart);
+      // remove from svgCharts
+      $scope.svgCharts.forEach(function (svgChart, i) {
+        if (svgChart === chart) {
+          delete $scope.svgCharts.splice(i, 1);
+        }
+      });
+    };
+
+    // 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);
+      $scope.svgCharts.push(nchart);
+      $timeout( function () {
+        nchart.generate();
+        QDRChartService.sendChartRequest(chart.request(), true);
+      });
+    };
+
+    $scope.$on('$destroy', function() {
+      if (updateTimer) {
+        clearTimeout(updateTimer);
+        updateTimer = null;
+      }
+      for (let i=$scope.svgCharts.length-1; i>=0; --i) {
+        delete $scope.svgCharts.splice(i, 1);
+      }
+    });
+
+    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 () { return updateCharts; };
+          },
+          dashboard: function () {
+            return $scope;
+          },
+          adding: function () {
+            return false;
+          }
+        }
+      });
+    }
+
+  });
+
+
+  return QDR;
+
+}(QDR || {}));
+

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/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
new file mode 100644
index 0000000..af06c55
--- /dev/null
+++ b/console/stand-alone/plugin/js/qdrGlobals.js
@@ -0,0 +1,46 @@
+/*
+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';
+
+var QDR = (function(QDR) {
+
+  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;
+  })();
+
+  QDR.Core = {
+    notification: function (severity, msg) {
+      $.notify(msg, severity);
+    }
+  };
+
+  return QDR;
+
+} (QDR || {}));
\ 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


[4/6] qpid-dispatch git commit: DISPATCH-1000 Restore the html/css/js files that were in the npm repository

Posted by ea...@apache.org.
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/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
new file mode 100644
index 0000000..056e686
--- /dev/null
+++ b/console/stand-alone/plugin/js/qdrList.js
@@ -0,0 +1,938 @@
+/*
+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
+ */
+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'); }
+        }
+      ];
+      $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()
+                  };
+                });
+              });
+            });
+        }
+      };
+      $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;
+        }
+      };
+
+      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);
+          }
+        });
+        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
+          }
+          */
+        }
+        return true;
+      };
+      var onTreeNodeExpanded = function () {
+        saveExpanded();
+        updateExpandedEntities();
+      };
+      // 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 === '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);
+        }
+        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;
+          }
+        } 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);
+        }
+        // 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;
+          }
+        }
+        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);
+      };
+      $(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;
+              }
+            });
+          });
+        }
+        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);
+
+            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);
+              }
+            }
+
+            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;
+        });
+      };
+
+      $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]);
+                }
+              }
+              if (!rowName) {
+                let msg = 'response attributeNames did not contain a name field';
+                QDR.log.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'
+              }
+            );
+          }
+        }
+        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,
+        onRegisterApi: function(gridApi) {
+          $scope.gridApi = gridApi;
+        }
+      };
+
+      $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]);
+        }
+      }
+      $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);
+        });
+      }
+      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('/' + QDR.pluginName + '/connect');
+          $location.search('org', 'list');
+        }
+        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);
+          }
+        }
+        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 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,
+          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
+        });
+      };
+      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;
+
+} (QDR || {}));
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/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
new file mode 100644
index 0000000..711046e
--- /dev/null
+++ b/console/stand-alone/plugin/js/qdrListChart.js
@@ -0,0 +1,146 @@
+/*
+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


[6/6] qpid-dispatch git commit: DISPATCH-1000 Restore the html/css/js files that were in the npm repository

Posted by ea...@apache.org.
DISPATCH-1000 Restore the html/css/js files that were in the npm repository


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

Branch: refs/heads/master
Commit: dfd3cf7cb982fe8ffe9296a0100c3341660fb2b7
Parents: 94e55a8
Author: Ernest Allen <ea...@redhat.com>
Authored: Thu May 17 12:09:07 2018 -0400
Committer: Ernest Allen <ea...@redhat.com>
Committed: Thu May 17 12:09:07 2018 -0400

----------------------------------------------------------------------
 .gitignore                                      |    1 +
 console/stand-alone/index.html                  |   34 +-
 console/stand-alone/plugin/css/brokers.ttf      |  Bin 0 -> 2272 bytes
 console/stand-alone/plugin/css/dispatch.css     |  706 ++++++
 console/stand-alone/plugin/css/dispatchpf.css   |  473 ++++
 console/stand-alone/plugin/css/plugin.css       | 1019 +++++++++
 console/stand-alone/plugin/html/qdrCharts.html  |   48 +
 console/stand-alone/plugin/html/qdrConnect.html |   87 +
 console/stand-alone/plugin/html/qdrList.html    |  144 ++
 .../stand-alone/plugin/html/qdrOverview.html    |  283 +++
 console/stand-alone/plugin/html/qdrSchema.html  |   62 +
 .../stand-alone/plugin/html/qdrTopology.html    |  146 ++
 .../plugin/html/tmplChartConfig.html            |   85 +
 .../stand-alone/plugin/html/tmplListChart.html  |   40 +
 .../stand-alone/plugin/html/tmplListTree.html   |   42 +
 .../plugin/html/tmplOverviewTree.html           |   48 +
 .../stand-alone/plugin/js/dlgChartController.js |  204 ++
 .../stand-alone/plugin/js/qdrChartService.js    |  859 +++++++
 console/stand-alone/plugin/js/qdrCharts.js      |  160 ++
 console/stand-alone/plugin/js/qdrGlobals.js     |   46 +
 console/stand-alone/plugin/js/qdrList.js        |  938 ++++++++
 console/stand-alone/plugin/js/qdrListChart.js   |  146 ++
 console/stand-alone/plugin/js/qdrOverview.js    | 1774 +++++++++++++++
 .../plugin/js/qdrOverviewChartsController.js    |  134 ++
 .../plugin/js/qdrOverviewLogsController.js      |   70 +
 console/stand-alone/plugin/js/qdrSchema.js      |   83 +
 console/stand-alone/plugin/js/qdrService.js     |  269 +++
 console/stand-alone/plugin/js/qdrSettings.js    |  172 ++
 .../plugin/js/qdrTopAddressesController.js      |  218 ++
 console/stand-alone/plugin/js/qdrTopology.js    | 2098 ++++++++++++++++++
 30 files changed, 10372 insertions(+), 17 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/.gitignore
----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
index b4164bb..4ab831e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@ tests/policy-1/policy-*.json
 .settings
 console/test/topolgies/config-*
 .history
+.vscode

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/console/stand-alone/index.html
----------------------------------------------------------------------
diff --git a/console/stand-alone/index.html b/console/stand-alone/index.html
index 95c8c28..9602b42 100644
--- a/console/stand-alone/index.html
+++ b/console/stand-alone/index.html
@@ -44,9 +44,9 @@ under the License.
     <link rel="stylesheet" href="node_modules/angular-ui-grid/ui-grid.css" type="text/css"/>
 
     <!-- local Styles -->
-    <link rel="stylesheet" href="node_modules/dispatch-console-pages/dist/css/plugin.css" type="text/css"/>
-    <link rel="stylesheet" href="node_modules/dispatch-console-pages/dist/css/dispatch.css" type="text/css"/>
-    <link rel="stylesheet" href="node_modules/dispatch-console-pages/dist/css/dispatchpf.css" type="text/css"/>
+    <link rel="stylesheet" href="plugin/css/plugin.css" type="text/css"/>
+    <link rel="stylesheet" href="plugin/css/dispatch.css" type="text/css"/>
+    <link rel="stylesheet" href="plugin/css/dispatchpf.css" type="text/css"/>
 
 </head>
 <body ng-app="QDR" ng-controller="QDR.Core">
@@ -133,20 +133,20 @@ under the License.
 <script type="text/javascript" src="node_modules/dispatch-management/dist/dispatch-management.js"></script>
 <script type="text/javascript" src="plugin/js/dispatchPlugin.js"></script>
 <script type="text/javascript" src="plugin/js/navbar.js"></script>
-<script type="text/javascript" src="node_modules/dispatch-console-pages/dist/js/qdrGlobals.js"></script>
-<script type="text/javascript" src="node_modules/dispatch-console-pages/dist/js/qdrOverview.js"></script>
-<script type="text/javascript" src="node_modules/dispatch-console-pages/dist/js/qdrOverviewLogsController.js"></script>
-<script type="text/javascript" src="node_modules/dispatch-console-pages/dist/js/qdrOverviewChartsController.js"></script>
-<script type="text/javascript" src="node_modules/dispatch-console-pages/dist/js/qdrTopAddressesController.js"></script>
-<script type="text/javascript" src="node_modules/dispatch-console-pages/dist/js/dlgChartController.js"></script>
-<script type="text/javascript" src="node_modules/dispatch-console-pages/dist/js/qdrList.js"></script>
-<script type="text/javascript" src="node_modules/dispatch-console-pages/dist/js/qdrListChart.js"></script>
-<script type="text/javascript" src="node_modules/dispatch-console-pages/dist/js/qdrCharts.js"></script>
-<script type="text/javascript" src="node_modules/dispatch-console-pages/dist/js/qdrSchema.js"></script>
-<script type="text/javascript" src="node_modules/dispatch-console-pages/dist/js/qdrService.js"></script>
-<script type="text/javascript" src="node_modules/dispatch-console-pages/dist/js/qdrChartService.js"></script>
-<script type="text/javascript" src="node_modules/dispatch-console-pages/dist/js/qdrTopology.js"></script>
-<script type="text/javascript" src="node_modules/dispatch-console-pages/dist/js/qdrSettings.js"></script>
+<script type="text/javascript" src="plugin/js/qdrGlobals.js"></script>
+<script type="text/javascript" src="plugin/js/qdrOverview.js"></script>
+<script type="text/javascript" src="plugin/js/qdrOverviewLogsController.js"></script>
+<script type="text/javascript" src="plugin/js/qdrOverviewChartsController.js"></script>
+<script type="text/javascript" src="plugin/js/qdrTopAddressesController.js"></script>
+<script type="text/javascript" src="plugin/js/dlgChartController.js"></script>
+<script type="text/javascript" src="plugin/js/qdrList.js"></script>
+<script type="text/javascript" src="plugin/js/qdrListChart.js"></script>
+<script type="text/javascript" src="plugin/js/qdrCharts.js"></script>
+<script type="text/javascript" src="plugin/js/qdrSchema.js"></script>
+<script type="text/javascript" src="plugin/js/qdrService.js"></script>
+<script type="text/javascript" src="plugin/js/qdrChartService.js"></script>
+<script type="text/javascript" src="plugin/js/qdrTopology.js"></script>
+<script type="text/javascript" src="plugin/js/qdrSettings.js"></script>
 <script type="text/javascript" src="plugin/js/chord/ribbon/ribbon.js"></script>
 <script type="text/javascript" src="plugin/js/chord/matrix.js"></script>
 <script type="text/javascript" src="plugin/js/chord/filters.js"></script>

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/console/stand-alone/plugin/css/brokers.ttf
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/css/brokers.ttf b/console/stand-alone/plugin/css/brokers.ttf
new file mode 100644
index 0000000..ae83968
Binary files /dev/null and b/console/stand-alone/plugin/css/brokers.ttf differ

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/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
new file mode 100644
index 0000000..0096f95
--- /dev/null
+++ b/console/stand-alone/plugin/css/dispatch.css
@@ -0,0 +1,706 @@
+/*
+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.
+*/
+
+svg {
+  background-color: transparent;
+  cursor: default;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  -o-user-select: none;
+  user-select: none;
+}
+
+svg:not(.active):not(.ctrl) {
+  cursor: crosshair;
+}
+#end-arrow-selected, #start-arrow-selected {
+	stroke: #33F;
+	fill: #33F;
+}
+path.link.selected {
+  /* stroke-dasharray: 10,2; */
+  stroke: #33F  !important;
+}
+
+path.link {
+  fill: #000;
+  stroke: #000;
+  stroke-width: 4px;
+  cursor: default;
+}
+
+svg:not(.active):not(.ctrl) path.link {
+  cursor: pointer;
+}
+
+path.link.small {
+  stroke-width: 2.5;
+  stroke: darkgray;
+}
+path.link.highlighted {
+    stroke: #6F6 !important;
+}
+marker#start-arrow-highlighted,
+marker#end-arrow-highlighted {
+    fill: #6F6;
+}
+marker#start-arrow-small,
+marker#end-arrow-small {
+    fill: darkgray;
+}
+
+path.link.dragline {
+  pointer-events: none;
+}
+
+path.link.hidden {
+  stroke-width: 0;
+}
+
+circle.node {
+  stroke-width: 1.5px;
+  cursor: pointer;
+  stroke: darkgray;
+}
+
+circle.node.reflexive {
+  stroke: #F00 !important;
+  stroke-width: 2.5px;
+}
+circle.node.selected {
+  stroke: #6F6 !important;
+  stroke-width: 2px;
+  fill: #e0e0ff !important;
+}
+circle.node.highlighted {
+  stroke: #6F6;
+}
+circle.node.inter-router {
+    fill: #EAEAEA;
+}
+circle.node.normal.in {
+    fill: #F0F000;
+}
+circle.node.normal.out {
+    fill: #C0F0C0;
+}
+circle.node.on-demand {
+    fill: #C0FFC0;
+}
+circle.node.on-demand.artemis {
+	fill: #FCC;
+	/*opacity: 0.2; */
+}
+
+circle.node.fixed {
+    stroke-dasharray: 10,2;
+}
+text {
+  font: 12px sans-serif;
+  pointer-events: none;
+  /*font-family: monospace;*/
+
+}
+
+.tooltipsy
+{
+    padding: 10px;
+/*    max-width: 320px;*/
+    color: #303030;
+    background-color: #fcfcfe;
+    border: 1px solid #deca7e;
+    border-radius: 5px;
+}
+
+.tiptable {
+
+}
+.tiptable tr {
+	border-bottom: 1px solid #ccc;
+}
+
+.tiptable tr:last-child {
+	border-bottom: 0px;
+}
+
+.tiptable tr:nth-child(even) {
+	background: #fcfcfe;
+}
+.tiptable tr:nth-child(odd) {
+	background: #FFF
+}
+
+text.id {
+  text-anchor: middle;
+  font-weight: bold;
+}
+
+text.label {
+  text-anchor: start;
+  font-weight: bold;
+}
+
+.row-fluid.tertiary {
+  position: relative;
+  left: 20px;
+}
+
+.row-fluid.tertiary.left {
+  float: left;
+}
+
+.row-fluid.tertiary.panel {
+  width: 410px;
+  /*height: 100%; */
+}
+
+div#topologyForm .ngViewport, div#topologyForm .gridStyle {
+  height: auto !important;
+  min-height: initial !important;
+  overflow: initial;
+}
+
+div#multiple_details, div#link_details {
+	height: 300px;
+	width: 700px;
+	display: none;
+	padding: 0.5em;
+    border: 1px solid;
+	position: absolute;
+	background-color: white;
+	max-height: 330px !important;
+    overflow: hidden;
+}
+div#multiple_details div.ngRow.selected {
+	background-color: #c9dde1 !important;
+}
+
+div.grid-values {
+	text-align: right;
+}
+
+div.grid-values.ngCellText span {
+	padding-right: 4px;
+}
+
+.panel-adjacent {
+  margin-left: 430px;
+}
+
+/* globe */
+.land {
+  fill: #999;
+  stroke-opacity: 1;
+}
+
+.graticule {
+  fill: none;
+  stroke: black;
+  stroke-width:.5;
+  opacity:.1;
+}
+
+.labels {
+    font: 18px sans-serif;
+    fill: black;
+    opacity: .85;
+	text-anchor: middle;
+}
+
+.noclicks { pointer-events:none; }
+
+.point {  opacity:.6; }
+
+.arcs {
+  opacity:.7;
+  stroke: darkgreen;
+  stroke-width: 3;
+}
+.flyers {
+  stroke-width:1;
+  opacity: 0;
+  stroke: darkred;
+}
+.arc, .flyer {
+  stroke-linejoin: round;
+  fill:none;
+}
+.arc { }
+.arc:hover {
+  stroke: darkred;
+}
+.flyer { }
+.flyer:hover {
+  stroke: darkgreen;
+}
+.arc.inter-router {
+  stroke: darkblue;
+}
+
+#addNodeForm {
+  padding: 1em;
+}
+
+
+li.currentStep {
+  font-weight: bold;
+}
+
+.qdrTopology div.panel {
+  position: absolute;
+}
+/*
+.ui-dialog-titlebar {
+    border: 0;
+    background: transparent;
+}
+*/
+
+/*
+.ui-tabs.ui-tabs-vertical {
+    padding: 0;
+    width: 48em;
+}
+.ui-tabs.ui-tabs-vertical .ui-widget-header {
+    border: none;
+}
+.ui-tabs.ui-tabs-vertical .ui-tabs-nav {
+    float: left;
+    width: 10em;
+    background: #CCC;
+    border-radius: 4px 0 0 4px;
+    border-right: 1px solid gray;
+}
+.ui-tabs.ui-tabs-vertical .ui-tabs-nav li {
+    clear: left;
+    width: 100%;
+    margin: 0.1em 0;
+    border: 1px solid gray;
+    border-width: 1px 0 1px 1px;
+    border-radius: 4px 0 0 4px;
+    overflow: hidden;
+    position: relative;
+    right: -2px;
+    z-index: 2;
+}
+.ui-tabs.ui-tabs-vertical .ui-tabs-nav li a {
+    display: block;
+    width: 100%;
+    padding: 0.1em 1em;
+}
+.ui-tabs.ui-tabs-vertical .ui-tabs-nav li a:hover {
+    cursor: pointer;
+}
+.ui-tabs.ui-tabs-vertical .ui-tabs-nav li.ui-tabs-active {
+    margin-bottom: 0.2em;
+    padding-bottom: 0;
+    border-right: 1px solid white;
+}
+.ui-tabs.ui-tabs-vertical .ui-tabs-nav li:last-child {
+    margin-bottom: 10px;
+}
+.ui-tabs.ui-tabs-vertical .ui-tabs-panel {
+    float: left;
+    width: 34em;
+    border-left: 1px solid gray;
+    border-radius: 0;
+    position: relative;
+    left: -1px;
+}
+
+.ui-tabs .ui-tabs-nav li.ui-tabs-selected {
+    right: -3px !important;
+}
+
+.ui-tabs li i.ui-icon {
+    display: inline-block;
+}
+*/
+.ui-tabs .ui-tabs-panel {
+    /* padding-top: 0 !important; */
+}
+
+.ui-widget-content fieldset {
+  float: left;
+  padding: 0 1em 0 0;
+}
+
+.entity-description {
+  color: #960;
+  font-size: 90%;
+}
+
+.attr-description {
+  padding-top: 1.5em;
+  float: right;
+  width: 17em;
+}
+.attr-annotations {
+    padding-top: 2.5em;
+    clear: both;
+}
+.attr-annotations > span {
+    padding-top: 0.5em;
+    border-top: 1px dashed darkgray;
+    display: block;
+}
+
+.attr-type {
+    color: #990;
+    font-size: 85%;
+}
+.attr-required {
+    color: red;
+    font-size: 85%;
+}
+.attr-unique {
+    color: green;
+    font-size: 85%;
+}
+
+#tabs.nodeEntities {
+  border: 0;
+}
+
+#tabs ul.nodeTabs {
+  background: #fff;
+}
+
+#tabs #Container {
+  border-left: 1px solid #aaa;
+}
+
+#tabs.ui-tabs .ui-tabs-nav li {
+  border-bottom: 1px solid #aaa !important;
+}
+
+.entity-fields {
+  /* height: 400px; */
+  overflow-y: scroll;
+  overflow-x: hidden;
+}
+
+div.boolean label:first-child {
+    float: left;
+    margin-right: 1em;
+}
+div.boolean {
+    padding-bottom: 1em;
+}
+
+.entity-fields label {
+    font-weight: 600;
+    margin-top: 0.5em;
+	display: inline;
+}
+
+.aggregate {
+	text-align: right;
+}
+
+.aggregate i {
+  float: right;
+  /*  margin: 3px 3px 3px 8px; */
+}
+
+.aggregate .hastip {
+	padding: 5px;
+}
+
+.subTip .tipsy-inner {
+	background-color: white;
+	color: black;
+	font-size: 1.3em;
+	border: 1px solid black;
+}
+
+.subTip .tipsy-arrow-n { border-bottom-color: black; }
+.subTip .tipsy-arrow-s { border-top-color: black; }
+.subTip .tipsy-arrow-e { border-left-color: black; }
+.subTip .tipsy-arrow-w { border-right-color: black; }
+
+
+.contextMenu {
+    display:none;
+	position:absolute;
+	left:30px;
+	top:-30px;
+	z-index:999;
+	/* width:300px; */
+}
+.contextMenu ul {
+	width:300px;
+	margin:0;
+	padding-left:0;
+	list-style:none;
+	background:#fff;
+	color:#333;
+    font-weight: 600;
+	/* -moz-border-radius:5px; -webkit-border-radius:5px; border-radius:5px; */
+	-moz-box-shadow:5px 5px 5px #ddd; -webkit-box-shadow:5px 5px 5px #999; box-shadow:5px 5px 5px #ddd;
+	border: 1px solid #aaa;
+}
+.contextMenu ul li {
+	padding:5px 10px;
+	/* border-bottom: solid 1px #ccc; */
+}
+.contextMenu ul li:hover {
+	background:#4a90d9; color:#fff;
+}
+.contextMenu ul li:last-child {
+    border:none;
+}
+
+.na {
+    display: none;
+}
+.contextMenu ul li.new {
+    display: block;
+}
+.contextMenu ul li.adding, .contextMenu ul li.adding + li {
+    display: block;
+}
+.contextMenu ul li.force-display {
+    display: block;
+}
+.contextMenu ul li.context-separator {
+    background-color: lightgray;
+    height: 1px;
+    padding: 0;
+}
+
+.ui-tabs.ui-tabs-vertical .ui-tabs-nav li.separated {
+    margin-top: 1em;
+}
+
+#crosssection {
+    display: none;
+    position: absolute;
+    top: 200px;
+    left: 600px;
+}
+
+.cross-rect {
+  /* fill: #cfe2f3; */
+}
+.cross-line {
+  stroke: black;
+  stroke-width: 4px;
+}
+
+.node circle {
+/*  fill: rgb(31, 119, 180);
+  fill-opacity: .25; */
+  fill: #cfe2f3;
+  fill-opacity: .98;
+  stroke: black;
+  stroke-width: 3px;
+}
+
+circle.subcircle {
+    stroke-width: 1px;
+    /* stroke-dasharray: 2; */
+    fill-opacity: 0;
+    stroke: darkgray;
+}
+
+.leaf circle {
+  fill: #6fa8dc;
+  fill-opacity: 0.95;
+  stroke-width: 3px;
+}
+
+.leaf circle[title] {
+    font-family: monospace;
+
+}
+
+#svg_legend {
+    position: absolute;
+    top: 110px;
+    right: 0;
+    border: 1px solid #ccc;
+    border-radius: 5px;
+    background-color: #fcfcfc;
+    margin-right: 1.3em;
+	padding: 1em;
+}
+
+#svg_legend svg {
+    height: 235px;
+    width: 180px;
+}
+
+#multiple_details div.gridStyle {
+/*	height: 50em; */
+	min-height: 70px !important;
+	height: auto !important;
+}
+
+#multiple_details .ngViewport {
+    height: auto !important;
+}
+
+#multiple_details .gridCellButton button, #link_details .gridCellButton button {
+    margin: .25em .4em;
+    font-size: 12px;
+    height: 2em;
+	padding-top: .1em;
+}
+
+#linkFilter {
+	display: none;
+	padding: 0.5em;
+	border: 1px solid grey;
+	background-color: #F0F0F0;
+	position: absolute;
+	z-index: 100;
+	right: 1em;
+}
+div.formLine label, div.formLine input {
+	display: inline-block;
+	padding: 0 8px;
+}
+
+span.filter-icon {
+	padding-left: 1em;
+}
+
+button.filter-close {
+    width: 15px;
+    height: 20px;
+    padding: 0;
+    position: absolute;
+    right: 4px;
+    top: 4px;
+}
+
+div.filter-title h6 {
+	margin: 0 0 0.5em 0;
+}
+
+.links button.btn-filter {
+	padding: 0 1em 0 0;
+    margin-left: 1em;
+    font-size: 1em;
+}
+
+button.btn-filter {
+	float: right;
+}
+span.fancytree-expanded button.btn-filter,
+a.fancytree-title:hover button.btn-filter {
+ 	visibility: visible;
+}
+
+div.hdash-button a {
+	color: white;
+}
+
+.linkDirIn {
+	color: red;
+	background-color: #f3f3f3;
+}
+
+.linkDirOut {
+	color: blue;
+	background-color: white;
+}
+
+div.topoGrid .ui-grid-viewport {
+	overflow: hidden !important;
+}
+
+@-moz-document url-prefix() {
+    .btn {padding: 2px 12px 8px !important;}
+    #overview-controller .btn {padding: 4px 12px !important;}
+    #overview-controller .btn.filter-close {padding: 0 !important;}
+}
+
+.ui-fancytree.fancytree-container {
+	font-size: 14px;
+}
+
+.grid-title {
+    background-color: #FAFAFA;
+    background-image: -moz-linear-gradient(top, #ffffff, #f2f2f2);
+    background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f2f2f2));
+    background-image: -webkit-linear-gradient(top, #ffffff, #f2f2f2);
+    background-image: -o-linear-gradient(top, #ffffff, #f2f2f2);
+    background-image: linear-gradient(to bottom, #ffffff, #f2f2f2);
+    background-repeat: repeat-x;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFF', endColorstr='#F2F2F2', GradientType=0);
+    border-bottom: 1px solid #d4d4d4;
+    text-shadow: 0 1px 0 #FFFFFF;
+    border-top-left-radius: 5px;
+    border-top-right-radius: 5px;
+	margin: 0 0 10px 0;
+    padding-bottom: 4px;
+}
+
+.expand-collapse {
+	float: right;
+	margin-right: 0.5em;
+}
+
+.pane-viewport {
+  top: 26px !important;
+}
+.fancytree-node.loading {
+  position: initial;
+}
+
+.newChart {
+  float: right;
+}
+
+select.unique, input.unique {
+  border: 2px solid blue;
+}
+select.required, input.required {
+  border: 2px solid black;
+}
+
+.required-indicator {
+  padding-left: 0.5em;
+  font-size: 0.85em;
+  vertical-align: super;
+}
+
+.required-indicator::before {
+  content: '(required)'
+}
+
+.unique-indicator {
+  padding-left: 0.5em;
+  font-size: 0.85em;
+  vertical-align: super;
+}
+
+.unique-indicator::before {
+  content: '(must be unique if supplied)'
+}
+
+.unique-indicator
+
+.error {
+  color: red;
+  font-weight: bold;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/console/stand-alone/plugin/css/dispatchpf.css
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/css/dispatchpf.css b/console/stand-alone/plugin/css/dispatchpf.css
new file mode 100644
index 0000000..49e7d4b
--- /dev/null
+++ b/console/stand-alone/plugin/css/dispatchpf.css
@@ -0,0 +1,473 @@
+/*
+ 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
+*/
+
+/* The following are for matching Patternfly styles */
+span.logo {
+  letter-spacing: 3px;
+}
+
+.navbar-pf .navbar-brand {
+  padding: 2px 0 2px;
+}
+/* using fancytree instead of treeview */
+
+#overtree ul.fancytree-container,
+#entityTree ul.fancytree-container,
+#schema ul.fancytree-container {
+    background-color: transparent;
+    border: 0px;
+}
+
+span.fancytree-folder a {
+    font-weight: normal;
+    font-size: 12px;
+}
+
+ul.fancytree-container li,
+span.fancytree-icon {
+  background-image: none;
+}
+
+.fancytree-exp-c span.fa-angle, .fancytree-exp-e span.fa-angle,
+.fancytree-exp-cl span.fa-angle, .fancytree-exp-el span.fa-angle {
+  margin-right: 5px;
+}
+.fancytree-exp-c span.fa-angle:before, .fancytree-exp-cl span.fa-angle:before {
+  font-family: FontAwesome;
+  content: "\f105";
+}
+.fancytree-exp-e .fancytree-expander:before, .fancytree-exp-el .fancytree-expander:before {
+  font-family: FontAwesome;
+  content: "\f107";
+}
+.fancytree-exp-c .fancytree-expander:before, .fancytree-exp-cl .fancytree-expander:before {
+  font-family: FontAwesome;
+  content: "\f105";
+}
+.fancytree-exp-e span.fa-angle:before, .fancytree-exp-el span.fa-angle:before {
+  font-family: FontAwesome;
+  content: "\f107";
+}
+
+.fancytree-ico-ef span.fancytree-icon, .fancytree-ico-cf span.fancytree-icon {
+  background-image: initial;
+  font-family: FontAwesome;
+}
+
+/* closed folder */
+.fancytree-ico-cf span.fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f07b";
+}
+/* expanded folder */
+.fancytree-ico-ef span.fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f07c";
+}
+/* chart */
+.fancytree-ico-cf.charts span.fancytree-icon:before, .fancytree-ico-ef.charts span.fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f080";
+}
+
+
+ul.fancytree-container a {
+    color: #363636;
+}
+
+/* override left and right pane styles */
+#schema .col-sm-3,
+.qdr-topology-form.col-sm-3 {
+  padding-left: 0px;
+}
+
+.qdr-topology-form.col-sm-3 {
+  padding-right: 0px;
+}
+
+#overview-controller .ngViewport {
+  height: auto !important;
+}
+
+.pane-wrapper {
+  padding-top: 0.5em;
+}
+
+.pane-viewport {
+    position: relative;
+    /* height: calc(100vh - 20px); */
+    overflow: auto;
+}
+
+[class^="icon-"],
+[class*=" icon-"] {
+  display: inline;
+  width: auto;
+  height: auto;
+  line-height: normal;
+  vertical-align: baseline;
+  background-image: none;
+  background-position: 0% 0%;
+  background-repeat: repeat;
+  margin-top: 0;
+}
+[class^="icon-"],
+[class*=" icon-"] {
+  font-family: FontAwesome;
+  font-weight: normal;
+  font-style: normal;
+  text-decoration: inherit;
+  -webkit-font-smoothing: antialiased;
+  *margin-right: .3em;
+}
+
+[class^="icon-"]:before,
+[class*=" icon-"]:before {
+  text-decoration: inherit;
+  display: inline-block;
+  speak: none;
+}
+
+.icon-bar-chart:before {
+  font-family: FontAwesome;
+    content: "\f080";
+}
+
+.icon-step-forward:before {
+  font-family: FontAwesome;
+  content: "\f051";
+}
+.icon-step-backward:before {
+  font-family: FontAwesome;
+  content: "\f048";
+}
+
+.icon-close:before {
+  font-family: FontAwesome;
+  content: "\f410";
+}
+
+[class^="icon-"]:before, [class*=" icon-"]:before {
+    text-decoration: inherit;
+    display: inline-block;
+    speak: none;
+}
+
+#svg_legend {
+  top: 10px;
+}
+
+#topologyForm > div {
+  width: auto;
+}
+
+div.chartContainer {
+  width: auto;
+  margin-top: 1em;
+}
+
+#dispatch-login-container {
+  width: 90em;
+}
+
+#dispatch-login-container .alert {
+  padding-right: 47px;
+}
+
+.fa-edit:before, .fa-trash-o:before {
+  font-family: FontAwesome;
+}
+
+div.qdr-topology.pane.left {
+  width: 400px;
+  top: 0;
+}
+/*
+.treeview .list-group-item.node-selected {
+  color: #FFFFFF !important;
+  background-color: #428bca !important;
+}
+*/
+span.fancytree-expander {
+  display: none;
+}
+
+#overview-controller span.fancytree-expander {
+  display: inline-block;
+}
+
+.fancytree-active .fancytree-title,
+.fancytree-active .fancytree-icon,
+.fancytree-active .fancytree-expander {
+  color: white;
+}
+
+.ui-grid {
+  border: 0px;
+}
+
+.ui-grid-header {
+  border: 1px solid #d4d4d4;
+}
+
+#overview-controller .sidebar-pf-left, #list-controller .sidebar-pf-left {
+  padding-right: 0px;
+}
+
+#crosshtml {
+  position: absolute;
+  display: none;
+  padding: 1em;
+  border: 1px solid grey;
+  background-color: white;
+  border-radius: 4px;
+}
+
+.pane-bar {
+  float: right;
+  height: calc(100vh - 75px);
+  width: 6px;
+  background-color: white;
+  border: 1px solid #e0e0e0;
+  position: absolute;
+  top: 0;
+  right: -6px;
+  cursor: ew-resize;
+
+
+  display: none;
+}
+
+.dispatch-c3-tooltip {
+  border-collapse: collapse;
+  border-spacing: 0;
+  background-color: #fff;
+  empty-cells: show;
+  -webkit-box-shadow: 7px 7px 12px -9px #777777;
+  -moz-box-shadow: 7px 7px 12px -9px #777777;
+  box-shadow: 7px 7px 12px -9px #777777;
+  opacity: 0.9;
+
+}
+
+.dispatch-c3-tooltip tr {
+  border: 1px solid #CCC;
+}
+
+.dispatch-c3-tooltip th {
+  background-color: #aaa;
+  font-size: 13px;
+  padding: 2px 5px;
+  text-align: left;
+  color: #FFF;
+  font-weight: normal;
+}
+
+.dispatch-c3-tooltip td {
+  font-size: 13px;
+  padding: 3px 6px;
+  background-color: #fff;
+  border-left: 1px dotted #999;
+}
+
+.chart-tip-legend {
+  width: 10px;
+  height: 10px;
+  display: inline-block;
+  margin-right: 4px;
+}
+
+th.text-center {
+  text-align: center;
+}
+
+.clearfix:after {
+  content: "";
+  display: table;
+  clear: both;
+}
+
+#topAddresses.grid {
+  min-height: 190px;
+}
+
+circle.c3-circle {
+  display: none;
+}
+
+circle.c3-circle._expanded_ {
+  display: inline;
+}
+
+#main.container-fluid {
+  position: relative;
+}
+
+.chord-diagram:before {
+  font-style: normal;
+  font-family: FontAwesome;
+  content: "\f021";
+}
+
+.code-branch:before {
+  font-style: normal;
+  font-family: FontAwesome;
+  /* content: "\f126"; */
+  content: "\f085"; /* cogs */
+}
+
+.icon-list:before {
+  font-style: normal;
+  font-family: FontAwesome;
+  content: "\f0db";
+}
+
+.icon-power:before {
+  font-style: normal;
+  font-family: FontAwesome;
+  content: "\f011";
+}
+
+.icon-ellipsis-v:before {
+  font-style: normal;
+  font-family: FontAwesome;
+  content: "\f142";
+}
+
+.icon-schema:before {
+  font-style: normal;
+  font-family: FontAwesome;
+  content: "\f0ca";
+}
+
+/* convert fancytree theme to dark */
+span.fancytree-title {
+  color: white;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  width: 16em;
+}
+
+span.fancytree-node:hover {
+  color: black;
+  background-color: white;
+}
+span.fancytree-node:hover span.fancytree-title {
+  color: black;
+}
+
+span.fancytree-node.fancytree-active:hover, 
+span.fancytree-node.fancytree-active:hover span.fancytree-title {
+  color: white;
+}
+
+span.fancytree-expanded.fancytree-has-children, span.fancytree-expanded.fancytree-has-children + ul {
+  background-color: #444444;
+}
+span.fancytree-expanded.fancytree-has-children:hover {
+  background-color: white;
+}
+
+span.fancytree-node.fancytree-active {
+  background-color: #6aa3d5;
+}
+
+.navbar-fixed-top + .container-fluid {
+  padding-top: 71px;
+}
+
+@media (max-width: 768px) {
+  .navbar-fixed-top + .container-fluid {
+    padding-top: 38px;
+  }
+}
+
+.navbar-pf .navbar-nav>li>a {
+  padding: 14px 16px;
+}
+
+@media (min-width: 768px) {
+  .navbar-collapse.page-menu {
+    padding-right: 0;
+    /* height: calc(100vh - 74px) !important; */
+  }
+}
+@media (max-width: 768px) {
+  .navbar-collapse.page-menu {
+    position: absolute;
+    z-index: 1;
+    height: auto;
+    left: 0;
+  }
+}
+
+.page-menu {
+    padding-left: 0;
+    background: #333333;
+    z-index: 1;
+    transition: all .2s cubic-bezier(.35,0,.25,1);
+    position: absolute;
+    left: 0;
+    color: white;
+    width: 300px;
+}
+
+.page-menu-button {
+  /*color: black;
+  background: white; */
+  top: 0;
+  z-index: 2;
+  width: 1em;
+  padding: 4px 0 0 4px;
+  margin: 4px 0 0 4px;
+}
+.page-menu-button span.icon-bar {
+  display: block;
+  color: black;
+  background-color: black;
+  height: 4px;
+  width: 4px;
+  font-weight: bold;
+  border-radius: 2px;
+}
+
+.navbar-toggle.left {
+  float: left;
+}
+
+.hideLeft {
+  position: absolute;
+  right: -1.5em;
+  top: 0em;
+  border: 0;
+  color: white;
+  background-color: #444444;
+}
+
+.showLeft {
+  position: absolute;
+  top: 0em;
+  left: 0;
+  border: 0;
+  z-index: 3;
+  color: white;
+  background-color: #444444;
+}
+

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/console/stand-alone/plugin/css/plugin.css
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/css/plugin.css b/console/stand-alone/plugin/css/plugin.css
new file mode 100644
index 0000000..f7259dd
--- /dev/null
+++ b/console/stand-alone/plugin/css/plugin.css
@@ -0,0 +1,1019 @@
+/*
+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.
+*/
+main-display > .span8 {
+  height: 100%;
+  position: relative;
+}
+
+ul.qdrListNodes > li > span {
+  padding: 6px 20px; 6px; 6px;
+  display: block;
+}
+
+.qdrList .grid {
+    width: 20em;
+    margin-right: 0;
+    float: left;
+}
+
+
+.qdrList div.gridDetails {
+    width: auto;
+}
+
+.selectedItems {
+    /* margin-left: 21em; */
+}
+
+.qdrListPane {
+    top: 110px;
+}
+
+.qdrListActions {
+    width: auto;
+    margin-left: 1em;
+}
+
+div.listAttrName {
+    padding-top: 5px;
+}
+
+div.listAttrName button {
+    float: right;
+    margin: 3px 5px;
+}
+
+div.listAggrValue button {
+    margin: 3px 5px;
+}
+
+@-moz-document url-prefix() {
+  div.listAttrName button {
+    top: -24px;
+    position: relative;
+    margin: 3px 0px;
+    padding: 0 5px !important;
+  }
+}
+
+div.listAttrName i.icon-bar-chart.active, div.hastip i.icon-bar-chart.active, li.haschart i {
+    /*background-color: #AAFFAA; */
+}
+
+div#main div ul.nav li a:not(.btn) {
+    background: initial !important;
+}
+
+div#main div ul.nav li.active a {
+    background-color: #f0f0ff !important;
+}
+
+div#main.qdr {
+    margin-top: 56px !important;
+}
+
+div.charts-header {
+  font-size: 1.2em;
+  color: #666666;
+  margin: 1em 0;
+}
+
+.selectedNode, .selectedAction, .selectedEntity {
+    font-weight: 600;
+    color: #606066;
+}
+
+.okButton {
+    text-align: center;
+    margin: 1em;
+}
+
+span.showChartsLink {
+    border: 1px solid blue;
+    padding: 1px 2px;
+}
+
+div.listGraphs p {
+    margin: 1em 0 2em 2em;
+    text-align: center;
+}
+
+div.centered {
+    text-align: center;
+    margin: 4em;
+}
+
+.modal-body.centered {
+	margin: 0;
+}
+
+/* dialog */
+div.aChart {
+    height: 200px;
+    width:  400px;
+    margin: 1em;
+}
+
+/* dashboard */
+div.aChart.hDash {
+	/* width: 21em; */
+	/* height: 17em; */
+	width: 100%;
+	height: 87%;
+	margin: 0;
+	padding: 0;
+
+}
+div.chartContainer {
+    float: left;
+	width: 100%;
+	height: 100%;
+	overflow: hidden;
+}
+
+/* the x and y axis lines */
+.d3Chart g.axis path.domain {
+    stroke-width: 1;
+    stroke: black;
+}
+
+/* the line surrounding the area chart */
+div.d3Chart path {
+/*    stroke: black; */
+/*    stroke-width: 0; */
+/*	opacity: 0.5; */
+}
+
+/* the line above the area chart */
+/* the color gets overridden */
+div.d3Chart path.line {
+    stroke: steelblue;
+    stroke-width: 1.5;
+    fill: none;
+    opacity: 1;
+}
+
+.mo-rect {
+    fill: #ffffdd;
+    stroke: #f0f0f0;
+    stroke-width: 1;
+}
+
+.mo-guide {
+    fill: none;
+    stroke: #d0d0d0;
+    stroke-width: 2;
+    stroke-dasharray: 3,3;
+}
+
+div.d3Chart .title {
+    text-decoration: underline;
+}
+
+
+.axis line, .axis path {
+  fill: none;
+  shape-rendering: crispEdges;
+  stroke-width: 1;
+  stroke: #000000;
+}
+
+.axis line {
+  stroke: #C0C0C0;
+  stroke-dasharray: 1,1;
+  opacity: 0.5;
+}
+
+.y.axis text, .x.axis text, .focus text, div.d3Chart .title {
+    font-size: 12px;
+}
+
+.y.axis path {
+   stroke: #000;
+ }
+
+.overlay {
+   fill: none;
+   pointer-events: all;
+ }
+
+.focus circle {
+   fill: none;
+   stroke: steelblue;
+ }
+.focus .fo-table {
+	/* box-shadow: 2px 2px 3px #EEE; */
+}
+
+div.d3Chart {
+    /* padding: 1em 0; */
+    /* border: 1px solid #C0C0C0; */
+    margin-bottom: 2em;
+}
+div.d3Chart.hDash {
+    border: 0px;
+}
+
+div.d3Chart .axis path {
+	display: inherit;
+}
+
+.c3-circle {
+	/* display: none; */
+}
+
+.fo-table {
+	border: 1px solid darkgray;
+	background-color: white;
+	font-size: .85em;
+}
+
+.fo-table td {
+	padding: 4px;
+	border-left: 1px solid darkgray;
+}
+.fo-table tr.detail td {
+	padding: 1px 4px;
+}
+.fo-title {
+	color: white;
+	background-color: darkgray;
+}
+
+.fo-table-legend {
+	width: 8px;
+	height: 8px;
+	border: 1px solid black;
+	margin: 0 4px;
+	display: inline-block;
+}
+
+svg .legend {
+	dominant-baseline: central;
+}
+
+div.chartContainer div.aChart {
+    margin-top: 0.5em;
+}
+
+div#main.qdr div ul.nav li.active a {
+  background-color: #e0e0ff !important;
+  color: #000000;
+}
+
+div#main.qdr .selected, .box.selected {
+  color: #000000;
+  text-shadow: none;
+}
+
+/* the selected node on the list page */
+div.qdrList li.active, ul.qdrListNodes li.active {
+    background-color: #e0e0ff;
+}
+
+div.qdr-attributes span.fancytree-selected a {
+    background-color: #e0e0ff;
+}
+div.qdr-attributes.pane, div.qdr-topology.pane {
+	position: absolute;
+	margin-left: 10px;
+}
+div.qdr-overview.pane {
+	position: absolute;
+}
+div.qdr-topology.pane.left {
+	width: auto;
+  /*border-right: 1px solid lightgray; */
+}
+
+/* the selected row in the name table */
+div#main.qdr div.qdrList div.selected {
+  background-color: #e0e0ff !important;
+}
+
+#dialogChart, #dialogEditChart {
+    height: 220px;
+}
+
+.chartOptions .modal-body {
+  overflow-y: initial;
+}
+
+div.qdrCharts p.chartLabels button {
+    float: right;
+}
+
+div.qdrCharts p.chartLabels {
+     padding-right: 1em;;
+ }
+
+p.dialogHeader {
+    text-align: center;
+}
+
+p.dialogHeader input {
+    margin-top: 10px;
+    width: 480px;
+}
+
+.ui-slider-tick {
+  position: absolute;
+  background-color: #666;
+  width: 2px;
+  height: 8px;
+  top: 12px;
+  z-index: -1;
+}
+
+label.rateGroup {
+    float: left;
+}
+
+div.chartOptions div.dlg-slider {
+    float: left;
+    width: 28em;
+    font-size: 14px;
+}
+
+div.chartOptions div.duration {
+  width: 35em !important;
+}
+
+div.chartOptions .slider {
+    margin-top: 1em;
+    margin-bottom: 1em;
+}
+
+input[type="radio"] {
+    margin-top: 0 !important;
+}
+
+div.chartOptions legend {
+    font-size: 1.2em;
+    font-weight: bold;
+    margin-bottom: 10px;
+}
+
+div.chartOptions tab > div {
+  margin-left: 1em;
+}
+
+div.chartOptions span.minicolors-swatch {
+    width: 14px;
+    height: 14px;
+}
+
+.minicolors-input {
+    width: 4em;
+    padding: 0 0 0 24px !important;
+}
+
+div.colorPicker div.colorText {
+	display: inline-block;
+	width: 10em;
+}
+div.colorPicker div:nth-of-type(1), /* first span under div.colorPicker */
+ div.minicolors{
+    float:left;
+    margin-right: 0.5em;
+}
+
+div.chartOptions p.sep {
+    height: 1em;
+}
+
+ul.nav-tabs {
+    border-bottom: 1px solid #ddd !important;
+}
+
+.chartOptions ul.nav-tabs {
+    margin-bottom: 0px !important;
+}
+
+div.tabbable div.tab-content {
+    overflow: visible;
+}
+
+div.tabbable ul.nav-tabs > .active > a {
+  background-color: #f8f8f8;
+  border: 1px solid #ddd;
+  border-bottom-color: transparent;
+}
+
+
+div.tabbable .tab-pane {
+    background-color: #f8f8f8;
+    padding: 12px;
+    border-right: 1px solid #ddd;
+    border-left: 1px solid #ddd;
+    border-bottom: 1px solid #ddd;
+}
+div.dlg-large div.tabbable .tab-pane {
+	margin-left: 11em;
+}
+
+div.tabbable ul.nav-tabs {
+  margin-bottom: 0;
+}
+
+ul.qdrTopoModes {
+    position: relative;
+    top: -10px;
+}
+.overview.section {
+	/* width: 35em; */
+}
+.overview.section .ngGrid {
+	height: 12em !important;
+	min-height: 12em !important;
+}
+
+.overview.routers.section .ngGrid {
+ 	height: 16em !important;
+ 	min-height: 16em !important;
+}
+.overview.routers.section {
+ 	/*width: 15em; */
+ }
+
+.grid-align-value {
+	text-align: right;
+}
+
+.grid-align-value .ngCellText {
+	padding-right: 10px;
+}
+
+.overview .ngRow:hover {
+	background:#e0e0ff;
+}
+
+.overview-cell .ngCell:hover {
+	background:#e0e0ff;
+}
+.overview-cell .ngCell.col0:hover, .overview-cell .ngCell.col1:hover {
+	background: initial;
+}
+
+
+.qdr-overview.pane.left, .qdr-attributes.pane.left {
+	top: 104px;
+}
+.qdr-topology.pane.left {
+	top: 104px;
+}
+.qdr-overview.pane.left, .qdr-attributes.pane.left, .qdr-topology.pane.left {
+	left: 10px;
+}
+
+.overview-tree, .qdr-attributes {
+  min-height: calc(100vh - 90px)
+}
+/*
+.treeContainer {
+    width: 100%;
+    float: left;
+}
+*/
+.pane-content {
+	overflow: auto;
+}
+
+#entityNames {
+    width: 20em;
+    float: left;
+}
+
+.treeDetails {
+	margin-left: 260px;
+}
+
+.grid:not(.noHighlight) .ui-grid-row:hover .ui-grid-cell-contents {
+  /*background-color: #e0e0ff; */
+  background-color: #e9f2f9
+}
+.ngCellText {
+	padding: 4px 0 0 4px;
+}
+
+.ui-grid-row.ui-grid-row-selected > [ui-grid-row] > .ui-grid-cell {
+  background-color: #e0e0ff;
+}
+
+.tab-content .tab-pane {
+    background-color: #f8f8f8;
+    padding: 12px;
+    border-right: 1px solid #ddd;
+    border-left: 1px solid #ddd;
+    border-bottom: 1px solid #ddd;
+}
+
+div.chartOptions ul.nav-tabs > .active > a {
+  background-color: #f8f8f8;
+  border: 1px solid #ddd;
+  border-bottom-color: transparent;
+}
+
+div.chartOptions label:nth-of-type(2) {
+    margin-left: 1em;
+}
+div.chartOptions label {
+	font-weight: normal;
+	display: inline-block;
+}
+
+/*
+.form-horizontal .control-label {
+    float: left;
+    width: 160px;
+    padding-top: 5px;
+    text-align: right;
+}
+
+.form-horizontal .controls {
+    margin-left: 180px;
+}
+
+.form-horizontal input,  {
+    display: inline-block;
+    margin-bottom: 0;
+    vertical-align: middle;
+}
+
+input[type="text"], input[type="number"], input[type="password"] {
+    background-color: #ffffff;
+    border: 1px solid #cccccc;
+    -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+    -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+    box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+    -webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
+    -moz-transition: border linear 0.2s, box-shadow linear 0.2s;
+    -o-transition: border linear 0.2s, box-shadow linear 0.2s;
+    transition: border linear 0.2s, box-shadow linear 0.2s;
+}
+
+input[type="text"], input[type="number"], input[type="password"] {
+    display: inline-block;
+    width: 200px;
+    padding: 4px 6px;
+    margin-bottom: 10px;
+    font-size: 14px;
+    line-height: 20px;
+    color: #555555;
+    vertical-align: middle;
+    -webkit-border-radius: 4px;
+    -moz-border-radius: 4px;
+    border-radius: 4px;
+}
+
+.login input[type="checkbox"] {
+	margin-top: 0.75em;
+}
+*/
+
+#dispatch-login-container {
+	/* width: 18.5em; */
+	margin-top: 2em;
+}
+/*
+div.login.container {
+	width: 550px;
+}
+*/
+
+
+#overtree .fancytree-container {
+	border: 0px;
+}
+
+#overtree span.fancytree-alert-icon.ui-icon-refresh {
+	background-position: -64px -80px;
+}
+#overtree span.fancytree-alert-icon.ui-icon-transfer-e-w {
+	background-position: -112px -80px;
+}
+
+#alerts {
+	position: fixed;
+	right: 0;
+	top: 0;
+	z-index: 100;
+}
+
+.alert-enter,
+.alert-leave,
+.alert-move {
+  -webkit-transition: 1s linear all;
+  -moz-transition: 1s linear all;
+  -o-transition: 1s linear all;
+  transition: 1s linear all;
+  position:relative;
+}
+
+.alert-enter {
+  left:-10px;
+  opacity:0;
+}
+.alert-enter.alert-enter-active {
+  left:0;
+  opacity:1;
+}
+
+.alert-leave {
+  left:0;
+  opacity:1;
+}
+.alert-leave.alert-leave-active {
+  left:-10px;
+  opacity:0;
+}
+
+.alert-move {
+  opacity:0.5;
+}
+.alert-move.alert-move-active {
+  opacity:1;
+}
+
+.overview .table-striped tr:hover  td {
+	background-color: #e0e0ff !important;
+}
+
+#entityNames div.ngViewport {
+	overflow-x: hidden;
+}
+
+.connect-column.connect-form {
+	width: 20em;
+}
+
+.chartLabels button a {
+	text-decoration: none;
+}
+
+.fancytree-ico-c.router .fancytree-icon {
+
+}
+
+.tabs-left .nav-tabs {
+	float: left;
+}
+.tabs-left .nav-tabs > li {
+/*	float: initial; */
+}
+
+div.modal.dlg-large {
+	width: 53em;
+}
+
+button.hdash-button a {
+	text-decoration: none;
+	color: #fff;
+}
+
+div.widget-body > div {
+	height: 100%;
+}
+
+div.qdrCharts {
+	height: 100%;
+}
+
+ul.dispatch-view {
+	margin-bottom: 0 !important;
+}
+
+.qdr-overview.pane.left span:not(.fancytree-has-children) .fancytree-icon:before,
+.qdr-attributes.pane.left span:not(.fancytree-has-children) .fancytree-icon:before {
+    color: green;
+}
+
+span:not(.fancytree-has-children).address .fancytree-icon:before,
+span:not(.fancytree-has-children).router\.address .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f0ac";
+}
+span:not(.fancytree-has-children).address.mobile .fancytree-icon:before,
+span:not(.fancytree-has-children).router\.address.mobile .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f109";
+}
+span:not(.fancytree-has-children).address.internal.mobile .fancytree-icon:before,
+span:not(.fancytree-has-children).router\.address.internal.mobile .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f0ac";
+}
+span:not(.fancytree-has-children).address.router .fancytree-icon:before,
+span:not(.fancytree-has-children).router\.address.router .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f047";
+}
+
+span.address-link .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f0ac";
+}
+
+span:not(.fancytree-has-children).connection.external .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f109";
+}
+span:not(.fancytree-has-children).connection.normal .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f08e";
+}
+span:not(.fancytree-has-children).connection.external.quiesced .fancytree-icon:before {
+  font-family: FontAwesome;
+	content: "\f14c";
+	color: red;
+}
+span:not(.fancytree-has-children).connection.inter-router .fancytree-icon:before {
+  font-family: FontAwesome;
+   content: "\f07e";
+}
+span:not(.fancytree-has-children).connection.router-control .fancytree-icon:before {
+  font-family: FontAwesome;
+   content: "\f013";
+}
+span:not(.fancytree-has-children).no-data .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f05e";
+  color: red !important;
+}
+span:not(.fancytree-has-children).loading .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f254";
+}
+span:not(.fancytree-has-children).connector .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f126";
+}
+span:not(.fancytree-has-children).container .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f16c";
+}
+span:not(.fancytree-has-children).log .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f0f6";
+}
+span:not(.fancytree-has-children).router\.node .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f013";
+}
+span:not(.fancytree-has-children).link.inter-router .fancytree-icon:before,
+span:not(.fancytree-has-children).router\.link.inter-router .fancytree-icon:before{
+  font-family: FontAwesome;
+  content: "\f07e";
+}
+span:not(.fancytree-has-children).link.router-control .fancytree-icon:before,
+span:not(.fancytree-has-children).router\.link.router-control .fancytree-icon:before{
+  font-family: FontAwesome;
+  content: "\f013";
+}
+span:not(.fancytree-has-children).link.endpoint .fancytree-icon:before,
+span:not(.fancytree-has-children).router\.link.endpoint .fancytree-icon:before{
+  font-family: FontAwesome;
+  content: "\f109";
+}
+span:not(.fancytree-has-children).link.console .fancytree-icon:before,
+span:not(.fancytree-has-children).router\.link.console .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f108";
+}
+span:not(.fancytree-has-children).listener .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f025";
+}
+span:not(.fancytree-has-children).connection .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f07e";
+}
+span:not(.fancytree-has-children).connection.console .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f108";
+}
+span:not(.fancytree-has-children).waypoint .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f0ec";
+}
+span:not(.fancytree-has-children).router .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f047";
+}
+span:not(.fancytree-has-children).fixedAddress .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f015";
+}
+span:not(.fancytree-has-children).linkRoutePattern .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f039";
+}
+span:not(.fancytree-has-children).allocator .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f170";
+}
+span:not(.fancytree-has-children).policy .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f256";
+}
+span:not(.fancytree-has-children).logStats .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f080";
+}
+span:not(.fancytree-has-children).autoLink .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f0c1";
+}
+span:not(.fancytree-has-children).authServicePlugin .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f0c1";
+}
+span:not(.fancytree-has-children).sslProfile .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f023";
+}
+span:not(.fancytree-has-children).vhost .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f108";
+}
+span:not(.fancytree-has-children).vhostStats .fancytree-icon:before {
+  font-family: FontAwesome;
+  content: "\f080";
+}
+
+
+.ngCellText {
+/*    color: #333333; */
+}
+
+.changed {
+    color: #339933;
+}
+
+div.dispatch-router div.help {
+    width: auto;
+    padding: 1em;
+    background-color: lavender;
+    border-radius: 6px;
+    margin-top: 1em;
+    text-align: center;
+}
+
+div.operations tr:nth-child(even) {
+	background: #f3f3f3;
+}
+div.operations tr:nth-child(odd), div.operations tr:last-child {
+	background: #fff;
+}
+
+div.operations tr input {
+	margin: 0;
+	padding: 3px 6px;
+}
+div.operations table {
+    width: 100%;
+}
+div.operations th {
+    width: 50%;
+    border-bottom: 1px solid #cccccc;
+    text-align: left;
+}
+div.operations td:nth-child(odd), div.operations th:nth-child(odd) {
+	border-right: 1px solid #cccccc;
+}
+div.operations td:nth-child(odd) {
+	padding-left: 0;
+}
+div.operations td:nth-child(even), div.operations th:nth-child(even) {
+	padding-left: 5px;
+}
+div.operations th {
+	padding: 5px;
+}
+div.operations .tab-pane.active {
+    padding: 12px 12px 12px 0;
+}
+div.operations label {
+    padding-top: 4px;
+    margin-bottom: 4px;
+}
+.qdrListActions .ngGrid {
+	/*min-height: 40em;
+	height: 100%; */
+}
+div.qdrListActions .ngViewport {
+    height: initial !important;
+}
+
+div.operations .boolean {
+    padding-bottom: 0;
+}
+
+table.log-entry {
+    margin-bottom: 1em;
+    border-top: 1px solid black;
+}
+
+table.log-entry pre {
+    background-color: #f5f5f5;
+    color: inherit;
+    margin: 0;
+    white-space: pre-wrap;
+}
+
+circle.node.normal.console {
+    fill: lightcyan;
+}
+circle.node.artemis {
+    fill: lightgreen;
+}
+circle.node.route-container {
+    fill: orange;
+}
+
+text.console, text.on-demand, text.normal {
+	font-family: FontAwesome;
+	font-weight: normal;
+	font-size: 16px;
+}
+
+@font-face {
+    font-family:"Brokers";
+    src: url("brokers.ttf") /* TTF file for CSS3 browsers */
+}
+
+text.artemis {
+    font-family: Brokers;
+    font-size: 20px;
+    font-weight: bold;
+}
+
+text.qpid-cpp {
+    font-family: Brokers;
+    font-size: 18px;
+    font-weight: bold;
+}
+
+i.red {
+	color: red;
+}
+
+.qdrListActions div.delete {
+    width: 20em;
+    margin: auto;
+    border: 1px solid #eaeaea;
+    height: 5em;
+    padding: 4em;
+    background-color: #fcfcfc;
+}
+
+.btn:focus {
+    box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
+}
+
+select:focus, input[type="file"]:focus, input[type="radio"]:focus, input[type="checkbox"]:focus {
+	outline:3px solid rgba(82, 168, 236, 0.6);
+    box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
+    outline: 5px auto -webkit-focus-ring-color;
+    outline-offset: -2px;
+}
+
+btn.disabled, .btn[disabled] {
+    opacity: 0.35;
+}
+
+#dispatch-login-container .ng-invalid-range {
+	border-color: #e9322d !important;
+}
+
+div#durationSlider, div#rateSlider {
+	margin-top: 1em;
+}
+
+.list-grid {
+	padding-left: 10px;
+}
+
+.ngViewport.ng-scope {
+  height: auto !important;
+}
+
+.listening-on {
+    background-color: #CCFFCC;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/console/stand-alone/plugin/html/qdrCharts.html
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/html/qdrCharts.html b/console/stand-alone/plugin/html/qdrCharts.html
new file mode 100644
index 0000000..dc57874
--- /dev/null
+++ b/console/stand-alone/plugin/html/qdrCharts.html
@@ -0,0 +1,48 @@
+<!--
+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.
+-->
+
+<style>
+    .page-menu-button {
+        display: none;
+    }
+
+  @media (max-width: 768px) {
+    div.aChart {
+        width: 380px;
+        margin-left: 0;
+    }
+}
+</style>
+<div class="main-display row-fluid qdrCharts" ng-controller="QDR.ChartsController">
+    <div ng-show="hasCharts()">
+        <div ng-repeat="chart in svgCharts" class="chartContainer">
+            <p class="chartLabels">
+                <button ng-click="delChart(chart)" title="Delete"><span class="fa-trash-o"></span></button>
+                <button ng-click="editChart(chart)" title="Configure"><span class="fa-edit"></span></button>
+<!--                <button ng-click="zoomChart(chart)" title="Zoom {{chart.zoomed ? 'in' : 'out'}}" ng-if="!chart.chart.request().nodeList"><i ng-class="chart.zoomed ? 'fa-zoom-in' : 'icon-zoom-out'"></i></button> -->
+            </p><div style="clear:both"></div>
+            <div id="{{chart.chart.id()}}" class="line-chart-pf aChart d3Chart"></div>
+        </div>
+        <div ng-init="chartsLoaded()"></div>
+    </div>
+    <div ng-hide="hasCharts()" class="centered">
+        There are no charts. To add charts to this page, click on a <i class="icon-bar-chart"></i> icon on the <button ng-click="showListPage()"><i class="icon-list "></i> Entities</button> page.
+    </div>
+</div>
+

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/console/stand-alone/plugin/html/qdrConnect.html
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/html/qdrConnect.html b/console/stand-alone/plugin/html/qdrConnect.html
new file mode 100644
index 0000000..5a4399b
--- /dev/null
+++ b/console/stand-alone/plugin/html/qdrConnect.html
@@ -0,0 +1,87 @@
+<!--
+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.
+-->
+
+<style>
+    .alert {
+        margin-top: 4em;
+    }
+
+</style>
+<div class="row-fluid" ng-controller="QDR.SettingsController">
+    <div class="login" ng-hide="connecting">
+        <div class="col-xs-1"></div>
+        <div class="col-xs-10">
+            <div class="alert alert-success">
+                    <p>
+                        Enter the address and a HTML-enabled port of a <strong><a href="http://qpid.apache.org/components/dispatch-router/" target="_blank">Qpid Dispatch Router</a></strong>.
+                    </p>
+                    <!--
+                    <p>
+                        If Autostart is checked, you will be automatically logged in to the router the next time you start the console.
+                    </p> -->
+
+            </div>
+            <div class="connect-form">
+                <form class="form-horizontal no-bottom-margin" novalidate ng-submit="connect()" name="settings">
+                        <div class="form-group">
+                            <label class="col-md-2 control-label" for="address">Address: </label>
+                            <div class="col-md-6">
+                                <input tabindex="1" id="address" type="text" ng-model="formEntity.address" placeholder="localhost" name="address" autofocus="autofocus" class="ng-pristine ng-valid ng-valid-required"></div>
+                        </div>
+                        <div class="form-group">
+                            <label tabindex="-1" class="col-md-2 control-label" title="Ports to connect to, by default 5673" for="port">Port: </label>
+                            <div class="col-md-6">
+                                <input tabindex="2" id="port" posint type="number" placeholder="5673" tooltip="Ports to connect to, by default 5673" ng-model="formEntity.port" name="port" title="Ports to connect to, by default 5673">
+                                <span ng-show="settings.port.$error.range">Must be 1 through 65535</span>
+                            </div>
+                        </div>
+<!--
+                        <div class="form-group">
+                            <label tabindex="-1" class="col-md-2 control-label" title="Whether or not the connection should be started as soon as you log in" for="autostart">Autostart: </label>
+                            <div class="col-md-6">
+                                <input tabindex="3" id="autostart" type="checkbox" tooltip="Whether or not the connection should be started as soon as you log into hawtio" ng-model="formEntity.autostart" name="autostart" title="Whether or not the connection should be started as soon as you log into hawtio"
+                                       class="ng-scope ng-pristine ng-valid"></div>
+                        </div>
+-->
+                        <input tabindex="-1" type="submit" style="position: absolute; left: -9999px; width: 1px; height: 1px;">
+                        <p></p>
+                        <div class="form-group">
+                            <label tabindex="-1" class="col-md-2 control-label" title="button"></label>
+                            <div class="col-md-6">
+                                <button type="submit" tabindex="4" class="btn btn-primary pull-left" ng-disabled="settings.$invalid">{{buttonText()}}</button>
+                            </div>
+                        </div>
+                </form>
+
+
+            </div>
+        </div>
+        <div class="col-xs-1"></div>
+    </div>
+
+    <div class="centered" ng-show="connecting">
+        <i class="icon-spin icon-spinner icon-4x"></i>
+        <p>Please wait, connecting now...</p>
+    </div>
+    <div class="centered error" ng-show="connectionError">
+        <p>There was a connection error: {{connectionErrorText}}</p>
+    </div>
+
+</div>
+

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/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
new file mode 100644
index 0000000..61ed0a7
--- /dev/null
+++ b/console/stand-alone/plugin/html/qdrList.html
@@ -0,0 +1,144 @@
+<!--
+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.
+-->
+
+<style>
+@media (min-width: 768px) {
+  .list-grid {
+    padding-left: 300px;
+  }
+}
+@media (max-width: 768px) {
+  .list-grid {
+      padding-left: 0;
+  }
+
+}
+
+span.fancytree-icon {
+    margin-left: 0;
+}
+
+._treeContainer {
+    width: 300px;
+    padding-right: 10px;
+}
+
+.tree-header {
+    z-index: 4;
+}
+
+#list-controller .tree-header {
+  height: auto;
+}
+
+#list-controller select {
+  height: 25px;
+  width: 300px;
+  background-color: #333333;
+}
+</style>
+
+<div id="list-controller" ng-controller="QDR.ListController">
+
+    <div class="page-menu navbar-collapse collapse">
+        <div class="tree-header"><select id="routerSelect"
+            ng-options="node as node.name for node in nodes" 
+            ng-model="currentNode" 
+            ng-change="selectNode(currentNode)"></select></div>
+        <div class="_treeContainer">
+            <div id="entityTree" onSelect="onTreeSelected" onRoot="onRootReady" hideRoot="true"></div>
+            <div ng-init="treeReady()"></div>
+        </div>
+    </div>
+
+    <div class="list-grid col-xs-12">
+        <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>
+            <div ng-show="currentMode.id === 'attributes'" class="selectedItems">
+                <div ng-show="selectedRecordName === selectedEntity" class="no-content">There are no {{selectedEntity | safePlural}}</div>
+                <div id='details-grid' ng-hide="selectedRecordName === selectedEntity" ui-grid="details" ui-grid-resize-columns ui-grid-save-state
+                     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}}
+                </div>
+                <div ng-hide="selectedRecordName !== selectedEntity">
+                    There are no {{selectedEntity | safePlural}}
+                </div>
+            </div>
+            <div class="operations" ng-show="currentMode.id === 'operations'">
+                <fieldset ng-show="operation != ''">
+                    <table>
+                        <tr>
+                            <th>Attribute</th>
+                            <th>Value</th>
+                        </tr>
+                    <tr title="{{attribute.title}}" ng-repeat="attribute in detailFields">
+                        <td><label for="{{attribute.name}}">{{attribute.name | humanify}}</label></td>
+                        <!-- we can't do <input type="{angular expression}"> because... jquery throws an exception because... -->
+                        <td>
+                        <div ng-if="attribute.input == 'input'">
+                            <!-- ng-pattern="testPattern(attribute)" -->
+                            <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>
+                        </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>
+                            <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.input == 'boolean'" class="boolean">
+                            <label><input type="radio" ng-model="attribute.rawValue" ng-value="true"> True</label>
+                            <label><input type="radio" ng-model="attribute.rawValue" ng-value="false"> False</label>
+                        </div>
+                        </td>
+                    </tr>
+                    <tr><td></td><td><button class="btn btn-primary" type="button" ng-click="ok()">{{operation | Pascalcase}}</button></td></tr>
+                    </table>
+                </fieldset>
+            </div>
+            <div ng-show="currentMode.id === 'log'">
+                <div ng-if="logResults.length > 0">
+                    <table class="log-entry" ng-repeat="entry in logResults track by $index">
+                        <tr>
+                            <td align="left" colspan="2">{{entry.time}}</td>
+                        </tr>
+                        <tr>
+                            <td>Type</td><td>{{entry.type}}</td>
+                        </tr>
+                        <tr>
+                            <td>Source</td><td>{{entry.source}}:{{entry.line}}</td>
+                        </tr>
+                        <tr>
+                            <td valign="middle">Message</td><td valign="middle"><pre>{{entry.message}}</pre></td>
+                        </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>
+        </div>
+    </div>
+</div>
+

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/console/stand-alone/plugin/html/qdrOverview.html
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/html/qdrOverview.html b/console/stand-alone/plugin/html/qdrOverview.html
new file mode 100644
index 0000000..3b4acb9
--- /dev/null
+++ b/console/stand-alone/plugin/html/qdrOverview.html
@@ -0,0 +1,283 @@
+<!--
+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.
+-->
+
+<style>
+@media (min-width: 768px) {
+  .overview-grid {
+    padding-left: 300px;
+  }
+}
+@media (max-width: 768px) {
+  .overview-grid {
+      padding-left: 0;
+  }
+
+}
+
+span.fancytree-icon {
+    margin-left: 0;
+}
+
+.treeContainer {
+    width: 300px;
+    padding-right: 10px;
+}
+
+  .gridDetails .aChart {
+    margin-left: -1em;
+    margin-right: 2em;
+    width: 400px;
+  }
+
+@media (max-width: 768px) {
+  .gridDetails .aChart {
+    margin-right: 1em;
+    width: 380px;
+  }
+}
+
+.page-menu {
+    top: 0;
+}
+
+.overview-grid {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: calc(100% - 40px);
+    margin-left: 20px;
+}
+
+</style>
+
+<div id="overview-controller" ng-controller="QDR.OverviewController">
+
+    <div class="page-menu navbar-collapse collapse">
+        <div class="treeContainer ng-scope">
+            <div id="overtree"></div>
+            <div ng-init="overviewLoaded()"></div>
+        </div>
+    </div>
+
+    <div class="overview-grid">
+        <div class="gridDetails" ng-include="template.url"></div>
+    </div>
+</div>
+
+<script type="text/ng-template" id="overviewCharts.html">
+    <div ng-controller="QDR.OverviewChartsController" class="clearfix">
+        <div ng-repeat="chart in overviewCharts" class="chartContainer" >
+            <p class="chartLabels">
+            </p><div style="clear:both"></div>
+            <div id="{{chart.chart.id()}}" class="line-chart-pf aChart d3Chart"></div>
+        </div>
+    </div>
+    <div ng-controller="QDR.TopAddressesController">
+        <h1>Most active addresses</h1>
+        <div ng-if="anyAddresses()" id="topAddresses" class="grid" ui-grid="topAddressesGrid"  ui-grid-selection ui-grid-auto-resize ui-grid-resize-columns ui-grid-save-state></div>
+        <div ng-if="!anyAddresses()">There are no addresses to which messages are being delivered.</div>
+    </div>
+</script>
+
+<!-- the following scripts are content that gets loaded into the above div that has the temple.url -->
+<script type="text/ng-template" id="routers.html">
+    <div class="row-fluid">
+        <h3>Routers</h3>
+        <div class="overview">
+            <div class="grid" ui-grid="allRouters" ui-grid-selection ui-grid-auto-resize ui-grid-resize-columns ui-grid-save-state></div>
+        </div>
+    </div>
+</script>
+<script type="text/ng-template" id="router.html">
+    <div class="row-fluid">
+        <h3>Router {{router.title}} attributes</h3>
+        <div class="grid noHighlight" ui-grid="routerGrid" ui-grid-auto-resize ui-grid-resize-columns ui-grid-save-state></div>
+    </div>
+</script>
+
+<script type="text/ng-template" id="addresses.html">
+    <div class="row-fluid">
+        <h3>Addresses</h3>
+        <div class="overview">
+            <div class="grid" ui-grid="addressesGrid" ui-grid-selection ui-grid-auto-resize ui-grid-resize-columns ui-grid-save-state></div>
+        </div>
+    </div>
+</script>
+<script type="text/ng-template" id="address.html">
+    <div class="row-fluid">
+        <ul class="nav nav-tabs">
+            <li ng-repeat="mode in gridModes" ng-click="selectMode(mode,'Address')" ng-class="{active : isModeSelected(mode,'Address')}" title="{{mode.title}}" ng-bind-html="mode.content"> </li>
+        </ul>
+        <div ng-if="isModeVisible('Address','attributes')" class="selectedItems">
+            <h3>Address {{address.title}}</h3>
+            <div class="grid noHighlight" ui-grid="addressGrid" ui-grid-resize-columns ui-grid-save-state></div>
+        </div>
+        <div ng-if="isModeVisible('Address','links')" class="selectedItems">
+            <h3>Links for address {{address.title}}</h3>
+            <div class="grid" ui-grid="linksGrid" ui-grid-auto-resize ui-grid-resize-columns ui-grid-save-state></div>
+        </div>
+    </div>
+</script>
+
+<script type="text/ng-template" id="links.html">
+    <div class="row-fluid">
+        <h3>Links <button type='button' ng-click="filterToggle()" class='btn btn-secondary btn-filter'>Filter</button></h3>
+        <div id="linkFilter">
+            <div class="filter-title">
+                <h6>Filter links</h6>
+                <button ng-click="filterToggle()" type="button" class="btn btn-primary filter-close">x</button>
+            </div>
+            <div class="formLine"><label for="hideConsoles">Hide console links</label><input name="hideConsoles" id="hideConsoles" type="checkbox" ng-model="filter.hideConsoles"/></div>
+            <div class="formLine"><label for="endpointOnly">Endpoints only</label><input name="linkTypes" id="endpointOnly" type="radio" ng-model="filter.endpointsOnly" value="true"/>
+                <label for="allLinks">All link types</label><input name="linkTypes" id="allLinks" type="radio" ng-model="filter.endpointsOnly" value="false"/></div>
+        </div>
+        <div class="grid" ui-grid="linksGrid" ui-grid-selection ui-grid-auto-resize ui-grid-resize-columns ui-grid-save-state></div>
+    </div>
+</script>
+<script type="text/ng-template" id="link.html">
+    <div class="row-fluid">
+        <h3>Link {{link.title}}</h3>
+        <div class="grid noHighlight" ui-grid="linkGrid" ui-grid-auto-resize ui-grid-resize-columns ui-grid-save-state></div>
+    </div>
+</script>
+
+<script type="text/ng-template" id="connections.html">
+    <div class="row-fluid">
+        <h3>Connections</h3>
+        <div class="overview">
+            <div class="grid" ui-grid="allConnectionGrid" ui-grid-selection ui-grid-auto-resize ui-grid-resize-columns ui-grid-save-state></div>
+        </div>
+    </div>
+</script>
+<script type="text/ng-template" id="connection.html">
+    <div class="row-fluid">
+        <ul class="nav nav-tabs">
+            <li ng-repeat="mode in gridModes" ng-click="selectMode(mode,'Connection')" ng-class="{active : isModeSelected(mode,'Connection')}" title="{{mode.title}}" ng-bind-html="mode.content"> </li>
+        </ul>
+        <div ng-if="isModeVisible('Connection','attributes')" class="selectedItems">
+            <h3>Connection {{connection.title}}</h3>
+            <div class="grid noHighlight" ui-grid="connectionGrid" ui-grid-auto-resize ui-grid-resize-columns ui-grid-save-state></div>
+        </div>
+        <div ng-if="isModeVisible('Connection','links')" class="selectedItems">
+            <h3>Links for connection {{connection.title}}</h3>
+            <div class="grid" ui-grid="linksGrid" ui-grid-auto-resize ui-grid-resize-columns ui-grid-save-state></div>
+        </div>
+    </div>
+</script>
+
+<script type="text/ng-template" id="titleHeaderCellTemplate.html">
+    <div title="{{col.displayName}}" class="ngHeaderSortColumn {{col.headerClass}}" ng-style="{'cursor': col.cursor}" ng-class="{ 'ngSorted': !noSortVisible }">
+        <div ng-click="col.sort($event)" ng-class="'colt' + col.index" class="ngHeaderText">{{col.displayName}}</div>
+        <div class="ngSortButtonDown" ng-show="col.showSortButtonDown()"></div>
+        <div class="ngSortButtonUp" ng-show="col.showSortButtonUp()"></div>
+        <div class="ngSortPriority">{{col.sortPriority}}</div>
+    </div>
+</script>
+<script type="text/ng-template" id="titleCellTemplate.html">
+    <div title="{{row.entity[col.field]}}" class="ngCellText">{{row.entity[col.field]}}</div>
+</script>
+<script type="text/ng-template" id="logs.html">
+    <div class="row-fluid">
+        <h3>Recent log events</h3>
+        <div class="overview">
+            <div class="grid" ui-grid="allLogGrid" ui-grid-selection ui-grid-auto-resize ui-grid-resize-columns ui-grid-save-state></div>
+        </div>
+    </div>
+</script>
+<script type="text/ng-template" id="logModule.html">
+    <div class="row-fluid">
+        <h3>{{logModule.module}} events</h3>
+        <div class="overview-cell">
+            <div class="grid" ui-grid="logModuleGrid" ui-grid-auto-resize ui-grid-resize-columns ui-grid-save-state></div>
+        </div>
+    </div>
+</script>
+<script type="text/ng-template" id="log.html">
+    <div class="row-fluid">
+        <h3>{{log.title}}</h3>
+        <div ng-if="logFields.length > 0">
+            <table class="log-entry" ng-repeat="entry in logFields track by $index">
+                <tr>
+                    <td>Router</td><td>{{entry.nodeId}}</td>
+                </tr>
+                <tr>
+                    <td align="left" colspan="2">{{entry.time}}</td>
+                </tr>
+                <tr>
+                    <td>Source</td><td>{{entry.source}}:{{entry.line}}</td>
+                </tr>
+                <tr>
+                    <td valign="middle">Message</td><td valign="middle"><pre>{{entry.message}}</pre></td>
+                </tr>
+            </table>
+        </div>
+        <div ng-if="logFields.length == 0">No log entries for {{log.title}}</div>
+    </div>
+</script>
+
+<script type="text/ng-template" id="linkRowTemplate.html">
+    <div ng-class="{'linkDirIn': row.entity.linkDir == 'in', 'linkDirOut': row.entity.linkDir == 'out' }">
+        <div ng-repeat="(colRenderIndex, col) in colContainer.renderedColumns track by col.colDef.name"
+             class="ui-grid-cell" ng-class="{ 'ui-grid-row-header-cell': col.isRowHeader }" ui-grid-cell>
+        </div>
+    </div>
+</script>
+
+<script type="text/ng-template" id="linkAggTemplate.html">
+    <div ng-click='row.toggleExpand(); saveGroupState()' ng-style='rowStyle(row)' class='ngAggregate ng-scope' style='top: 0px; height: 48px; left: 0px;'>
+    <span class='ngAggregateText ng-binding'>
+            {{row.label CUSTOM_FILTERS}} ({{row.totalChildren()}} {{AggItemsLabel}})
+        </span>
+    <div ng-class="{true: 'ngAggArrowCollapsed', false: 'ngAggArrowExpanded'}[row.collapsed]"></div>
+    </div>
+</script>
+
+<script type="text/ng-template" id="viewLogs.html">
+    <div class="modal-header">
+        <h3 class="modal-title">Logs for {{nodeName}} {{module}}:{{level | humanify}}</h3>
+    </div>
+    <div class="modal-body">
+        <div ng-if="loading == false">
+            <div class="log-details" ng-if="logFields.length > 0">
+                <table class="log-entry" ng-repeat="entry in logFields track by $index">
+                    <tr>
+                        <td>Router</td><td>{{entry.nodeId}}</td>
+                    </tr>
+                    <tr>
+                        <td align="left" colspan="2">{{entry.time}}</td>
+                    </tr>
+                    <tr>
+                        <td>Source</td><td>{{entry.source}}:{{entry.line}}</td>
+                    </tr>
+                    <tr>
+                        <td valign="middle">Message</td><td valign="middle"><pre>{{entry.message}}</pre></td>
+                    </tr>
+                </table>
+            </div>
+            <div ng-if="logFields.length == 0">No log entries</div>
+        </div>
+        <div ng-if="loading == true">
+            Loading...
+        </div>
+    </div>
+    <div class="modal-footer">
+        <button class="btn btn-primary" type="button" ng-click="ok()">Close</button>
+    </div>
+</script>
+

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/console/stand-alone/plugin/html/qdrSchema.html
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/html/qdrSchema.html b/console/stand-alone/plugin/html/qdrSchema.html
new file mode 100644
index 0000000..5bd9207
--- /dev/null
+++ b/console/stand-alone/plugin/html/qdrSchema.html
@@ -0,0 +1,62 @@
+<!--
+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.
+-->
+<style>
+    .page-menu-button {
+        display: none;
+    }
+
+
+span.fancytree-title {
+  color: black;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  width: 16em;
+}
+
+span.fancytree-node:hover {
+  color: white !important;
+  background-color: black !important;
+}
+span.fancytree-node:hover span.fancytree-title {
+  color: white;
+}
+
+span.fancytree-node.fancytree-active:hover, 
+span.fancytree-node.fancytree-active:hover span.fancytree-title {
+  color: white;
+}
+
+span.fancytree-expanded.fancytree-has-children, span.fancytree-expanded.fancytree-has-children + ul {
+  background-color: white;
+}
+span.fancytree-expanded.fancytree-has-children:hover {
+  background-color: black;
+}
+
+span.fancytree-node.fancytree-active {
+  background-color: #6aa3d5;
+}
+
+
+</style>
+
+<div class="main-display row-fluid" ng-controller="QDR.SchemaController">
+    <div id="schema"></div>
+</div>

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/console/stand-alone/plugin/html/qdrTopology.html
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/html/qdrTopology.html b/console/stand-alone/plugin/html/qdrTopology.html
new file mode 100644
index 0000000..2b82e34
--- /dev/null
+++ b/console/stand-alone/plugin/html/qdrTopology.html
@@ -0,0 +1,146 @@
+<!--
+Licensed t  o 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.
+-->
+
+<style>
+@media (min-width: 768px) {
+  .diagram {
+    padding-left: 300px;
+  }
+  .showLeft {
+      display: block;
+  }
+}
+@media (max-width: 768px) {
+  .diagram {
+      padding-left: 0 !important;
+  }
+  .showLeft {
+      display: none;
+  }
+
+}
+
+button.page-menu-button {
+    display: none;
+}
+  #popover-div {
+    position: absolute;
+    z-index: 200;
+    border-radius: 4px;
+    background-color: black;
+    color: white;
+    opacity: .95;
+    padding: 12px;
+    font-size: 14px;
+    display: none;
+  }
+
+  table.popupTable td {
+    padding-right: 5px;
+    font-size: 10px;
+  }
+
+#topologyForm {
+  border-right: 1px solid lightgray;
+  border-bottom: 1px solid lightgray;
+  padding: 2px;
+  /* position: absolute; */
+  background-color: #333333;
+  width: 300px;
+}
+
+  #topologyForm .infoGrid span {
+      display: inline-block;
+      width: 50%;
+      height: 28px;
+      padding: 3px 6px 2px 6px;
+      border-right: 1px solid #666666;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      overflow: hidden;
+  }
+
+  #topologyForm .infoGrid span:last-child {
+      text-align: right;
+      border-right: 0;
+  }
+  #topologyForm .infoGrid div {
+      height: 28px;
+  }
+  #topologyForm .infoGrid div.odd {
+      background-color: #444444;
+  }
+
+  #topologyForm .infoGrid div.listening-on {
+      background-color: #336633;
+  }
+  .page-menu {
+      width: 300px;
+  }
+  
+</style>
+<div class="qdrTopology row" ng-controller="QDR.TopologyController">
+    <div  ng-controller="QDR.TopologyFormController">
+        <div class="page-menu navbar-collapse collapse">
+            <div id="topologyForm">
+                <div>
+                    <h4>{{form}} Info</h4>
+                    <!-- <div id="routerInfo" class="grid" ui-grid="topoGridRouter" ng-style="infoStyle()"></div> -->
+                    <div id="formInfo"></div>
+                </div>
+            </div>
+            <button ng-if="panelVisible" ng-click="hideLeftPane()" class="hideLeft" title="Hide"><i class="icon-step-backward"></i></button>
+        </div>
+        <button ng-if="!panelVisible" ng-click="showLeftPane()" class="showLeft" title="Show"><i class="icon-step-forward"></i></button>
+    </div>
+
+    <div class="diagram col-xs-12">
+        <div id="topology"><!-- d3 toplogy here --></div>
+        <div id="crosssection"></div><div id="crosshtml" ng-bind-html="crosshtml"></div>
+
+        <div id="node_context_menu" class="contextMenu">
+            <ul>
+                <li class="na" ng-class="{'force-display': !isFixed()}" ng-click="setFixed(true)">Freeze in place</li>
+                <li class="na" ng-class="{'force-display': isFixed()}" ng-click="setFixed(false)">Unfreeze</li>
+            </ul>
+        </div>
+        <div id="svg_legend" class="hidden-xs"></div>
+        <div id="multiple_details">
+            <h4 class="grid-title">Connections</h4>
+            <div class="grid" ui-grid="multiDetails" ui-grid-selection ui-grid-auto-resize ng-style="connectionsStyle()"></div>
+        </div>
+        <div id="link_details">
+            <h4 class="grid-title">Links</h4>
+            <div class="grid" ui-grid="linkDetails" ui-grid-auto-resize ng-style="linksStyle()"></div>
+        </div>
+    </div>
+    <div id="popover-div" ng-bind-html="trustedpopoverContent"></div>
+</div>
+
+<script type="text/ng-template" id="titleHeaderCellTemplate.html">
+    <div title="{{col.displayName}}" class="ngHeaderSortColumn {{col.headerClass}}" ng-style="{'cursor': col.cursor}" ng-class="{ 'ngSorted': !noSortVisible }">
+        <div ng-click="col.sort($event)" ng-class="'colt' + col.index" class="ngHeaderText">{{col.displayName}}</div>
+        <div class="ngSortButtonDown" ng-show="col.showSortButtonDown()"></div>
+        <div class="ngSortButtonUp" ng-show="col.showSortButtonUp()"></div>
+        <div class="ngSortPriority">{{col.sortPriority}}</div>
+    </div>
+</script>
+<script type="text/ng-template" id="titleCellTemplate.html">
+    <div title="{{row.entity.attributeValue}}" class="ui-grid-cell-contents">{{COL_FIELD CUSTOM_FILTERS | pretty}}</div>
+</script>


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


[3/6] qpid-dispatch git commit: DISPATCH-1000 Restore the html/css/js files that were in the npm repository

Posted by ea...@apache.org.
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/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
new file mode 100644
index 0000000..709702c
--- /dev/null
+++ b/console/stand-alone/plugin/js/qdrOverview.js
@@ -0,0 +1,1774 @@
+/*
+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
+ */
+var QDR = (function (QDR) {
+
+  /**
+   *
+   * 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 updateIntervalHandle = undefined;
+    const updateInterval = 5000;
+
+    const COLUMNSTATEKEY = 'QDRColumnKey.';
+    const OVERVIEWEXPANDEDKEY = 'QDROverviewExpanded';
+    const OVERVIEWACTIVATEDKEY = 'QDROverviewActivated';
+    const FILTERKEY = 'QDROverviewFilters';
+    const OVERVIEWMODEIDS = 'QDROverviewModeIds';
+    let treeRoot;   // the fancytree root node. initialized once log data is received
+
+    // we want attributes to be listed first, so add it at index 0
+    $scope.subLevelTabs = [{
+      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'; },
+      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'; },
+      index: 1
+    }];
+    $scope.activeTab = $scope.subLevelTabs[0];
+    $scope.setActive = function (nav) {
+      $scope.activeTab = nav;
+    };
+    $scope.isValid = function (nav) {
+      return nav.isValid();
+    };
+    $scope.isActive = function (nav) {
+      return nav == $scope.activeTab;
+    };
+    $scope.linkFields = [];
+    $scope.link = null;
+    $scope.modes = [
+      {title: 'Overview', name: 'Overview', right: false}
+    ];
+
+    $scope.tmplOverviewTree = QDR.templatePath + 'tmplOverviewTree.html';
+    $scope.templates = [
+      { name: 'Charts', url: 'overviewCharts.html'},
+      { name: 'Routers', url: 'routers.html'},
+      { name: 'Router', url: 'router.html'},
+      { name: 'Addresses', url: 'addresses.html'},
+      { name: 'Address', url: 'address.html'},
+      { name: 'Links', url: 'links.html'},
+      { name: 'Link', url: 'link.html'},
+      { name: 'Connections', url: 'connections.html'},
+      { name: 'Connection', url: 'connection.html'},
+      { name: 'Logs', url: 'logs.html'},
+      { name: 'Log', url: 'logModule.html'}
+    ];
+    let topLevelChildren = [];
+
+    var selectRow = function (gridApi) {
+      if (!gridApi.selection)
+        return;
+      gridApi.selection.on.rowSelectionChanged($scope,function(row){
+        let treeKey = row.grid.options.treeKey;
+        if (treeKey && row.entity[treeKey]) {
+          let key = row.entity[treeKey];
+          $('#overtree').fancytree('getTree').activateKey(key);
+        }
+      });
+    };
+    $scope.routerPagingOptions = {
+      pageSizes: [50, 100, 500],
+      pageSize: 50,
+      currentPage: 1
+    };
+    var getPagedData = function (pageSize, page) {
+      $scope.totalRouters = $scope.allRouterFields.length;
+      $scope.allRouters.showFooter = $scope.totalRouters > 50;
+      $scope.pagedRouterFields = $scope.allRouterFields.slice((page - 1) * pageSize, page * pageSize);
+    };
+    $scope.$watch('pagingOptions', function (newVal, oldVal) {
+      if (newVal !== oldVal && newVal.currentPage !== oldVal.currentPage) {
+        getPagedData($scope.routerPagingOptions.pageSize, $scope.routerPagingOptions.currentPage);
+      }
+    }, true);
+
+    $scope.totalRouters = 0;
+    $scope.allRouterFields = [];
+    $scope.pagedRouterFields = [];
+    $scope.allRouters = {
+      saveKey: 'allRouters',
+      treeKey: 'nodeId',
+      data: 'pagedRouterFields',
+      columnDefs: [
+        {
+          field: 'id',
+          saveKey: 'allRouters',
+          displayName: 'Router'
+        },
+        {
+          field: 'area',
+          displayName: 'Area'
+        },
+        {
+          field: 'mode',
+          displayName: 'Mode'
+        },
+        {
+          field: 'connections',
+          displayName: 'External connections'
+        },
+        {
+          field: 'addrCount',
+          displayName: 'Address count'
+        },
+        {
+          field: 'linkCount',
+          displayName: 'Link count'
+        }
+      ],
+      enableColumnResize: true,
+      enableHorizontalScrollbar: 0,
+      enableVerticalScrollbar: 0,
+      jqueryUIDraggable: true,
+      enablePaging: true,
+      showFooter: $scope.totalRouters > 50,
+      totalServerItems: 'totalRouters',
+      pagingOptions: $scope.routerPagingOptions,
+      multiSelect: false,
+      enableSelectAll: false,
+      onRegisterApi: selectRow,
+      enableSelectionBatchEvent: false,
+      enableRowHeaderSelection: false,
+      minRowsToShow: Math.min(QDRService.management.topology.nodeIdList().length, 50),
+      noUnselect: true,
+    };
+
+    // get info for all routers
+    var allRouterInfo = function (node, callback) {
+      let nodes = {};
+      // gets called each node/entity response
+      var gotNode = function (nodeName, entity, response) {
+        if (!nodes[nodeName])
+          nodes[angular.copy(nodeName)] = {};
+        nodes[nodeName][entity] = response;
+      };
+      // send the requests for all connection and router info for all routers
+      QDRService.management.topology.fetchAllEntities([
+        {entity: 'connection', attrs: ['role']},
+        {entity: 'router'}], function () {
+        // we have all the data now in the nodes object
+        let allRouterFields = [];
+        for (let node in nodes) {
+          let connections = 0;
+          for (let i=0; i<nodes[node]['connection'].results.length; ++i) {
+            // we only requested "role" so it will be at [0]
+            if (nodes[node]['connection'].results[i][0] === 'inter-router')
+              ++connections;
+          }
+          let routerRow = {connections: connections, nodeId: node, id: QDRService.management.topology.nameFromId(node)};
+          nodes[node]['router'].attributeNames.forEach( function (routerAttr, i) {
+            if (routerAttr !== 'routerId' && routerAttr !== 'id')
+              routerRow[routerAttr] = nodes[node]['router'].results[0][i];
+          });
+          allRouterFields.push(routerRow);
+        }
+        $scope.allRouterFields = allRouterFields;
+        expandGridToContent('Routers', $scope.allRouterFields.length);
+        getPagedData($scope.routerPagingOptions.pageSize, $scope.routerPagingOptions.currentPage);
+        updateRouterTree(nodeIds);
+        callback(null);
+      }, gotNode);
+    };
+
+    $scope.routerFields = [];
+    $scope.routerGrid = {
+      saveKey: 'routerGrid',
+      data: 'routerFields',
+      columnDefs: [
+        {
+          field: 'attribute',
+          displayName: 'Attribute',
+          saveKey: 'routerGrid',
+        },
+        {
+          field: 'value',
+          displayName: 'Value',
+        }
+      ],
+      jqueryUIDraggable: true,
+      enableColumnResize: true,
+      enableHorizontalScrollbar: 0,
+      enableVerticalScrollbar: 0,
+      minRowsToShow: 25,
+      multiSelect: false
+    };
+
+    $scope.router = null;
+    // get info for a single router
+    var routerInfo = function (node, callback) {
+      $scope.router = node;
+
+      let routerFields = [];
+      $scope.allRouterFields.some( function (field) {
+        if (field.id === node.title) {
+          Object.keys(field).forEach ( function (key) {
+            let attr = (key === 'connections') ? 'External connections' : key;
+            routerFields.push({attribute: attr, value: field[key]});
+          });
+          return true;
+        }
+      });
+      $scope.routerFields = routerFields;
+      expandGridToContent('Router', $scope.routerFields.length);
+      callback(null);
+    };
+
+    $scope.addressPagingOptions = {
+      pageSizes: [50, 100, 500],
+      pageSize: 50,
+      currentPage: 1
+    };
+    var getAddressPagedData = function (pageSize, page) {
+      $scope.totalAddresses = $scope.addressesData.length;
+      $scope.addressesGrid.showFooter = $scope.totalAddresses > 50;
+      $scope.pagedAddressesData = $scope.addressesData.slice((page - 1) * pageSize, page * pageSize);
+    };
+    $scope.$watch('addressPagingOptions', function (newVal, oldVal) {
+      if (newVal !== oldVal && newVal.currentPage !== oldVal.currentPage) {
+        getAddressPagedData($scope.addressPagingOptions.pageSize, $scope.addressPagingOptions.currentPage);
+      }
+    }, true);
+
+    $scope.totalAddresses = 0;
+    $scope.pagedAddressesData = [];
+    $scope.addressesData = [];
+    $scope.selectedAddresses = [];
+    $scope.addressesGrid = {
+      saveKey: 'addressesGrid',
+      treeKey: 'uid',
+      data: 'pagedAddressesData',
+      columnDefs: [
+        {
+          field: 'address',
+          saveKey: 'addressesGrid',
+          displayName: 'address'
+        },
+        {
+          field: 'class',
+          displayName: 'class'
+        },
+        {
+          field: 'phase',
+          displayName: 'phase',
+          cellClass: 'grid-align-value'
+        },
+        {
+          field: 'inproc',
+          displayName: 'in-proc'
+        },
+        {
+          field: 'local',
+          displayName: 'local',
+          cellClass: 'grid-align-value'
+        },
+        {
+          field: 'remote',
+          displayName: 'remote',
+          cellClass: 'grid-align-value'
+        },
+        {
+          field: 'in',
+          displayName: 'in',
+          cellClass: 'grid-align-value'
+        },
+        {
+          field: 'out',
+          displayName: 'out',
+          cellClass: 'grid-align-value'
+        }
+      ],
+      enablePaging: true,
+      showFooter: $scope.totalAddresses > 50,
+      totalServerItems: 'totalAddresses',
+      pagingOptions: $scope.addressPagingOptions,
+      enableColumnResize: true,
+      enableHorizontalScrollbar: 0,
+      enableVerticalScrollbar: 0,
+      multiSelect: false,
+      enableSelectAll: false,
+      onRegisterApi: selectRow,
+      enableSelectionBatchEvent: false,
+      enableRowHeaderSelection: false,
+      noUnselect: true
+    };
+
+    // get info for all addresses
+    var allAddressInfo = function (address, callback) {
+      let nodes = {};
+      // gets called each node/entity response
+      var gotNode = function (nodeName, entity, response) {
+        if (!nodes[nodeName])
+          nodes[nodeName] = {};
+        nodes[nodeName][entity] = angular.copy(response);
+      };
+      var addr_phase = function (addr) {
+        if (!addr)
+          return '-';
+        if (addr[0] == 'M')
+          return addr[1];
+        return '';
+      };
+      var prettyVal = function (val) {
+        return QDRService.utilities.pretty(val || '-');
+      };
+      let addressFields = [];
+      let addressObjs = {};
+      // send the requests for all connection and router info for all routers
+      QDRService.management.topology.fetchAllEntities({entity: 'router.address'}, function () {
+        for (let node in nodes) {
+          let response = nodes[node]['router.address'];
+          response.results.forEach( function (result) {
+            let address = QDRService.utilities.flatten(response.attributeNames, result);
+
+            var addNull = function (oldVal, newVal) {
+              if (oldVal != null && newVal != null)
+                return oldVal + newVal;
+              if (oldVal != null)
+                return oldVal;
+              return newVal;
+            };
+
+            let uid = address.identity;
+            let identity = QDRService.utilities.identity_clean(uid);
+
+            if (!addressObjs[QDRService.utilities.addr_text(identity)+QDRService.utilities.addr_class(identity)])
+              addressObjs[QDRService.utilities.addr_text(identity)+QDRService.utilities.addr_class(identity)] = {
+                address: QDRService.utilities.addr_text(identity),
+                'class': QDRService.utilities.addr_class(identity),
+                phase:   addr_phase(identity),
+                inproc:  address.inProcess,
+                local:   address.subscriberCount,
+                remote:  address.remoteCount,
+                'in':    address.deliveriesIngress,
+                out:     address.deliveriesEgress,
+                thru:    address.deliveriesTransit,
+                toproc:  address.deliveriesToContainer,
+                fromproc:address.deliveriesFromContainer,
+                uid:     uid
+              };
+            else {
+              let sumObj = addressObjs[QDRService.utilities.addr_text(identity)+QDRService.utilities.addr_class(identity)];
+              sumObj.inproc = addNull(sumObj.inproc, address.inProcess);
+              sumObj.local = addNull(sumObj.local, address.subscriberCount);
+              sumObj.remote = addNull(sumObj.remote, address.remoteCount);
+              sumObj['in'] = addNull(sumObj['in'], address.deliveriesIngress);
+              sumObj.out = addNull(sumObj.out, address.deliveriesEgress);
+              sumObj.thru = addNull(sumObj.thru, address.deliveriesTransit);
+              sumObj.toproc = addNull(sumObj.toproc, address.deliveriesToContainer);
+              sumObj.fromproc = addNull(sumObj.fromproc, address.deliveriesFromContainer);
+            }
+          });
+        }
+        for (let obj in addressObjs) {
+          addressObjs[obj].inproc = prettyVal(addressObjs[obj].inproc);
+          addressObjs[obj].local = prettyVal(addressObjs[obj].local);
+          addressObjs[obj].remote = prettyVal(addressObjs[obj].remote);
+          addressObjs[obj]['in'] = prettyVal(addressObjs[obj]['in']);
+          addressObjs[obj].out = prettyVal(addressObjs[obj].out);
+          addressObjs[obj].thru = prettyVal(addressObjs[obj].thru);
+          addressObjs[obj].toproc = prettyVal(addressObjs[obj].toproc);
+          addressObjs[obj].fromproc = prettyVal(addressObjs[obj].fromproc);
+          addressFields.push(addressObjs[obj]);
+        }
+        if (addressFields.length === 0)
+          return;
+        // update the grid's data
+        addressFields.sort ( function (a,b) {
+          return a.address + a['class'] < b.address + b['class'] ? -1 : a.address + a['class'] > b.address + b['class'] ? 1 : 0;}
+        );
+        addressFields[0].title = addressFields[0].address;
+        for (let i=1; i<addressFields.length; ++i) {
+          // if this address is the same as the previous address, add a class to the display titles
+          if (addressFields[i].address === addressFields[i-1].address) {
+            addressFields[i-1].title = addressFields[i-1].address + ' (' + addressFields[i-1]['class'] + ')';
+            addressFields[i].title = addressFields[i].address + ' (' + addressFields[i]['class'] + ')';
+          } else
+            addressFields[i].title = addressFields[i].address;
+        }
+        $scope.addressesData = addressFields;
+        expandGridToContent('Addresses', $scope.addressesData.length);
+        getAddressPagedData($scope.addressPagingOptions.pageSize, $scope.addressPagingOptions.currentPage);
+        // repopulate the tree's child nodes
+        updateAddressTree(addressFields);
+        callback(null);
+      }, gotNode);
+    };
+
+    var updateLinkGrid = function ( linkFields ) {
+      // apply the filter
+      let filteredLinks = linkFields.filter( function (link) {
+        let include = true;
+        if ($scope.filter.endpointsOnly === 'true') {
+          if (link.linkType !== 'endpoint')
+            include = false;
+        }
+        if ($scope.filter.hideConsoles) {
+          if (QDRService.utilities.isConsole(QDRService.management.topology.getConnForLink(link)))
+            include = false;
+        }
+        return include;
+      });
+      QDR.log.info('setting linkFields in updateLinkGrid');
+      $scope.linkFields = filteredLinks;
+      expandGridToContent('Links', $scope.linkFields.length);
+      getLinkPagedData($scope.linkPagingOptions.pageSize, $scope.linkPagingOptions.currentPage);
+      // if we have a selected link
+      if ($scope.link) {
+        // find the selected link in the array of all links
+        let links = $scope.linkFields.filter(function (link) {
+          return link.name === $scope.link.data.fields.name;
+        });
+        if (links.length > 0) {
+          // linkInfo() is the function that is called by fancytree when a link is selected
+          // It is passed a fancytree node. We need to simulate that node type to update the link grid
+          linkInfo({data: {title: links[0].title, fields: links[0]}}, function () {$timeout(function (){});});
+        }
+      }
+    };
+
+    // get info for a all links
+    $scope.linkPagingOptions = {
+      pageSizes: [50, 100, 500],
+      pageSize: 50,
+      currentPage: 1
+    };
+    var getLinkPagedData = function (pageSize, page) {
+      $scope.totalLinks = $scope.linkFields.length;
+      $scope.linksGrid.showFooter = $scope.totalLinks > 50;
+      $scope.pagedLinkData = $scope.linkFields.slice((page - 1) * pageSize, page * pageSize);
+    };
+    $scope.$watch('linkPagingOptions', function (newVal, oldVal) {
+      if (newVal !== oldVal && newVal.currentPage !== oldVal.currentPage) {
+        getLinkPagedData($scope.linkPagingOptions.pageSize, $scope.linkPagingOptions.currentPage);
+      }
+    }, true);
+
+    $scope.totalLinks = 0;
+    $scope.pagedLinkData = [];
+    $scope.selectedLinks = [];
+
+    $scope.linksGrid = {
+      saveKey: 'linksGrid',
+      treeKey: 'uid',
+      data: 'pagedLinkData',
+      columnDefs: [
+        {
+          field: 'link',
+          displayName: 'Link',
+          groupable:  false,
+          saveKey: 'linksGrid',
+          width: '11%'
+        },
+        {
+          field: 'linkType',
+          displayName: 'Link type',
+          groupable:  false,
+          width: '9%'
+        },
+        {
+          field: 'linkDir',
+          displayName: 'Link dir',
+          groupable:  false,
+          width: '8%'
+        },
+        {
+          field: 'adminStatus',
+          displayName: 'Admin status',
+          groupable:  false,
+          width: '9%'
+        },
+        {
+          field: 'operStatus',
+          displayName: 'Oper status',
+          groupable:  false,
+          width: '9%'
+        },
+        {
+          field: 'deliveryCount',
+          displayName: 'Delivery Count',
+          groupable:  false,
+          cellClass: 'grid-align-value',
+          width: '11%'
+        },
+        {
+          field: 'rate',
+          displayName: 'Rate',
+          groupable:  false,
+          cellClass: 'grid-align-value',
+          width: '8%'
+        },
+        {
+          field: 'uncounts',
+          displayName: 'Outstanding',
+          groupable:  false,
+          cellClass: 'grid-align-value',
+          width: '9%'
+        },
+        {
+          field: 'owningAddr',
+          displayName: 'Address',
+          groupable:  false,
+          width: '15%'
+        }/*,
+        {
+          displayName: 'Quiesce',
+                    cellClass: 'gridCellButton',
+                    cellTemplate: '<button title="{{quiesceLinkText(row)}} this link" type="button" ng-class="quiesceLinkClass(row)" class="btn" ng-click="quiesceLink(row, $event)" ng-disabled="quiesceLinkDisabled(row)">{{quiesceLinkText(row)}}</button>',
+          width: '10%'
+                }*/
+      ],
+      enablePaging: true,
+      showFooter: $scope.totalLinks > 50,
+      totalServerItems: 'totalLinks',
+      pagingOptions: $scope.linkPagingOptions,
+      enableColumnResize: true,
+      enableHorizontalScrollbar: 0,
+      enableVerticalScrollbar: 0,
+      enableColumnReordering: true,
+      showColumnMenu: true,
+      rowTemplate: 'linkRowTemplate.html',
+      minRowsToShow: 15,
+      multiSelect: false,
+      enableSelectAll: false,
+      onRegisterApi: selectRow,
+      enableSelectionBatchEvent: false,
+      enableRowHeaderSelection: false,
+      noUnselect: true
+    };
+
+    $scope.$on('ngGridEventColumns', function (e, columns) {
+      let saveInfo = columns.map( function (col) {
+        return [col.width, col.visible];
+      });
+      let saveKey = columns[0].colDef.saveKey;
+      if (saveKey)
+        localStorage.setItem(COLUMNSTATEKEY+saveKey, JSON.stringify(saveInfo));
+    });
+
+    var allLinkInfo = function (node, callback) {
+      var gridCallback = function (linkFields) {
+        QDRService.management.topology.ensureAllEntities({entity: 'connection', force: true}, function () {
+          // only update the grid with these fields if the List tree node is selected
+          // this is becuase we are using the link grid in other places and we don't want to overwrite it
+          if ($scope.template.name === 'Links')
+            updateLinkGrid(linkFields);
+          updateLinkTree(linkFields);
+          callback(null);
+        });
+      };
+      getAllLinkFields([gridCallback]);
+    };
+
+    var getAllLinkFields = function (completionCallbacks, selectionCallback) {
+      if (!$scope.linkFields) {
+        QDR.log.debug('$scope.linkFields was not defined!');
+        return;
+      }
+      let nodeIds = QDRService.management.topology.nodeIdList();
+      let linkFields = [];
+      let now = Date.now();
+      var rate = function (linkName, response, result) {
+        if (!$scope.linkFields)
+          return 0;
+        let oldname = $scope.linkFields.filter(function (link) {
+          return link.link === linkName;
+        });
+        if (oldname.length === 1) {
+          let elapsed = (now - oldname[0].timestamp) / 1000;
+          if (elapsed < 0)
+            return 0;
+          let delivered = QDRService.utilities.valFor(response.attributeNames, result, 'deliveryCount') - oldname[0].rawDeliveryCount;
+          //QDR.log.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")
+          return 0;
+        }
+      };
+      let expected = nodeIds.length;
+      let received = 0;
+      var gotLinkInfo = function (nodeName, entity, response) {
+        if (response.results)
+          response.results.forEach( function (result) {
+            var prettyVal = function (field) {
+              let fieldIndex = response.attributeNames.indexOf(field);
+              if (fieldIndex < 0) {
+                return '-';
+              }
+              let val = result[fieldIndex];
+              return QDRService.utilities.pretty(val);
+            };
+            var uncounts = function () {
+              let und = QDRService.utilities.valFor(response.attributeNames, result, 'undeliveredCount');
+              let uns = QDRService.utilities.valFor(response.attributeNames, result, 'unsettledCount');
+              return QDRService.utilities.pretty(und + uns);
+            };
+            var getLinkName = function () {
+              let namestr = QDRService.management.topology.nameFromId(nodeName);
+              return namestr + ':' + QDRService.utilities.valFor(response.attributeNames, result, 'identity');
+            };
+            var fixAddress = function () {
+              let addresses = [];
+              let owningAddr = QDRService.utilities.valFor(response.attributeNames, result, 'owningAddr') || '';
+              let rawAddress = owningAddr;
+              /*
+                 - "L*"  =>  "* (local)"
+                 - "M0*" =>  "* (direct)"
+                 - "M1*" =>  "* (dequeue)"
+                 - "MX*" =>  "* (phase X)"
+            */
+              let address = undefined;
+              let starts = {'L': '(local)', 'M0': '(direct)', 'M1': '(dequeue)'};
+              for (let start in starts) {
+                if (owningAddr.startsWith(start)) {
+                  let ends = owningAddr.substr(start.length);
+                  address = ends + ' ' + starts[start];
+                  rawAddress = ends;
+                  break;
+                }
+              }
+              if (!address) {
+              // check for MX*
+                if (owningAddr.length > 3) {
+                  if (owningAddr[0] === 'M') {
+                    let phase = parseInt(owningAddr.substr(1));
+                    if (phase && !isNaN(phase)) {
+                      let phaseStr = phase + '';
+                      let star = owningAddr.substr(2 + phaseStr.length);
+                      address = star + ' ' + '(phase ' + phaseStr + ')';
+                    }
+                  }
+                }
+              }
+              addresses[0] = address || owningAddr;
+              addresses[1] = rawAddress;
+              return addresses;
+            };
+            if (!selectionCallback || selectionCallback(response, result)) {
+              let adminStatus = QDRService.utilities.valFor(response.attributeNames, result, 'adminStatus');
+              let operStatus = QDRService.utilities.valFor(response.attributeNames, result, 'operStatus');
+              let linkName = getLinkName();
+              let linkType = QDRService.utilities.valFor(response.attributeNames, result, 'linkType');
+              let addresses = fixAddress();
+              let link = QDRService.utilities.flatten(response.attributeNames, result);
+              linkFields.push({
+                link:       linkName,
+                title:      linkName,
+                uncounts:   uncounts(),
+                operStatus: operStatus,
+                adminStatus:adminStatus,
+                owningAddr: addresses[0],
+
+                acceptedCount: prettyVal('acceptedCount'),
+                modifiedCount: prettyVal('modifiedCount'),
+                presettledCount: prettyVal('presettledCount'),
+                rejectedCount: prettyVal('rejectedCount'),
+                releasedCount: prettyVal('releasedCount'),
+                deliveryCount:prettyVal('deliveryCount') + ' ',
+
+                rate: QDRService.utilities.pretty(rate(linkName, response, result)),
+                capacity: link.capacity,
+                undeliveredCount: link.undeliveredCount,
+                unsettledCount: link.unsettledCount,
+
+                rawAddress: addresses[1],
+                rawDeliveryCount: link.deliveryCount,
+                name: link.name,
+                linkName: link.linkName,
+                connectionId: link.connectionId,
+                linkDir: link.linkDir,
+                linkType: linkType,
+                peer: link.peer,
+                type: link.type,
+
+                uid:     linkName,
+                timestamp: now,
+                nodeId: nodeName,
+                identity: link.identity,
+              });
+            }
+          });
+        if (expected === ++received) {
+          linkFields.sort ( function (a,b) { return a.link < b.link ? -1 : a.link > b.link ? 1 : 0;});
+          completionCallbacks.forEach( function (cb) {
+            cb(linkFields);
+          });
+        }
+      };
+      nodeIds.forEach( function (nodeId) {
+        QDRService.management.topology.fetchEntity(nodeId, 'router.link', [], gotLinkInfo);
+      });
+    };
+
+    $scope.connectionPagingOptions = {
+      pageSizes: [50, 100, 500],
+      pageSize: 50,
+      currentPage: 1
+    };
+    var getConnectionPagedData = function (pageSize, page) {
+      $scope.totalConnections = $scope.allConnectionFields.length;
+      $scope.allConnectionGrid.showFooter = $scope.totalConnections > 50;
+      $scope.pagedConnectionsData = $scope.allConnectionFields.slice((page - 1) * pageSize, page * pageSize);
+    };
+    $scope.$watch('connectionPagingOptions', function (newVal, oldVal) {
+      if (newVal !== oldVal && newVal.currentPage !== oldVal.currentPage) {
+        getConnectionPagedData($scope.connectionPagingOptions.pageSize, $scope.connectionPagingOptions.currentPage);
+      }
+    }, true);
+
+    $scope.totalConnections = 0;
+    $scope.pagedConnectionsData = [];
+    $scope.allConnectionFields = [];
+    $scope.allConnectionSelections = [];
+    $scope.allConnectionGrid = {
+      saveKey: 'allConnGrid',
+      treeKey: 'uid',
+      data: 'pagedConnectionsData',
+      columnDefs: [
+        {
+          field: 'host',
+          saveKey: 'allConnGrid',
+          displayName: 'host'
+        },
+        {
+          field: 'container',
+          displayName: 'container'
+        },
+        {
+          field: 'role',
+          displayName: 'role'
+        },
+        {
+          field: 'dir',
+          displayName: 'dir'
+        },
+        {
+          field: 'security',
+          displayName: 'security'
+        },
+        {
+          field: 'authentication',
+          displayName: 'authentication'
+        },
+      ],
+      enablePaging: true,
+      showFooter: $scope.totalConnections > 50,
+      totalServerItems: 'totalConnections',
+      pagingOptions: $scope.connectionPagingOptions,
+      enableColumnResize: true,
+      enableHorizontalScrollbar: 0,
+      enableVerticalScrollbar: 0,
+      multiSelect: false,
+      enableSelectAll: false,
+      onRegisterApi: selectRow,
+      enableSelectionBatchEvent: false,
+      enableRowHeaderSelection: false,
+      noUnselect: true
+    };
+    // get info for a all connections
+    var allConnectionInfo = function (connection, callback) {
+      getAllConnectionFields([updateConnectionGrid, updateConnectionTree, function () {callback(null);}]);
+    };
+    // called after conection data is available
+    var updateConnectionGrid = function (connectionFields) {
+      $scope.allConnectionFields = connectionFields;
+      expandGridToContent('Connections', $scope.allConnectionFields.length);
+      getConnectionPagedData($scope.connectionPagingOptions.pageSize, $scope.connectionPagingOptions.currentPage);
+    };
+
+    // get the connection data for all nodes
+    // called periodically
+    // creates a connectionFields array and calls the callbacks (updateTree and updateGrid)
+    var getAllConnectionFields = function (callbacks) {
+      let nodeIds = QDRService.management.topology.nodeIdList();
+      let connectionFields = [];
+      let expected = nodeIds.length;
+      let received = 0;
+      let gotConnectionInfo = function (nodeName, entity, response) {
+        response.results.forEach( function (result) {
+
+          let auth = 'no_auth';
+          let connection = QDRService.utilities.flatten(response.attributeNames, result);
+          let sasl = connection.sasl;
+          if (connection.isAuthenticated) {
+            auth = sasl;
+            if (sasl === 'ANONYMOUS')
+              auth = 'anonymous-user';
+            else {
+              if (sasl === 'GSSAPI')
+                sasl = 'Kerberos';
+              if (sasl === 'EXTERNAL')
+                sasl = 'x.509';
+              auth = connection.user + '(' + connection.sslCipher + ')';
+            }
+          }
+
+          let sec = 'no-security';
+          if (connection.isEncrypted) {
+            if (sasl === 'GSSAPI')
+              sec = 'Kerberos';
+            else
+              sec = connection.sslProto + '(' + connection.sslCipher + ')';
+          }
+
+          let host = connection.host;
+          let connField = {
+            host: host,
+            security: sec,
+            authentication: auth,
+            routerId: nodeName,
+            uid: host + connection.container + connection.identity
+          };
+          response.attributeNames.forEach( function (attribute, i) {
+            connField[attribute] = result[i];
+          });
+          connectionFields.push(connField);
+        });
+        if (expected === ++received) {
+          connectionFields.sort ( function (a,b) { return a.host+a.container+a.identity < b.host+b.container+b.identity ?
+            -1 : a.host+a.container+a.identity > b.host+b.container+b.identity ? 1 : 0;});
+          callbacks.forEach( function (cb) {
+            cb(connectionFields);
+          });
+        }
+      };
+      nodeIds.forEach( function (nodeId) {
+        QDRService.management.topology.fetchEntity(nodeId, 'connection', [], gotConnectionInfo);
+      });
+    };
+
+    $scope.addressFields = [];
+    $scope.addressGrid = {
+      saveKey: 'addGrid',
+      data: 'addressFields',
+      columnDefs: [
+        {
+          field: 'attribute',
+          displayName: 'Attribute',
+          saveKey: 'addGrid',
+          width: '40%'
+        },
+        {
+          field: 'value',
+          displayName: 'Value',
+          width: '40%'
+        }
+      ],
+      enableColumnResize: true,
+      enableHorizontalScrollbar: 0,
+      enableVerticalScrollbar: 0,
+      minRowsToShow: 11,
+      jqueryUIDraggable: true,
+      multiSelect: false
+    };
+
+    // get info for a single address
+    var addressInfo = function (address, callback) {
+      $scope.address = address;
+      let currentEntity = getCurrentLinksEntity();
+      // we are viewing the addressLinks page
+      if (currentEntity === 'Address' && entityModes[currentEntity].currentModeId === 'links') {
+        updateModeLinks();
+        callback(null);
+        return;
+      }
+
+      $scope.addressFields = [];
+      let fields = Object.keys(address.data.fields);
+      fields.forEach( function (field) {
+        if (field != 'title' && field != 'uid')
+          $scope.addressFields.push({attribute: field, value: address.data.fields[field]});
+      });
+      expandGridToContent('Address', $scope.addressFields.length);
+      callback(null);
+    };
+
+    $scope.singleLinkFields = [];
+    $scope.linkGrid = {
+      saveKey: 'linkGrid',
+      data: 'singleLinkFields',
+      columnDefs: [
+        {
+          field: 'attribute',
+          displayName: 'Attribute',
+          width: '40%'
+        },
+        {
+          field: 'value',
+          displayName: 'Value',
+          width: '40%'
+        }
+      ],
+      enableColumnResize: true,
+      enableHorizontalScrollbar: 0,
+      enableVerticalScrollbar: 0,
+      minRowsToShow: 24,
+      jqueryUIDraggable: true,
+      multiSelect: false
+    };
+
+    // display the grid detail info for a single link
+    var linkInfo = function (link, callback) {
+      if (!link) {
+        callback(null);
+        return;
+      }
+      $scope.link = link;
+
+      $scope.singleLinkFields = [];
+      let fields = Object.keys(link.data.fields);
+      let excludeFields = ['title', 'uid', 'uncounts', 'rawDeliveryCount', 'timestamp', 'rawAddress'];
+      fields.forEach( function (field) {
+        if (excludeFields.indexOf(field) == -1)
+          $scope.singleLinkFields.push({attribute: field, value: link.data.fields[field]});
+      });
+      expandGridToContent('Link', $scope.singleLinkFields.length);
+      callback(null);
+    };
+
+    // get info for a single connection
+    $scope.gridModes = [{
+      content: '<a><i class="icon-list"></i> Attributes</a>',
+      id: 'attributes',
+      title: 'View attributes'
+    },
+    {
+      content: '<a><i class="icon-link"></i> Links</a>',
+      id: 'links',
+      title: 'Show links'
+    }
+    ];
+    var saveModeIds = function () {
+      let modeIds = {Address: entityModes.Address.currentModeId, Connection: entityModes.Connection.currentModeId};
+      localStorage[OVERVIEWMODEIDS] = JSON.stringify(modeIds);
+    };
+    var loadModeIds = function () {
+      return angular.fromJson(localStorage[OVERVIEWMODEIDS]) ||
+        {Address: 'attributes', Connection: 'attributes'};
+    };
+    var savedModeIds = loadModeIds();
+    let entityModes = {
+      Address: {
+        currentModeId: savedModeIds.Address,
+        filter: function (response, result) {
+          let owningAddr = QDRService.utilities.valFor(response.attributeNames, result, 'owningAddr');
+          return (owningAddr === $scope.address.data.fields.uid);
+        }
+      },
+      Connection: {
+        currentModeId: savedModeIds.Connection,
+        filter: function (response, result) {
+          let connectionId = QDRService.utilities.valFor(response.attributeNames, result, 'connectionId');
+          return (connectionId === $scope.connection.data.fields.identity);
+        }
+      }
+    };
+    $scope.selectMode = function (mode, entity) {
+      if (!mode || !entity)
+        return;
+      entityModes[entity].currentModeId = mode.id;
+      saveModeIds();
+      if (mode.id === 'links') {
+        $scope.linkFields = [];
+        getLinkPagedData($scope.linkPagingOptions.pageSize, $scope.linkPagingOptions.currentPage);
+        updateModeLinks();
+      }
+      updateExpanded();
+    };
+    $scope.isModeSelected = function (mode, entity) {
+      return mode.id === entityModes[entity].currentModeId;
+    };
+    $scope.isModeVisible = function (entity, id) {
+      return entityModes[entity].currentModeId === id;
+    };
+
+    var updateEntityLinkGrid = function (linkFields) {
+      $scope.linkFields = linkFields;
+      expandGridToContent('Link', $scope.linkFields.length);
+      getLinkPagedData($scope.linkPagingOptions.pageSize, $scope.linkPagingOptions.currentPage);
+    };
+    // based on which entity is selected, get and filter the links
+    var updateModeLinks = function () {
+      let currentEntity = getCurrentLinksEntity();
+      if (!currentEntity)
+        return;
+      getAllLinkFields([updateEntityLinkGrid], entityModes[currentEntity].filter);
+    };
+    var getCurrentLinksEntity = function () {
+      let currentEntity;
+      let active = $('#overtree').fancytree('getActiveNode');
+      if (active) {
+        currentEntity = active.type ? active.type : active.data.fields.type;
+      }
+      return currentEntity;
+    };
+
+    $scope.quiesceLinkClass = function (row) {
+      let stateClassMap = {
+        enabled: 'btn-primary',
+        disabled: 'btn-danger'
+      };
+      return stateClassMap[row.entity.adminStatus];
+    };
+
+    $scope.quiesceLink = function (row, $event) {
+      $event.stopPropagation();
+      QDRService.management.topology.quiesceLink(row.entity.nodeId, row.entity.name)
+        .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);
+          }
+        });
+    };
+
+    $scope.quiesceLinkDisabled = function (row) {
+      return (row.entity.operStatus !== 'up' && row.entity.operStatus !== 'down');
+    };
+    $scope.quiesceLinkText = function (row) {
+      return row.entity.adminStatus === 'disabled' ? 'Revive' : 'Quiesce';
+    };
+
+    $scope.expandAll = function () {
+      $('#overtree').fancytree('getRoot').visit(function(node){
+        node.expand(true);
+      });
+    };
+    $scope.contractAll = function () {
+      $('#overtree').fancytree('getRoot').visit(function(node){
+        node.expand(false);
+      });
+    };
+    $scope.connectionFields = [];
+    $scope.connectionGrid = {
+      saveKey: 'connGrid',
+      data: 'connectionFields',
+      columnDefs: [
+        {
+          field: 'attribute',
+          displayName: 'Attribute',
+          saveKey: 'connGrid',
+          width: '40%'
+        },
+        {
+          field: 'value',
+          displayName: 'Value',
+          width: '40%'
+        }
+      ],
+      enableColumnResize: true,
+      enableHorizontalScrollbar: 0,
+      enableVerticalScrollbar: 0,
+      minRowsToShow: 21,
+      jqueryUIDraggable: true,
+      multiSelect: false
+    };
+
+    var connectionInfo = function (connection, callback) {
+      if (!connection) {
+        callback(null);
+        return;
+      }
+      $scope.connection = connection;
+
+      let currentEntity = getCurrentLinksEntity();
+      // we are viewing the connectionLinks page
+      if (currentEntity === 'Connection' && entityModes[currentEntity].currentModeId === 'links') {
+        updateModeLinks();
+        callback(null);
+        return;
+      }
+
+      $scope.connectionFields = [];
+      for (let field in connection.data.fields) {
+        if (field != 'title' && field != 'uid')
+          $scope.connectionFields.push({attribute: field, value: connection.data.fields[field]});
+      }
+      expandGridToContent('Connection', $scope.connectionFields.length);
+      callback(null);
+    };
+
+
+    const logModuleCellTemplate = '<div class="ui-grid-cell-contents" ng-click="grid.appScope.logInfoFor(row, col)">{{COL_FIELD CUSTOM_FILTERS | pretty}}</div>';
+
+    $scope.logModule = {};
+    $scope.logModuleSelected = [];
+    $scope.logModuleData = [];
+    $scope.logModuleGrid = {
+      data: 'logModuleData',
+      columnDefs: [
+        {
+          field: 'nodeName',
+          displayName: 'Router',
+        },
+        {
+          field: 'enable',
+          displayName: 'Enable level',
+        },
+        {
+          field: 'noticeCount',
+          displayName: 'Notice',
+          cellTemplate: logModuleCellTemplate,
+          cellClass: 'grid-align-value',
+        },
+        {
+          field: 'infoCount',
+          displayName: 'Info',
+          cellTemplate: logModuleCellTemplate,
+          cellClass: 'grid-align-value',
+        },
+        {
+          field: 'traceCount',
+          displayName: 'Trace',
+          cellTemplate: logModuleCellTemplate,
+          cellClass: 'grid-align-value',
+        },
+        {
+          field: 'debugCount',
+          displayName: 'Debug',
+          cellTemplate: logModuleCellTemplate,
+          cellClass: 'grid-align-value',
+        },
+        {
+          field: 'warningCount',
+          displayName: 'Warning',
+          cellTemplate: logModuleCellTemplate,
+          cellClass: 'grid-align-value',
+        },
+        {
+          field: 'errorCount',
+          displayName: 'Error',
+          cellTemplate: logModuleCellTemplate,
+          cellClass: 'grid-align-value',
+        },
+        {
+          field: 'criticalCount',
+          displayName: 'Critical',
+          cellTemplate: logModuleCellTemplate,
+          cellClass: 'grid-align-value',
+        },
+      ],
+      enableColumnResize: true,
+      enableHorizontalScrollbar: 0,
+      enableVerticalScrollbar: 0,
+      jqueryUIDraggable: true,
+      multiSelect: false,
+      enableSelectAll: false,
+      onRegisterApi: selectRow,
+      enableSelectionBatchEvent: false,
+      enableRowHeaderSelection: false,
+      minRowsToShow: Math.min(QDRService.management.topology.nodeIdList().length, 50),
+      noUnselect: true
+    };
+
+    $scope.logInfoFor = function (row, col) {
+      logDialog(row, col);
+    };
+
+    function logDialog(row, col) {
+      let d = $uibModal.open({
+        animation: true,
+        templateUrl: 'viewLogs.html',
+        controller: 'QDR.OverviewLogsController',
+        resolve: {
+          nodeName: function () {
+            return row.entity.nodeName;
+          },
+          module: function () {
+            return row.entity.name;
+          },
+          level: function () {
+            return col.displayName;
+          },
+          nodeId: function () {
+            return row.entity.nodeId;
+          },
+        }
+      });
+      d.result.then(function () {
+        QDR.log.debug('d.open().then');
+      }, function () {
+        QDR.log.debug('Modal dismissed at: ' + new Date());
+      });
+    }
+
+    const numberTemplate = '<div class="ngCellText" ng-class="col.colIndex()"><span ng-cell-text>{{COL_FIELD | pretty}}</span></div>';
+    $scope.allLogFields = [];
+    $scope.allLogSelections = [];
+    $scope.allLogGrid = {
+      saveKey: 'allLogGrid',
+      treeKey: 'name',
+      data: 'allLogFields',
+      columnDefs: [
+        {
+          field: 'name',
+          saveKey: 'allLogGrid',
+          displayName: 'Module'
+        },
+        {
+          field: 'noticeCount',
+          displayName: 'Notice',
+          cellTemplate: numberTemplate,
+          cellClass: 'grid-align-value',
+        },
+        {
+          field: 'infoCount',
+          displayName: 'Info',
+          cellTemplate: numberTemplate,
+          cellClass: 'grid-align-value',
+        },
+        {
+          field: 'traceCount',
+          displayName: 'Trace',
+          cellTemplate: numberTemplate,
+          cellClass: 'grid-align-value',
+        },
+        {
+          field: 'debugCount',
+          displayName: 'Debug',
+          cellTemplate: numberTemplate,
+          cellClass: 'grid-align-value',
+        },
+        {
+          field: 'warningCount',
+          displayName: 'Warning',
+          cellTemplate: numberTemplate,
+          cellClass: 'grid-align-value',
+        },
+        {
+          field: 'errorCount',
+          displayName: 'Error',
+          cellTemplate: numberTemplate,
+          cellClass: 'grid-align-value',
+        },
+        {
+          field: 'criticalCount',
+          displayName: 'Critical',
+          cellTemplate: numberTemplate,
+          cellClass: 'grid-align-value',
+        },
+      ],
+      //enableCellSelection: true,
+      enableColumnResize: true,
+      enableHorizontalScrollbar: 0,
+      enableVerticalScrollbar: 0,
+      minRowsToShow: 17,
+      multiSelect: false,
+      enableSelectAll: false,
+      onRegisterApi: selectRow,
+      enableSelectionBatchEvent: false,
+      enableRowHeaderSelection: false,
+      noUnselect: true
+    };
+
+    let allLogEntries = {};
+    var allLogInfo = function (log, callback) {
+      // update the count of entries for each module
+      $scope.allLogFields = [];
+      let logResults = {};
+      let logDetails = {};
+
+      var gotLogStats = function (node, entity, response) {
+        logDetails[node] = [];
+        response.results.forEach( function (result) {
+          let oresult = QDRService.utilities.flatten(response.attributeNames, result);
+          // make a copy for the details grid since logResults has the same object reference
+          logDetails[node].push(angular.copy(oresult));
+          if (!(oresult.name in logResults)) {
+            logResults[oresult.name] = oresult;
+          }
+          else {
+            response.attributeNames.forEach( function (attr, i) {
+              if (attr.substr(attr.length-5) === 'Count') {
+                logResults[oresult.name][attr] += result[i];
+              }
+            });
+          }
+        });
+      };
+      var gotAllLogStats = function () {
+        let sortedModules = Object.keys(logResults);
+        sortedModules.sort(function (a,b) {return a<b?-1:a>b?1:0;});
+        $scope.allLogFields = [];
+        sortedModules.forEach( function (module) {
+          $scope.allLogFields.push(logResults[module]);
+        });
+        expandGridToContent('Logs', $scope.allLogFields.length);
+        allLogEntries = logDetails;
+        updateLogTree($scope.allLogFields);
+        callback(null);
+      };
+      QDRService.management.topology.fetchAllEntities({entity: 'logStats'}, gotAllLogStats, gotLogStats);
+    };
+
+    var expandGridToContent = function (type, rows) {
+      let tree = $('#overtree').fancytree('getTree');
+      let node = null;
+      if (tree && tree.getActiveNode)
+        node = tree.getActiveNode();
+      if (node) {
+        if (node.type === type || node.data.type === type) {
+          let height = (rows+1) * 30 + 46; // header is 40px
+          let gridDetails = $('#overview-controller .grid');
+          gridDetails.css('height', height + 'px');
+        }
+      }
+    };
+
+    $scope.logFields = [];
+    // get info for a single log
+    var logInfo = function (node, callback) {
+
+      var gotLogInfo = function (responses) {
+        $scope.logModuleData = [];
+        $scope.logModule.module = node.key;
+        for (let n in responses) {
+          let moduleIndex = responses[n]['log'].attributeNames.indexOf('module');
+          let result = responses[n]['log'].results.filter( function (r) {
+            return r[moduleIndex] === node.key;
+          })[0];
+          let logInfo = QDRService.utilities.flatten(responses[n]['log'].attributeNames, result);
+          let entry = allLogEntries[n];
+          entry.forEach( function (module) {
+            if (module.name === node.key) {
+              module.nodeName = QDRService.management.topology.nameFromId(n);
+              module.nodeId = n;
+              module.enable = logInfo.enable;
+              $scope.logModuleData.push(module);
+            }
+          });
+        }
+        $scope.logModuleData.sort ( function (a,b) { return a.nodeName < b.nodeName? -1 : a.nodeName> b.nodeName? 1 : 0;});
+        expandGridToContent('Log', $scope.logModuleData.length);
+        callback(null);
+      };
+      QDRService.management.topology.fetchAllEntities({entity: 'log', attrs: ['module', 'enable']}, gotLogInfo);
+    };
+
+    var getExpandedList = function () {
+      if (!treeRoot)
+        return;
+      let list = [];
+      if (treeRoot.visit) {
+        treeRoot.visit(function(node){
+          if (node.isExpanded()) {
+            list.push(node.data.parentKey);
+          }
+        });
+      }
+      return list;
+    };
+
+    // loads the tree node name that was last selected
+    var loadActivatedNode = function () {
+      return localStorage[OVERVIEWACTIVATEDKEY] || 'Routers';
+    };
+    // saved the tree node name that is currently selected
+    var saveActivated = function (key) {
+      localStorage[OVERVIEWACTIVATEDKEY] = key;
+    };
+    // loads list that was saved
+    var loadExpandedNodeList = function () {
+      return angular.fromJson(localStorage[OVERVIEWEXPANDEDKEY]) || [];
+    };
+    // called when a node is expanded
+    // here we save the expanded node so it can be restored when the page reloads
+    var saveExpanded = function () {
+      let list = getExpandedList();
+      localStorage[OVERVIEWEXPANDEDKEY] = JSON.stringify(list);
+      expandedNodeList = list;
+    };
+
+    var setTemplate = function (node) {
+      let type = node.type;
+      let template = $scope.templates.filter( function (tpl) {
+        return tpl.name == type;
+      });
+      $timeout( function () {
+        $scope.template = template[0];
+      });
+    };
+    // activated is called each time a tree node is clicked
+    // based on which node is clicked, load the correct data grid template and start getting the data
+    var onTreeNodeActivated = function (event, data) {
+      saveExpanded();
+      saveActivated(data.node.key);
+      $scope.ActivatedKey = data.node.key;
+      setTemplate(data.node);
+      updateExpanded();
+    };
+
+    var onTreeNodeExpanded = function () {
+      saveExpanded();
+      updateExpanded();
+    };
+
+    if (!QDRService.management.connection.is_connected()) {
+      QDR.redirectWhenConnected($location, 'overview');
+      return;
+    }
+    $scope.template = $scope.templates[0];
+
+    /* --------------------------------------------------
+     *
+     * setup the tree on the left
+     *
+     * -------------------------------------------------
+     */
+    var getActiveChild = function (node) {
+      let active = node.children.filter(function (child) {
+        return child.isActive();
+      });
+      if (active.length > 0)
+        return active[0].key;
+      return null;
+    };
+    // utility function called by each top level tree node when it needs to populate its child nodes
+    var updateLeaves = function (leaves, entity, worker) {
+      let tree = $('#overtree').fancytree('getTree'), node;
+      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();
+      let children = [];
+      leaves.forEach( function (leaf) {
+        children.push(worker(leaf));
+      });
+      node.addNode(children);
+      // top level node was expanded
+      if (wasExpanded)
+        node.setExpanded(true, {noAnimation: true, noEvents: true});
+      if (wasActive) {
+        node.setActive(true, {noAnimation: true, noEvents: true});
+      } else {
+        // re-active the previously active child node
+        if (activeChildKey) {
+          let newNode = tree.getNodeByKey(activeChildKey);
+          // the node may not be there after the update
+          if (newNode)
+            newNode.setActive(true, {noAnimation: true, noEvents: 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
+      //let viewport = $('#overview-controller .pane-viewport');
+      //viewport.height( window.innerHeight - viewport.offset().top);
+
+      // 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;'));
+      });
+
+      // remove the comments to allow the tree to take all the height it needs
+
+      let gridDetails = $('#overview-controller .grid');
+      if (gridDetails.offset())
+        gridDetails.height( window.innerHeight - gridDetails.offset().top);
+
+      let gridViewport = $('#overview-controller .ui-grid-viewport');
+      if (gridViewport.offset())
+        gridViewport.height( window.innerHeight - gridViewport.offset().top );
+      //gridViewport.
+
+    };
+    $(window).resize(resizer);
+
+    // get saved tree state
+    $scope.ActivatedKey = loadActivatedNode();
+    let expandedNodeList = loadExpandedNodeList();
+
+    var showCharts = function () {
+
+    };
+    let charts = new QDR.Folder('Charts');
+    charts.info = {fn: showCharts};
+    charts.type = 'Charts';  // for the charts template
+    charts.key = 'Charts';
+    charts.extraClasses = 'charts';
+    topLevelChildren.push(charts);
+
+    // create a routers tree branch
+    let routers = new QDR.Folder('Routers');
+    routers.type = 'Routers';
+    routers.info = {fn: allRouterInfo};
+    routers.expanded = (expandedNodeList.indexOf('Routers') > -1);
+    routers.key = 'Routers';
+    routers.parentKey= 'Routers';
+    routers.extraClasses = 'routers';
+    topLevelChildren.push(routers);
+    // 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);
+        router.type = 'Router';
+        router.info = {fn: routerInfo};
+        router.nodeId = node;
+        router.key = node;
+        router.extraClasses = 'router';
+        router.parentKey = 'Routers';
+        return router;
+      };
+      updateLeaves(nodes, 'Routers', worker);
+    };
+
+    // create an addresses tree branch
+    let addresses = new QDR.Folder('Addresses');
+    addresses.type = 'Addresses';
+    addresses.info = {fn: allAddressInfo};
+    addresses.expanded = (expandedNodeList.indexOf('Addresses') > -1);
+    addresses.key = 'Addresses';
+    addresses.parentKey = 'Addresses';
+    addresses.extraClasses = 'addresses';
+    topLevelChildren.push(addresses);
+    var updateAddressTree = function (addressFields) {
+      var worker = function (address) {
+        let a = new QDR.Leaf(address.title);
+        a.info = {fn: addressInfo};
+        a.key = address.uid;
+        a.fields = address;
+        a.type = 'Address';
+        a.tooltip = address['class'] + ' address';
+        if (address.address === '$management')
+          a.tooltip = 'internal ' + a.tooltip;
+        a.extraClasses = a.tooltip;
+        a.parentKey = 'Addresses';
+        return a;
+      };
+      updateLeaves(addressFields, 'Addresses', worker);
+    };
+
+    $scope.$watch('filter', function (newValue, oldValue) {
+      if (newValue !== oldValue) {
+        allLinkInfo(null, function () {$timeout(function (){});});
+        localStorage[FILTERKEY] = JSON.stringify($scope.filter);
+      }
+    }, true);
+
+    $scope.filterToggle = function () {
+      let filter = $('#linkFilter');
+      filter.toggle();
+    };
+
+    $scope.filter = angular.fromJson(localStorage[FILTERKEY]) || {endpointsOnly: 'true', hideConsoles: true};
+    let links = new QDR.Folder('Links');
+    links.type = 'Links';
+    links.info = {fn: allLinkInfo};
+    links.expanded = (expandedNodeList.indexOf('Links') > -1);
+    links.key = 'Links';
+    links.parentKey = 'Links';
+    links.extraClasses = 'links';
+    topLevelChildren.push(links);
+
+    // 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 isConsole = QDRService.utilities.isConsole(QDRService.management.topology.getConnForLink(link));
+        l.info = {fn: linkInfo};
+        l.key = link.uid;
+        l.fields = link;
+        l.type = 'Link';
+        l.parentKey = 'Links';
+        if (isConsole)
+          l.tooltip = 'console link';
+        else
+          l.tooltip = link.linkType  + ' link';
+        l.extraClasses = l.tooltip;
+        return l;
+      };
+      updateLeaves(linkFields, 'Links', worker);
+    };
+
+    let connections = new QDR.Folder('Connections');
+    connections.type = 'Connections';
+    connections.info = {fn: allConnectionInfo};
+    connections.expanded = (expandedNodeList.indexOf('Connections') > -1);
+    connections.key = 'Connections';
+    connections.parentKey = 'Connections';
+    connections.extraClasses = 'connections';
+    topLevelChildren.push(connections);
+
+    var updateConnectionTree = function (connectionFields) {
+      var worker = function (connection) {
+        let host = connection.host;
+        if (connection.name === 'connection/' && connection.role === 'inter-router' && connection.host === '')
+          host = connection.container + ':' + connection.identity;
+
+        let c = new QDR.Leaf(host);
+        let isConsole = QDRService.utilities.isAConsole (connection.properties, connection.identity, connection.role, connection.routerId);
+        c.type = 'Connection';
+        c.info = {fn: connectionInfo};
+        c.key = connection.uid;
+        c.fields = connection;
+        if (isConsole)
+          c.tooltip = 'console connection';
+        else
+          c.tooltip = connection.role === 'inter-router' ? 'inter-router connection' : 'external connection';
+        c.extraClasses = c.tooltip;
+        c.parentKey = 'Connections';
+        return c;
+      };
+      updateLeaves(connectionFields, 'Connections', worker);
+    };
+
+    var updateLogTree = function (logFields) {
+      var worker = function (log) {
+        let l = new QDR.Leaf(log.name);
+        l.type = 'Log';
+        l.info = {fn: logInfo};
+        l.key = log.name;
+        l.parentKey = 'Logs';
+        l.extraClasses = 'log';
+        return l;
+      };
+      updateLeaves(logFields, 'Logs', worker);
+    };
+
+    let htmlReady = false;
+    let dataReady = false;
+    $scope.largeNetwork = QDRService.management.topology.isLargeNetwork();
+    let logs = new QDR.Folder('Logs');
+    logs.type = 'Logs';
+    logs.info = {fn: allLogInfo};
+    logs.expanded = (expandedNodeList.indexOf('Logs') > -1);
+    logs.key = 'Logs';
+    logs.parentKey = 'Logs';
+    if (QDRService.management.connection.versionCheck('0.8.0'))
+      topLevelChildren.push(logs);
+
+    var onTreeInitialized = function (event, data) {
+      treeRoot = data.tree;
+      let activeNode = treeRoot.getNodeByKey($scope.ActivatedKey);
+      if (activeNode)
+        activeNode.setActive(true);
+      updateExpanded();
+    };
+    var initTreeAndGrid = function () {
+      if (!htmlReady || !dataReady)
+        return;
+      let div = angular.element('#overtree');
+      if (!div.width()) {
+        setTimeout(initTreeAndGrid, 100);
+        return;
+      }
+      $('#overtree').fancytree({
+        activate:       onTreeNodeActivated,
+        expand:         onTreeNodeExpanded,
+        init:           onTreeInitialized,
+        autoCollapse:   $scope.largeNetwork,
+        activeVisible:! $scope.largeNetwork,
+        clickFolderMode: 1,
+        source:         topLevelChildren
+      });
+    };
+
+    $scope.overviewLoaded = function () {
+      htmlReady = true;
+      initTreeAndGrid();
+    };
+
+    let nodeIds = QDRService.management.topology.nodeIdList();
+    // 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...');
+      placeHolder.extraClasses = 'loading';
+      parent.children = [placeHolder];
+    });
+
+    var updateExpanded = function () {
+      QDR.log.debug('updateExpandedEntities');
+      clearTimeout(updateIntervalHandle);
+
+      let tree = $('#overtree').fancytree('getTree');
+      if (tree && tree.visit) {
+        let q = d3.queue(10);
+        tree.visit( function (node) {
+          if (node.isActive() || node.isExpanded()) {
+            q.defer(node.data.info.fn, node);
+          }
+        });
+        q.await( function (error) {
+          if (error)
+            QDR.log.error(error.message);
+
+          // if there are no active nodes, activate the saved one
+          let tree = $('#overtree').fancytree('getTree');
+          if (!tree.getActiveNode()) {
+            let active = tree.getNodeByKey($scope.ActivatedKey);
+            if (active) {
+              active.setActive(true);
+            } else {
+              tree.getNodeByKey('Routers').setActive(true);
+            }
+          }
+
+          // fancytree animations sometimes stop short of shrinking its placeholder spans
+          d3.selectAll('.ui-effects-placeholder').style('height', '0px');
+          // once all expanded tree nodes have been update, schedule another update
+          $timeout(function () {
+            updateIntervalHandle = setTimeout(updateExpanded, updateInterval);
+          });
+        });
+      }
+    };
+
+    dataReady = true;
+    initTreeAndGrid();
+    $scope.$on('$destroy', function() {
+      clearTimeout(updateIntervalHandle);
+      $(window).off('resize', resizer);
+    });
+  }]);
+
+  return QDR;
+
+}(QDR || {}));

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/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
new file mode 100644
index 0000000..e47e597
--- /dev/null
+++ b/console/stand-alone/plugin/js/qdrOverviewChartsController.js
@@ -0,0 +1,134 @@
+/*
+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.OverviewChartsController', function ($scope, QDRService, QDRChartService, $timeout) {
+
+    $scope.overviewCharts = [];
+    let updateTimer;
+
+    // called when it is time to update the chart's data
+    var sum = function (request, saveResponse) {
+      let attrs = angular.copy(request.overrideAttrs);
+      let totalAttr = attrs.shift(); // remove totals attr
+      let attrEntities = [{entity: request.entity, attrs: attrs}];
+      QDRService.management.topology.fetchAllEntities(attrEntities, function (responses) {
+        let total = 0;
+        let response = {attributeNames: [totalAttr, 'name'], results: [[]]};
+        // for each router
+        for (let router in responses) {
+          let record = responses[router][request.entity];
+          let accessor = charts.find( function (chart) { return chart.nodeId === request.nodeId;}).accessor;
+          // for each attribute-value (ie each address or each link)
+          for (let i=0; i<record.results.length; i++) {
+            total += accessor(record.attributeNames, record.results[i]);
+          }
+        }
+        response.results[0][0] = total;
+        response.results[0][1] = request.names()[0];
+        saveResponse(request.nodeId, request.entity, response);
+      });
+    };
+    let charts = [
+
+      {
+        nodeId:     '///Throughput/',
+        entity:     'router.address',
+        name:       'throughput',
+        overrideAttrs: ['throughput', 'deliveriesEgress'],
+        attr:       'throughput',
+        type:       'rate',
+        hideLabel:  true,
+        hideLegend:  true,
+        rateWindow: 5000,   // update data once every 5 seconds
+        visibleDuration: 1, // show data for the last 1 minute
+        forceCreate: true,
+        accessor: function (attributes, results) {
+          return results[attributes.indexOf('deliveriesEgress')];
+        },
+        override: sum  // called to fetch the chart data
+      },
+      {
+        nodeId:       '///Outstanding-Deliveries/',
+        entity:       'router.link',
+        name:         'outstandingDeliveries',
+        overrideAttrs:['outstandingDeliveries', 'undeliveredCount', 'unsettledCount', 'linkType', 'linkDir'],
+        attr:         'outstandingDeliveries',
+        hideLabel:  true,
+        hideLegend:  true,
+        visibleDuration: 1, // show data for the last 1 minute
+        forceCreate:  true,
+        accessor: function (attributes, results) {
+          return results[attributes.indexOf('linkType')] === 'endpoint' && results[attributes.indexOf('linkDir')] === 'out'
+            ? results[attributes.indexOf('unsettledCount')] + results[attributes.indexOf('undeliveredCount')]
+            : 0;
+        },
+        now: new Date(),
+        override: sum  // called to fetch the chart data
+      }
+    ];
+    $scope.overviewCharts = charts.map( function (chart) {
+      let c = QDRChartService.registerChart(chart);
+      return new QDRChartService.pfAreaChart(c, c.id(), true);
+    });
+
+
+    // redraw the chart every update period
+    var updateCharts = function () {
+      $scope.overviewCharts.forEach(function (svgChart) {
+        svgChart.tick(svgChart.chart.id()); // on this page we are using the chart.id() as the div id in which to render the chart
+      });
+    };
+
+    var createCharts = function () {
+      // ensure the div for our chart is loaded in the dom
+      let div = angular.element('.chartContainer');
+      if (!div.width()) {
+        setTimeout(createCharts, 100);
+        return;
+      }
+      // create an svg object for each chart
+      $scope.overviewCharts.forEach ( function (c) {
+        // tell c3 to create the svg
+        c.generate(360);
+      });
+      // redraw the charts once every second
+      const updateRate = localStorage['updateRate'] ?  localStorage['updateRate'] : 1000;
+      updateTimer = setInterval(updateCharts, updateRate);
+    };
+    $timeout( function () {
+      createCharts();
+    });
+
+    $scope.$on('$destroy', function() {
+      if (updateTimer)
+        clearInterval(updateTimer);
+      $scope.overviewCharts.forEach( function (svg) {
+        QDRChartService.unRegisterChart(svg.chart);
+      });
+    });
+  });
+  return QDR;
+
+} (QDR || {}));

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/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
new file mode 100644
index 0000000..1e541a5
--- /dev/null
+++ b/console/stand-alone/plugin/js/qdrOverviewLogsController.js
@@ -0,0 +1,70 @@
+/*
+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';
+/**
+ * @module QDR
+ */
+var QDR = (function(QDR) {
+
+  QDR.module.controller('QDR.OverviewLogsController', function ($scope, $uibModalInstance, QDRService, $timeout, nodeName, nodeId, module, level) {
+
+    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);
+      } else {
+        let levelLogs = response.filter( function (result) {
+          if (result[1] == null)
+            result[1] = 'error';
+          return result[1].toUpperCase() === level.toUpperCase() && result[0] === module;
+        });
+        let logFields = levelLogs.map( function (result) {
+          return {
+            nodeId: QDRService.management.topology.nameFromId(nodeId),
+            name: result[0],
+            type: result[1],
+            message: result[2],
+            source: result[3],
+            line: result[4],
+            time: Date(result[5]).toString()
+          };
+        });
+        $timeout(function () {
+          $scope.loading = false;
+          $scope.logFields = logFields;
+        });
+      }
+    };
+    QDRService.management.connection.sendMethod(nodeId, undefined, {}, 'GET-LOG', {module: module})
+      .then( function (response) {gotLogInfo(nodeId, response.response, response.context);});
+
+    $scope.loading = true;
+    $scope.module = module;
+    $scope.level = level;
+    $scope.nodeName = nodeName;
+    $scope.logFields = [];
+    $scope.ok = function () {
+      $uibModalInstance.close(true);
+    };
+
+  });
+  return QDR;
+
+} (QDR || {}));

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/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
new file mode 100644
index 0000000..69569ea
--- /dev/null
+++ b/console/stand-alone/plugin/js/qdrSchema.js
@@ -0,0 +1,83 @@
+/*
+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';
+/**
+ * @module QDR
+ */
+var QDR = (function (QDR) {
+
+  QDR.module.controller('QDR.SchemaController', ['$scope', '$location', '$timeout', 'QDRService', function($scope, $location, $timeout, QDRService) {
+    if (!QDRService.management.connection.is_connected()) {
+      QDR.redirectWhenConnected($location, 'schema');
+      return;
+    }
+    var onDisconnect = function () {
+      $timeout( function () {QDR.redirectWhenConnected('schema');});
+    };
+    // we are currently connected. setup a handler to get notified if we are ever disconnected
+    QDRService.management.connection.addDisconnectAction( onDisconnect );
+
+    var keys2kids = function (tree, obj) {
+      if (obj === Object(obj)) {
+        tree.children = [];
+        let keys = Object.keys(obj).sort();
+        for (let i=0; i<keys.length; ++i) {
+          let key = keys[i];
+          let kid = {title: key};
+          if (obj[key] === Object(obj[key])) {
+            kid.folder = true;
+            keys2kids(kid, obj[key]);
+          } else {
+            kid.title += (': ' + JSON.stringify(obj[key],null,2));
+          }
+          tree.children.push(kid);
+        }
+      }
+    };
+
+    let tree = [];
+    for (let key in QDRService.management.schema()) {
+      let kid = {title: key};
+      kid.folder = true;
+      let val = QDRService.management.schema()[key];
+      if (val === Object(val))
+        keys2kids(kid, val);
+      else
+        kid.title += (': ' + JSON.stringify(val,null,2));
+
+      tree.push(kid);
+    }
+    $('#schema').fancytree({
+      minExpandLevel: 2,
+      clickFolderMode: 3,
+      classNames: {
+        expander: 'fa-angle',
+        connector: 'fancytree-no-connector'
+      },
+      source: tree
+    });
+
+    $scope.$on('$destroy', function() {
+      QDRService.management.connection.delDisconnectAction( onDisconnect );
+    });
+
+  }]);
+
+  return QDR;
+}(QDR || {}));

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/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
new file mode 100644
index 0000000..f4effdd
--- /dev/null
+++ b/console/stand-alone/plugin/js/qdrService.js
@@ -0,0 +1,269 @@
+/*
+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 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()
+                .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();
+                      });
+                    });
+                });
+              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;
+
+}(QDR || {}));
+
+(function() {
+  console.dump = function(o) {
+    if (window.JSON && window.JSON.stringify)
+      QDR.log.info(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


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


[2/6] qpid-dispatch git commit: DISPATCH-1000 Restore the html/css/js files that were in the npm repository

Posted by ea...@apache.org.
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/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
new file mode 100644
index 0000000..7f34758
--- /dev/null
+++ b/console/stand-alone/plugin/js/qdrSettings.js
@@ -0,0 +1,172 @@
+/*
+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) {
+
+  /**
+   * @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) {
+
+    $scope.connecting = false;
+    $scope.connectionError = false;
+    $scope.connectionErrorText = undefined;
+    $scope.forms = {};
+
+    $scope.formEntity = angular.fromJson(localStorage[QDR.SETTINGS_KEY]) || {
+      address: '',
+      port: '',
+      username: '',
+      password: '',
+      autostart: false
+    };
+
+    $scope.$watch('formEntity', function(newValue, oldValue) {
+      if (newValue !== oldValue) {
+        localStorage[QDR.SETTINGS_KEY] = angular.toJson(newValue);
+      }
+    }, true);
+
+    $scope.buttonText = function() {
+      if (QDRService.management.connection.is_connected()) {
+        return 'Disconnect';
+      } else {
+        return 'Connect';
+      }
+    };
+
+    // connect/disconnect button clicked
+    $scope.connect = function() {
+      if (QDRService.management.connection.is_connected()) {
+        $timeout( function () {
+          QDRService.disconnect();
+        });
+        return;
+      }
+
+      if ($scope.settings.$valid) {
+        $scope.connectionError = false;
+        $scope.connecting = true;
+        // timeout so connecting animation can display
+        $timeout(function () {
+          doConnect();
+        });
+      }
+    };
+
+    var doConnect = function() {
+      QDR.log.info('doConnect called on connect page');
+      if (!$scope.formEntity.address)
+        $scope.formEntity.address = 'localhost';
+      if (!$scope.formEntity.port)
+        $scope.formEntity.port = 5673;
+
+      var failed = function() {
+        $timeout(function() {
+          $scope.connecting = false;
+          $scope.connectionErrorText = 'Unable to connect to ' + $scope.formEntity.address + ':' + $scope.formEntity.port;
+          $scope.connectionError = true;
+        });
+      };
+      let options = {address: $scope.formEntity.address, port: $scope.formEntity.port, reconnect: true};
+      QDRService.connect(options)
+        .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);
+          // will have redirected to last known page or /overview
+        }, function (e) {
+          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;
+        });
+      }
+    };
+  });
+
+  return QDR;
+}(QDR || {}));

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/dfd3cf7c/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
new file mode 100644
index 0000000..ece11ef
--- /dev/null
+++ b/console/stand-alone/plugin/js/qdrTopAddressesController.js
@@ -0,0 +1,218 @@
+/*
+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.TopAddressesController', function ($scope, QDRService, $timeout) {
+
+    $scope.addressesData = [];
+    $scope.topAddressesGrid = {
+      data: 'addressesData',
+      columnDefs: [
+        {
+          field: 'address',
+          displayName: 'address'
+        },
+        {
+          field: 'class',
+          displayName: 'class'
+        },
+        /*
+        {
+          field: 'phase',
+          displayName: 'phase',
+          cellClass: 'grid-align-value'
+        },
+        {
+          field: 'inproc',
+          displayName: 'in-proc'
+        },
+        {
+          field: 'local',
+          displayName: 'local',
+          cellClass: 'grid-align-value'
+        },
+        {
+          field: 'remote',
+          displayName: 'remote',
+          cellClass: 'grid-align-value'
+        },
+        */
+        {
+          field: 'in',
+          displayName: 'in',
+          cellClass: 'grid-align-value'
+        },
+        {
+          field: 'out',
+          displayName: 'out',
+          cellClass: 'grid-align-value'
+        }
+      ],
+      enableColumnResize: true,
+      enableHorizontalScrollbar: 0,
+      enableVerticalScrollbar: 0,
+      multiSelect: false,
+      enableSelectAll: false,
+      enableSelectionBatchEvent: false,
+      enableRowHeaderSelection: false,
+      noUnselect: true
+    };
+
+    // get info for all addresses
+    var allAddressInfo = function (address, callback) {
+      let nodes = {};
+      // gets called each node/entity response
+      var gotNode = function (nodeName, entity, response) {
+        if (!nodes[nodeName])
+          nodes[nodeName] = {};
+        nodes[nodeName][entity] = angular.copy(response);
+      };
+      var addr_phase = function (addr) {
+        if (!addr)
+          return '-';
+        if (addr[0] == 'M')
+          return addr[1];
+        return '';
+      };
+      var prettyVal = function (val) {
+        return QDRService.utilities.pretty(val || '-');
+      };
+      let addressFields = [];
+      let addressObjs = {};
+      let addNull = function (oldVal, newVal) {
+        if (oldVal != null && newVal != null)
+          return oldVal + newVal;
+        if (oldVal != null)
+          return oldVal;
+        return newVal;
+      };
+      // send the requests for all connection and router info for all routers
+      QDRService.management.topology.fetchAllEntities({entity: 'router.address'}, function () {
+        for (let node in nodes) {
+          let response = nodes[node]['router.address'];
+          response.results.forEach( function (result) {
+            let address = QDRService.utilities.flatten(response.attributeNames, result);
+            let uid = address.identity;
+            let identity = QDRService.utilities.identity_clean(uid);
+
+            let objname = QDRService.utilities.addr_text(identity)+QDRService.utilities.addr_class(identity);
+            if (!addressObjs[objname]) {
+              addressObjs[objname] = {
+                address: QDRService.utilities.addr_text(identity),
+                'class': QDRService.utilities.addr_class(identity),
+                phase:   addr_phase(identity),
+                inproc:  address.inProcess,
+                local:   address.subscriberCount,
+                remote:  address.remoteCount,
+                'in':    address.deliveriesIngress,
+                out:     address.deliveriesEgress,
+                thru:    address.deliveriesTransit,
+                toproc:  address.deliveriesToContainer,
+                fromproc:address.deliveriesFromContainer,
+                uid:     uid
+              };
+            }
+            else {
+              let sumObj = addressObjs[objname];
+              sumObj.inproc = addNull(sumObj.inproc, address.inProcess);
+              sumObj.local = addNull(sumObj.local, address.subscriberCount);
+              sumObj.remote = addNull(sumObj.remote, address.remoteCount);
+              sumObj['in'] = addNull(sumObj['in'], address.deliveriesIngress);
+              sumObj.out = addNull(sumObj.out, address.deliveriesEgress);
+              sumObj.thru = addNull(sumObj.thru, address.deliveriesTransit);
+              sumObj.toproc = addNull(sumObj.toproc, address.deliveriesToContainer);
+              sumObj.fromproc = addNull(sumObj.fromproc, address.deliveriesFromContainer);
+            }
+          });
+        }
+        for (let obj in addressObjs) {
+          addressObjs[obj].inproc = prettyVal(addressObjs[obj].inproc);
+          addressObjs[obj].local = prettyVal(addressObjs[obj].local);
+          addressObjs[obj].remote = prettyVal(addressObjs[obj].remote);
+          addressObjs[obj]['in'] = prettyVal(addressObjs[obj]['in']);
+          addressObjs[obj].rawout = addressObjs[obj].out;
+          addressObjs[obj].out = prettyVal(addressObjs[obj].out);
+          addressObjs[obj].thru = prettyVal(addressObjs[obj].thru);
+          addressObjs[obj].toproc = prettyVal(addressObjs[obj].toproc);
+          addressObjs[obj].fromproc = prettyVal(addressObjs[obj].fromproc);
+          addressFields.push(addressObjs[obj]);
+        }
+        if (addressFields.length === 0)
+          return;
+        // update the grid's data
+        addressFields.sort ( function (a,b) {
+          return a.address + a['class'] < b.address + b['class'] ? -1 : a.address + a['class'] > b.address + b['class'] ? 1 : 0;}
+        );
+        // callapse all records with same addres+class
+        addressFields[0].title = addressFields[0].address;
+        for (let i=1; i<addressFields.length; ++i) {
+          // if this address is the same as the previous address, add a class to the display titles
+          if (addressFields[i].address === addressFields[i-1].address) {
+            addressFields[i-1].title = addressFields[i-1].address + ' (' + addressFields[i-1]['class'] + ')';
+            addressFields[i].title = addressFields[i].address + ' (' + addressFields[i]['class'] + ')';
+          } else
+            addressFields[i].title = addressFields[i].address;
+        }
+        addressFields = addressFields.filter( function (address) {
+          return address.rawout > 0 || address.rawin > 0;
+        });
+        addressFields.sort ( function (a,b) {
+          return a.rawout < b.rawout ? -1 : a.rawout > b.rawout ? 1 : 0;}
+        );
+        // take top 5 records
+        addressFields.splice(5);
+
+        $scope.addressesData = addressFields;
+        callback(null);
+      }, gotNode);
+    };
+    let timer;
+    var updateGrid = function () {
+      $timeout( function () {
+        allAddressInfo(null, function () {});
+        expandGridToContent($scope.addressesData.length);
+      });
+    };
+    timer = setInterval(updateGrid, 5000);
+    updateGrid();
+
+    var expandGridToContent = function (rows) {
+      let height = (rows+1) * 30 + 40; // header is 40px
+      let gridDetails = $('#overview-controller .grid');
+      gridDetails.css('height', height + 'px');
+    };
+
+    $scope.anyAddresses = function () {
+      return $scope.addressesData.length > 0;
+    };
+
+    $scope.$on('$destroy', function() {
+      if (timer)
+        clearInterval(timer);
+    });
+
+  });
+  return QDR;
+
+} (QDR || {}));


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