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 2016/03/29 16:43:17 UTC

qpid-dispatch git commit: DISPATCH-201 Updates for dispatch 0.6.0

Repository: qpid-dispatch
Updated Branches:
  refs/heads/master 8475aca8f -> 96ce4a6c8


DISPATCH-201 Updates for dispatch 0.6.0


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

Branch: refs/heads/master
Commit: 96ce4a6c869c7caa1d8d762c9227da23fb98ca04
Parents: 8475aca
Author: Ernest Allen <ea...@redhat.com>
Authored: Tue Mar 29 10:42:05 2016 -0400
Committer: Ernest Allen <ea...@redhat.com>
Committed: Tue Mar 29 10:42:05 2016 -0400

----------------------------------------------------------------------
 .../src/main/webapp/plugin/css/brokers.ttf      | Bin 0 -> 2272 bytes
 .../src/main/webapp/plugin/css/plugin.css       | 106 +++-
 .../src/main/webapp/plugin/css/qdrTopology.css  |  76 ++-
 .../src/main/webapp/plugin/html/qdrList.html    |  54 +-
 .../main/webapp/plugin/html/qdrOverview.html    |   2 +-
 .../main/webapp/plugin/html/qdrTopology.html    |  16 +-
 .../src/main/webapp/plugin/js/dispatchPlugin.js |  21 +-
 .../hawtio/src/main/webapp/plugin/js/navbar.js  |   4 +-
 .../hawtio/src/main/webapp/plugin/js/qdrList.js | 260 ++++++++--
 .../src/main/webapp/plugin/js/qdrOverview.js    |  54 +-
 .../src/main/webapp/plugin/js/qdrService.js     | 208 +++++---
 .../src/main/webapp/plugin/js/qdrTopology.js    | 492 ++++++++++++-------
 12 files changed, 979 insertions(+), 314 deletions(-)
