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/02/22 22:09:53 UTC

[2/5] qpid-dispatch git commit: DISPATCH-221 - Check in hawtio plugin code

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/eb2e027a/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
new file mode 100644
index 0000000..ff5b696
--- /dev/null
+++ b/console/hawtio/src/main/webapp/plugin/js/qdrTopology.js
@@ -0,0 +1,1651 @@
+/*
+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.
+*/
+/**
+ * @module QDR
+ */
+var QDR = (function (QDR) {
+
+  /**
+   * @method TopologyController
+   *
+   * Controller that handles the QDR topology page
+   */
+    QDR.module.controller("QDR.TopologyController", ['$scope', '$rootScope', 'QDRService', '$location', '$timeout', '$dialog',
+    function($scope, $rootScope, QDRService, $location, $timeout, $dialog) {
+		if (!QDRService.connected) {
+			// we are not connected. we probably got here from a bookmark or manual page reload
+			$location.path("/dispatch_plugin/connect")
+			$location.search('org', "topology");
+			return;
+		}
+
+		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;
+        }
+		$scope.editNewRouter = function () {
+			$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,
+	        selected_link = null,
+	        mousedown_link = null,
+	        mousedown_node = null,
+	        mouseup_node = null,
+	        initial_mouse_down_position = null;
+
+        $scope.schema = "Not connected";
+
+	    $scope.modes = [
+	    	{title: 'Topology view', name: 'Diagram', right: false},
+	    	/* {title: 'Add a new router node', name: 'Add Router', right: true} */
+	    	];
+		$scope.mode = "Diagram";
+		$scope.contextNode = null; // node that is associated with the current context menu
+
+		$scope.isModeActive = function (name) {
+			if ((name == 'Add Router' || name == 'Diagram') && $scope.addingNode.step > 0)
+				return true;
+			return ($scope.mode == name);
+		}
+		$scope.selectMode = function (name) {
+			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;
+			}
+
+			$scope.mode = name;
+		}
+		$scope.$watch(function () {return $scope.addingNode.step}, function (newValue, oldValue) {
+			if (newValue == 0 && oldValue != 0) {
+				// we are cancelling the add
+
+				// find the New node
+				nodes.every(function (n, i) {
+					// for the placeholder node, the key will be __internal__
+					if (QDRService.nameFromId(n.key) == '__internal__') {
+						var newLinks = links.filter(function (e, i) {
+							return e.source.id == n.id || e.target.id == n.id;
+						})
+						// newLinks is an array of links to remove
+						newLinks.map(function (e) {
+							links.splice(links.indexOf(e), 1);
+						})
+						// i is the index of the node to remove
+						nodes.splice(i, 1);
+						force.nodes(nodes).links(links).start();
+		                restart(false);
+						return false; // stop looping
+					}
+					return true;
+				})
+				$scope.topoForm = 'general'
+				$scope.topoFormSelected = '';
+			} else if (newValue > 0) {
+				// we are starting the add mode
+				$scope.topoForm = 'addNode';
+                $scope.topoFormSelected = 'addNode';
+
+				resetMouseVars();
+                selected_node = null;
+                selected_link = null;
+                // add a new node
+                var id = "amqp:/_topo/0/__internal__/$management";
+                var x = radiusNormal * 4;
+                var y = x;;
+                if (newValue > 1) {   // add at current mouse position
+                    var offset = jQuery('#topology').offset();
+                    x = mouseX - offset.left + $(document).scrollLeft();
+                    y = mouseY - offset.top + $(document).scrollTop();;
+                }
+                NewRouterName = genNewName();
+                nodes.push( aNode(id, NewRouterName, "inter-router", undefined, nodes.length, x, y, undefined, true) );
+                force.nodes(nodes).links(links).start();
+                restart(false);
+			}
+
+		})
+		$scope.isRight = function (mode) {
+			return mode.right;
+		}
+
+
+		// generate unique name for router and containerName
+		var genNewName = function () {
+			var nodeInfo = QDRService.topology.nodeInfo();
+			var nameIndex = 1;
+			var newName = "R." + nameIndex;
+
+			var names = [];
+			for (key in nodeInfo) {
+				var node = nodeInfo[key];
+				var router = node['.router'];
+				var attrNames = router.attributeNames;
+				var name = QDRService.valFor(attrNames, router.results[0], 'routerId')
+				if (!name)
+					name = QDRService.valFor(attrNames, router.results[0], 'name')
+				names.push(name);
+			}
+
+			while (names.indexOf(newName) >= 0) {
+				newName = "R." + nameIndex++;
+			}
+			return newName;
+		}
+
+		$scope.$watch(function () {return $scope.addingNode.trigger}, function (newValue, oldValue) {
+			if (newValue == 'editNode') {
+				$scope.addingNode.trigger = "";
+				editNode();
+			}
+		})
+
+	    function editNode() {
+	        doAddDialog(NewRouterName);
+	    };
+		$scope.reverseLink = function () {
+			if (!mousedown_link)
+				return;
+			var d = mousedown_link;
+			var tmp = d.left;
+			d.left = d.right;;
+			d.right = tmp;
+		    restart(false);
+		    tick();
+		}
+		$scope.removeLink = function () {
+			if (!mousedown_link)
+				return;
+			var d = mousedown_link;
+			 links.every( function (l, i) {
+				if (l.source.id == d.source.id && l.target.id == d.target.id) {
+			        links.splice(i, 1);
+					force.links(links).start();
+					return false; // exit the 'every' loop
+				}
+				return true;
+			});
+		    restart(false);
+		    tick();
+		}
+		$scope.setFixed = function (b) {
+			if ($scope.contextNode) {
+				$scope.contextNode.fixed = b;
+			}
+			restart();
+		}
+		$scope.isFixed = function () {
+			if (!$scope.contextNode)
+				return false;
+			return ($scope.contextNode.fixed & 0b1);
+		}
+
+		var mouseX, mouseY;
+		// event handlers for popup context menu
+		$(document).mousemove(function (e) {
+		    mouseX = e.clientX;
+		    mouseY = e.clientY;
+		});
+		$(document).mousemove();
+		$(document).click(function (e) {
+			$scope.contextNode = null;
+            $(".contextMenu").fadeOut(200);
+        });
+
+
+		// set up SVG for D3
+	    var width, height;
+	    var tpdiv = $('#topology');
+	    var colors = {'inter-router': "#EAEAEA", 'normal': "#F0F000", 'on-demand': '#00F000'};
+	    var gap = 5;
+	    var radii = {'inter-router': 25, 'normal': 15, 'on-demand': 15};
+	    var radius = 25;
+	    var radiusNormal = 15;
+	    width = tpdiv.width() - gap;
+	    height = $(document).height() - gap;
+
+	    var svg;
+		var force;
+		var animate = false; // should the force graph organize itself when it is displayed
+		var path, circle;
+		var savedKeys = {};
+		var dblckickPos = [0,0];
+
+	    // 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'.
+		var nodes = [];
+		var links = [];
+
+		var aNode = function (id, name, nodeType, nodeInfo, nodeIndex, x, y, resultIndex, fixed) {
+			var containerName;
+			if (nodeInfo) {
+				var node = nodeInfo[id];
+				if (node) {
+					containerName = node['.container'].results[0][0];
+				}
+			}
+			return {   key: id,
+				name: name,
+				nodeType: nodeType,
+				containerName: containerName,
+				x: x,
+				y: y,
+				id: nodeIndex,
+				resultIndex: resultIndex,
+				fixed: fixed,
+				cls: name == NewRouterName ? 'temp' : ''
+			};
+		};
+
+
+        var initForm = function (attributes, results, entityType, formFields) {
+        
+            while(formFields.length > 0) {
+                // remove all existing attributes
+                    formFields.pop();
+            }
+
+            for (var i=0; i<attributes.length; ++i) {
+                var name = attributes[i];
+                var val = results[i];
+                var desc = "";
+                if (entityType.attributes[name])
+                    if (entityType.attributes[name].description)
+                        desc = entityType.attributes[name].description;
+
+                formFields.push({'attributeName': name, 'attributeValue': val, 'description': desc});
+            }
+        }
+
+		//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");
+			nodes = [];
+			links = [];
+
+			svg = d3.select('#topology')
+				.append('svg')
+				.attr("id", "SVG_ID")
+				.attr('width', width)
+				.attr('height', height)
+	            .on("contextmenu", function(d) {
+	                if (d3.event.defaultPrevented)
+	                    return;
+                    d3.event.preventDefault();
+					if ($scope.addingNode.step != 0)
+						return;
+					if (d3.select('#svg_context_menu').style('display') !== 'block')
+	                    $(document).click();
+                    d3.select('#svg_context_menu')
+                      .style('left', (mouseX + $(document).scrollLeft()) + "px")
+                      .style('top', (mouseY + $(document).scrollTop()) + "px")
+                      .style('display', 'block');
+                })
+                .on('click', function (d) {
+                    removeCrosssection()
+                });
+
+			// mouse event vars
+			selected_node = null;
+			selected_link = null;
+			mousedown_link = null;
+			mousedown_node = null;
+			mouseup_node = null;
+
+			// initialize the list of nodes
+			var yInit = 10;
+			var nodeInfo = QDRService.topology.nodeInfo();
+			var nodeCount = Object.keys(nodeInfo).length;
+			for (var id in nodeInfo) {
+				var name = QDRService.nameFromId(id);
+                // if we have any new nodes, animate the force graph to position them
+				var position = angular.fromJson(localStorage[name]);
+				if (!angular.isDefined(position)) {
+				    animate = true;
+				    position = {x: width / 4 + ((width / 2)/nodeCount) * nodes.length,
+                				y: height / 2 + yInit,
+                				fixed: false};
+				}
+				if (position.y > height)
+					position.y = height / 2 - 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);
+			}
+
+			// initialize the list of links
+			var source = 0;
+			var client = 1;
+			for (var id in nodeInfo) {
+				var onode = nodeInfo[id];
+				var conns = onode['.connection'].results;
+				var attrs = onode['.connection'].attributeNames;
+
+				for (var j = 0; j < conns.length; j++) {
+                    var role = QDRService.valFor(attrs, conns[j], "role");
+                    var dir = QDRService.valFor(attrs, conns[j], "dir");
+					if (role == "inter-router") {
+						var connId = QDRService.valFor(attrs, conns[j], "container");
+						var target = getContainerIndex(connId);
+						if (target >= 0)
+							getLink(source, target, dir);
+					} else if (role == "normal" || role == "on-demand") {
+						// not a router, but an external client
+						//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]);
+                        if (!angular.isDefined(position)) {
+                            animate = true;
+                            position = {x: nodes[parent].x + 40 + Math.sin(Math.PI/2 * client),
+                                        y: nodes[parent].y + 40 + Math.cos(Math.PI/2 * client),
+                                        fixed: false};
+                        }
+						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++;
+					}
+				}
+				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)
+				.links(links)
+				.size([width, height])
+				.linkDistance(function(d) { return d.target.nodeType === 'inter-router' ? 150 : 65 })
+				.charge(-1800)
+				.friction(.10)
+				.gravity(0.0001)
+				.on('tick', tick)
+				.start()
+
+			//drag = force.drag()
+            //    .on("dragstart", dragstart);
+
+			svg.append("svg:defs").selectAll('marker')
+				.data(["end-arrow", "end-arrow-selected"])      // 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", 25)
+				.attr("markerWidth", 4)
+				.attr("markerHeight", 4)
+				.attr("orient", "auto")
+				.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"])      // 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');
+
+			// handles to link and node element groups
+			path = svg.append('svg:g').selectAll('path'),
+			circle = svg.append('svg:g').selectAll('g');
+            
+			force.on('end', function() {
+				//QDR.log.debug("force end called");
+				circle
+					.attr('cx', function(d) {
+						localStorage[d.name] = angular.toJson({x: d.x, y: d.y, fixed: d.fixed});
+						return d.x; });
+			});
+
+			// app starts here
+			restart(false);
+    	    force.start();
+		}
+/*
+		function dragstart(d) {
+		  d3.select(this).classed("fixed", d.fixed = true);
+		}
+
+		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);
+			var nodeInfo = QDRService.topology.nodeInfo();
+			var onode = nodeInfo[d.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();
+		}
+
+		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];
+
+                        }
+                        } catch (err) {
+							QDR.log.error("error updating form" + err)
+                        }
+                    }
+                }
+			}
+            $scope.topoForm = "connections";
+            $scope.$apply();
+		}
+
+
+        function getContainerIndex(_id) {
+            var nodeIndex = 0;
+            var nodeInfo = QDRService.topology.nodeInfo();
+            for (var id in nodeInfo) {
+                var node = nodeInfo[id];
+                if (node['.container'].results[0][0] == _id)
+                    return nodeIndex;
+                nodeIndex++
+            }
+            QDR.log.warn("unable to find containerIndex for " + _id);
+            return -1;
+        }
+
+        function getNodeIndex (_id) {
+            var nodeIndex = 0;
+            var nodeInfo = QDRService.topology.nodeInfo();
+            for (var id in nodeInfo) {
+                if (QDRService.nameFromId(id) == _id) return nodeIndex;
+                nodeIndex++
+            }
+            QDR.log.warn("unable to find nodeIndex for " + _id);
+            return -1;
+        }
+
+        function getLink (_source, _target, dir, cls) {
+            for (var i=0; i < links.length; i++) {
+                var 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);
+            var link = {
+                source: _source,
+                target: _target,
+                left: dir != "out",
+                right: dir == "out",
+                cls: cls
+            };
+            return links.push(link) - 1;
+        }
+
+
+	    function resetMouseVars() {
+	        mousedown_node = null;
+	        mouseup_node = null;
+	        mousedown_link = null;
+	    }
+
+	    // update force layout (called automatically each iteration)
+	    function tick() {
+	        // draw directed edges with proper padding from node centers
+	        path.attr('d', function (d) {
+				//QDR.log.debug("in tick for d");
+				//console.dump(d);
+
+	            var deltaX = d.target.x - d.source.x,
+	                deltaY = d.target.y - d.source.y,
+	                dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY),
+	                normX = deltaX / dist,
+	                normY = deltaY / dist;
+	                var sourcePadding, targetPadding;
+	                if (d.target.nodeType == "inter-router") {
+						//                       right arrow  left line start
+						sourcePadding = d.left ? radius + 8  : radius;
+						//                      left arrow      right line start
+						targetPadding = d.right ? radius + 16 : radius;
+	                } else {
+						sourcePadding = d.left ? radiusNormal + 18  : radiusNormal;
+						targetPadding = d.right ? radiusNormal + 16 : radiusNormal;
+	                }
+	                var sourceX = d.source.x + (sourcePadding * normX),
+	                sourceY = d.source.y + (sourcePadding * normY),
+	                targetX = d.target.x - (targetPadding * normX),
+	                targetY = d.target.y - (targetPadding * normY);
+	            return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY;
+	        });
+
+	        circle.attr('transform', function (d) {
+	            d.x = Math.max(d.x, radiusNormal * 2);
+	            d.y = Math.max(d.y, radiusNormal * 2);
+	            return 'translate(' + d.x + ',' + d.y + ')';
+	        });
+	        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);
+            var sInfo = QDRService.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;
+            var aAr = sInfo['.router.node'].attributeNames;
+            var vAr = sInfo['.router.node'].results;
+            for (var hIdx=0; hIdx<vAr.length; ++hIdx) {
+                var addrT = QDRService.valFor(aAr, vAr[hIdx], "routerId" );
+                if (addrT == d.name) {
+                    //QDR.log.debug("found " + d.name + " at " + hIdx);
+                    var nextHop = QDRService.valFor(aAr, vAr[hIdx], "nextHop");
+                    //QDR.log.debug("nextHop was " + nextHop);
+                    return (nextHop == null) ? nodeFor(addrT) : nodeFor(nextHop);
+                }
+            }
+            return null;
+        }
+
+        function nodeFor(name) {
+            for (var i=0; i<nodes.length; ++i) {
+                if (nodes[i].name == name)
+                    return nodes[i];
+            }
+            return null;
+        }
+
+        function linkFor(source, target) {
+            for (var 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 removeCrosssection() {
+			setTimeout(function () {
+				d3.select("[id^=tooltipsy]").remove()
+				$('.hastip').empty();
+			}, 1010);
+			d3.select("#crosssection svg g").transition()
+                .duration(1000)
+				.attr("transform", "translate("+(dblckickPos[0]-140) + "," + (dblckickPos[1]-100) + ") scale(0)")
+                .style("opacity", 0)
+                .each("end", function (d) {
+                    d3.select("#crosssection svg").remove();
+                    d3.select("#crosssection").style("display","none");
+                });
+		}
+
+	    // 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);
+	        //svg.classed('ctrl', true);
+
+	        // path (link) group
+	        path = path.data(links);
+
+			// update existing links
+  			path.classed('selected', function(d) { return d === selected_link; })
+  			    .classed('highlighted', function(d) { return d.highlighted; } )
+  			    .classed('temp', function(d) { return d.cls == 'temp'; } )
+                .attr('marker-start', function(d) {
+                    var sel = d===selected_link ? '-selected' : '';
+                    return d.left ? 'url('+urlPrefix+'#start-arrow' + sel + ')' : ''; })
+                .attr('marker-end', function(d) {
+                    var sel = d===selected_link ? '-selected' : '';
+                    return d.right ? 'url('+urlPrefix+'#end-arrow' + sel +')' : ''; })
+
+
+			// add new links. if links[] is longer than the existing paths, add a new path for each new element
+			path.enter().append('svg:path')
+				.attr('class', 'link')
+                .attr('marker-start', function(d) {
+                        var sel = d===selected_link ? '-selected' : '';
+						return d.left ? 'url('+urlPrefix+'#start-arrow' + sel + ')' : ''; })
+                .attr('marker-end', function(d) {
+					var sel = d===selected_link ? '-selected' : '';
+                    return d.right ? 'url('+urlPrefix+'#end-arrow' + sel + ')' : ''; })
+  			    .classed('temp', function(d) { return d.cls == 'temp'; } )
+	            .on('mouseover', function (d) {
+				  if($scope.addingNode.step > 0) {
+				    if (d.cls == 'temp') {
+				        d3.select(this).classed('over', true);
+				    }
+				    return;
+				  }
+				        //QDR.log.debug("showing connections form");
+					var resultIndex = 0; // the connection to use
+                    var left = d.left ? d.target : d.source;
+					// right is the node that the arrow points to, left is the other node
+					var right = d.left ? d.source : d.target;
+					var onode = QDRService.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) {
+                            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) {
+                                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;
+                        }
+                        updateConnForm(left, resultIndex);
+                    }
+
+					// select link
+					mousedown_link = d;
+					selected_link = mousedown_link;
+					//selected_node = null;
+					//mousedown_node = null;
+					restart();
+				})
+	            .on('mouseout', function (d) {
+				  if($scope.addingNode.step > 0) {
+				    if (d.cls == 'temp') {
+				        d3.select(this).classed('over', false);
+				    }
+				    return;
+				  }
+				        //QDR.log.debug("showing connections form");
+					// select link
+					selected_link = null;
+					//selected_node = null;
+					//mousedown_node = null;
+					restart();
+				})
+	            .on("contextmenu", function(d) {
+	                $(document).click();
+                    d3.event.preventDefault();
+	                if (d.cls !== "temp")
+	                    return;
+
+					mousedown_link = d;
+                    d3.select('#link_context_menu')
+                      .style('left', (mouseX + $(document).scrollLeft()) + "px")
+                      .style('top', (mouseY + $(document).scrollTop()) + "px")
+                      .style('display', 'block');
+                })
+                .on("click", function (d) {
+                    dblckickPos = d3.mouse(this);
+                    d3.event.stopPropagation();
+
+                    var diameter = 400;
+                    var format = d3.format(",d");
+                    var pack = d3.layout.pack()
+                        .size([diameter - 4, diameter - 4])
+                        .padding(-10)
+                        .value(function(d) { return d.size; });
+
+                    var svg = d3.select("#crosssection").append("svg")
+                        .attr("width", diameter)
+                        .attr("height", diameter)
+                    var svgg = svg.append("g")
+                        .attr("transform", "translate(2,2)");
+
+					svg.on('click', function (d) {
+						removeCrosssection();
+					})
+
+					var root = {
+						name: "links between " + d.source.name + " and " + d.target.name,
+						children: []
+					}
+					var nodeInfo = QDRService.topology.nodeInfo();
+					var connections = nodeInfo[d.source.key]['.connection'];
+					var containerIndex = connections.attributeNames.indexOf('container');
+					connections.results.some ( function (connection) {
+                        if (connection[containerIndex] == d.target.containerName) {
+                            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
+					var links = nodeInfo[d.source.key]['.router.link'];
+					containerIndex = links.attributeNames.indexOf('remoteContainer');
+					var nameIndex = links.attributeNames.indexOf('name');
+					var linkDirIndex = links.attributeNames.indexOf('linkDir');
+					links.results.forEach ( function (link) {
+						if (link[containerIndex] == d.target.containerName)
+							root.children.push (
+								{ name: "(" + link[linkDirIndex] + ") " + link[nameIndex],
+								size: 100,
+								obj: link,
+	                            desc: "Link",
+								attributeNames: links.attributeNames
+							})
+					})
+					if (root.children.length == 0)
+						return;
+	                var 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)" : ""); })
+	                      .attr("title", function (d) {
+	                          var 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>"
+	                          return title
+	                      })
+
+	                node.append("circle")
+	                      .attr("r", function(d) { return d.r; });
+
+//	                node.filter(function(d) { return !d.children; }).append("text")
+	                node.append("text")
+	                      .attr("dy", function (d) { return d.children ? "-10em" : ".3em"})
+	                      .style("text-anchor", "middle")
+	                      .text(function(d) {
+	                          return d.name.substring(0, d.r / 3);
+	                      });
+
+					$('.hastip').tooltipsy({ alignTo: 'cursor'});
+					svgg.attr("transform", "translate("+(dblckickPos[0]-140) + "," + (dblckickPos[1]-100) + ") scale(0.01)")
+	                d3.select("#crosssection").style("display","block");
+
+					svgg.transition().attr("transform", "translate(2,2) scale(1)")
+                })
+
+
+	        // remove old links
+	        path.exit().remove();
+
+
+	        // circle (node) group
+	        // nodes are known by id
+	        circle = circle.data(nodes, function (d) {
+	            return d.id;
+	        });
+
+	        // update existing nodes visual states
+	        circle.selectAll('circle')
+	            .classed('selected', function (d) { return (d === selected_node) })
+	            .classed('fixed', function (d) { return (d.fixed & 0b1) })
+
+			// 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) {
+	                if ($scope.addingNode.step > 0) {
+		                d3.select(this).attr('transform', 'scale(1.1)');
+						return;
+	                }
+					if (!selected_node) {
+                        if (d.nodeType === 'inter-router') {
+                            //QDR.log.debug("showing general form");
+                            updateNodeForm(d);
+                        } else if (d.nodeType === 'normal' || d.nodeType === 'on-demand') {
+                            //QDR.log.debug("showing connections form");
+                            updateConnForm(d, 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;
+	                }
+                    setTimeout(nextHop, 1, selected_node, d);
+	            })
+	            .on('mouseout', function (d) {
+	                // unenlarge target node
+	                d3.select(this).attr('transform', '');
+                    for (var i=0; i<links.length; ++i) {
+                        links[i]['highlighted'] = false;
+                    }
+                    restart();
+	            })
+	            .on('mousedown', function (d) {
+	                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.parentElement.parentElement.parentElement).slice();
+	            })
+	            .on('mouseup', function (d) {
+	                if (!mousedown_node)
+	                    return;
+
+                    selected_link = null;
+	                // unenlarge target node
+	                d3.select(this).attr('transform', '');
+
+	                // check for drag
+	                mouseup_node = d;
+	                var mySvg = this.parentElement.parentElement.parentElement;
+                    // if we dragged the node, make it fixed
+                    var cur_mouse = d3.mouse(mySvg);
+                    if (cur_mouse[0] != initial_mouse_down_position[0] ||
+                        cur_mouse[1] != initial_mouse_down_position[1]) {
+						console.log("mouse pos changed. making this node fixed")
+						d3.select(this).classed("fixed", d.fixed = true);
+                        resetMouseVars();
+                        return;
+	                }
+
+					// we didn't drag, we just clicked on the node
+	                if ($scope.addingNode.step > 0) {
+                        if (d.nodeType !== 'inter-router')
+                            return;
+						if (QDRService.nameFromId(d.key) == '__internal__')
+							return;
+
+						// add a link from the clicked node to the new node
+						getLink(d.id, nodes.length-1, "in", "temp");
+						$scope.addingNode.hasLink = true;
+						$scope.$apply();
+						// add new elements to the svg
+						force.links(links).start();
+						restart();
+						return;
+
+	                }
+
+					// 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";
+                        }
+                    }
+                    for (var i=0; i<links.length; ++i) {
+                        links[i]['highlighted'] = false;
+                    }
+	                mousedown_node = null;
+                    $scope.$apply();
+                    restart(false);
+
+	            })
+	            .on("dblclick", function (d) {
+	                if (d.fixed) {
+						d3.select(this).classed("fixed", d.fixed = false);
+						force.start();  // let the nodes move to a new position
+	                }
+	                if (QDRService.nameFromId(d.key) == '__internal__') {
+	                    editNode();
+	                    $scope.$apply();
+	                }
+	            })
+	            .on("contextmenu", function(d) {
+	                $(document).click();
+                    d3.event.preventDefault();
+	                $scope.contextNode = d;
+	                $scope.$apply();    // we just changed a scope valiable during an async event
+                    d3.select('#node_context_menu')
+                      .style('left', (mouseX + $(document).scrollLeft()) + "px")
+                      .style('top', (mouseY + $(document).scrollTop()) + "px")
+                      .style('display', 'block');
+
+                });
+
+	        // 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;
+	        });
+
+	        // remove old nodes
+	        circle.exit().remove();
+
+	        if (!mousedown_node || !selected_node)
+	            return;
+
+            if (!start)
+                return;
+	        // set the graph in motion
+	        //QDR.log.debug("mousedown_node is " + mousedown_node);
+	        force.start();
+
+	    }
+
+        function nextHop(thisNode, d) {
+            if ((thisNode) && (thisNode != d)) {
+                var target = findNextHopNode(thisNode, d);
+                //QDR.log.debug("highlight link from node ");
+                 //console.dump(nodeFor(selected_node.name));
+                 //console.dump(target);
+                if (target) {
+                    var hlLink = linkFor(nodeFor(thisNode.name), target);
+                    //QDR.log.debug("need to highlight");
+                    //console.dump(hlLink);
+                    if (hlLink)
+                        hlLink['highlighted'] = true;
+                    else
+                        target = null;
+                }
+                setTimeout(nextHop, 1, target, d);
+            }
+            restart();
+        }
+
+
+	    function mousedown() {
+	        // prevent I-bar on drag
+	        //d3.event.preventDefault();
+
+	        // because :active only works in WebKit?
+	        svg.classed('active', true);
+	    }
+
+        QDRService.addUpdatedAction("topology", function() {
+            //QDR.log.debug("Topology controller was notified that the model was updated");
+            if (hasChanged()) {
+                QDR.log.info("svg graph changed")
+                saveChanged();
+                // TODO: update graph nodes instead of rebuilding entire graph
+                d3.select("#SVG_ID").remove();
+                animate = true;
+                initForceGraph();
+                //if ($location.path().startsWith("/topology"))
+                //    Core.notification('info', "Qpid dispatch router topology changed");
+
+            } else {
+                //QDR.log.debug("no changes")
+            }
+        });
+
+		function hasChanged () {
+			var nodeInfo = QDRService.topology.nodeInfo();
+			if (Object.keys(nodeInfo).length != Object.keys(savedKeys).length)
+				return true;
+			for (var key in nodeInfo) {
+                // if this node isn't in the saved node list
+                if (!savedKeys.hasOwnProperty(key))
+                    return true;
+                // if the number of connections for this node chaanged
+                if (nodeInfo[key]['.connection'].results.length != savedKeys[key]) {
+					/*
+					QDR.log.debug("number of connections changed for " + key);
+					QDR.log.debug("QDRService.topology._nodeInfo[key]['.connection'].results.length");
+					console.dump(QDRService.topology._nodeInfo[key]['.connection'].results.length);
+					QDR.log.debug("savedKeys[key]");
+					console.dump(savedKeys[key]);
+					*/
+                    return true;
+                }
+			}
+			return false;
+		};
+		function saveChanged () {
+            savedKeys = {};
+            var nodeInfo = QDRService.topology.nodeInfo();
+            // save the number of connections per node
+		    for (var key in nodeInfo) {
+		        savedKeys[key] = nodeInfo[key]['.connection'].results.length;
+		    }
+			//QDR.log.debug("saving current keys");
+			console.dump(savedKeys);
+		};
+		// we are about to leave the page, save the node positions
+		$rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
+			//QDR.log.debug("locationChangeStart");
+			nodes.forEach( function (d) {
+	           localStorage[d.name] = angular.toJson({x: d.x, y: d.y, fixed: d.fixed});
+			});
+            $scope.addingNode.step = 0;
+
+		});
+		// When the DOM element is removed from the page,
+        // AngularJS will trigger the $destroy event on
+        // the scope
+        $scope.$on("$destroy", function( event ) {
+   			//QDR.log.debug("scope on destroy");
+            QDRService.stopUpdating();
+            QDRService.delUpdatedAction("topology");
+			d3.select("#SVG_ID").remove();
+        });
+
+		initForceGraph();
+		saveChanged();
+        QDRService.startUpdating();
+
+	    function doAddDialog(NewRouterName) {
+		    var d = $dialog.dialog({
+				dialogClass: "modal dlg-large",
+				backdrop: true,
+				keyboard: true,
+				backdropClick: true,
+		        controller: 'QDR.NodeDialogController',
+		        templateUrl: 'node-config-template.html',
+		        resolve: {
+		            newname: function () {
+		                return NewRouterName;
+		            }
+		        }
+		    });
+		    d.open().then(function (result) {
+				if (result)
+					doDownloadDialog(result);
+		    });
+        };
+
+	    function doDownloadDialog(result) {
+		    d = $dialog.dialog({
+				backdrop: true,
+				keyboard: true,
+				backdropClick: true,
+				controller: 'QDR.DownloadDialogController',
+		        templateUrl: 'download-dialog-template.html',
+		        resolve: {
+		            results: function () {
+		                return result;
+		            }
+		        }
+		    });
+		    d.open().then(function (result) {
+		    //QDR.log.debug("download dialog done")
+		    })
+            if (!$scope.$$phase) $scope.$apply()
+        };
+  }]);
+
+  QDR.module.controller("QDR.NodeDialogController", function($scope, QDRService, dialog, newname) {
+   		var schema = QDRService.schema;
+   		var myEntities = ['container', 'router', 'log', 'listener' ];
+   		var typeMap = {integer: 'number', string: 'text', path: 'text', boolean: 'boolean'};
+		var newLinks = $('path.temp').toArray();    // jquery array of new links for the added router
+		var nodeInfo = QDRService.topology.nodeInfo();
+		var separatedEntities = []; // additional entities required if a link is reversed
+		var myPort = 0, myAddr = '0.0.0.0'; // port and address for new router
+   		$scope.entities = [];
+
+		// find max port number that is used in all the listeners
+		var getMaxPort = function (nodeInfo) {
+			var maxPort = 5674;
+			for (var key in nodeInfo) {
+				var node = nodeInfo[key];
+				var listeners = node['.listener'];
+				var attrs = listeners.attributeNames;
+				for (var i=0; i<listeners.results.length; ++i) {
+					var res = listeners.results[i];
+					var port = QDRService.valFor(attrs, res, 'port');
+					if (parseInt(port, 10) > maxPort)
+						maxPort = parseInt(port, 10);
+				}
+			}
+			return maxPort;
+		}
+		var maxPort = getMaxPort(nodeInfo);
+
+		// construct an object that contains all the info needed for a single tab's fields
+		var entity = function (actualName, tabName, humanName, ent, icon, link) {
+			var nameIndex = -1; // the index into attributes that the name field was placed
+			var index = 0;
+			var info = {
+			    actualName: actualName,
+				tabName:    tabName,
+				humanName:  humanName,
+				description:ent.description,
+				icon:       angular.isDefined(icon) ? icon : '',
+				references: ent.references,
+				link:       link,
+
+   		        attributes: $.map(ent.attributes, function (value, key) {
+					// skip identity and depricated fields
+   		            if (key == 'identity' || value.description.startsWith('Deprecated'))
+   		                return null;
+					var val = value['default'];
+					if (key == 'name')
+						nameIndex = index;
+					index++;
+					return {    name:       key,
+								humanName:  QDRService.humanify(key),
+                                description:value.description,
+                                type:       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
+                    };
+                })
+			}
+			// move the 'name' attribute to the 1st position
+			if (nameIndex > -1) {
+				var tmp = info.attributes[0];
+				info.attributes[0] = info.attributes[nameIndex];
+				info.attributes[nameIndex] = tmp;
+			}
+			return info;
+		}
+
+		// remove the annotation fields
+		var stripAnnotations = function (entityName, ent, annotations) {
+			if (ent.references) {
+				var newEnt = {attributes: {}};
+				ent.references.forEach( function (annoKey) {
+					if (!annotations[annoKey])
+						annotations[annoKey] = {};
+					annotations[annoKey][entityName] = true;    // create the key/consolidate duplicates
+					var keys = Object.keys(schema.annotations[annoKey].attributes);
+					for (var attrib in ent.attributes) {
+						if (keys.indexOf(attrib) == -1) {
+							newEnt.attributes[attrib] = ent.attributes[attrib];
+						}
+					}
+					// add a field for the reference name
+					newEnt.attributes[annoKey] = {type: 'string',
+							description: 'Name of the ' + annoKey + ' section.',
+							'default': annoKey, required: true};
+				})
+				newEnt.references = ent.references;
+				newEnt.description = ent.description;
+				return newEnt;
+			}
+			return ent;
+		}
+
+		var annotations = {};
+   		myEntities.forEach(function (entityName) {
+   		    var ent = schema.entityTypes[entityName];
+   		    var hName = QDRService.humanify(entityName);
+   		    if (entityName == 'listener')
+   		        hName = "Listener for clients";
+   		    var noAnnotations = stripAnnotations(entityName, ent, annotations);
+			var ediv = entity(entityName, entityName, hName, noAnnotations, undefined);
+			if (ediv.actualName == 'router') {
+				ediv.attributes.filter(function (attr) { return attr.name == 'name'})[0].value = newname;
+				// if we have any new links (connectors), then the router's mode should be interior
+				if (newLinks.length) {
+					var roleAttr = ediv.attributes.filter(function (attr) { return attr.name == 'mode'})[0];
+					roleAttr.value = roleAttr.selected = "interior";
+				}
+			}
+			if (ediv.actualName == 'container') {
+				ediv.attributes.filter(function (attr) { return attr.name == 'containerName'})[0].value = newname + "-container";
+			}
+			if (ediv.actualName == 'listener') {
+				// find max port number that is used in all the listeners
+				ediv.attributes.filter(function (attr) { return attr.name == 'port'})[0].value = ++maxPort;
+			}
+			// special case for required log.module since it doesn't have a default
+			if (ediv.actualName == 'log') {
+				var moduleAttr = ediv.attributes.filter(function (attr) { return attr.name == 'module'})[0];
+				moduleAttr.value = moduleAttr.selected = "DEFAULT";
+			}
+			$scope.entities.push( ediv );
+   		})
+
+		// add a tab for each annotation that was found
+		var annotationEnts = [];
+		for (var key in annotations) {
+			ent = angular.copy(schema.annotations[key]);
+			ent.attributes.name = {type: "string", unique: true, description: "Unique name that is used to refer to this set of attributes."}
+			var ediv = entity(key, key+'tab', QDRService.humanify(key), ent, undefined);
+			ediv.attributes.filter(function (attr) { return attr.name == 'name'})[0].value = key;
+			$scope.entities.push( ediv );
+			annotationEnts.push( ediv );
+		}
+
+		// add an additional listener tab if any links are reversed
+		ent = schema.entityTypes['listener'];
+		newLinks.some(function (link) {
+			if (link.__data__.right) {
+	   		    var noAnnotations = stripAnnotations('listener', ent, annotations);
+				var ediv = entity("listener", "listener0", "Listener (internal)", noAnnotations, undefined);
+				ediv.attributes.filter(function (attr) { return attr.name == 'port'})[0].value = ++maxPort;
+				// connectors from other routers need to connect to this addr:port
+				myPort = maxPort;
+				myAddr = ediv.attributes.filter(function (attr) { return attr.name == 'addr'})[0].value
+
+				// override the role. 'normal' is the default, but we want inter-router
+				ediv.attributes.filter(function( attr ) { return attr.name == 'role'})[0].selected = 'inter-router';
+				separatedEntities.push( ediv );
+				return true; // stop looping
+			}
+			return false;   // continue looping
+		})
+
+		// Add connector tabs for each new link on the topology graph
+		ent = schema.entityTypes['connector'];
+		newLinks.forEach(function (link, i) {
+   		    var noAnnotations = stripAnnotations('connector', ent, annotations);
+			var ediv = entity('connector', 'connector' + i, " " + link.__data__.source.name, noAnnotations, link.__data__.right, link)
+
+			// override the connector role. 'normal' is the default, but we want inter-router
+			ediv.attributes.filter(function( attr ) { return attr.name == 'role'})[0].selected = 'inter-router';
+
+			// find the addr:port of the inter-router listener to use
+			var listener = nodeInfo[link.__data__.source.key]['.listener'];
+			var attrs = listener.attributeNames;
+			for (var i=0; i<listener.results.length; ++i) {
+				var res = listener.results[i];
+				var role = QDRService.valFor(attrs, res, 'role');
+				if (role == 'inter-router') {
+					ediv.attributes.filter(function( attr ) { return attr.name == 'addr'})[0].value =
+						QDRService.valFor(attrs, res, 'addr')
+					ediv.attributes.filter(function( attr ) { return attr.name == 'port'})[0].value =
+						QDRService.valFor(attrs, res, 'port')
+					break;
+				}
+			}
+			if (link.__data__.right) {
+				// connectors from other nodes need to connect to the new router's listener addr:port
+   				ediv.attributes.filter(function (attr) { return attr.name == 'port'})[0].value = myPort;
+   				ediv.attributes.filter(function (attr) { return attr.name == 'addr'})[0].value = myAddr;
+
+				separatedEntities.push(ediv)
+			}
+			else
+				$scope.entities.push( ediv );
+		})
+		Array.prototype.push.apply($scope.entities, separatedEntities);
+
+		// update the description on all the annotation tabs
+		annotationEnts.forEach ( function (ent) {
+			var shared = Object.keys(annotations[ent.actualName]);
+			ent.description += " These fields are shared by " + shared.join(" and ") + ".";
+
+		})
+
+		$scope.testPattern = function (attr) {
+			if (attr.rawtype == 'path')
+				return /^(\/)?([^/\0]+(\/)?)+$/;
+				//return /^(.*\/)([^/]*)$/;
+			return /(.*?)/;
+		}
+
+		$scope.attributeDescription = '';
+		$scope.attributeType = '';
+		$scope.attributeRequired = '';
+		$scope.attributeUnique = '';
+		$scope.active = 'container'
+		$scope.fieldsetDivs = "/fieldsetDivs.html"
+		$scope.setActive = function (tabName) {
+			$scope.active = tabName
+		}
+		$scope.isActive = function (tabName) {
+			return $scope.active === tabName
+		}
+		$scope.showDescription = function (attr, e) {
+			$scope.attributeDescription = attr.description;
+			var offset = jQuery(e.currentTarget).offset()
+			jQuery('.attr-description').offset({top: offset.top})
+
+			$scope.attributeType = "Type: " + JSON.stringify(attr.rawtype);
+			$scope.attributeRequired = attr.required ? 'required' : '';
+			$scope.attributeUnique = attr.unique ? 'Must be unique' : '';
+		}
+        // handle the download button click
+        // copy the dialog's values to the original node
+        $scope.download = function () {
+	        dialog.close({entities: $scope.entities, annotations: annotations});
+        }
+        $scope.cancel = function () {
+            dialog.close()
+        };
+
+		$scope.selectAnnotationTab = function (tabName) {
+            var tabs = $( "#tabs" ).tabs();
+            tabs.tabs("select", tabName);
+		}
+
+        var initTabs = function () {
+            var div = angular.element("#tabs");
+            if (!div.width()) {
+                setTimeout(initTabs, 100);
+                return;
+            }
+            $( "#tabs" )
+                .tabs()
+                .addClass('ui-tabs-vertical ui-helper-clearfix');
+        }
+        // start the update loop
+        initTabs();
+
+  });
+
+QDR.module.controller("QDR.DownloadDialogController", function($scope, QDRService, $templateCache, $window, dialog, results) {
+		var result = results.entities;
+		var annotations = results.annotations;
+		var annotationKeys = Object.keys(annotations);
+		var annotationSections = {};
+
+		// use the router's name as the file name if present
+		$scope.newRouterName = 'router';
+		result.forEach( function (e) {
+			if (e.actualName == 'router') {
+				e.attributes.forEach( function (a) {
+					if (a.name == 'name') {
+						$scope.newRouterName = a.value;
+					}
+				})
+			}
+		})
+		$scope.newRouterName = $scope.newRouterName + ".conf";
+
+		var template = $templateCache.get('config-file-header.html');
+		$scope.verbose = true;
+		$scope.$watch('verbose', function (newVal) {
+			if (newVal !== undefined) {
+				// recreate output using current verbose setting
+				getOutput();
+			}
+		})
+
+		var getOutput = function () {
+			$scope.output = template + '\n';
+			$scope.parts = [];
+			var commentChar = '#'
+			result.forEach(function (entity) {
+				// don't output a section for annotations, they get flattened into the entities
+				var section = "";
+				if (entity.icon) {
+					section += "##\n## Add to " + entity.link.__data__.source.name + "'s configuration file\n##\n";
+				}
+				section += "##\n## " + QDRService.humanify(entity.actualName) + " - " + entity.description + "\n##\n";
+				section += entity.actualName + " {\n";
+				entity.attributes.forEach(function (attribute) {
+					if (attribute.input == 'select')
+						attribute.value = attribute.selected;
+
+					// treat values with all spaces and empty strings as undefined
+					attribute.value = String(attribute.value).trim();
+					if (attribute.value === 'undefined' || attribute.value === '')
+						attribute.value = undefined;
+
+					if ($scope.verbose) {
+						commentChar = attribute.required || attribute.value != attribute['default'] ? ' ' : '#';
+						if (!attribute.value) {
+							commentChar = '#';
+							attribute.value = '';
+						}
+						section += commentChar + "    "
+							+ attribute.name + ":" + Array(Math.max(20 - attribute.name.length, 1)).join(" ")
+							+ attribute.value
+						    + Array(Math.max(20 - ((attribute.value)+"").length, 1)).join(" ")
+							+ '# ' + attribute.description
+						    + "\n";
+					} else {
+						if (attribute.value) {
+							if (attribute.value != attribute['default'] || attribute.required)
+								section += "    "
+									+ attribute.name + ":" + Array(20 - attribute.name.length).join(" ")
+									+ attribute.value + "\n";
+
+						}
+					}
+				})
+				section += "}\n\n";
+				// if entity.icon is true, this is a connector intended for another router
+				if (entity.icon)
+					$scope.parts.push({output: section,
+								link: entity.link,
+								name: entity.link.__data__.source.name,
+								references: entity.references});
+				else
+					$scope.output += section;
+
+				// if this section is actually an annotation
+				if (annotationKeys.indexOf(entity.actualName) > -1) {
+					annotationSections[entity.actualName] = section;
+				}
+			})
+			// go back and add annotation sections to the parts
+			$scope.parts.forEach (function (part) {
+				for (var section in annotationSections) {
+					if (part.references.indexOf(section) > -1) {
+						part.output += annotationSections[section];
+					}
+				}
+			})
+			QDR.log.debug($scope.output);
+		}
+
+        // handle the download button click
+        $scope.download = function () {
+			var blob = new Blob([$scope.output], { type: 'text/plain' });
+	        var downloadLink = angular.element('<a></a>');
+	        downloadLink.attr('href', ($window.URL || $window.webkitURL).createObjectURL(blob));
+	        downloadLink.attr('download', $scope.newRouterName);
+	        downloadLink[0].click();
+        }
+
+		$scope.downloadPart = function (part) {
+			var linkName = part.link.__data__.source.name + 'additional.conf';
+			var blob = new Blob([part.output], { type: 'text/plain' });
+	        var downloadLink = angular.element('<a></a>');
+	        downloadLink.attr('href', ($window.URL || $window.webkitURL).createObjectURL(blob));
+	        downloadLink.attr('download', linkName);
+	        downloadLink[0].click();
+
+			QDR.log.debug(part);
+		}
+
+		$scope.done = function () {
+	        dialog.close();
+		}
+});
+
+  return QDR;
+}(QDR || {}));


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