http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/0c58c381/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/topology/topology.controller.js ---------------------------------------------------------------------- diff --git a/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/topology/topology.controller.js b/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/topology/topology.controller.js new file mode 100644 index 0000000..a500cea --- /dev/null +++ b/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/topology/topology.controller.js @@ -0,0 +1,1703 @@ +/* +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) { + 'use strict'; + + angular + .module('horizon.dashboard.dispatch.topology') + .controller('horizon.dashboard.dispatch.topology.TopologyController', TopologyController); + + TopologyController.$inject = [ + '$scope', + '$rootScope', + 'horizon.dashboard.dispatch.comService', + '$location', + '$timeout', + '$modal', + ] + + var mouseX, mouseY; + var dontHide = false; + function hideLinkDetails() { + d3.select("#link_details").transition() + .duration(500) + .style("opacity", 0) + .each("end", function (d) { + d3.select("#link_details").style("visibility", "hidden") + }) + } + + function TopologyController( + $scope, + $rootScope, + QDRService, + $location, + $timeout, + $modal) { + + var ctrl = this; + QDRService.addConnectAction( function () { + Topology($scope, $rootScope, QDRService, $location, $timeout, $modal) + }) + QDRService.loadConnectOptions(QDRService.connect); + + $scope.multiData = [] + $scope.multiDetails = { + data: 'multiData', + enableRowSelection: true, + enableRowHeaderSelection: false, + multiSelect: false, + enableColumnResize: true, + enableColumnReordering: true, + enableVerticalScrollbar: 0, + enableHorizontalScrollbar: 0, + onRegisterApi: function(gridApi){ + gridApi.selection.on.rowSelectionChanged($scope, function(row){ + var detailsDiv = d3.select('#link_details') + var isVis = detailsDiv.style('visibility') === 'visible'; + if (!dontHide && isVis && $scope.connectionId === row.entity.connectionId) { + hideLinkDetails(); + return; + } + dontHide = false; + $scope.multiDetails.showLinksList(row) + }); + }, + showLinksList: function (obj) { + $scope.linkData = obj.entity.linkData; + $scope.connectionId = obj.entity.connectionId; + var visibleLen = Math.min(obj.entity.linkData.length, 10) + var left = parseInt(d3.select('#multiple_details').style("left")) + var bounds = $("#topology").position() + var detailsDiv = d3.select('#link_details') + detailsDiv + .style({ + visibility: 'visible', + opacity: 1, + left: (left + 20) + "px", + top: (mouseY + 40 - bounds.top + $(document).scrollTop()) + "px", + height: ((visibleLen + 1) * 30) + 40 + "px", // +1 for the header row + 'overflow-y': obj.entity.linkData > 10 ? 'scroll' : 'hidden'}) + }, + columnDefs: [ + { + field: 'host', + displayName: 'Connection host' + }, + { + field: 'user', + displayName: 'User' + }, + { + field: 'properties', + displayName: 'Properties' + }, +/* + { + cellClass: 'gridCellButton', + cellTemplate: '<button title="{{quiesceText(row)}} the links" type="button" ng-class="quiesceClass(row)" class="btn" ng-click="$event.stopPropagation();quiesceConnection(row)" ng-disabled="quiesceDisabled(row)">{{quiesceText(row)}}</button>' + }, +*/ + ] + }; + $scope.linkData = []; + $scope.linkDetails = { + data: 'linkData', + enableRowSelection: true, + enableRowHeaderSelection: false, + multiSelect: false, + enableColumnResize: true, + enableColumnReordering: true, + enableVerticalScrollbar: 0, + enableHorizontalScrollbar: 0, + columnDefs: [ + { + field: 'adminStatus', + displayName: 'Admin state' + }, + { + field: 'operStatus', + displayName: 'Oper state' + }, + { + field: 'dir', + displayName: 'dir' + }, + { + field: 'owningAddr', + displayName: 'Address' + }, + { + field: 'deliveryCount', + displayName: 'Delivered', + cellClass: 'grid-values' + + }, + { + field: 'uncounts', + displayName: 'Outstanding', + cellClass: 'grid-values' + }/*, + { + cellClass: 'gridCellButton', + cellTemplate: '<button title="{{quiesceLinkText(row)}} this link" type="button" ng-class="quiesceLinkClass(row)" class="btn" ng-click="quiesceLink(row)" ng-disabled="quiesceLinkDisabled(row)">{{quiesceLinkText(row)}}</button>' + }*/ + ] + } + } + + function Topology( + $scope, + $rootScope, + QDRService, + $location, + $timeout, + $modal) { + + $scope.quiesceState = {} + $scope.quiesceConnection = function (row) { + // call method to set adminStatus + } + $scope.quiesceDisabled = function (row) { + return false; + } + $scope.quiesceText = function (row) { + return 'Quiesce' + } + $scope.quiesceClass = function (row) { + var stateClassMap = { + enabled: 'btn-primary', + quiescing: 'btn-warning', + reviving: 'btn-warning', + quiesced: 'btn-danger' + } + return 'btn-primary' + } + $scope.quiesceLinkClass = function (row) { + var stateClassMap = { + enabled: 'btn-primary', + disabled: 'btn-danger' + } + return stateClassMap[row.entity.adminStatus] + } + $scope.quiesceLink = function (row) { + QDRService.quiesceLink(row.entity.nodeId, row.entity.name); + } + $scope.quiesceLinkDisabled = function (row) { + return (row.entity.operStatus !== 'up' && row.entity.operStatus !== 'down') + } + $scope.quiesceLinkText = function (row) { + return row.entity.operStatus === 'down' ? "Revive" : "Quiesce"; + } + + // we are currently connected. setup a handler to get notified if we are ever disconnected + QDRService.addDisconnectAction( function () { + QDR.log.debug("disconnected from router. show a toast message"); + }) + + var urlPrefix = $location.absUrl(); + urlPrefix = urlPrefix.split("#")[0] + QDR.log.debug("started QDR.TopologyController with urlPrefix: " + urlPrefix); + + $scope.addingNode = { + step: 0, + hasLink: false, + trigger: '' + }; + + $scope.cancel = function () { + $scope.addingNode.step = 0; + } + $scope.editNewRouter = function () { + $scope.addingNode.trigger = 'editNode'; + } + + 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.addingNode.step = 0; + } else { + // start adding node mode + $scope.addingNode.step = 1; + } + } else { + $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; + }) + updateForm(Object.keys(QDRService.topology.nodeInfo())[0], 'router', 0); + + } else if (newValue > 0) { + // we are starting the add mode + $scope.$broadcast('showAddForm') + + 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; + } + + // for ng-grid that shows details for multiple consoles/clients + // 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 (var 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); + } + + // event handlers for popup context menu + $(document).mousemove(function (e) { + mouseX = e.clientX; + mouseY = e.clientY; + //console.log("("+mouseX+"," + mouseY+")") + }); + $(document).mousemove(); + $(document).click(function (e) { + $scope.contextNode = null; + $(".contextMenu").fadeOut(200); + }); + + // set up SVG for D3 + var colors = {'inter-router': "#EAEAEA", 'normal': "#F0F000", 'on-demand': '#00F000'}; + var radii = {'inter-router': 25, 'normal': 15, 'on-demand': 15}; + var radius = 25; + var radiusNormal = 15; + var svg, lsvg; + var force; + var animate = false; // should the force graph organize itself when it is displayed + var path, circle; + var savedKeys = {}; + var width = 0; + var height = 0; + + var getSizes = function () { + var legendWidth = 196; + var gap = 5; + var width = $('.qdrTopology').width() - gap - legendWidth; + var top = $('#topology').offset().top + var tpformHeight = $('#topologyForm').height() + var height = window.innerHeight - tpformHeight - top - gap; + if (height < 400) + height = 400; +/* + QDR.log.debug("window.innerHeight:" + window.innerHeight + + " tpformHeight:" + tpformHeight + + " top:" + top + + " gap:" + gap + + " width:" + width + + " height:" + height) +*/ + if (width < 10 || height < 30) { + QDR.log.info("page width and height are abnormal w:" + width + " height:" + height) + return [0,0]; + } + return [width, height] + } + var resize = function () { + var sizes = getSizes(); + width = sizes[0] + height = sizes[1] + if (width > 0) { + // set attrs and 'resume' force + svg.attr('width', width); + svg.attr('height', height); + force.size(sizes).resume(); + } + } + window.addEventListener('resize', resize); + var sizes = getSizes() + width = sizes[0] + height = sizes[1] + height = 300 + if (width <= 0 || height <= 0) + return + + // set up initial nodes and links + // - nodes are known by 'id', not by index in array. + // - selected edges are indicated on the node (as a bold red circle). + // - links are always source < target; edge directions are set by 'left' and 'right'. + var nodes = []; + var links = []; + + var aNode = function (id, name, nodeType, nodeInfo, nodeIndex, x, y, resultIndex, fixed, properties) { + properties = properties || {}; + var routerId; + if (nodeInfo) { + var node = nodeInfo[id]; + if (node) { + var router = node['.router']; + routerId = QDRService.valFor(router.attributeNames, router.results[0], 'id') + if (!routerId) + routerId = QDRService.valFor(router.attributeNames, router.results[0], 'routerId') + } + } + return { key: id, + name: name, + nodeType: nodeType, + properties: properties, + routerId: routerId, + 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}); + } + } + + // initialize the nodes and links array from the QDRService.topology._nodeInfo object + var initForceGraph = function () { + nodes = []; + links = []; + + svg = d3.select('#topology') + .append('svg') + .attr("id", "SVG_ID") + .attr('width', width) + .attr('height', height) + .on("contextmenu", function(d) { + if (QDR.isHorizon) + return; + 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() + }); + + $(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; + 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: 200 + yInit, + fixed: false}; + } + if (position.y > height) + 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); + } + + // 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; + 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"); + 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); + + // 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) + var node = aNode(id, name, role, nodeInfo, nodes.length, position.x, position.y, j, position.fixed, properties) + var nodeType = QDRService.isAConsole(properties, QDRService.valFor(attrs, conns[j], "identity"), role, node.key) + + 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") + node.connectionId = QDRService.valFor(attrs, conns[j], "identity") + + 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; + // 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() + + 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(); + setTimeout(function () { + updateForm(Object.keys(QDRService.topology.nodeInfo())[0], 'router', 0); + }, 10) + + } + + function updateForm (key, entity, resultIndex) { + var nodeInfo = QDRService.topology.nodeInfo(); + var onode = nodeInfo[key] + if (onode) { + 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) }) + + // 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, description: 'The port on which this router is listening for connections'}); + } + + $scope.$broadcast('showEntityForm', {entity: entity, attributes: attributes}) + } + if (!$scope.$$phase) $scope.$apply() + } + + function getContainerIndex(_id) { + var nodeIndex = 0; + var nodeInfo = QDRService.topology.nodeInfo(); + for (var id in nodeInfo) { + var node = nodeInfo[id]['.router']; + // there should be only one router entity for each node, so using results[0] should be fine + if (QDRService.valFor( node.attributeNames, node.results[0], "id") === _id) + return nodeIndex; + if (QDRService.valFor( node.attributeNames, node.results[0], "routerId") === _id) + return nodeIndex; + nodeIndex++ + } + // there was no router.id that matched, check deprecated router.routerId + nodeIndex = 0; + for (var id in nodeInfo) { + var node = nodeInfo[id]['.container']; + if (node) { + if (QDRService.valFor ( node.attributeNames, node.results[0], "containerName") === _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() { + circle.attr('transform', function (d) { + var cradius; + if (d.nodeType == "inter-router") { + cradius = d.left ? radius + 8 : radius; + } else { + cradius = d.left ? radiusNormal + 18 : radiusNormal; + } + d.x = Math.max(d.x, radiusNormal * 2); + d.y = Math.max(d.y, radiusNormal * 2); + d.x = Math.max(0, Math.min(width-cradius, d.x)) + d.y = Math.max(0, Math.min(height-cradius, d.y)) + return 'translate(' + d.x + ',' + d.y + ')'; + }); + + // draw directed edges with proper padding from node centers + path.attr('d', function (d) { + //QDR.log.debug("in tick for d"); + //console.dump(d); + var sourcePadding, targetPadding, r; + + if (d.target.nodeType == "inter-router") { + r = radius; + // right arrow left line start + sourcePadding = d.left ? radius + 8 : radius; + // left arrow right line start + targetPadding = d.right ? radius + 16 : radius; + } else { + r = radiusNormal - 18; + sourcePadding = d.left ? radiusNormal + 18 : radiusNormal; + targetPadding = d.right ? radiusNormal + 16 : radiusNormal; + } + var dtx = Math.max(targetPadding, Math.min(width-r, d.target.x)), + dty = Math.max(targetPadding, Math.min(height-r, d.target.y)), + dsx = Math.max(sourcePadding, Math.min(width-r, d.source.x)), + dsy = Math.max(sourcePadding, Math.min(height-r, d.source.y)); + + var deltaX = dtx - dsx, + deltaY = dty - dsy, + dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY), + normX = deltaX / dist, + normY = deltaY / dist; + var sourceX = dsx + (sourcePadding * normX), + sourceY = dsy + (sourcePadding * normY), + targetX = dtx - (targetPadding * normX), + targetY = dty - (targetPadding * normY); + sourceX = Math.max(0, Math.min(width, sourceX)) + sourceY = Math.max(0, Math.min(width, sourceY)) + targetX = Math.max(0, Math.min(width, targetX)) + targetY = Math.max(0, Math.min(width, targetY)) + + return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY; + }); + + if (!animate) { + animate = true; + force.stop(); + } + } + + // highlight the paths between the selected node and the hovered node + function findNextHopNode(from, d) { + // d is the node that the mouse is over + // from is the selected_node .... + if (!from) + return null; + + if (from == d) + return selected_node; + + //QDR.log.debug("finding nextHop from: " + from.name + " to " + d.name); + 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], "id" ); + 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 clearPopups() { + d3.select("#crosssection").style("display", "none"); + $('.hastip').empty(); + d3.select("#multiple_details").style("visibility", "hidden") + d3.select("#link_details").style("visibility", "hidden") + d3.select('#node_context_menu').style('display', 'none'); + } + function removeCrosssection() { + setTimeout(function () { + d3.select("[id^=tooltipsy]").remove() + $('.hastip').empty(); + }, 1010); + d3.select("#crosssection svg g").transition() + .duration(1000) + .attr("transform", "scale(0)") + .style("opacity", 0) + .each("end", function (d) { + 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("visibility", "hidden") + stopUpdateConnectionsGrid(); + }) + hideLinkDetails(); + } + + // takes the nodes and links array of objects and adds svg elements for everything that hasn't already + // been added + function restart(start) { + circle.call(force.drag); + + // path (link) group + path = path.data(links); + + // 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'; } ) + // mouseover a line + .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.routerId) { + break; + } + } + // did not find connection. this is a connection to a non-interrouter node + if (resultIndex === onode['.connection'].results.length) { + // use the non-interrouter node's connection info + left = d.target; + resultIndex = left.resultIndex; + } + if (resultIndex) + updateForm(left.key, 'connection', resultIndex); + } + + mousedown_link = d; + selected_link = mousedown_link; + restart(); + }) + // mouseout a line + .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"); + selected_link = null; + restart(); + }) + // contextmenu for a line + .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'); + }) + // clicked on a line + .on("click", function (d) { + var clickPos = d3.mouse(this); + d3.event.stopPropagation(); + clearPopups(); + 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; }); + + d3.select("#crosssection svg").remove(); + var svg = d3.select("#crosssection").append("svg") + .attr("width", diameter) + .attr("height", diameter) + var svgg = svg.append("g") + .attr("transform", "translate(2,2)"); + + 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.routerId) { + root.attributeNames = connections.attributeNames; + root.obj = connection; + root.desc = "Connection"; + return true; // stop looping after 1 match + } + return false; + }) + + // find router.links where link.remoteContainer is d.source.name + var links = nodeInfo[d.source.key]['.router.link']; + var identityIndex = connections.attributeNames.indexOf('identity') + var roleIndex = connections.attributeNames.indexOf('role') + var connectionIdIndex = links.attributeNames.indexOf('connectionId'); + var linkTypeIndex = links.attributeNames.indexOf('linkType'); + var nameIndex = links.attributeNames.indexOf('name'); + var linkDirIndex = links.attributeNames.indexOf('linkDir'); + + if (roleIndex < 0 || identityIndex < 0 || connectionIdIndex < 0 + || linkTypeIndex < 0 || nameIndex < 0 || linkDirIndex < 0) + return; + links.results.forEach ( function (link) { + if (root.obj && link[connectionIdIndex] == root.obj[identityIndex] && link[linkTypeIndex] == root.obj[roleIndex]) + root.children.push ( + { name: " " + link[linkDirIndex] + " ", + size: 100, + obj: link, + desc: "Link", + attributeNames: links.attributeNames + }) + }) + if (root.children.length == 0) + return; + 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" : ".5em"}) + .style("text-anchor", "middle") + .text(function(d) { + return d.name.substring(0, d.r / 3); + }); + $('.hastip').tooltipsy({ alignTo: 'cursor'}); + svgg.attr("transform", "translate(2,2) scale(0.01)") + + var bounds = $("#topology").position() + d3.select("#crosssection") + .style("display", "block") + .style("left", (clickPos[0] + bounds.left) + "px") + .style("top", (clickPos[1] + bounds.top) + "px") + + svgg.transition() + .attr("transform", "translate(2,2) scale(1)") + .each("end", function () { + d3.selectAll("#crosssection g.leaf text").attr("dy", ".3em") + }) + }) + + // 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') + .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 QDRService.isConsole(d) } ) + .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) { // mouseover a circle + 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"); + updateForm(d.key, 'router', 0); + } else if (d.nodeType === 'normal' || d.nodeType === 'on-demand') { + //QDR.log.debug("showing connections form"); + updateForm(d.key, 'connection', d.resultIndex); + } + } + + if (d === mousedown_node) + return; + //if (d === selected_node) + // return; + // enlarge target node + d3.select(this).attr('transform', 'scale(1.1)'); + // highlight the next-hop route from the selected node to this node + mousedown_node = null; + if (!selected_node) { + return; + } + setTimeout(nextHop, 1, selected_node, d); + }) + .on('mouseout', function (d) { // mouseout a circle + // 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) { // mousedown a circle + if (d3.event.button !== 0) { // ignore all but left button + return; + } + mousedown_node = d; + // mouse position relative to svg + initial_mouse_down_position = d3.mouse(this.parentElement.parentElement.parentElement).slice(); + }) + .on('mouseup', function (d) { // mouseup a circle + 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; + if (!$scope.$$phase) $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; + } + else { + if (d.nodeType !== 'normal' && d.nodeType !== 'on-demand') + selected_node = mousedown_node; + } + for (var i=0; i<links.length; ++i) { + links[i]['highlighted'] = false; + } + mousedown_node = null; + if (!$scope.$$phase) $scope.$apply() + restart(false); + }) + .on("dblclick", function (d) { // dblclick a circle + 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(); + if (!$scope.$$phase) $scope.$apply() + } + }) + .on("contextmenu", function(d) { // rightclick a circle + $(document).click(); + d3.event.preventDefault(); + $scope.contextNode = d; + if (!$scope.$$phase) $scope.$apply() // we just changed a scope valiable during an async event + var bounds = $(QDR.offsetParent).offset() + d3.select('#node_context_menu') + .style('left', (mouseX - bounds.left + $(document).scrollLeft()) + "px") + .style('top', (mouseY - bounds.top + $(document).scrollTop()) + "px") + .style('display', 'block'); + }) + .on("click", function (d) { // leftclick a circle + var clickPos = d3.mouse(this); + clearPopups(); + if (!d.normals) { + // circle was a router or a broker + if ( QDRService.isArtemis(d) && Core.ConnectionName === 'Artemis' ) { + $location.path('/jmx/attributes?tab=artemis&con=Artemis') + } + return; + } + // circle was a client or console + d3.event.stopPropagation(); + startUpdateConnectionsGrid(d, clickPos); + }) + + 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 QDRService.isConsole(d) } ) + .classed('normal', function(d) { return d.nodeType === 'normal' } ) + .classed('on-demand', function(d) { return d.nodeType === 'on-demand' } ) + .classed('artemis', function(d) { return QDRService.isArtemis(d) } ) + .classed('qpid-cpp', function(d) { return QDRService.isQpid(d) } ) + .text(function (d) { + if (QDRService.isConsole(d)) { + 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 (QDRService.isConsole(d)) { + 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(); + var multiples = svg.selectAll('.multiple') + multiples.each( function (d) { + d.normals.forEach( function (n, i) { + if (i<d.normals.length-1 && i<3) // only show a few shadow circles + this.insert('svg:circle', ":first-child") + .attr('class', 'subcircle node') + .attr('r', 15 - i) + .attr('transform', "translate("+ 4 * (i+1) +", 0)") + }, d3.select(this)) + }) + + // dynamically create the legend based on which node types are present + 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') + if (svgEl) { + var bb; + // firefox can throw an exception on getBBox on an svg element + try { + bb = svgEl.getBBox(); + } catch (e) { + bb = {y: 0, height: 200, x: 0, width: 200} + } + svgEl.style.height = (bb.y + bb.height) + 'px'; + svgEl.style.width = (bb.x + bb.width) + 'px'; + } + + if (!mousedown_node || !selected_node) + return; + + if (!start) + return; + // set the graph in motion + //QDR.log.debug("mousedown_node is " + mousedown_node); + force.start(); + } + + // show the links popup and update it periodically + var startUpdateConnectionsGrid = function (d, clickPos) { + // called every update tick + var extendConnections = function () { + $scope.multiData = [] + var normals = d.normals; + // find updated normals for d + d3.selectAll('.normal') + .each(function(newd) { + if (newd.id == d.id && newd.name == d.name) { + normals = newd.normals; + } + }); + if (normals) { + normals.forEach( function (n) { + var nodeInfo = QDRService.topology.nodeInfo(); + var links = nodeInfo[n.key]['.router.link']; + var linkTypeIndex = links.attributeNames.indexOf('linkType'); + var connectionIdIndex = links.attributeNames.indexOf('connectionId'); + n.linkData = []; + links.results.forEach( function (linkArray) { + var link = QDRService.flatten(links.attributeNames, linkArray) + if (link.linkType === 'endpoint' && link.connectionId === n.connectionId) { + var l = {}; + l.owningAddr = link.owningAddr; + l.dir = link.linkDir; + if (l.owningAddr && l.owningAddr.length > 2) + if (l.owningAddr[0] === 'M') + l.owningAddr = l.owningAddr.substr(2) + else + l.owningAddr = l.owningAddr.substr(1) + + l.deliveryCount = QDRService.pretty(link.deliveryCount); + l.uncounts = QDRService.pretty(link.undeliveredCount + link.unsettledCount) + l.adminStatus = link.adminStatus; + l.operStatus = link.operStatus; + l.identity = link.identity + l.connectionId = link.connectionId + l.nodeId = n.key + l.type = link.type + l.name = link.name + + //QDR.log.debug("pushing link state for " + l.owningAddr + " status: "+ l.adminStatus) + n.linkData.push(l) + } + }) + $scope.multiData.push(n) + if (n.connectionId == $scope.connectionId) + $scope.linkData = n.linkData; + }) + } + $scope.$apply(); + + d3.select('#multiple_details') + .style({ + height: ((normals.length + 1) * 30) + 40 + "px", + 'overflow-y': normals.length > 10 ? 'scroll' : 'hidden' + }) + + } + + // call extendConnections whenever the background data is updated + QDRService.addUpdatedAction("normalsStats", extendConnections) + extendConnections(); + clearPopups(); + var visibility = 'visible' + var left = mouseX + $(document).scrollLeft() + var bounds = $("#topology").position() + if (d.normals.length === 1) { + visibility = 'hidden' + left = left - 30; + mouseY = mouseY - 20 + } + d3.select('#multiple_details') + .style({ + visibility: visibility, + opacity: 1, + left: (clickPos[0] + bounds.left) + "px", + top: (clickPos[1] + bounds.top) + "px"}) + if (d.normals.length === 1) { + // simulate a click on the connection to popup the link details + $scope.multiDetails.showLinksList( {entity: d} ) + } + } + + var stopUpdateConnectionsGrid = function () { + QDRService.delUpdatedAction("normalsStats"); + } + + 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(); + d3.select("#svg_legend svg").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 () { + // Don't update the underlying topology diagram if we are adding a new node. + // Once adding is completed, the topology will update automatically if it has changed + if ($scope.addingNode.step > 0) + return false; + 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(); + window.removeEventListener('resize', resize); + }); + + initForceGraph(); + saveChanged(); + QDRService.startUpdating(); + + function doAddDialog(NewRouterName) { + var d = $modal.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 = modal.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() + }; + }; + + return QDR; +}(QDR || {}));
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/0c58c381/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/topology/topology.download-controller.js ---------------------------------------------------------------------- diff --git a/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/topology/topology.download-controller.js b/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/topology/topology.download-controller.js new file mode 100644 index 0000000..2eb812f --- /dev/null +++ b/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/topology/topology.download-controller.js @@ -0,0 +1,150 @@ +/* +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) { + 'use strict'; + + angular + .module('horizon.dashboard.dispatch.topology') + .controller('horizon.dashboard.dispatch.topology.TopologyDownloadController', TopologyDownloadController); + + TopologyDownloadController.$inject = [ + '$scope', + 'horizon.dashboard.dispatch.comService' + ] + + function TopologyDownloadController($scope, QDRService, 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 output = $scope.output + "\n\n" + var blob = new Blob([output], { type: 'text/plain;charset=utf-16' }); + saveAs(blob, $scope.newRouterName); + } + + $scope.downloadPart = function (part) { + var linkName = part.link.__data__.source.name + 'additional.conf'; + var blob = new Blob([part.output], { type: 'text/plain;charset=utf-16' }); + saveAs(blob, linkName); + } + + $scope.done = function () { + dialog.close(); + } + }; + + return QDR; +}(QDR || {})); http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/0c58c381/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/topology/topology.form-controller.js ---------------------------------------------------------------------- diff --git a/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/topology/topology.form-controller.js b/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/topology/topology.form-controller.js new file mode 100644 index 0000000..19af366 --- /dev/null +++ b/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/topology/topology.form-controller.js @@ -0,0 +1,73 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +/** + * @module QDR + */ +var QDR = (function (QDR) { + 'use strict'; + + angular + .module('horizon.dashboard.dispatch.topology') + .controller('horizon.dashboard.dispatch.topology.TopologyFormController', TopologyFormController); + + TopologyFormController.$inject = [ + '$scope', + 'horizon.dashboard.dispatch.comService' + ]; + + function TopologyFormController($scope, QDRService) { + var fctrl = this; + $scope.attributes = [] + var nameTemplate = '<div title="{{row.entity.description}}" class="ngCellText"><span>{{row.entity.attributeName}}</span></div>'; + var valueTemplate = '<div title="{{row.entity.attributeValue}}" class="ngCellText"><span>{{row.entity.attributeValue}}</span></div>'; + $scope.topoGridOptions = { + data: 'attributes', + enableColumnResize: true, + multiSelect: false, + columnDefs: [ + { + field: 'attributeName', +// cellTemplate: nameTemplate, + displayName: 'Attribute' + }, + { + field: 'attributeValue', +// cellTemplate: valueTemplate, + displayName: 'Value' + } + ] + }; + $scope.form = '' + $scope.$on('showEntityForm', function (event, args) { + var attributes = args.attributes; + var entityTypes = QDRService.schema.entityTypes[args.entity].attributes; + attributes.forEach( function (attr) { + if (entityTypes[attr.attributeName] && entityTypes[attr.attributeName].description) + attr.description = entityTypes[attr.attributeName].description + }) + $scope.attributes = attributes; + $scope.form = args.entity; + }) + $scope.$on('showAddForm', function (event) { + $scope.form = 'add'; + }) + } + + return QDR; +}(QDR || {})); \ No newline at end of file http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/0c58c381/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/topology/topology.module.js ---------------------------------------------------------------------- diff --git a/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/topology/topology.module.js b/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/topology/topology.module.js new file mode 100644 index 0000000..e5a5242 --- /dev/null +++ b/console/dispatch-dashboard/dispatch/static/dashboard/dispatch/topology/topology.module.js @@ -0,0 +1,112 @@ +/* + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +(function () { + 'use strict'; + + angular + .module('horizon.dashboard.dispatch.topology', []) + .config(config) + .run(addTemplates) + + config.$inject = [ + '$provide', + '$windowProvider' + ]; + + addTemplates.$inject = [ + '$templateCache', + ]; + + /** + * @name config + * @param {Object} $provide + * @param {Object} $windowProvider + * @description Base path for the overview code + * @returns {undefined} No return value + */ + function config($provide, $windowProvider) { + var path = $windowProvider.$get().STATIC_URL + 'dashboard/dispatch/topology/'; + $provide.constant('horizon.dashboard.dispatch.topology.basePath', path); + } + + function addTemplates($templateCache) { + $templateCache.put("dispatch/topology.html", + "<div class=\"qdrTopology\" ng-controller=\"horizon.dashboard.dispatch.topology.TopologyController as ctrl\">" + + " <div>" + + "<!--" + + " <ul class=\"nav nav-tabs ng-scope qdrTopoModes\">" + + " <li ng-repeat=\"mode in modes\" ng-class=\"{active : isModeActive(mode.name), 'pull-right' : isRight(mode)}\" ng-click=\"selectMode('{{mode.name}}')\" >" + + " <a data-placement=\"bottom\" class=\"ng-binding\"> {{mode.name}} </a></li>" + + " </ul>" + + "-->" + + " <div id=\"topology\" ng-show=\"mode == 'Diagram'\"><!-- d3 toplogy here --></div>" + + " <div id=\"geology\" ng-show=\"mode == 'Globe'\"><!-- d3 globe here --></div>" + + " <div id=\"crosssection\"><!-- d3 pack here --></div>" + + " <!-- <div id=\"addRouter\" ng-show=\"mode == 'Add Node'\"></div> -->" + + " <div id=\"node_context_menu\" class=\"contextMenu\">" + + " <ul>" + + " <li class=\"na\" ng-class=\"{new: contextNode.cls == 'temp'}\" ng-click=\"addingNode.trigger = 'editNode'\">Edit...</li>" + + " <li class=\"na\" ng-class=\"{adding: addingNode.step > 0}\" ng-click=\"addingNode.step = 0\">Cancel add</li>" + + " <li class=\"context-separator\"></li>" + + " <li class=\"na\" ng-class=\"{'force-display': !isFixed()}\" ng-click=\"setFixed(true)\">Freeze in place</li>" + + " <li class=\"na\" ng-class=\"{'force-display': isFixed()}\" ng-click=\"setFixed(false)\">Unfreeze</li>" + + " </ul>" + + " </div>" + + " <div id=\"svg_context_menu\" class=\"contextMenu\">" + + " <ul>" + + " <li ng-click=\"addingNode.step = 2\">Add a new router</li>" + + " </ul>" + + " </div>" + + " <div id=\"link_context_menu\" class=\"contextMenu\">" + + " <ul>" + + " <li ng-click=\"reverseLink()\">Reverse connection direction</li>" + + " <li ng-click=\"removeLink()\">Remove connection</li>" + + " </ul>" + + " </div>" + + " <div id=\"svg_legend\"></div>" + + " <div id=\"multiple_details\">" + + " <h4 class=\"grid-title\">Connections</h4>" + + " <div class=\"grid\" ui-grid=\"multiDetails\" ui-grid-selection></div>" + + " </div>" + + " <div id=\"link_details\">" + + " <h4 class=\"grid-title\">Links</h4>" + + " <div class=\"grid\" ui-grid=\"linkDetails\" ui-grid-selection></div>" + + " </div>" + + " </div>" + + " <div ng-controller=\"horizon.dashboard.dispatch.topology.TopologyFormController as fctrl\">" + + " <div id=\"topologyForm\" ng-class=\"{selected : isSelected()}\">" + + " <!-- <div ng-repeat=\"form in forms\" ng-show=\"isVisible(form)\" ng-class='{selected : isSelected(form)}'> -->" + + " <div ng-if=\"form == 'router'\">" + + " <h3>Router Info</h3>" + + " <div class=\"grid\" ui-grid=\"topoGridOptions\"></div>" + + " </div>" + + " <div ng-if=\"form == 'connection'\">" + + " <h3>Connection Info</h3>" + + " <div class=\"grid\" ui-grid=\"topoGridOptions\"></div>" + + " </div>" + + " <div id=\"addNodeForm\" ng-show=\"form == 'add'\">" + + " <h3>Add a new router</h3>" + + " <ul>" + + " <li>Click on an existing router to create a connection to the new router</li>" + + " <li>Double-click on the new router to <button ng-click=\"editNewRouter()\">edit</button> its properties</li>" + + " <li ng-show=\"addingNode.hasLink\" >Right-click on a new connection to edit its properties</li>" + + " </ul>" + + " <button ng-click=\"cancel()\">Cancel</button>" + + " </div>" + + " </div>" + + " </div>" + + "</div>" + ); + } +})(); --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@qpid.apache.org For additional commands, e-mail: commits-h...@qpid.apache.org