----------------------------------------------------------------------


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

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/96ce4a6c/console/hawtio/src/main/webapp/plugin/css/plugin.css
----------------------------------------------------------------------
diff --git a/console/hawtio/src/main/webapp/plugin/css/plugin.css b/console/hawtio/src/main/webapp/plugin/css/plugin.css
index c6f5cc1..889bf72 100644
--- a/console/hawtio/src/main/webapp/plugin/css/plugin.css
+++ b/console/hawtio/src/main/webapp/plugin/css/plugin.css
@@ -279,10 +279,13 @@ div.qdrList li.active, ul.qdrListNodes li.active {
 div.qdr-attributes span.dynatree-selected a {
     background-color: #e0e0ff;
 }
-div.qdr-attributes.pane {
+div.qdr-attributes.pane, div.qdr-topology.pane {
 	position: absolute;
 	margin-left: 10px;
 }
+div.qdr-topology.pane.left {
+	width: auto;
+}
 
 /* the selected row in the name table */
 div#main.qdr div.qdrList div.selected {
@@ -428,8 +431,8 @@ ul.qdrTopoModes {
 	background:#e0e0ff;
 }
 
-.qdr-overview.pane.left, .qdr-attributes.pane.left {
-	top: 100px;
+.qdr-overview.pane.left, .qdr-attributes.pane.left, .qdr-topology.pane.left {
+	top: 104px;
 }
 .qdr-overview.pane.left {
 	left: 10px;
@@ -723,3 +726,100 @@ span:not(.dynatree-has-children).allocator .dynatree-icon:before {
 .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;
+}
+
+circle.node.normal.console {
+    fill: lightcyan;
+}
+
+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.on-demand {
+    font-family: Brokers;
+    font-size: 20px;
+    font-weight: bold;
+}
+
+text.qpid-cpp.on-demand {
+    font-family: Brokers;
+    font-size: 18px;
+    font-weight: bold;
+}

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/96ce4a6c/console/hawtio/src/main/webapp/plugin/css/qdrTopology.css
----------------------------------------------------------------------
diff --git a/console/hawtio/src/main/webapp/plugin/css/qdrTopology.css b/console/hawtio/src/main/webapp/plugin/css/qdrTopology.css
index 0eac80d..bc7ccfc 100644
--- a/console/hawtio/src/main/webapp/plugin/css/qdrTopology.css
+++ b/console/hawtio/src/main/webapp/plugin/css/qdrTopology.css
@@ -97,7 +97,11 @@ circle.node.normal {
     fill: #F0F000;
 }
 circle.node.on-demand {
-    fill: #00F000;
+    fill: #C0FFC0;
+}
+circle.node.on-demand.artemis {
+	fill: #FCC;
+	/*opacity: 0.2; */
 }
 
 circle.node.fixed {
@@ -148,6 +152,11 @@ text.id {
   font-weight: bold;
 }
 
+text.label {
+  text-anchor: start;
+  font-weight: bold;
+}
+
 .row-fluid.tertiary {
   position: relative;
   left: 20px;
@@ -159,7 +168,26 @@ text.id {
 
 .row-fluid.tertiary.panel {
   width: 410px;
-  height: 100%;
+  /*height: 100%; */
+}
+
+/*, div.qdrTopology div#multiple_details .ngViewport*/
+div#topologyForm .ngViewport, div#topologyForm .gridStyle {
+    height: inherit !important;
+	min-height: initial !important;
+	overflow: initial;
+}
+
+div#multiple_details {
+	height: 300px;
+	width: 500px;
+	display: none;
+	padding: 1em;
+    border: 1px solid;
+	position: absolute;
+	background-color: white;
+	max-height: 330px !important;
+    overflow: hidden;
 }
 
 .panel-adjacent {
@@ -170,10 +198,13 @@ text.id {
   border: 1px solid red;
 }
 #topologyForm {
-  border: 1px solid white;
-  padding: 2px;
-  position: relative;
-  top: -8px;
+    border: 1px solid white;
+    padding: 2px;
+    /* position: relative; */
+    /* top: -8px; */
+}
+div.qdr-topology.pane.left .ngViewport {
+    /* border: 1px solid lightgray; */
 }
 
 #topologyForm > div {
@@ -479,6 +510,13 @@ div.boolean {
   stroke-width: 3px;
 }
 
+circle.subcircle {
+    stroke-width: 1px;
+    /* stroke-dasharray: 2; */
+    fill-opacity: 0;
+    stroke: black;
+}
+
 .leaf circle {
   fill: #6fa8dc;
   fill-opacity: 0.95;
@@ -490,6 +528,28 @@ div.boolean {
 
 }
 
-.qdrListActions .ngGrid {
-	height: 100vh;
+#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;
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/96ce4a6c/console/hawtio/src/main/webapp/plugin/html/qdrList.html
----------------------------------------------------------------------
diff --git a/console/hawtio/src/main/webapp/plugin/html/qdrList.html b/console/hawtio/src/main/webapp/plugin/html/qdrList.html
index adb249f..d07d3ca 100644
--- a/console/hawtio/src/main/webapp/plugin/html/qdrList.html
+++ b/console/hawtio/src/main/webapp/plugin/html/qdrList.html
@@ -26,12 +26,60 @@ under the License.
         </div>
     </hawtio-pane>
     <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-unsafe="mode.content"> </li>
+        </ul>
         <h4>{{selectedRecordName}}</h4>
         <div ng-show="currentMode.id === 'attributes'" class="selectedItems">
-            <div ng-grid="details"></div>
+            <div ng-show="selectedRecordName === selectedEntity" class="no-content">There are no {{selectedEntity}}s</div>
+            <div ng-hide="selectedRecordName === selectedEntity" ng-grid="details"></div>
         </div>
-        <div ng-show="currentMode.id === 'operations'">
-            Operations are not implemented yet.
+        <div class="operations" ng-show="currentMode.id === 'operations'">
+            <!-- <div ng-grid="detailsCREATE"></div> -->
+            <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)" -->
+                        <input ng-if="attribute.type == 'number'" type="number" name="{{attribute.name}}" id="{{attribute.name}}" ng-model="attribute.rawValue" ng-required="attribute.required" class="ui-widget-content ui-corner-all"/>
+                        <input ng-if="attribute.type == 'text'" type="text" name="{{attribute.name}}" id="{{attribute.name}}" ng-model="attribute.attributeValue" ng-required="attribute.required" class="ui-widget-content ui-corner-all"/>
+                        <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-options="item for item in attribute.rawtype track by item"></select>
+                    </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'">
+            <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>
 </div>

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/96ce4a6c/console/hawtio/src/main/webapp/plugin/html/qdrOverview.html
----------------------------------------------------------------------
diff --git a/console/hawtio/src/main/webapp/plugin/html/qdrOverview.html b/console/hawtio/src/main/webapp/plugin/html/qdrOverview.html
index 61d5143..2a66a94 100644
--- a/console/hawtio/src/main/webapp/plugin/html/qdrOverview.html
+++ b/console/hawtio/src/main/webapp/plugin/html/qdrOverview.html
@@ -47,7 +47,7 @@ under the License.
 
 <script type="text/ng-template" id="router.html">
     <div class="row-fluid">
-        <h3>Router {{router.data.title}}</h3>
+        <h3>Router {{router.data.title}} attributes</h3>
         <div class="gridStyle noHighlight" ng-grid="routerGrid"></div>
     </div>
 </script>

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/96ce4a6c/console/hawtio/src/main/webapp/plugin/html/qdrTopology.html
----------------------------------------------------------------------
diff --git a/console/hawtio/src/main/webapp/plugin/html/qdrTopology.html b/console/hawtio/src/main/webapp/plugin/html/qdrTopology.html
index 704c8e2..61ca2c1 100644
--- a/console/hawtio/src/main/webapp/plugin/html/qdrTopology.html
+++ b/console/hawtio/src/main/webapp/plugin/html/qdrTopology.html
@@ -17,19 +17,18 @@ specific language governing permissions and limitations
 under the License.
 -->
 <div class="qdrTopology row-fluid" ng-controller="QDR.TopologyController">
-    <div class="tertiary left panel">
+    <div class="qdr-topology pane left" ng-controller="QDR.TopologyFormController">
         <div id="topologyForm" ng-class="{selected : isSelected()}">
             <!-- <div ng-repeat="form in forms" ng-show="isVisible(form)" ng-class='{selected : isSelected(form)}'> -->
-
-            <div ng-show="isGeneral()">
+            <div ng-show="form == 'router'">
                 <h4>Router Info</h4>
                 <div class="gridStyle" ng-grid="topoGridOptions"></div>
             </div>
-            <div ng-show="isConnections()">
+            <div ng-show="form == 'connection'">
                 <h4>Connection Info</h4>
-                <div class="gridStyle" ng-grid="topoConnOptions"></div>
+                <div class="gridStyle" ng-grid="topoGridOptions"></div>
             </div>
-            <div id="addNodeForm" ng-show="isAddNode()">
+            <div id="addNodeForm" ng-show="form == 'add'">
                 <h4>Add a new router</h4>
                 <ul>
                     <li>Click on an existing router to create a connection to the new router</li>
@@ -71,7 +70,10 @@ under the License.
                 <li ng-click="removeLink()">Remove connection</li>
             </ul>
         </div>
-
+        <div id="svg_legend"></div>
+        <div id="multiple_details">
+            <div class="gridStyle" ng-grid="multiDetails"></div>
+        </div>
     </div>
 </div>
 <!--

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/96ce4a6c/console/hawtio/src/main/webapp/plugin/js/dispatchPlugin.js
----------------------------------------------------------------------
diff --git a/console/hawtio/src/main/webapp/plugin/js/dispatchPlugin.js b/console/hawtio/src/main/webapp/plugin/js/dispatchPlugin.js
index 59ea4d3..5061317 100644
--- a/console/hawtio/src/main/webapp/plugin/js/dispatchPlugin.js
+++ b/console/hawtio/src/main/webapp/plugin/js/dispatchPlugin.js
@@ -88,10 +88,12 @@ var QDR = (function(QDR) {
 			$compileProvider.urlSanitizationWhitelist(/^\s*(https?|ftp|mailto|file|blob):/);
 			cur = $compileProvider.urlSanitizationWhitelist();
 	  })
+
 	  .config(function (JSONFormatterConfigProvider) {
 			// Enable the hover preview feature
 			JSONFormatterConfigProvider.hoverPreviewEnabled = true;
 	  })
+
 	  .filter('to_trusted', function($sce){
 			return function(text) {
 				return $sce.trustAsHtml(text);
@@ -107,7 +109,15 @@ var QDR = (function(QDR) {
 				var nameParts = name.split('/')
 				return nameParts.length > 1 ? nameParts[nameParts.length-1] : name;
 			};
-	  });
+	  })
+	  .filter('Pascalcase', function () {
+	        return function (str) {
+				if (!str)
+					return "";
+	            return str.replace(/(\w)(\w*)/g,
+                        function(g0,g1,g2){return g1.toUpperCase() + g2.toLowerCase();});
+	        }
+	  })
 /*
 	QDR.module.config(['$locationProvider', function($locationProvider) {
         $locationProvider.html5Mode(true);
@@ -134,6 +144,7 @@ var QDR = (function(QDR) {
 		Core.addCSS("https://cdn.rawgit.com/mohsen1/json-formatter/master/dist/json-formatter.min.css");
 		Core.addCSS("https://cdnjs.cloudflare.com/ajax/libs/jquery.tipsy/1.0.2/jquery.tipsy.css");
 		Core.addCSS("https://code.jquery.com/ui/1.8.24/themes/base/jquery-ui.css");
+		Core.addCSS("https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css");
 
 		// tell hawtio that we have our own custom layout for
 		// our view
@@ -217,16 +228,14 @@ var QDR = (function(QDR) {
 
 })(QDR || {});
 
-
-// tell the hawtio plugin loader about our plugin so it can be
-// bootstrapped with the rest of angular
-hawtioPluginLoader.addModule(QDR.pluginName);
-
 $.getScript('https://cdn.rawgit.com/angular-ui/ui-slider/master/src/slider.js', function() {
 	hawtioPluginLoader.addModule('ui.slider');
 });
 $.getScript('https://cdn.rawgit.com/mohsen1/json-formatter/master/dist/json-formatter.min.js', function() {
 	hawtioPluginLoader.addModule('jsonFormatter');
+	// tell the hawtio plugin loader about our plugin so it can be
+	// bootstrapped with the rest of angular
+	hawtioPluginLoader.addModule(QDR.pluginName);
 });
 
 // force an more modern version of d3 to load

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/96ce4a6c/console/hawtio/src/main/webapp/plugin/js/navbar.js
----------------------------------------------------------------------
diff --git a/console/hawtio/src/main/webapp/plugin/js/navbar.js b/console/hawtio/src/main/webapp/plugin/js/navbar.js
index d305f38..8cfecdf 100644
--- a/console/hawtio/src/main/webapp/plugin/js/navbar.js
+++ b/console/hawtio/src/main/webapp/plugin/js/navbar.js
@@ -43,8 +43,8 @@ var QDR = (function (QDR) {
         href: "#/dispatch_plugin/overview"
       },
     {
-        content: '<i class="icon-list "></i> Details',
-        title: "View the attributes of the router nodes",
+        content: '<i class="icon-list "></i> Entities',
+        title: "View the attributes of the router entities",
         isValid: function (QDRService) { return QDRService.isConnected(); },
         href: "#/dispatch_plugin/list"
       },

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/96ce4a6c/console/hawtio/src/main/webapp/plugin/js/qdrList.js
----------------------------------------------------------------------
diff --git a/console/hawtio/src/main/webapp/plugin/js/qdrList.js b/console/hawtio/src/main/webapp/plugin/js/qdrList.js
index 72fbc04..d816521 100644
--- a/console/hawtio/src/main/webapp/plugin/js/qdrList.js
+++ b/console/hawtio/src/main/webapp/plugin/js/qdrList.js
@@ -28,9 +28,10 @@ var QDR = (function(QDR) {
    *
    * Controller for the main interface
    */
-	QDR.module.controller("QDR.ListController", ['$scope', '$location', '$dialog', 'localStorage', 'QDRService', 'QDRChartService',
-		function ($scope, $location, $dialog, localStorage, QDRService, QDRChartService) {
+	QDR.module.controller("QDR.ListController", ['$scope', '$location', '$dialog', '$filter', 'localStorage', 'QDRService', 'QDRChartService',
+		function ($scope, $location, $dialog, $filter, localStorage, QDRService, QDRChartService) {
 
+		$scope.details = [];
 		if (!QDRService.connected) {
 			// we are not connected. we probably got here from a bookmark or manual page reload
 			$location.path("/dispatch_plugin/connect")
@@ -43,6 +44,75 @@ var QDR = (function(QDR) {
 		$scope.selectedNodeId = localStorage['QDRSelectedNodeId'];
 		$scope.selectedRecordName = localStorage['QDRSelectedRecordName'];
 
+		$scope.modes = [{
+	        content: '<a><i class="icon-list"></i> Attriutes</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-chart"></i> Chart</a>',
+	        id: 'charts',
+	        op: 'graph',
+	        title: "Graph attributes",
+	        isValid: function () { return false; return $scope.detailFields.some( function (field ) {
+                                                 					return field.graph;
+                                                 				})}
+	    },
+	    {
+	        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.selectMode = function (mode) {
+			$scope.currentMode = mode;
+			if (mode.id === 'log') {
+				$scope.logResults = "getting recent log entries...";
+				QDRService.sendMethod($scope.currentNode.id, $scope.selectedEntity, {}, $scope.currentMode.op, function (nodeName, entity, response, context) {
+					$scope.logResults = 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.$apply();
+				})
+			}
+		}
+		$scope.isValid = function (mode) {
+			return mode.isValid()
+		}
+
 		$scope.nodes = QDRService.nodeList().sort(function (a, b) { return a.name.toLowerCase() > b.name.toLowerCase()});
 		if (!angular.isDefined($scope.selectedNode)) {
 			//QDR.log.debug("selectedNode was " + $scope.selectedNode);
@@ -69,33 +139,20 @@ var QDR = (function(QDR) {
 				return row.linkType.value;
 			}
 		}
-		$scope.modes = [
-		    {
-		        content: '<a><i class="icon-list"></i> Attriutes</a>',
-				id: 'attributes',
-				title: "View router attributes",
-		        isValid: function () { return true; }
-		    },
-		    {
-		        content: '<a><i class="icon-leaf"></i> Operations</a>',
-		        id: 'operations',
-		        title: "Execute operations",
-		        isValid: function () { return true; }
-		    }
-        ];
-        $scope.currentMode = $scope.modes[0];
-		$scope.isModeSelected = function (mode) {
-			return mode === $scope.currentMode;
-		}
-		$scope.selectMode = function (mode) {
-			$scope.currentMode = mode;
+
+		var lookupOperations = function () {
+			var ops = QDRService.schema.entityTypes[$scope.selectedEntity].operations.filter( function (op) { return op !== 'READ'});
+			$scope.operation = ops.length ? ops[0] : "";
+			return ops;
 		}
 
         var entityTreeChildren = [];
-		for (var entity in QDRService.schema.entityTypes) {
+        var sortedEntities = Object.keys(QDRService.schema.entityTypes).sort();
+		sortedEntities.forEach( function (entity) {
 			if (excludedEntities.indexOf(entity) == -1) {
 				if (!angular.isDefined($scope.selectedEntity)) {
 					$scope.selectedEntity = entity;
+					$scope.operations = lookupOperations()
 				}
 				var current = entity === $scope.selectedEntity;
 				var e = new Folder(entity)
@@ -106,7 +163,7 @@ var QDR = (function(QDR) {
 			    e.children = [placeHolder]
 				entityTreeChildren.push(e)
 			}
-		}
+		})
 		$scope.treeReady = function () {
 			$('#entityTree').dynatree({
 				onActivate: onTreeSelected,
@@ -123,10 +180,16 @@ var QDR = (function(QDR) {
 		}
 		// a tree node was selected
 		var onTreeSelected = function (selectedNode) {
+			if ($scope.currentMode.id === 'operations')
+				$scope.currentMode = $scope.modes[0];
+			else if ($scope.currentMode.id === 'log')
+				$scope.selectMode($scope.currentMode)
 			if (selectedNode.data.typeName === "entity") {
 				$scope.selectedEntity = selectedNode.data.key;
+				$scope.operations = lookupOperations()
 			} else if (selectedNode.data.typeName === 'attribute') {
 				$scope.selectedEntity = selectedNode.parent.data.key;
+				$scope.operations = lookupOperations()
 				$scope.selectedRecordName = selectedNode.data.key;
 				updateDetails(selectedNode.data.details);
 				$("#entityTree").dynatree("getRoot").visit(function(node){
@@ -136,6 +199,28 @@ var QDR = (function(QDR) {
 			}
 			$scope.$apply();
 		}
+		// fill in an empty results recoord based on the entities schema
+		var fromSchema = function (entityName) {
+			var row = {}
+			var schemaEntity = QDRService.schema.entityTypes[entityName]
+			for (attr in schemaEntity.attributes) {
+				var entity = schemaEntity.attributes[attr]
+				row[attr] = {
+					value: "",
+					type: entity.type,
+					graph: false,
+					title: entity.description,
+					aggregate: false,
+					aggregateTip: ''
+				}
+			}
+			return row;
+		}
+		$scope.hasCreate = function () {
+			var schemaEntity = QDRService.schema.entityTypes[$scope.selectedEntity]
+			return (schemaEntity.operations.indexOf("CREATE") > -1)
+		}
+
 		// the data for the selected entity is available, populate the tree
 		var updateEntityChildren = function (tableRows, expand) {
 			var tree = $("#entityTree").dynatree("getTree");
@@ -148,8 +233,8 @@ var QDR = (function(QDR) {
 			        typeName:   "none",
 			        title:      "no data"
 			    })
-			    $scope.selectedRecordName = "";
-			    updateDetails({});
+			    $scope.selectedRecordName = $scope.selectedEntity;
+			    updateDetails(fromSchema($scope.selectedEntity));
 			} else {
 				tableRows.forEach( function (row) {
 					var addClass = $scope.selectedEntity;
@@ -184,24 +269,85 @@ var QDR = (function(QDR) {
 			}
 		}
 
+		var schemaProps = function (entityName, key, currentNode) {
+	   		var typeMap = {integer: 'number', string: 'text', path: 'text', boolean: 'boolean'};
+
+			var entity = QDRService.schema.entityTypes[entityName]
+			var value = entity.attributes[key]
+			// skip identity and depricated fields
+			if (!value)
+				return {input: 'input', type: 'disabled', required: false, selected: "", rawtype: 'string', disabled: true, 'default': ''}
+			var description = value.description || ""
+			var val = value['default'];
+			var 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.getNodeInfo(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.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) {
+			var value = attribute.attributeValue;
+			if ($scope.currentMode.op === "CREATE" && attribute.name === 'identity')
+				value = "<assigned by system>"
+			return value;
+		}
 		var updateDetails = function (row) {
 			var details = [];
-			for (var attr in row) {
+			$scope.detailsObject = {};
+			var attrs = Object.keys(row).sort();
+			attrs.forEach( function (attr) {
 				var changed = $scope.detailFields.filter(function (old) {
 					return (old.name === attr) ? old.graph && old.rawValue != row[attr].value : false;
 				})
+				var schemaEntity = schemaProps($scope.selectedEntity, attr, $scope.currentNode)
 				details.push( {
 					attributeName:  QDRService.humanify(attr),
-					attributeValue: QDRService.pretty(row[attr].value),
+					attributeValue: attr === 'port' ? row[attr].value : QDRService.pretty(row[attr].value),
 					name:           attr,
 					changed:        changed.length,
 					rawValue:       row[attr].value,
 					graph:          row[attr].graph,
 					title:          row[attr].title,
 					aggregateValue: QDRService.pretty(row[attr].aggregate),
-					aggregateTip:   row[attr].aggregateTip
+					aggregateTip:   row[attr].aggregateTip,
+
+					input:          schemaEntity.input,
+					type:           schemaEntity.type,
+					required:       schemaEntity.required,
+					selected:       schemaEntity.selected,
+					rawtype:        schemaEntity.rawtype,
+					disabled:       schemaEntity.disabled,
+					'default':      schemaEntity['default']
 				})
-			}
+				$scope.detailsObject[attr] = row[attr].value;
+			})
 			$scope.detailFields = details;
 			aggregateColumn();
 			$scope.$apply();
@@ -224,6 +370,7 @@ var QDR = (function(QDR) {
 			if (newValue !== oldValue) {
 				localStorage['QDRSelectedEntity'] = $scope.selectedEntity;
 				restartUpdate();
+				$scope.operations = lookupOperations()
 			}
 		})
 		$scope.$watch('selectedNode', function(newValue, oldValue) {
@@ -241,14 +388,19 @@ var QDR = (function(QDR) {
 		$scope.tableRows = [];
 		var selectedRowIndex = 0;
 		var updateTableData = function (entity, expand) {
+			// don't update the data when on the operations tab
+			if ($scope.currentMode.id === 'operations') {
+				return;
+			}
+
 			var gotNodeInfo = function (nodeName, dotentity, response) {
 				//QDR.log.debug("got results for  " + nodeName);
 				//console.dump(response);
-
 				var records = response.results;
 				var aggregates = response.aggregates;
 				var attributeNames = response.attributeNames;
 				var nameIndex = attributeNames.indexOf("name");
+				var identityIndex = attributeNames.indexOf("identity");
 				var ent = QDRService.schema.entityTypes[entity];
 				var tableRows = [];
 				for (var i=0; i<records.length; ++i) {
@@ -258,7 +410,11 @@ var QDR = (function(QDR) {
 					var rowName;
 					if (nameIndex > -1) {
 						rowName = record[nameIndex];
-					} else {
+						if (!rowName && identityIndex > -1) {
+							rowName = record[nameIndex] = (dotentity + '/' + record[identityIndex])
+						}
+					}
+					if (!rowName) {
 						QDR.log.error("response attributeNames did not contain a name field");
 						console.dump(response.attributeNames);
 						return;
@@ -290,15 +446,15 @@ var QDR = (function(QDR) {
 					}
 					tableRows.push(row);
 				}
+				tableRows.sort( function (a, b) { return a.name.value.localeCompare(b.name.value) })
 				setTimeout(selectRow, 0, {rows: tableRows, expand: expand});
 			}
-
 			// 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) {
 				var nodeInfo = QDRService.topology.nodeInfo();
 				QDRService.getMultipleNodeInfo(Object.keys(nodeInfo), entity, [], gotNodeInfo, $scope.selectedNodeId);
 			} else {
-				QDRService.getNodeInfo($scope.selectedNodeId, '.' + entity, [], gotNodeInfo);
+				QDRService.getNodeInfo($scope.selectedNodeId, entity, [], gotNodeInfo);
 			}
 		};
 
@@ -420,7 +576,6 @@ var QDR = (function(QDR) {
 				  return false;
 			}
 		};
-
 		updateTableData($scope.selectedEntity, true);
 		stop = setInterval(updateTableData, 5000, $scope.selectedEntity);
 
@@ -432,6 +587,41 @@ var QDR = (function(QDR) {
 			};
 		});
 
+		function gotMethodResponse (nodeName, entity, response, context) {
+			var statusCode = context.message.application_properties.statusCode;
+			if (statusCode < 200 || statusCode >= 300) {
+				Core.notification('error', context.message.application_properties.statusDescription);
+				QDR.log.debug(context.message.application_properties.statusDescription)
+			} else {
+				var note = entity + " " + $filter('Pascalcase')($scope.currentMode.op) + "d"
+				QDR.log.info(note)
+				Core.notification('success', note);
+				$scope.selectMode($scope.modes[0]);
+				$scope.$apply();
+			}
+		}
+		$scope.ok = function () {
+			var attributes = {}
+			$scope.detailFields.forEach( function (field) {
+				var value = field.rawValue;
+				if (field.input === 'input') {
+					if (field.type === 'text' || field.type === 'disabled')
+						value = field.attributeValue;
+				}
+				if (field.input === 'select')
+					value = field.selected;
+
+				if (value != field['default'] || field.required || (field.name === 'role')) {
+					if (field.name !== 'identity')
+						attributes[field.name] = value
+				}
+				if (!attributes.type)
+					attributes.type = $scope.selectedEntity;
+
+			})
+			QDRService.sendMethod($scope.currentNode.id, $scope.selectedEntity, attributes, $scope.currentMode.op, gotMethodResponse)
+		}
+
 		function doDialog(template, chart) {
 		    var d = $dialog.dialog({
 		      backdrop: true,

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/96ce4a6c/console/hawtio/src/main/webapp/plugin/js/qdrOverview.js
----------------------------------------------------------------------
diff --git a/console/hawtio/src/main/webapp/plugin/js/qdrOverview.js b/console/hawtio/src/main/webapp/plugin/js/qdrOverview.js
index cc52868..bf75ffd 100644
--- a/console/hawtio/src/main/webapp/plugin/js/qdrOverview.js
+++ b/console/hawtio/src/main/webapp/plugin/js/qdrOverview.js
@@ -44,7 +44,31 @@ var QDR = (function (QDR) {
 			$location.path("/dispatch_plugin/connect")
 			return;
 		}
-
+		// 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 (workspace) { return true; },
+		    href: function () { return "#/dispatch-plugin/attributes"; },
+		    index: 0
+		},
+		{
+		    content: '<i class="icon-leaf"></i> Operations',
+		    title: "Execute operations on your selection",
+		    isValid: function (workspace) { return true; },
+		    href: function () { return "#/dispatch-plugin/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;
+		}
 		var nodeIds = QDRService.nodeIdList();
 		var currentTimer;
 		var refreshInterval = 5000
@@ -141,7 +165,28 @@ var QDR = (function (QDR) {
 				if (expected == received) {
 					allRouterFields.sort ( function (a,b) { return a.routerId < b.routerId ? -1 : a.routerId > b.routerId ? 1 : 0})
 					// now get each router's node info
-					QDRService.getMultipleNodeInfo(nodeIds, "router", [], function (nodeIds, entity, response) {
+					QDRService.getMultipleNodeInfo(nodeIds, "router", [], function (nodeIds, entity, responses) {
+						for(var r in responses) {
+							var result = responses[r]
+							var routerId = QDRService.valFor(result.attributeNames, result.results[0], "routerId")
+							allRouterFields.some( function (connField) {
+								if (routerId === connField.routerId) {
+									result.attributeNames.forEach ( function (attrName) {
+										connField[attrName] = QDRService.valFor(result.attributeNames, result.results[0], attrName)
+									})
+									return true
+								}
+								return false
+							})
+						}
+						$scope.allRouterFields = allRouterFields
+						$scope.$apply()
+						if (currentTimer) {
+							clearTimeout(currentTimer)
+						}
+						currentTimer = setTimeout(allRouterInfo, refreshInterval);
+/*
+
 						var results = response.aggregates
 						results.forEach ( function (result) {
 
@@ -162,10 +207,11 @@ var QDR = (function (QDR) {
 							clearTimeout(currentTimer)
 						}
 						currentTimer = setTimeout(allRouterInfo, refreshInterval);
-					}, nodeIds[0])
+*/
+					}, nodeIds[0], false)
 				}
 			}
-			nodeIds.forEach ( function (nodeId) {
+			nodeIds.forEach ( function (nodeId, i) {
 				QDRService.getNodeInfo(nodeId, ".connection", ["role"], gotNodeInfo)
 			})
 

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/96ce4a6c/console/hawtio/src/main/webapp/plugin/js/qdrService.js
----------------------------------------------------------------------
diff --git a/console/hawtio/src/main/webapp/plugin/js/qdrService.js b/console/hawtio/src/main/webapp/plugin/js/qdrService.js
index 06bf113..aebb71b 100644
--- a/console/hawtio/src/main/webapp/plugin/js/qdrService.js
+++ b/console/hawtio/src/main/webapp/plugin/js/qdrService.js
@@ -143,7 +143,7 @@ var QDR = (function(QDR) {
         // called by receiver's on('message') handler when a response arrives
         resolve: function(context) {
 			var correlationID = context.message.properties.correlation_id;
-            this._objects[correlationID].resolver(context.message.body);
+            this._objects[correlationID].resolver(context.message.body, context);
             delete this._objects[correlationID];
         }
     },
@@ -247,6 +247,14 @@ var QDR = (function(QDR) {
           return null;
       },
 
+		isArtemis: function (d) {
+			return d.nodeType ==='on-demand' && !d.properties.product;
+		},
+
+		isQpid: function (d) {
+			return d.nodeType ==='on-demand' && (d.properties && d.properties.product === 'qpid-cpp');
+		},
+
       /*
        * send the management messages that build up the topology
        *
@@ -482,74 +490,81 @@ The response looks like:
         }, ret.error);
       },
 
-		getMultipleNodeInfo: function (nodeNames, entity, attrs, callback, selectedNodeId) {
+		getMultipleNodeInfo: function (nodeNames, entity, attrs, callback, selectedNodeId, aggregate) {
+			if (!angular.isDefined(aggregate))
+				aggregate = true;
 			var responses = {};
 			var gotNodesResult = function (nodeName, dotentity, response) {
 				responses[nodeName] = response;
 				if (Object.keys(responses).length == nodeNames.length) {
-					aggregateNodeInfo(nodeNames, entity, responses, callback);
+					if (aggregate)
+						self.aggregateNodeInfo(nodeNames, entity, selectedNodeId, responses, callback);
+					else {
+						callback(nodeNames, entity, responses)
+					}
 				}
 			}
 
-			var aggregateNodeInfo = function (nodeNames, entity, responses, callback) {
-				//QDR.log.debug("got all results for  " + entity);
-				// aggregate the responses
-				var newResponse = {};
-				var thisNode = responses[selectedNodeId];
-				newResponse['attributeNames'] = thisNode.attributeNames;
-				newResponse['results'] = thisNode.results;
-				newResponse['aggregates'] = [];
-				for (var i=0; i<thisNode.results.length; ++i) {
-					var result = thisNode.results[i];
-					var vals = [];
-					result.forEach( function (val) {
-						vals.push({sum: val, detail: []})
-					})
-					newResponse.aggregates.push(vals);
-				}
-				var nameIndex = thisNode.attributeNames.indexOf("name");
-				var ent = self.schema.entityTypes[entity];
-				var ids = Object.keys(responses);
-				ids.sort();
-				ids.forEach( function (id) {
-					var response = responses[id];
-					var results = response.results;
-					results.forEach( function (result) {
-						// find the matching result in the aggregates
-						var found = newResponse.aggregates.some( function (aggregate, j) {
-							if (aggregate[nameIndex].sum === result[nameIndex]) {
-								// result and aggregate are now the same record, add the graphable values
-								newResponse.attributeNames.forEach( function (key, i) {
-									if (ent.attributes[key] && ent.attributes[key].graph) {
-										if (id != selectedNodeId)
-											aggregate[i].sum += result[i];
-										aggregate[i].detail.push({node: self.nameFromId(id)+':', val: result[i]})
-									}
-								})
-								return true; // stop looping
-							}
-							return false; // continute looking for the aggregate record
-						})
-						if (!found) {
-							// this attribute was not found in the aggregates yet
-							// because it was not in the selectedNodeId's results
-							var vals = [];
-							result.forEach( function (val) {
-								vals.push({sum: val, detail: []})
-							})
-							newResponse.aggregates.push(vals)
-						}
-					})
-				})
-				callback(nodeNames, entity, newResponse);
-			}
-
 			nodeNames.forEach( function (id) {
 	            self.getNodeInfo(id, '.'+entity, attrs, gotNodesResult);
 	        })
 			//TODO: implement a timeout in case not all requests complete
 		},
 
+		aggregateNodeInfo: function (nodeNames, entity, selectedNodeId, responses, callback) {
+			//QDR.log.debug("got all results for  " + entity);
+			// aggregate the responses
+			var newResponse = {};
+			var thisNode = responses[selectedNodeId];
+			newResponse['attributeNames'] = thisNode.attributeNames;
+			newResponse['results'] = thisNode.results;
+			newResponse['aggregates'] = [];
+			for (var i=0; i<thisNode.results.length; ++i) {
+				var result = thisNode.results[i];
+				var vals = [];
+				result.forEach( function (val) {
+					vals.push({sum: val, detail: []})
+				})
+				newResponse.aggregates.push(vals);
+			}
+			var nameIndex = thisNode.attributeNames.indexOf("name");
+			var ent = self.schema.entityTypes[entity];
+			var ids = Object.keys(responses);
+			ids.sort();
+			ids.forEach( function (id) {
+				var response = responses[id];
+				var results = response.results;
+				results.forEach( function (result) {
+					// find the matching result in the aggregates
+					var found = newResponse.aggregates.some( function (aggregate, j) {
+						if (aggregate[nameIndex].sum === result[nameIndex]) {
+							// result and aggregate are now the same record, add the graphable values
+							newResponse.attributeNames.forEach( function (key, i) {
+								if (ent.attributes[key] && ent.attributes[key].graph) {
+									if (id != selectedNodeId)
+										aggregate[i].sum += result[i];
+									aggregate[i].detail.push({node: self.nameFromId(id)+':', val: result[i]})
+								}
+							})
+							return true; // stop looping
+						}
+						return false; // continute looking for the aggregate record
+					})
+					if (!found) {
+						// this attribute was not found in the aggregates yet
+						// because it was not in the selectedNodeId's results
+						var vals = [];
+						result.forEach( function (val) {
+							vals.push({sum: val, detail: []})
+						})
+						newResponse.aggregates.push(vals)
+					}
+				})
+			})
+			callback(nodeNames, entity, newResponse);
+		},
+
+
       getSchema: function () {
         //QDR.log.debug("getting schema");
         var ret;
@@ -564,13 +579,74 @@ The response looks like:
         }, ret.error);
       },
 
-    sendQuery: function(toAddr, entity, attrs) {
+      getNodeInfo: function (nodeName, entity, attrs, callback) {
+        //QDR.log.debug("getNodeInfo called with nodeName: " + nodeName + " and entity " + entity);
+        var ret;
+        self.correlator.request(
+            ret = self.sendQuery(nodeName, entity, attrs)
+        ).then(ret.id, function(response) {
+            callback(nodeName, entity, response);
+            //self.topology.addNodeInfo(nodeName, entity, response);
+            //self.topology.cleanUp(response);
+        }, ret.error);
+      },
+
+	sendMethod: function (nodeId, entity, attrs, operation, callback) {
+		var ret;
+		self.correlator.request(
+			ret = self._sendMethod(nodeId, entity, attrs, operation)
+		).then(ret.id, function (response, context) {
+				callback(nodeId, entity, response, context);
+		}, ret.error);
+	},
+
+	_fullAddr: function (toAddr) {
         var toAddrParts = toAddr.split('/');
         if (toAddrParts.shift() != "amqp:") {
             self.topology.error(Error("unexpected format for router address: " + toAddr));
             return;
         }
-        var fullAddr =  self.toAddress + "/" + toAddrParts.join('/');
+        //var fullAddr =  self.toAddress + "/" + toAddrParts.join('/');
+        var fullAddr =  toAddrParts.join('/');
+		return fullAddr;
+	},
+
+	_sendMethod: function (toAddr, entity, attrs, operation) {
+		var fullAddr = self._fullAddr(toAddr);
+		var ret = {id: self.correlator.corr()};
+		if (!self.sender || !self.sendable) {
+			ret.error = "no sender"
+			return ret;
+		}
+		try {
+			var application_properties = {
+				operation:  operation
+			}
+			if (attrs.type)
+				application_properties.type = attrs.type;
+			if (attrs.name)
+				application_properties.name = attrs.name;
+	        self.sender.send({
+	                body: attrs,
+	                properties: {
+	                    to:                     fullAddr,
+                        reply_to:               self.receiver.remote.attach.source.address,
+	                    correlation_id:         ret.id
+	                },
+	                application_properties: application_properties
+            })
+		}
+		catch (e) {
+			error = "error sending: " + e;
+			QDR.log.error(error)
+			ret.error = error;
+		}
+		return ret;
+	},
+
+    sendQuery: function(toAddr, entity, attrs, operation) {
+        operation = operation || "QUERY"
+		var fullAddr = self._fullAddr(toAddr);
 
 		var body;
         if (attrs)
@@ -581,13 +657,14 @@ The response looks like:
             body = {
                 "attributeNames": [],
             }
-
-		return self._send(body, fullAddr, "QUERY", "org.apache.qpid.dispatch" + entity);
+		if (entity[0] === '.')
+			entity = entity.substr(1, entity.length-1)
+		//return self._send(body, fullAddr, operation, entity);
+		return self._send(body, fullAddr, operation, "org.apache.qpid.dispatch." + entity);
     },
 
     sendMgmtQuery: function (operation) {
-		// TODO: file bug against dispatch - We should be able to just pass body: [], but that generates an 'invalid body'
-		return self._send([' '], self.toAddress + "/$management", operation);
+		return self._send([], "/$management", operation);
     },
 
 	_send: function (body, to, operation, entityType) {
@@ -649,7 +726,6 @@ The response looks like:
 				self.receiver = null;
 				self.sendable = false;
 			}
-
 			var maybeStart = function () {
 				if (okay.connection && okay.sender && okay.receiver && self.sendable && !self.connected) {
 					QDR.log.info("okay to start")
@@ -662,7 +738,7 @@ The response looks like:
 				}
 			}
 			var onDisconnect = function () {
-				QDR.log.warn("Disconnected");
+				//QDR.log.warn("Disconnected");
 				stop();
 				self.executeDisconnectActions();
 			}
@@ -690,7 +766,7 @@ The response looks like:
 				self.errorText = "Disconnected"
 			})
 
-			var sender = connection.open_sender("/$management");
+			var sender = connection.open_sender();
 			sender.on('sender_open', function (context) {
 				QDR.log.debug("sender_opened")
 				okay.sender = true
@@ -724,7 +800,7 @@ The response looks like:
 (function() {
     console.dump = function(object) {
         if (window.JSON && window.JSON.stringify)
-            console.log(JSON.stringify(object));
+            console.log(JSON.stringify(object,undefined,2));
         else
             console.log(object);
     };

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/96ce4a6c/console/hawtio/src/main/webapp/plugin/js/qdrTopology.js
----------------------------------------------------------------------
diff --git a/console/hawtio/src/main/webapp/plugin/js/qdrTopology.js b/console/hawtio/src/main/webapp/plugin/js/qdrTopology.js
index ff5b696..a59c8d3 100644
--- a/console/hawtio/src/main/webapp/plugin/js/qdrTopology.js
+++ b/console/hawtio/src/main/webapp/plugin/js/qdrTopology.js
@@ -21,6 +21,44 @@ under the License.
  */
 var QDR = (function (QDR) {
 
+
+	QDR.module.controller('QDR.TopologyFormController', function ($scope, QDRService) {
+
+		$scope.attributes = []
+        var generalCellTemplate = '<div class="ngCellText"><span title="{{row.entity.description}}">{{row.entity.attributeName}}</span></div>';
+        $scope.topoGridOptions = {
+            data: 'attributes',
+			enableColumnResize: true,
+			multiSelect: false,
+            columnDefs: [
+            {
+                field: 'attributeName',
+                displayName: 'Attribute',
+                cellTemplate: generalCellTemplate
+            },
+            {
+                field: 'attributeValue',
+                displayName: 'Value'
+            }
+            ]
+        };
+		$scope.form = ''
+		$scope.$on('showEntityForm', function (event, args) {
+			var attributes = args.attributes;
+			var entityTypes = QDRService.schema.entityTypes[args.entity].attributes;
+			Object.keys(attributes).forEach( function (attr) {
+				if (entityTypes[attr])
+					attributes[attr].description = entityTypes[attr]
+			})
+			$scope.attributes = attributes;
+			$scope.form = args.entity;
+		})
+		$scope.$on('showAddForm', function (event) {
+			$scope.form = 'add';
+		})
+	})
+
+
   /**
    * @method TopologyController
    *
@@ -38,37 +76,11 @@ var QDR = (function (QDR) {
 		QDR.log.debug("started QDR.TopologyController with urlPrefix: " + $location.absUrl());
 		var urlPrefix = $location.absUrl();
 
-		$scope.attributes = [];
-        $scope.connAttributes = [];
-        $scope.topoForm = "general";
-        $scope.topoFormSelected = "";
 		$scope.addingNode = {
 			step: 0,
 			hasLink: false,
 			trigger: ''
-		}; // shared object about the node that is be	    $scope.topoForm = "general";
-
-        var generalCellTemplate = '<div class="ngCellText"><span title="{{row.entity.description}}">{{row.entity.attributeName}}</span></div>';
-
-		$scope.isGeneral = function () {
-    	    //QDR.log.debug("$scope.topoForm=" + $scope.topoForm)
-    	    return $scope.topoForm === 'general';
-		};
-		$scope.isConnections = function () {
-    	    //QDR.log.debug("$scope.topoForm=" + $scope.topoForm)
-    	    return $scope.topoForm === 'connections';
 		};
-		$scope.isAddNode = function () {
-    	    //QDR.log.debug("$scope.topoForm=" + $scope.topoForm)
-			return $scope.topoForm === 'addNode';
-		}
-
-		$scope.getTableHeight = function (rows) {
-	        return {height: (rows.length * 30) + "px"};
-		}
-        $scope.isSelected = function () {
-            return ($scope.topoFormSelected != "");
-        }
 
         $scope.cancel = function () {
             $scope.addingNode.step = 0;
@@ -77,24 +89,6 @@ var QDR = (function (QDR) {
 			$scope.addingNode.trigger = 'editNode';
 		}
 
-        $scope.topoGridOptions = {
-            data: 'attributes',
-			enableColumnResize: true,
-			multiSelect: false,
-            columnDefs: [
-            {
-                field: 'attributeName',
-                displayName: 'Attribute',
-                cellTemplate: generalCellTemplate
-            },
-            {
-                field: 'attributeValue',
-                displayName: 'Value'
-            }
-            ]
-        };
-        $scope.topoConnOptions = angular.copy($scope.topoGridOptions);
-        $scope.topoConnOptions.data = 'connAttributes';
 		var NewRouterName = "__NEW__";
 	    // mouse event vars
 	    var selected_node = null,
@@ -122,16 +116,12 @@ var QDR = (function (QDR) {
 			if (name == "Add Router") {
 				name = 'Diagram';
 				if ($scope.addingNode.step > 0) {
-					$scope.topoForm = 'general'
-					$scope.topoFormSelected = '';
 					$scope.addingNode.step = 0;
 				} else {
 					// start adding node mode
 					$scope.addingNode.step = 1;
 				}
 			} else {
-				$scope.topoForm = 'general'
-				$scope.topoFormSelected = '';
 				$scope.addingNode.step = 0;
 			}
 
@@ -160,12 +150,11 @@ var QDR = (function (QDR) {
 					}
 					return true;
 				})
-				$scope.topoForm = 'general'
-				$scope.topoFormSelected = '';
+				updateForm(Object.keys(QDRService.topology.nodeInfo())[0], 'router', 0);
+
 			} else if (newValue > 0) {
 				// we are starting the add mode
-				$scope.topoForm = 'addNode';
-                $scope.topoFormSelected = 'addNode';
+				$scope.$broadcast('showAddForm')
 
 				resetMouseVars();
                 selected_node = null;
@@ -190,6 +179,29 @@ var QDR = (function (QDR) {
 			return mode.right;
 		}
 
+		// for ng-grid that shows details for multiple consoles/clients
+		$scope.multiData = [{name: ''}, {name: ''}, {name: ''}]
+        $scope.multiDetails = {
+            data: 'multiData',
+            columnDefs: [
+            {
+                field: 'host',
+                displayName: 'Host'
+            },
+            {
+                field: 'user',
+                displayName: 'User'
+            },
+			{
+				field: 'properties',
+				displayName: 'Properties'
+			},
+			{
+				field: 'isEncrypted',
+				displayName: 'Encrypted'
+			}
+            ]
+        };
 
 		// generate unique name for router and containerName
 		var genNewName = function () {
@@ -283,9 +295,9 @@ var QDR = (function (QDR) {
 	    var radius = 25;
 	    var radiusNormal = 15;
 	    width = tpdiv.width() - gap;
-	    height = $(document).height() - gap;
+	    height = $('#main').height() - $('#topology').position().top - gap;
 
-	    var svg;
+	    var svg, lsvg;
 		var force;
 		var animate = false; // should the force graph organize itself when it is displayed
 		var path, circle;
@@ -299,7 +311,8 @@ var QDR = (function (QDR) {
 		var nodes = [];
 		var links = [];
 
-		var aNode = function (id, name, nodeType, nodeInfo, nodeIndex, x, y, resultIndex, fixed) {
+		var aNode = function (id, name, nodeType, nodeInfo, nodeIndex, x, y, resultIndex, fixed, properties) {
+			properties = properties || {};
 			var containerName;
 			if (nodeInfo) {
 				var node = nodeInfo[id];
@@ -310,6 +323,7 @@ var QDR = (function (QDR) {
 			return {   key: id,
 				name: name,
 				nodeType: nodeType,
+				properties: properties,
 				containerName: containerName,
 				x: x,
 				y: y,
@@ -340,8 +354,6 @@ var QDR = (function (QDR) {
             }
         }
 
-		//var drag;
-		// create an bare svg element and
 		// initialize the nodes and links array from the QDRService.topology._nodeInfo object
 		var initForceGraph = function () {
             //QDR.log.debug("initForceGraph called");
@@ -370,6 +382,20 @@ var QDR = (function (QDR) {
                     removeCrosssection()
                 });
 
+                $(document).keyup(function(e) {
+                  if (e.keyCode === 27) {
+                    removeCrosssection()
+                  }
+                });
+
+			// the legend
+			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
 			selected_node = null;
 			selected_link = null;
@@ -388,11 +414,11 @@ var QDR = (function (QDR) {
 				if (!angular.isDefined(position)) {
 				    animate = true;
 				    position = {x: width / 4 + ((width / 2)/nodeCount) * nodes.length,
-                				y: height / 2 + yInit,
+                				y: 200 + yInit,
                 				fixed: false};
 				}
 				if (position.y > height)
-					position.y = height / 2 - yInit;
+					position.y = 200 - yInit;
 				nodes.push( aNode(id, name, "inter-router", nodeInfo, nodes.length, position.x, position.y, undefined, position.fixed) );
 				yInit *= -1;
 				//QDR.log.debug("adding node " + nodes.length-1);
@@ -405,9 +431,13 @@ var QDR = (function (QDR) {
 				var onode = nodeInfo[id];
 				var conns = onode['.connection'].results;
 				var attrs = onode['.connection'].attributeNames;
+				var parent = getNodeIndex(QDRService.nameFromId(id));
+				//QDR.log.debug("external client parent is " + parent);
+				var normalsParent = {console: undefined, client: undefined}; // 1st normal node for this parent
 
 				for (var j = 0; j < conns.length; j++) {
                     var role = QDRService.valFor(attrs, conns[j], "role");
+                    var properties = QDRService.valFor(attrs, conns[j], "properties") || {};
                     var dir = QDRService.valFor(attrs, conns[j], "dir");
 					if (role == "inter-router") {
 						var connId = QDRService.valFor(attrs, conns[j], "container");
@@ -419,8 +449,6 @@ var QDR = (function (QDR) {
 						//QDR.log.debug("found an external client for " + id);
 						var name = QDRService.nameFromId(id) + "." + client;
 						//QDR.log.debug("external client name is  " + name + " and the role is " + role);
-						var parent = getNodeIndex(QDRService.nameFromId(id));
-						//QDR.log.debug("external client parent is " + parent);
 
                         // if we have any new clients, animate the force graph to position them
                         var position = angular.fromJson(localStorage[name]);
@@ -432,26 +460,36 @@ var QDR = (function (QDR) {
                         }
 						if (position.y > height)
 							position.y = nodes[parent].y + 40 + Math.cos(Math.PI/2 * client)
-						//QDR.log.debug("adding node " + nodeIndex);
-						nodes.push(	aNode(id, name, role, nodeInfo, nodes.length, position.x, position.y, j, position.fixed) );
-						// now add a link
-						getLink(parent, nodes.length-1, dir);
-						client++;
+						var node = aNode(id, name, role, nodeInfo, nodes.length, position.x, position.y, j, position.fixed, properties)
+						var nodeType = role === 'normal' ? (properties.console_identifier == 'Dispatch console' ? 'console' : 'client') : 'broker';
+						if (role === 'normal') {
+							node.user = QDRService.valFor(attrs, conns[j], "user")
+							node.isEncrypted = QDRService.valFor(attrs, conns[j], "isEncrypted")
+							node.host = QDRService.valFor(attrs, conns[j], "host")
+
+							if (!normalsParent[nodeType]) {
+								normalsParent[nodeType] = node;
+								nodes.push(	node );
+								node.normals = [node];
+								// now add a link
+								getLink(parent, nodes.length-1, dir);
+								client++;
+							} else {
+
+								normalsParent[nodeType].normals.push(node)
+							}
+						} else {
+							nodes.push( node)
+							// now add a link
+							getLink(parent, nodes.length-1, dir);
+							client++;
+						}
 					}
 				}
 				source++;
 			}
 
             $scope.schema = QDRService.schema;
-			// add a row for each attribute in .router attributeNames array
-			for (var id in nodeInfo) {
-				var onode = nodeInfo[id];
-
-                initForm(onode['.connection'].attributeNames, onode['.connection'].results[0], QDRService.schema.entityTypes.connection, $scope.connAttributes);
-                initForm(onode['.router'].attributeNames, onode['.router'].results[0], QDRService.schema.entityTypes.router, $scope.attributes);
-                
-				break;
-			}
 			// init D3 force layout
 			force = d3.layout.force()
 				.nodes(nodes)
@@ -506,69 +544,54 @@ var QDR = (function (QDR) {
 			// app starts here
 			restart(false);
     	    force.start();
-		}
-/*
-		function dragstart(d) {
-		  d3.select(this).classed("fixed", d.fixed = true);
-		}
+			setTimeout(function () {
+	    	    updateForm(Object.keys(QDRService.topology.nodeInfo())[0], 'router', 0);
+			}, 10)
 
-		function dblclick(d) {
-		  d3.select(this).classed("fixed", d.fixed = false);
 		}
-*/
-        // called when we mouseover a node
-        // we need to update the table
-		function updateNodeForm (d) {
-			//QDR.log.debug("update form info for ");
-			//console.dump(d);
+
+		function updateForm (key, entity, resultIndex) {
 			var nodeInfo = QDRService.topology.nodeInfo();
-			var onode = nodeInfo[d.key];
+			var onode = nodeInfo[key]
 			if (onode) {
-				var nodeResults = onode['.router'].results[0];
-				var nodeAttributes = onode['.router'].attributeNames;
-
-                for (var i=0; i<$scope.attributes.length; ++i) {
-                    var idx = nodeAttributes.indexOf($scope.attributes[i].attributeName);
-                    if (idx > -1) {
-                        if ($scope.attributes[i].attributeValue != nodeResults[idx]) {
-                            // highlight the changed data
-                            $scope.attributes[i].attributeValue = nodeResults[idx];
-
-                        }
-                    }
-                }
-			}
-            $scope.topoForm = "general";
-            $scope.$apply();
-		}
+				var nodeResults = onode['.' + entity].results[resultIndex]
+				var nodeAttributes = onode['.' + entity].attributeNames
+				var 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) })
 
-		function updateConnForm (d, resultIndex) {
-			var nodeInfo = QDRService.topology.nodeInfo();
-			var onode = nodeInfo[d.key];
-			if (onode && onode['.connection']) {
-				var nodeResults = onode['.connection'].results[resultIndex];
-				var nodeAttributes = onode['.connection'].attributeNames;
-
-                for (var i=0; i<$scope.connAttributes.length; ++i) {
-                    var idx = nodeAttributes.indexOf($scope.connAttributes[i].attributeName);
-                    if (idx > -1) {
-                    	try {
-                        if ($scope.connAttributes[i].attributeValue != nodeResults[idx]) {
-                            // highlight the changed data
-                            $scope.connAttributes[i].attributeValue = nodeResults[idx];
+				// move the Name first
+				var 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') {
+					var listeners = onode['.listener'].results;
+					var listenerAttributes = onode['.listener'].attributeNames;
+					var normals = listeners.filter ( function (listener) {
+						return QDRService.valFor( listenerAttributes, listener, 'role') === 'normal';
+					})
+					var ports = []
+					normals.forEach (function (normalListener) {
+						ports.push(QDRService.valFor( listenerAttributes, normalListener, 'port'))
+					})
+					// add as 2nd row
+					if (ports.length)
+						attributes.splice(1, 0, {attributeName: 'Listening on', attributeValue: ports});
+				}
 
-                        }
-                        } catch (err) {
-							QDR.log.error("error updating form" + err)
-                        }
-                    }
-                }
+				$scope.$broadcast('showEntityForm', {entity: entity, attributes: attributes})
 			}
-            $scope.topoForm = "connections";
-            $scope.$apply();
+			$scope.$apply();
 		}
 
-
         function getContainerIndex(_id) {
             var nodeIndex = 0;
             var nodeInfo = QDRService.topology.nodeInfo();
@@ -738,6 +761,12 @@ var QDR = (function (QDR) {
                     d3.select("#crosssection svg").remove();
                     d3.select("#crosssection").style("display","none");
                 });
+            d3.select("#multiple_details").transition()
+                .duration(500)
+                .style("opacity", 0)
+                .each("end", function (d) {
+                    d3.select("#multiple_details").style("display", "none")
+                })
 		}
 
 	    // takes the nodes and links array of objects and adds svg elements for everything that hasn't already
@@ -793,7 +822,7 @@ var QDR = (function (QDR) {
                             var conn = onode['.connection'].results[resultIndex];
                             /// find the connection whose container is the right's name
                             var name = QDRService.valFor(onode['.connection'].attributeNames, conn, "container");
-                            if (name == right.name) {
+                            if (name == right.containerName) {
                                 break;
                             }
                         }
@@ -803,7 +832,7 @@ var QDR = (function (QDR) {
                             left = d.target;
                             resultIndex = left.resultIndex;
                         }
-                        updateConnForm(left, resultIndex);
+                        updateForm(left.key, 'connection', resultIndex);
                     }
 
 					// select link
@@ -944,33 +973,30 @@ var QDR = (function (QDR) {
 	        circle.selectAll('circle')
 	            .classed('selected', function (d) { return (d === selected_node) })
 	            .classed('fixed', function (d) { return (d.fixed & 0b1) })
+  			    //.classed('multiple', function(d) { return (d.normals && d.normals.length > 1)  } )
 
 			// add new circle nodes. if nodes[] is longer than the existing paths, add a new path for each new element
-	        var g = circle.enter().append('svg:g');
-
-			// add new circles and set their attr/class/behavior
-	        g.append('svg:circle')
-	            .attr('class', 'node')
-	            .attr('r', function (d) {
-	            	return radii[d.nodeType];
-	            })
-	            .classed('fixed', function (d) {return d.fixed})
-  			    .classed('temp', function(d) { return QDRService.nameFromId(d.key) == '__internal__'; } )
-  			    .classed('normal', function(d) { return d.nodeType == 'normal' } )
-  			    .classed('inter-router', function(d) { return d.nodeType == 'inter-router' } )
-  			    .classed('on-demand', function(d) { return d.nodeType == 'on-demand' } )
-
-/*
-	            .style('fill', function (d) {
-	                var sColor = colors[d.nodeType];
-	                return (d === selected_node) ? d3.rgb(sColor).brighter().toString() : d3.rgb(sColor);
-	            })
-	            .style('stroke', function (d) {
-	                var sColor = colors[d.nodeType];
-	                return d3.rgb(sColor).darker().toString();
-	            })
-*/
-	            .on('mouseover', function (d) {
+	        var 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];
+		            })
+		            .classed('fixed', function (d) {return d.fixed})
+	                .classed('temp', function(d) { return QDRService.nameFromId(d.key) == '__internal__'; } )
+	                .classed('normal', function(d) { return d.nodeType == 'normal' } )
+	                .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 d.properties.console_identifier == 'Dispatch console' } )
+	                .classed('artemis', function(d) { return QDRService.isArtemis(d) } )
+	                .classed('qpid-cpp', function(d) { return QDRService.isQpid(d) } )
+	                .classed('client', function(d) { return d.nodeType === 'normal' && !d.properties.console_identifier } )
+			}
+			appendCircle(g).on('mouseover', function (d) {
 	                if ($scope.addingNode.step > 0) {
 		                d3.select(this).attr('transform', 'scale(1.1)');
 						return;
@@ -978,10 +1004,10 @@ var QDR = (function (QDR) {
 					if (!selected_node) {
                         if (d.nodeType === 'inter-router') {
                             //QDR.log.debug("showing general form");
-                            updateNodeForm(d);
+                            updateForm(d.key, 'router', 0);
                         } else if (d.nodeType === 'normal' || d.nodeType === 'on-demand') {
                             //QDR.log.debug("showing connections form");
-                            updateConnForm(d, d.resultIndex);
+                            updateForm(d.key, 'connection', d.resultIndex);
                         }
 					}
 
@@ -1057,19 +1083,11 @@ var QDR = (function (QDR) {
 					// if this node was selected, unselect it
                     if (mousedown_node === selected_node) {
                         selected_node = null;
-                        $scope.topoFormSelected = "";
                     }
                     else {
-                        selected_node = mousedown_node;
-                        if (d.nodeType === 'inter-router') {
-                            //QDR.log.debug("showing general form");
-                            updateNodeForm(d);
-                            $scope.topoFormSelected = "general";
-                        } else if (d.nodeType === 'normal' || d.nodeType === 'on-demand') {
-                            //QDR.log.debug("showing connections form");
-                            updateConnForm(d, d.resultIndex);
-                            $scope.topoFormSelected = "connections";
-                        }
+						// don't select nodes that represent multiple clients/consoles
+                        if (!d.normals || d.normals.length < 2)
+                            selected_node = mousedown_node;
                     }
                     for (var i=0; i<links.length; ++i) {
                         links[i]['highlighted'] = false;
@@ -1099,21 +1117,136 @@ var QDR = (function (QDR) {
                       .style('top', (mouseY + $(document).scrollTop()) + "px")
                       .style('display', 'block');
 
-                });
+                })
+                .on("click", function (d) {
+					if (!d.normals || d.normals.length < 2) {
+			            if ( QDRService.isArtemis(d) && Core.ConnectionName === 'Artemis' ) {
+							$location.path('/jmx/attributes?tab=artemis&con=Artemis')
+						}
+						return;
+					}
+                    clickPos = d3.mouse(this);
+                    d3.event.stopPropagation();
+                    $scope.multiData = []
+                    d.normals.forEach( function (n) {
+                        $scope.multiData.push(n)
+                    })
+                    $scope.$apply();
+                    d3.select('#multiple_details')
+                        .style({
+                            display: 'block',
+                            opacity: 1,
+                            height: (d.normals.length + 1) * 30 + "px",
+                            'overflow-y': d.normals.length > 10 ? 'scroll' : 'hidden',
+		                    left: (mouseX + $(document).scrollLeft()) + "px",
+                            top:  (mouseY + $(document).scrollTop()) + "px"})
+				})
 
-	        // show node IDs
-	        g.append('svg:text')
-	            .attr('x', 0)
-	            .attr('y', 4)
-	            .attr('class', 'id')
-	            .text(function (d) {
-	                return (d.nodeType === 'normal' || d.nodeType == 'on-demand') ? d.name.slice(-1) :
-	                    d.name.length>7 ? d.name.substr(0,6)+'...' : d.name;
-	        });
+			var appendContent = function (g) {
+		        // show node IDs
+		        g.append('svg:text')
+		            .attr('x', 0)
+		            .attr('y', function (d) {
+		                var y = 6;
+		                if (QDRService.isArtemis(d))
+		                    y = 8;
+		                else if (QDRService.isQpid(d))
+		                    y = 9;
+		                else if (d.nodeType === 'inter-router')
+		                    y = 4;
+		                return y;})
+		            .attr('class', 'id')
+	                .classed('console', function(d) { return d.properties.console_identifier == 'Dispatch console' } )
+	                .classed('normal', function(d) { return d.nodeType === 'normal' } )
+	                .classed('on-demand', function(d) { return d.nodeType === 'on-demand' } )
+	                .classed('artemis', function(d) { return QDRService.isArtemis(d) } )
+	                .classed('qpid-cpp', function(d) { return QDRService.isQpid(d) } )
+		            .text(function (d) {
+		                if (d.properties.console_identifier == 'Dispatch console') {
+		                    return '\uf108'; // icon-desktop for this console
+		                }
+						if (QDRService.isArtemis(d)) {
+							return '\ue900'
+						}
+		                if (QDRService.isQpid(d)) {
+		                    return '\ue901';
+		                }
+						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) {
+	                var x = '';
+	                if (d.normals && d.normals.length > 1)
+	                    x = " x " + d.normals.length;
+		            if (d.properties.console_identifier == 'Dispatch console') {
+	                    return 'Dispatch console' + x
+	                }
+		            if (d.properties.product == 'qpid-cpp') {
+	                    return 'Broker - qpid-cpp' + x
+	                }
+		            if ( QDRService.isArtemis(d) ) {
+	                    return 'Broker - Artemis' + 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();
+
+			svg.selectAll('.multiple')
+				.insert('svg:circle', '.normal')
+					.attr('class', 'subcircle')
+					.attr('r', 18)
+
+
+			// dynamically create the legend based on which node types are present
+			var legendNodes = [];
+			legendNodes.push(aNode("Router", "", "inter-router", undefined, 0, 0, 0, 0, false, {}))
+
+			if (!svg.selectAll('circle.console').empty()) {
+				legendNodes.push(aNode("Dispatch console", "", "normal", undefined, 1, 0, 0, 0, false, {console_identifier: 'Dispatch console'}))
+			}
+			if (!svg.selectAll('circle.client').empty()) {
+				legendNodes.push(aNode("Client", "", "normal", undefined, 2, 0, 0, 0, false, {}))
+			}
+			if (!svg.selectAll('circle.qpid-cpp').empty()) {
+				legendNodes.push(aNode("Qpid cpp broker", "", "on-demand", undefined, 3, 0, 0, 0, false, {product: 'qpid-cpp'}))
+			}
+			if (!svg.selectAll('circle.artemis').empty()) {
+				legendNodes.push(aNode("Artemis broker", "", "on-demand", undefined, 4, 0, 0, 0, false, {}))
+			}
+		    lsvg = lsvg.data(legendNodes, function (d) {
+	            return d.id;
+            });
+	        var 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();
+			var svgEl = document.getElementById("svglegend"),
+				bb = svgEl.getBBox();
+			svgEl.style.height = bb.y + bb.height;
+			svgEl.style.width = bb.x + bb.width;
+
 	        if (!mousedown_node || !selected_node)
 	            return;
 
@@ -1161,6 +1294,7 @@ var QDR = (function (QDR) {
                 saveChanged();
                 // TODO: update graph nodes instead of rebuilding entire graph
                 d3.select("#SVG_ID").remove();
+                d3.select("#svg_legend svg").remove();
                 animate = true;
                 initForceGraph();
                 //if ($location.path().startsWith("/topology"))


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