Repository: qpid-dispatch Updated Branches: refs/heads/master 0d6dbac78 -> 3d79fda11
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/3d79fda1/console/stand-alone/plugin/html/qdrTopology.html ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/html/qdrTopology.html b/console/stand-alone/plugin/html/qdrTopology.html index b99e2ef..c94eb81 100644 --- a/console/stand-alone/plugin/html/qdrTopology.html +++ b/console/stand-alone/plugin/html/qdrTopology.html @@ -196,6 +196,39 @@ table.popupTable tr.header { table.popupTable td { padding: 0 4px; } + +.graticule { + fill: none; + stroke: #777; + stroke-width: .5px; + stroke-opacity: .5; +} + +g.geo path.land { + fill: #dcedf7; + stroke: #000; + stroke-opacity: 1; + stroke-width: 1px; +} + +.boundary { + fill: none; + stroke: #333; + stroke-width: 5px; +} + +.panel-group { + margin-bottom: 0; +} + +span.map-label { + display: inline-block; + width: 4em; +} +#main_container { + padding-left: 0; + padding-right: 0; +} </style> <div class="qdrTopology" ng-controller="QDR.TopologyController"> <div class="legend-container page-menu navbar-collapse collapse"> @@ -251,6 +284,20 @@ table.popupTable td { <div uib-accordion-group class="panel-default" is-open="legend.status.legendOpen" heading="Legend"> <div id="topo_svg_legend"></div> </div> + <div uib-accordion-group class="panel-default" is-open="legend.status.mapOpen" heading="Background map"> + <div id="topo_mapOptions"> + <div class="colorPicker"> + <ul> + <li> + <label><span class='map-label'>Land</span> <input id="areaColor" name="areaColor" type="color" ng-model="mapOptions.areaColor"/></label> + </li> + <li> + <label><span class='map-label'>Ocean</span> <input id="oceanColor" name="oceanColor" type="color" ng-model="mapOptions.oceanColor"/></label> + </li> + </ul> + </div> + </div> + </div> </uib-accordion> </div> <div class="diagram"> http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/3d79fda1/console/stand-alone/plugin/js/amqp/connection.js ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/js/amqp/connection.js b/console/stand-alone/plugin/js/amqp/connection.js index db21e01..b1eee5a 100644 --- a/console/stand-alone/plugin/js/amqp/connection.js +++ b/console/stand-alone/plugin/js/amqp/connection.js @@ -271,8 +271,9 @@ class ConnectionManager { this.connection = rhea.connect(c); }).bind(this)); } - sendMgmtQuery(operation) { - return this.send([], '/$management', operation); + sendMgmtQuery(operation, to) { + to = to || '/$management'; + return this.send([], to, operation); } sendQuery(toAddr, entity, attrs, operation) { operation = operation || 'QUERY'; http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/3d79fda1/console/stand-alone/plugin/js/amqp/topology.js ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/js/amqp/topology.js b/console/stand-alone/plugin/js/amqp/topology.js index e208a6f..6e806c9 100644 --- a/console/stand-alone/plugin/js/amqp/topology.js +++ b/console/stand-alone/plugin/js/amqp/topology.js @@ -74,48 +74,104 @@ class Topology { get() { return new Promise((function (resolve, reject) { this.connection.sendMgmtQuery('GET-MGMT-NODES') - .then((function (response) { - response = response.response; + .then((function (results) { + let response = results.response; if (Object.prototype.toString.call(response) === '[object Array]') { - var workInfo = {}; // if there is only one node, it will not be returned if (response.length === 0) { var parts = this.connection.getReceiverAddress().split('/'); parts[parts.length - 1] = '$management'; response.push(parts.join('/')); } - for (var i = 0; i < response.length; ++i) { - workInfo[response[i]] = {}; - } - var gotResponse = function (nodeName, entity, response) { - workInfo[nodeName][entity] = response; + let finish = function (workInfo) { + this._nodeInfo = utils.copy(workInfo); + this.onDone(this._nodeInfo); + resolve(this._nodeInfo); }; - var q = d3.queue(this.connection.availableQeueuDepth()); - for (var id in workInfo) { - for (var entity in this.entityAttribs) { - q.defer((this.q_fetchNodeInfo).bind(this), id, entity, this.entityAttribs[entity], q, gotResponse); - } - } - q.await((function () { - // filter out nodes that have no connection info - if (this.filtering) { - for (var id in workInfo) { - if (!(workInfo[id].connection)) { - this.flux = true; - delete workInfo[id]; + let connectedToEdge = function (response, workInfo) { + let routerId = null; + if (response.length === 1) { + let parts = response[0].split('/'); + // we are connected to an edge router + if (parts[1] === '_edge') { + // find the role:edge connection + let conn = workInfo[response[0]].connection; + if (conn) { + let roleIndex = conn.attributeNames.indexOf('role'); + for (let i=0; i<conn.results.length; i++) { + if (conn.results[i][roleIndex] === 'edge') { + let container = utils.valFor(conn.attributeNames, conn.results[i], 'container'); + return utils.idFromName(container, '_topo'); + } + } } } } - this._nodeInfo = utils.copy(workInfo); - this.onDone(this._nodeInfo); - resolve(this._nodeInfo); - }).bind(this)); + return routerId; + }; + this.doget(response) + .then( function (workInfo) { + // test for edge case + let routerId = connectedToEdge(response, workInfo); + if (routerId) { + let edgeId = response[0]; + this.connection.sendMgmtQuery('GET-MGMT-NODES', routerId) + .then((function (results) { + let response = results.response; + if (Object.prototype.toString.call(response) === '[object Array]') { + // special case of edge case: + // we are connected to an edge router that is connected to + // a router that is not connected to any other interior routers + if (response.length === 0) { + response = [routerId]; + } + this.doget(response) + .then( function (workInfo) { + finish.call(this, workInfo); + }.bind(this)); + + } + }).bind(this)); + } else { + finish.call(this, workInfo); + } + }.bind(this)); } }).bind(this), function (error) { reject(error); }); }).bind(this)); } + doget(ids) { + return new Promise((function (resolve) { + let workInfo = {}; + for (var i = 0; i < ids.length; ++i) { + workInfo[ids[i]] = {}; + } + var gotResponse = function (nodeName, entity, response) { + workInfo[nodeName][entity] = response; + }; + var q = d3.queue(this.connection.availableQeueuDepth()); + for (var id in workInfo) { + for (var entity in this.entityAttribs) { + q.defer((this.q_fetchNodeInfo).bind(this), id, entity, this.entityAttribs[entity], q, gotResponse); + } + } + q.await((function () { + // filter out nodes that have no connection info + if (this.filtering) { + for (var id in workInfo) { + if (!(workInfo[id].connection)) { + this.flux = true; + delete workInfo[id]; + } + } + } + resolve(workInfo); + }).bind(this)); + }).bind(this)); + } + onDone(result) { clearTimeout(this._getTimer); if (this.updating) http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/3d79fda1/console/stand-alone/plugin/js/amqp/utilities.js ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/js/amqp/utilities.js b/console/stand-alone/plugin/js/amqp/utilities.js index 7b10c57..c94b498 100644 --- a/console/stand-alone/plugin/js/amqp/utilities.js +++ b/console/stand-alone/plugin/js/amqp/utilities.js @@ -103,13 +103,22 @@ var utils = { }, // extract the name of the router from the router id nameFromId: function (id) { - // the router id looks like 'amqp:/topo/0/routerName/$management' + // the router id looks like 'amqp:/_topo/0/routerName/$management' var parts = id.split('/'); // handle cases where the router name contains a / - parts.splice(0, 3); // remove amqp, topo, 0 + parts.splice(0, parts.length - 2); // remove amqp, _topo, 0 parts.pop(); // remove $management return parts.join('/'); }, + + // construct a router id given a router name and type (_topo or _edge) + idFromName: function (name, type) { + let parts = ['amqp:', type, name, '$management']; + if (type === '_topo') + parts.splice(2, 0, '0'); + return parts.join('/'); + }, + // calculate the average rate of change per second for a list of fields on the given obj // store the historical raw values in storage[key] for future rate calcs // keep 'history' number of historical values http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/3d79fda1/console/stand-alone/plugin/js/chord/data.js ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/js/chord/data.js b/console/stand-alone/plugin/js/chord/data.js index d9625aa..6116436 100644 --- a/console/stand-alone/plugin/js/chord/data.js +++ b/console/stand-alone/plugin/js/chord/data.js @@ -114,7 +114,7 @@ class ChordData { // eslint-disable-line no-unused-vars if (link.linkType === 'endpoint' && link.linkDir === 'out' && !link.owningAddr.startsWith('Ltemp.')) { // keep track of the raw egress values as well as their ingress and egress routers and the address for (let j = 0; j < ingressRouters.length; j++) { - let messages = link.ingressHistogram[j]; + let messages = link.ingressHistogram ? link.ingressHistogram[j] : 0; if (messages) { values.push({ ingress: ingressRouters[j], http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/3d79fda1/console/stand-alone/plugin/js/topology/links.js ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/js/topology/links.js b/console/stand-alone/plugin/js/topology/links.js index d7f4110..dd722bb 100644 --- a/console/stand-alone/plugin/js/topology/links.js +++ b/console/stand-alone/plugin/js/topology/links.js @@ -79,6 +79,8 @@ export class Links { let source = 0; let client = 1.0; for (let id in nodeInfo) { + let parts = id.split('/'); + let routerType = parts[1]; // _topo || _edge let onode = nodeInfo[id]; if (!onode['connection']) continue; @@ -93,81 +95,85 @@ export class Links { let properties = connection.properties || {}; let dir = connection.dir; if (role == 'inter-router') { + // there are already 2 router nodes, just link them let connId = connection.container; let target = getContainerIndex(connId, nodeInfo, this.QDRService); if (target >= 0) { this.getLink(source, target, dir, '', source + '-' + target); } - } /* else if (role == "normal" || role == "on-demand" || role === "route-container")*/ { - // not an connection between routers, but an external connection - let name = this.QDRService.utilities.nameFromId(id) + '.' + connection.identity; + } + // handle external connections + let name = this.QDRService.utilities.nameFromId(id) + '.' + connection.identity; + // is this connection for a router connected to an edge router + if (role == 'edge' && routerType === '_edge') { + name = connection.container; + role = 'inter-router'; + } - // if we have any new clients, animate the force graph to position them - let position = localStorage[name] ? JSON.parse(localStorage[name]) : undefined; - if ((typeof position == 'undefined')) { - animate = true; - position = { - x: Math.round(nodes.get(source).x + 40 * Math.sin(client / (Math.PI * 2.0))), - y: Math.round(nodes.get(source).y + 40 * Math.cos(client / (Math.PI * 2.0))), - fixed: false - }; - //QDRLog.debug("new client pos (" + position.x + ", " + position.y + ")") - }// else QDRLog.debug("using previous location") - if (position.y > height) { - position.y = Math.round(nodes.get(source).y + 40 + Math.cos(client / (Math.PI * 2.0))); + // if we have any new clients, animate the force graph to position them + let position = localStorage[name] ? JSON.parse(localStorage[name]) : undefined; + if ((typeof position == 'undefined')) { + animate = true; + position = { + x: Math.round(nodes.get(source).x + 40 * Math.sin(client / (Math.PI * 2.0))), + y: Math.round(nodes.get(source).y + 40 * Math.cos(client / (Math.PI * 2.0))), + fixed: false + }; + } + if (position.y > height) { + position.y = Math.round(nodes.get(source).y + 40 + Math.cos(client / (Math.PI * 2.0))); + } + let existingNodeIndex = nodes.nodeExists(connection.container); + let normalInfo = nodes.normalExists(connection.container); + let node = nodes.getOrCreateNode(id, name, role, nodeInfo, nodes.getLength(), position.x, position.y, connection.container, j, position.fixed, properties); + let nodeType = this.QDRService.utilities.isAConsole(properties, connection.identity, role, node.key) ? 'console' : 'client'; + let cdir = getLinkDir(id, connection, onode, this.QDRService); + if (existingNodeIndex >= 0) { + // make a link between the current router (source) and the existing node + this.getLink(source, existingNodeIndex, dir, 'small', connection.name); + } else if (normalInfo.nodesIndex) { + // get node index of node that contained this connection in its normals array + let normalSource = this.getLinkSource(normalInfo.nodesIndex); + if (normalSource >= 0) { + if (cdir === 'unknown') + cdir = dir; + node.cdir = cdir; + nodes.add(node); + // create link from original node to the new node + this.getLink(this.links[normalSource].source, nodes.getLength()-1, cdir, 'small', connection.name); + // create link from this router to the new node + this.getLink(source, nodes.getLength()-1, cdir, 'small', connection.name); + // remove the old node from the normals list + nodes.get(normalInfo.nodesIndex).normals.splice(normalInfo.normalsIndex, 1); } - let existingNodeIndex = nodes.nodeExists(connection.container); - let normalInfo = nodes.normalExists(connection.container); - let node = nodes.getOrCreateNode(id, name, role, nodeInfo, nodes.getLength(), position.x, position.y, connection.container, j, position.fixed, properties); - let nodeType = this.QDRService.utilities.isAConsole(properties, connection.identity, role, node.key) ? 'console' : 'client'; - let cdir = getLinkDir(id, connection, onode, this.QDRService); - if (existingNodeIndex >= 0) { - // make a link between the current router (source) and the existing node - this.getLink(source, existingNodeIndex, dir, 'small', connection.name); - } else if (normalInfo.nodesIndex) { - // get node index of node that contained this connection in its normals array - let normalSource = this.getLinkSource(normalInfo.nodesIndex); - if (normalSource >= 0) { - if (cdir === 'unknown') - cdir = dir; - node.cdir = cdir; + } else if (role === 'normal' || role === 'edge') { + // normal nodes can be collapsed into a single node if they are all the same dir + if (cdir !== 'unknown') { + node.user = connection.user; + node.isEncrypted = connection.isEncrypted; + node.host = connection.host; + node.connectionId = connection.identity; + node.cdir = cdir; + // determine arrow direction by using the link directions + if (!normalsParent[nodeType+cdir]) { + normalsParent[nodeType+cdir] = node; nodes.add(node); - // create link from original node to the new node - this.getLink(this.links[normalSource].source, nodes.getLength()-1, cdir, 'small', connection.name); - // create link from this router to the new node - this.getLink(source, nodes.getLength()-1, cdir, 'small', connection.name); - // remove the old node from the normals list - nodes.get(normalInfo.nodesIndex).normals.splice(normalInfo.normalsIndex, 1); - } - } else if (role === 'normal') { - // normal nodes can be collapsed into a single node if they are all the same dir - if (cdir !== 'unknown') { - node.user = connection.user; - node.isEncrypted = connection.isEncrypted; - node.host = connection.host; - node.connectionId = connection.identity; - node.cdir = cdir; - // determine arrow direction by using the link directions - if (!normalsParent[nodeType+cdir]) { - normalsParent[nodeType+cdir] = node; - nodes.add(node); - node.normals = [node]; - // now add a link - this.getLink(source, nodes.getLength() - 1, cdir, 'small', connection.name); - client++; - } else { - normalsParent[nodeType+cdir].normals.push(node); - } + node.normals = [node]; + // now add a link + this.getLink(source, nodes.getLength() - 1, cdir, 'small', connection.name); + client++; } else { - node.id = nodes.getLength() - 1 + unknowns.length; - unknowns.push(node); + normalsParent[nodeType+cdir].normals.push(node); } } else { - nodes.add(node); - // now add a link - this.getLink(source, nodes.getLength() - 1, dir, 'small', connection.name); - client++; + node.id = nodes.getLength() - 1 + unknowns.length; + unknowns.push(node); } + } else { + nodes.add(node); + // now add a link + this.getLink(source, nodes.getLength() - 1, dir, 'small', connection.name); + client++; } } source++; http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/3d79fda1/console/stand-alone/plugin/js/topology/map.js ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/js/topology/map.js b/console/stand-alone/plugin/js/topology/map.js new file mode 100644 index 0000000..9886245 --- /dev/null +++ b/console/stand-alone/plugin/js/topology/map.js @@ -0,0 +1,255 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +/* global angular d3 topojson Promise */ +const maxnorth = 84; +const maxsouth = 60; +const MAPOPTIONSKEY = 'QDRMapOptions'; +const MAPPOSITIONKEY = 'QDRMapPosition'; +const defaultLandColor = '#A3D3E0'; +const defaultOceanColor = '#FFFFFF'; + +export class BackgroundMap { // eslint-disable-line no-unused-vars + constructor($scope, notifyFn) { + this.$scope = $scope; + this.initialized = false; + this.notify = notifyFn; + $scope.mapOptions = angular.fromJson(localStorage[MAPOPTIONSKEY]) || {areaColor: defaultLandColor, oceanColor: defaultOceanColor}; + initLast(this); + } + updateLandColor(color) { + localStorage[MAPOPTIONSKEY] = JSON.stringify(this.$scope.mapOptions); + d3.select('g.geo path.land') + .style('fill', color) + .style('stroke', d3.rgb(color).darker()); + } + updateOceanColor(color) { + localStorage[MAPOPTIONSKEY] = JSON.stringify(this.$scope.mapOptions); + if (!color) + color = this.$scope.mapOptions.oceanColor; + d3.select('g.geo rect.ocean') + .style('fill', color); + if (this.$scope.legend.status.mapOpen) { + d3.select('#main_container') + .style('background-color', color); + } else { + d3.select('#main_container') + .style('background-color', '#FFF'); + } + } + + init($scope, svg, width, height) { + return new Promise( (function (resolve, reject) { + + this.svg = svg; + this.width = width; + this.height = height; + // track last translation and scale event we processed + this.rotate = 20; + this.scaleExtent = [1, 10]; + + // handle ui events to change the colors + $scope.$watch('mapOptions.areaColor', function (newValue, oldValue) { + if (newValue !== oldValue) { + this.updateLandColor(newValue); + } + }.bind(this)); + $scope.$watch('mapOptions.oceanColor', function (newValue, oldValue) { + if (newValue !== oldValue) { + this.updateOceanColor(newValue); + } + }.bind(this)); + + // setup the projection with some defaults + this.projection = d3.geo.mercator() + .rotate([this.rotate,0]) + .scale(1) + .translate([width/2, height/2]); + + // this path will hold the land coordinates once they are loaded + this.geoPath = d3.geo.path() + .projection(this.projection); + + // set up the scale extent and initial scale for the projection + var b = getMapBounds(this.projection, Math.max(maxnorth, maxsouth)), + s = width/(b[1][0]-b[0][0]); + this.scaleExtent = [s, 15*s]; + + this.projection + .scale(this.scaleExtent[0]); + this.lastProjection = angular.fromJson(localStorage[MAPPOSITIONKEY]) || {rotate: 20, scale: this.scaleExtent[0], translate: [width/2, height/2]}; + + this.zoom = d3.behavior.zoom() + .scaleExtent(this.scaleExtent) + .scale(this.projection.scale()) + .translate([0,0]) // not linked directly to projection + .on('zoom', this.zoomed.bind(this)); + + this.geo = svg.append('g') + .attr('class', 'geo') + .style('opacity', this.$scope.legend.status.mapOpen ? '1': '0'); + + this.geo.append('rect') + .attr('class', 'ocean') + .attr('width', width) + .attr('height', height) + .attr('fill', '#FFF'); + + if (this.$scope.legend.status.mapOpen) + this.svg.call(this.zoom) + .on('dblclick.zoom', null); + + // async load of data file. calls resolve when this completes to let caller know + //d3.json('plugin/data/world-110m.json', function(error, world) { + d3.json('plugin/data/countries.json', function(error, world) { + if (error) + reject(error); + + this.geo.append('path') + .datum(topojson.feature(world, world.objects.countries)) + .attr('class', 'land') + .attr('d', this.geoPath) + .style('stroke', d3.rgb(this.$scope.mapOptions.areaColor).darker()); + + this.updateLandColor(this.$scope.mapOptions.areaColor); + this.updateOceanColor(this.$scope.mapOptions.oceanColor); + + // restore map rotate, scale, translate + this.restoreState(); + + // draw with current positions + this.geo.selectAll('.land') + .attr('d', this.geoPath); + + this.initialized = true; + resolve(); + }.bind(this)); + }.bind(this))); + } + + setMapOpacity(opacity) { + opacity = opacity ? 1 : 0; + if (this.geo) + this.geo.style('opacity', opacity); + } + restoreState() { + this.projection.rotate([this.lastProjection.rotate, 0]); + this.projection.translate(this.lastProjection.translate); + this.projection.scale(this.lastProjection.scale); + this.zoom.scale(this.lastProjection.scale); + this.zoom.translate(this.lastProjection.translate); + } + + // stop responding to pan/zoom events + cancelZoom() { + this.saveProjection(); + } + + // tell the svg to respond to mouse pan/zoom events + restartZoom() { + this.svg.call(this.zoom) + .on('dblclick.zoom', null); + this.restoreState(); + this.last.scale = null; + } + + getXY(lon, lat) { + return this.projection([lon, lat]); + } + getLonLat(x, y) { + return this.projection.invert([x, y]); + } + + zoomed() { + if (d3.event && !this.$scope.current_node && !this.$scope.mousedown_node && this.$scope.legend.status.mapOpen) { + let scale = d3.event.scale, + t = d3.event.translate, + dx = t[0]-this.last.translate[0], + dy = t[1]-this.last.translate[1], + yaw = this.projection.rotate()[0], + tp = this.projection.translate(); + // zoomed + if (scale != this.last.scale) { + // get the mouse's x,y relative to the svg + let top = d3.select('#main_container').node().offsetTop; + let left = d3.select('#main_container').node().offsetLeft; + let mx = d3.event.sourceEvent.clientX - left; + let my = d3.event.sourceEvent.clientY - top - 1; + + // get the lon,lat at the mouse position + let lonlat = this.projection.invert([mx, my]); + + // do the requested scale operation + this.projection.scale(scale); + + // get the lonlat that is under the mouse after the scale + let lonlat1 = this.projection.invert([mx, my]); + // calc the distance to rotate based on change in longitude + dx = lonlat1[0] - lonlat[0]; + // calc the distance to translate based on change in lattitude + dy = my - this.projection([0, lonlat[1]])[1]; + + // rotate the map so that the longitude under the mouse is where it was before the scale + this.projection.rotate([yaw+dx ,0, 0]); + + // translate the map so that the lattitude under the mouse is where it was before the scale + this.projection.translate([tp[0], tp[1]+dy]); + } else { + // rotate instead of translate in the x direction + this.projection.rotate([yaw+360.0*dx/this.width*this.scaleExtent[0]/scale, 0, 0]); + // translate only in the y direction. don't translate beyond the max lattitude north or south + var bnorth = getMapBounds(this.projection, maxnorth), + bsouth = getMapBounds(this.projection, maxsouth); + if (bnorth[0][1] + dy > 0) + dy = -bnorth[0][1]; + else if (bsouth[1][1] + dy < this.height) + dy = this.height-bsouth[1][1]; + this.projection.translate([tp[0],tp[1]+dy]); + } + this.last.scale = scale; + this.last.translate = t; + this.saveProjection(); + this.notify(); + } + // update the land path with our current projection + this.geo.selectAll('.land') + .attr('d', this.geoPath); + } + saveProjection() { + if (this.projection) { + this.lastProjection.rotate = this.projection.rotate()[0]; + this.lastProjection.scale = this.projection.scale(); + this.lastProjection.translate = this.projection.translate(); + localStorage[MAPPOSITIONKEY] = JSON.stringify(this.lastProjection); + } + } +} + +// find the top left and bottom right of current projection +function getMapBounds(projection, maxlat) { + var yaw = projection.rotate()[0], + xymax = projection([-yaw+180-1e-6,-maxlat]), + xymin = projection([-yaw-180+1e-6, maxlat]); + + return [xymin,xymax]; +} +function initLast(map) { + map.last = {translate: [0,0], scale: null}; +} + http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/3d79fda1/console/stand-alone/plugin/js/topology/nodes.js ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/js/topology/nodes.js b/console/stand-alone/plugin/js/topology/nodes.js index 45ef23f..4be7ff7 100644 --- a/console/stand-alone/plugin/js/topology/nodes.js +++ b/console/stand-alone/plugin/js/topology/nodes.js @@ -17,8 +17,9 @@ specific language governing permissions and limitations under the License. */ +/* global d3 Promise */ export class Node { - constructor(id, name, nodeType, properties, routerId, x, y, nodeIndex, resultIndex, fixed, connectionContainer) { + constructor(QDRService, id, name, nodeType, properties, routerId, x, y, nodeIndex, resultIndex, fixed, connectionContainer) { this.key = id; this.name = name; this.nodeType = nodeType; @@ -31,8 +32,108 @@ export class Node { this.fixed = !!+fixed; this.cls = ''; this.container = connectionContainer; + this.isConsole = QDRService.utilities.isConsole(this); + this.isArtemis = QDRService.utilities.isArtemis(this); } + title () { + let x = ''; + if (this.normals && this.normals.length > 1) + x = ' x ' + this.normals.length; + if (this.isConsole) + return 'Dispatch console' + x; + else if (this.isArtemis) + return 'Broker - Artemis' + x; + else if (this.properties.product == 'qpid-cpp') + return 'Broker - qpid-cpp' + x; + else if (this.nodeType === 'edge') + return 'Edge Router'; + else if (this.cdir === 'in') + return 'Sender' + x; + else if (this.cdir === 'out') + return 'Receiver' + x; + else if (this.cdir === 'both') + return 'Sender/Receiver' + x; + else if (this.nodeType === 'normal') + return 'client' + x; + else if (this.nodeType === 'on-demand') + return 'broker'; + else if (this.properties.product) { + return this.properties.product; + } + else { + return ''; + } + } + toolTip (QDRService) { + return new Promise( (function (resolve) { + if (this.nodeType === 'normal' || this.nodeType === 'edge') { + resolve(this.clientTooltip()); + } else + this.routerTooltip(QDRService) + .then( function (toolTip) { + resolve(toolTip); + }); + }.bind(this))); + } + + clientTooltip () { + let type = this.title(); + let title = `<table class="popupTable"><tr><td>Type</td><td>${type}</td></tr>`; + if (!this.normals || this.normals.length < 2) + title += `<tr><td>Host</td><td>${this.host}</td></tr>`; + else { + title += `<tr><td>Count</td><td>${this.normals.length}</td></tr>`; + } + title += '</table>'; + return title; + } + + routerTooltip (QDRService) { + return new Promise( (function (resolve) { + QDRService.management.topology.ensureEntities(this.key, [ + {entity: 'listener', attrs: ['role', 'port', 'http']}, + {entity: 'router', attrs: ['name', 'version', 'hostName']} + ], function () { + // update all the router title text + let nodes = QDRService.management.topology.nodeInfo(); + let node = nodes[this.key]; + let listeners = node['listener']; + let router = node['router']; + let r = QDRService.utilities.flatten(router.attributeNames, router.results[0]); + let title = '<table class="popupTable">'; + title += ('<tr><td>Router</td><td>' + r.name + '</td></tr>'); + if (r.hostName) + title += ('<tr><td>Host Name</td><td>' + r.hostHame + '</td></tr>'); + title += ('<tr><td>Version</td><td>' + r.version + '</td></tr>'); + let ports = []; + for (let l=0; l<listeners.results.length; l++) { + let listener = QDRService.utilities.flatten(listeners.attributeNames, listeners.results[l]); + if (listener.role === 'normal') { + ports.push(listener.port+''); + } + } + if (ports.length > 0) { + title += ('<tr><td>Ports</td><td>' + ports.join(', ') + '</td></tr>'); + } + title += '</table>'; + resolve(title); + return title; + }.bind(this)); + }.bind(this))); + } + } +const nodeProperties = { + // router types + 'inter-router': {radius: 28, linkDistance: [150, 70], charge: [-1800, -900]}, + '_edge': {radius: 20, linkDistance: [110, 55], charge: [-1350, -900]}, + '_topo': {radius: 28, linkDistance: [150, 70], charge: [-1800, -900]}, + // generated nodes from connections. key is from connection.role + 'normal': {radius: 15, linkDistance: [75, 40], charge: [-900, -900]}, + 'on-demand': {radius: 15, linkDistance: [75, 40], charge: [-900, -900]}, + 'route-container': {radius: 15, linkDistance: [75, 40], charge: [-900, -900]}, + 'edge': {radius: 20, linkDistance: [110, 55], charge: [-1350, -900]} +}; export class Nodes { constructor(QDRService, logger) { @@ -40,6 +141,39 @@ export class Nodes { this.QDRService = QDRService; this.logger = logger; } + static radius(type) { + if (nodeProperties[type].radius) + return nodeProperties[type].radius; + console.log(`Requested radius for unknown node type: ${type}`); + return 15; + } + static maxRadius() { + let max = 0; + for (let key in nodeProperties) { + max = Math.max(max, nodeProperties[key].radius); + } + return max; + } + // vary the following force graph attributes based on nodeCount + static forceScale (nodeCount, minmax) { + let count = Math.max(Math.min(nodeCount, 80), 6); + let x = d3.scale.linear() + .domain([6,80]) + .range(minmax); + return x(count); + } + linkDistance (d, nodeCount) { + let range = nodeProperties[d.target.nodeType].linkDistance; + return Nodes.forceScale(nodeCount, range); + } + charge (d, nodeCount) { + let charge = nodeProperties[d.nodeType].charge; + return Nodes.forceScale(nodeCount, charge); + } + gravity (d, nodeCount) { + return Nodes.forceScale(nodeCount, [0.0001, 0.1]); + } + getLength () { return this.nodes.length; } @@ -54,6 +188,8 @@ export class Nodes { this.nodes.some(function (n) { if (n.name === name) { n.fixed = b; + if (!b) + n.lat = n.lon = null; return true; } }); @@ -86,7 +222,12 @@ export class Nodes { } return normalInfo; } - savePositions () { + savePositions (nodes) { + if (!nodes) + nodes = this.nodes; + if (Object.prototype.toString.call(nodes) !== '[object Array]') { + nodes = [nodes]; + } this.nodes.forEach( function (d) { localStorage[d.name] = JSON.stringify({ x: Math.round(d.x), @@ -95,6 +236,46 @@ export class Nodes { }); }); } + // Convert node's x,y coordinates to longitude, lattitude + saveLonLat (backgroundMap, nodes) { + if (!backgroundMap) + return; + // didn't pass nodes, use all nodes + if (!nodes) + nodes = this.nodes; + // passed a single node, wrap it in an array + if (Object.prototype.toString.call(nodes) !== '[object Array]') { + nodes = [nodes]; + } + for (let i=0; i<nodes.length; i++) { + let n = nodes[i]; + if (n.fixed) { + let lonlat = backgroundMap.getLonLat(n.x, n.y); + if (lonlat) { + n.lon = lonlat[0]; + n.lat = lonlat[1]; + } + } else { + n.lon = n.lat = null; + } + } + } + // convert all nodes' longitude,lattitude to x,y coordinates + setXY (backgroundMap) { + if (!backgroundMap) + return; + for (let i=0; i<this.nodes.length; i++) { + let n = this.nodes[i]; + if (n.lon && n.lat) { + let xy = backgroundMap.getXY(n.lon, n.lat); + if (xy) { + n.x = n.px = xy[0]; + n.y = n.py = xy[1]; + } + } + } + } + find (connectionContainer, properties, name) { properties = properties || {}; for (let i=0; i<this.nodes.length; ++i) { @@ -114,7 +295,7 @@ export class Nodes { return gotNode; } let routerId = this.QDRService.utilities.nameFromId(id); - return new Node(id, name, nodeType, properties, routerId, x, y, + return new Node(this.QDRService, id, name, nodeType, properties, routerId, x, y, nodeIndex, resultIndex, fixed, connectionContainer); } add (obj) { @@ -153,10 +334,10 @@ export class Nodes { position.y = 200 - yInit; yInit *= -1; } - this.addUsing(id, name, 'inter-router', nodeInfo, this.nodes.length, position.x, position.y, name, undefined, position.fixed, {}); + let parts = id.split('/'); + this.addUsing(id, name, parts[1], nodeInfo, this.nodes.length, position.x, position.y, name, undefined, position.fixed, {}); } return animate; } - } http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/3d79fda1/console/stand-alone/plugin/js/topology/qdrTopology.js ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/js/topology/qdrTopology.js b/console/stand-alone/plugin/js/topology/qdrTopology.js index ed85f74..667f9ed 100644 --- a/console/stand-alone/plugin/js/topology/qdrTopology.js +++ b/console/stand-alone/plugin/js/topology/qdrTopology.js @@ -27,6 +27,7 @@ import { separateAddresses } from '../chord/filters.js'; import { Nodes } from './nodes.js'; import { Links } from './links.js'; import { nextHop, connectionPopupHTML } from './topoUtils.js'; +import { BackgroundMap } from './map.js'; /** * @module QDR */ @@ -36,25 +37,41 @@ export class TopologyController { let QDRLog = new QDRLogger($log, 'TopologyController'); const TOPOOPTIONSKEY = 'topoOptions'; - const radius = 25; - const radiusNormal = 15; // - nodes is an array of router/client info. these are the circles // - links is an array of connections between the routers. these are the lines with arrows let nodes = new Nodes(QDRService, QDRLog); let links = new Links(QDRService, QDRLog); let forceData = {nodes: nodes, links: links}; + + $scope.legendOptions = angular.fromJson(localStorage[TOPOOPTIONSKEY]) || {showTraffic: false, trafficType: 'dots', mapOpen: false, legendOpen: true}; + if (typeof $scope.legendOptions.mapOpen == 'undefined') + $scope.legendOptions.mapOpen = false; + if (typeof $scope.legendOptions.legendOpen == 'undefined') + $scope.legendOptions.legendOpen = false; + let backgroundMap = new BackgroundMap($scope, + // notify: called each time a pan/zoom is performed + function () { + if ($scope.legend.status.mapOpen) { + // set all the nodes' x,y position based on their saved lon,lat + nodes.setXY(backgroundMap); + nodes.savePositions(); + // redraw the nodes in their x,y position and let non-fixed nodes bungie + force.start(); + clearPopups(); + } + }); // urlPrefix is used when referring to svg:defs let urlPrefix = $location.absUrl(); urlPrefix = urlPrefix.split('#')[0]; - $scope.legendOptions = angular.fromJson(localStorage[TOPOOPTIONSKEY]) || {showTraffic: false, trafficType: 'dots'}; if (!$scope.legendOptions.trafficType) $scope.legendOptions.trafficType = 'dots'; - $scope.legend = {status: {legendOpen: true, optionsOpen: true}}; + $scope.legend = {status: {legendOpen: true, optionsOpen: true, mapOpen: false}}; $scope.legend.status.optionsOpen = $scope.legendOptions.showTraffic; + $scope.legend.status.mapOpen = $scope.legendOptions.mapOpen; let traffic = new Traffic($scope, $timeout, QDRService, separateAddresses, - radius, forceData, $scope.legendOptions.trafficType, urlPrefix); + Nodes.radius('inter-router'), forceData, $scope.legendOptions.trafficType, urlPrefix); // the showTraaffic checkbox was just toggled (or initialized) $scope.$watch('legend.status.optionsOpen', function () { @@ -72,21 +89,43 @@ export class TopologyController { localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions); if ($scope.legendOptions.showTraffic) { restart(); - traffic.setAnimationType($scope.legendOptions.trafficType, separateAddresses, radius); + traffic.setAnimationType($scope.legendOptions.trafficType, separateAddresses, Nodes.radius('inter-router')); traffic.start(); } }); + $scope.$watch('legend.status.mapOpen', function (newvalue, oldvalue) { + $scope.legendOptions.mapOpen = $scope.legend.status.mapOpen; + localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions); + // map was shown + if ($scope.legend.status.mapOpen && backgroundMap.initialized) { + // respond to pan/zoom events + backgroundMap.restartZoom(); + // set the main_container div's background color to the ocean color + backgroundMap.updateOceanColor(); + d3.select('g.geo') + .style('opacity', 1); + } else { + if (newvalue !== oldvalue) + backgroundMap.cancelZoom(); + // hide the map and reset the background color + d3.select('g.geo') + .style('opacity', 0); + d3.select('#main_container') + .style('background-color', '#FFF'); + } + }); // mouse event vars let selected_node = null, selected_link = null, mousedown_link = null, - mousedown_node = null, mouseover_node = null, mouseup_node = null, initial_mouse_down_position = null; $scope.schema = 'Not connected'; + $scope.current_node = null, + $scope.mousedown_node = null, $scope.contextNode = null; // node that is associated with the current context menu $scope.isRight = function(mode) { @@ -98,6 +137,7 @@ export class TopologyController { $scope.contextNode.fixed = b; nodes.setNodesFixed($scope.contextNode.name, b); nodes.savePositions(); + nodes.saveLonLat(backgroundMap, $scope.contextNode); } restart(); }; @@ -126,12 +166,6 @@ export class TopologyController { $('.contextMenu').fadeOut(200); }); - const radii = { - 'inter-router': 25, - 'normal': 15, - 'on-demand': 15, - 'route-container': 15, - }; let svg, lsvg; // main svg and legend svg let force; let animate = false; // should the force graph organize itself when it is displayed @@ -183,29 +217,6 @@ export class TopologyController { if (width <= 0 || height <= 0) return; - // vary the following force graph attributes based on nodeCount - // <= 6 routers returns min, >= 80 routers returns max, interpolate linearly - var forceScale = function(nodeCount, min, max) { - let count = Math.max(Math.min(nodeCount, 80), 6); - let x = d3.scale.linear() - .domain([6,80]) - .range([min, max]); - //QDRLog.debug("forceScale(" + nodeCount + ", " + min + ", " + max + " returns " + x(count) + " " + x(nodeCount)) - return x(count); - }; - var linkDistance = function (d, nodeCount) { - if (d.target.nodeType === 'inter-router') - return forceScale(nodeCount, 150, 70); - return forceScale(nodeCount, 75, 40); - }; - var charge = function (d, nodeCount) { - if (d.nodeType === 'inter-router') - return forceScale(nodeCount, -1800, -900); - return -900; - }; - var gravity = function (d, nodeCount) { - return forceScale(nodeCount, 0.0001, 0.1); - }; // initialize the nodes and links array from the QDRService.topology._nodeInfo object var initForceGraph = function() { forceData.nodes = nodes = new Nodes(QDRService, QDRLog); @@ -219,7 +230,6 @@ export class TopologyController { selected_node = null; selected_link = null; - nodes.savePositions(); d3.select('#SVG_ID').remove(); svg = d3.select('#topology') .append('svg') @@ -230,24 +240,38 @@ export class TopologyController { clearPopups(); }); + /* + var graticule = d3.geo.graticule(); + geo.append('path') + .datum(graticule) + .attr('class', 'graticule') + .attr('d', geoPath); + */ + // the legend d3.select('#topo_svg_legend svg').remove(); lsvg = d3.select('#topo_svg_legend') .append('svg') .attr('id', 'svglegend'); lsvg = lsvg.append('svg:g') - .attr('transform', `translate( ${(radii['inter-router'] + 2)},${(radii['inter-router'] + 2)})`) + .attr('transform', `translate(${Nodes.maxRadius()}, ${Nodes.maxRadius()})`) .selectAll('g'); // mouse event vars mousedown_link = null; - mousedown_node = null; + $scope.mousedown_node = null; mouseup_node = null; // initialize the list of nodes forceData.nodes = nodes = new Nodes(QDRService, QDRLog); animate = nodes.initialize(nodeInfo, localStorage, width, height); nodes.savePositions(); + // read the map data from the data file and build the map layer + backgroundMap.init($scope, svg, width, height) + .then( function () { + nodes.saveLonLat(backgroundMap); + backgroundMap.setMapOpacity($scope.legend.status.mapOpen); + }); // initialize the list of links let unknowns = []; @@ -261,12 +285,12 @@ export class TopologyController { .nodes(nodes.nodes) .links(links.links) .size([width, height]) - .linkDistance(function(d) { return linkDistance(d, nodeCount); }) - .charge(function(d) { return charge(d, nodeCount); }) + .linkDistance(function(d) { return nodes.linkDistance(d, nodeCount); }) + .charge(function(d) { return nodes.charge(d, nodeCount); }) .friction(.10) - .gravity(function(d) { return gravity(d, nodeCount); }) + .gravity(function(d) { return nodes.gravity(d, nodeCount); }) .on('tick', tick) - .on('end', function () {nodes.savePositions();}) + .on('end', function () {nodes.savePositions(); nodes.saveLonLat(backgroundMap);}) .start(); // This section adds in the arrows @@ -301,8 +325,8 @@ export class TopologyController { grad.append('stop').attr('offset', '50%').style('stop-color', '#F0F000'); // handles to link and node element groups - path = svg.append('svg:g').selectAll('path'), - circle = svg.append('svg:g').selectAll('g'); + path = svg.append('svg:g').attr('class', 'links').selectAll('path'), + circle = svg.append('svg:g').attr('class', 'nodes').selectAll('g'); // app starts here restart(false); @@ -339,7 +363,7 @@ export class TopologyController { setTimeout(continueForce, 100, extra); } }; - continueForce(forceScale(nodeCount, 0, 200)); // give large graphs time to settle down + continueForce(Nodes.forceScale(nodeCount, [0, 200])); // give large graphs time to settle down }; // To start up quickly, we only get the connection info for each router. @@ -367,7 +391,7 @@ export class TopologyController { }; function resetMouseVars() { - mousedown_node = null; + $scope.mousedown_node = null; mouseover_node = null; mouseup_node = null; mousedown_link = null; @@ -376,24 +400,17 @@ export class TopologyController { // update force layout (called automatically each iteration) function tick() { circle.attr('transform', function(d) { - let cradius; - if (d.nodeType == 'inter-router') { - cradius = d.left ? radius + 8 : radius; - } else { - cradius = d.left ? radiusNormal + 18 : radiusNormal; - } - d.x = Math.max(d.x, radiusNormal * 2); - d.y = Math.max(d.y, radiusNormal * 2); - d.x = Math.max(0, Math.min(width - cradius, d.x)); - d.y = Math.max(0, Math.min(height - cradius, d.y)); + let r = Nodes.radius(d.nodeType); + d.x = Math.max(Math.min(d.x, width - r), r); + d.y = Math.max(Math.min(d.y, height - r), r); + return `translate(${d.x},${d.y})`; }); - // draw directed edges with proper padding from node centers + // draw lines with arrows with proper padding from node centers path.attr('d', function(d) { - let sourcePadding, targetPadding, r; - - r = d.target.nodeType === 'inter-router' ? radius : radiusNormal - 18; + let sourcePadding, targetPadding; + let r = Nodes.radius(d.target.nodeType); sourcePadding = targetPadding = 0; let dtx = Math.max(targetPadding, Math.min(width - r, d.target.x)), dty = Math.max(targetPadding, Math.min(height - r, d.target.y)), @@ -568,12 +585,14 @@ export class TopologyController { appendCircle(g) .on('mouseover', function(d) { // mouseover a circle + $scope.current_node = d; QDRService.management.topology.delUpdatedAction('connectionPopupHTML'); - if (d.nodeType === 'normal') { - showClientTooltip(d, d3.event); - } else - showRouterTooltip(d, d3.event); - if (d === mousedown_node) + let e = d3.event; + d.toolTip(QDRService) + .then( function (toolTip) { + showToolTip(toolTip, e); + }); + if (d === $scope.mousedown_node) return; // enlarge target node d3.select(this).attr('transform', 'scale(1.1)'); @@ -590,6 +609,7 @@ export class TopologyController { }); }) .on('mouseout', function() { // mouse out for a circle + $scope.current_node = null; // unenlarge target node d3.select('#popover-div') .style('display', 'none'); @@ -599,15 +619,18 @@ export class TopologyController { restart(); }) .on('mousedown', function(d) { // mouse down for circle + backgroundMap.cancelZoom(); + $scope.current_node = d; if (d3.event.button !== 0) { // ignore all but left button return; } - mousedown_node = d; + $scope.mousedown_node = d; // mouse position relative to svg initial_mouse_down_position = d3.mouse(this.parentNode.parentNode.parentNode).slice(); }) .on('mouseup', function(d) { // mouse up for circle - if (!mousedown_node) + backgroundMap.restartZoom(); + if (!$scope.mousedown_node) return; selected_link = null; @@ -624,25 +647,31 @@ export class TopologyController { cur_mouse[1] != initial_mouse_down_position[1]) { d.fixed = true; nodes.setNodesFixed(d.name, true); + nodes.savePositions(d); + nodes.saveLonLat(backgroundMap, d); resetMouseVars(); restart(); return; } // if this node was selected, unselect it - if (mousedown_node === selected_node) { + if ($scope.mousedown_node === selected_node) { selected_node = null; } else { - if (d.nodeType !== 'normal' && d.nodeType !== 'on-demand') - selected_node = mousedown_node; + if (d.nodeType !== 'normal' && + d.nodeType !== 'on-demand' && + d.nodeType !== 'edge' && + d.nodeTYpe !== '_edge') + selected_node = $scope.mousedown_node; } clearAllHighlights(); - mousedown_node = null; + $scope.mousedown_node = null; if (!$scope.$$phase) $scope.$apply(); restart(false); }) .on('dblclick', function(d) { // circle + d3.event.preventDefault(); if (d.fixed) { d.fixed = false; nodes.setNodesFixed(d.name, false); @@ -691,18 +720,19 @@ export class TopologyController { let multiples = svg.selectAll('.multiple'); multiples.each(function(d) { let g = d3.select(this); + let r = Nodes.radius(d.nodeType); g.append('svg:text') - .attr('x', radiusNormal + 3) - .attr('y', Math.floor(radiusNormal / 2)) + .attr('x', r + 4) + .attr('y', Math.floor((r / 2) - 4)) .attr('class', 'subtext') - .text('x ' + d.normals.length); + .text('* ' + d.normals.length); }); // call createLegend in timeout because: // If we create the legend right away, then it will be destroyed when the accordian // gets initialized as the page loads. $timeout(createLegend); - if (!mousedown_node || !selected_node) + if (!$scope.mousedown_node || !selected_node) return; if (!start) @@ -714,51 +744,54 @@ export class TopologyController { } let createLegend = function () { // dynamically create the legend based on which node types are present - // the legend d3.select('#topo_svg_legend svg').remove(); lsvg = d3.select('#topo_svg_legend') .append('svg') .attr('id', 'svglegend'); lsvg = lsvg.append('svg:g') - .attr('transform', `translate(${(radii['inter-router'] + 2)},${(radii['inter-router'] + 2)})`) + .attr('transform', `translate(${Nodes.maxRadius()}, ${Nodes.maxRadius()})`) .selectAll('g'); let legendNodes = new Nodes(QDRService, QDRLog); legendNodes.addUsing('Router', '', 'inter-router', '', undefined, 0, 0, 0, 0, false, {}); - + if (!svg.selectAll('circle.edge').empty() || !svg.selectAll('circle._edge').empty()) { + legendNodes.addUsing('Router', 'Edge', 'edge', '', undefined, 0, 0, 1, 0, false, {}); + } if (!svg.selectAll('circle.console').empty()) { - legendNodes.addUsing('Console', 'Console', 'normal', '', undefined, 0, 0, 1, 0, false, { + legendNodes.addUsing('Console', 'Console', 'normal', '', undefined, 0, 0, 2, 0, false, { console_identifier: 'Dispatch console' }); } if (!svg.selectAll('circle.client.in').empty()) { - legendNodes.addUsing('Sender', 'Sender', 'normal', '', undefined, 0, 0, 2, 0, false, {}).cdir = 'in'; + legendNodes.addUsing('Sender', 'Sender', 'normal', '', undefined, 0, 0, 3, 0, false, {}).cdir = 'in'; } if (!svg.selectAll('circle.client.out').empty()) { - legendNodes.addUsing('Receiver', 'Receiver', 'normal', '', undefined, 0, 0, 3, 0, false, {}).cdir = 'out'; + legendNodes.addUsing('Receiver', 'Receiver', 'normal', '', undefined, 0, 0, 4, 0, false, {}).cdir = 'out'; } if (!svg.selectAll('circle.client.inout').empty()) { - legendNodes.addUsing('Sender/Receiver', 'Sender/Receiver', 'normal', '', undefined, 0, 0, 4, 0, false, {}).cdir = 'both'; + legendNodes.addUsing('Sender/Receiver', 'Sender/Receiver', 'normal', '', undefined, 0, 0, 5, 0, false, {}).cdir = 'both'; } if (!svg.selectAll('circle.qpid-cpp').empty()) { - legendNodes.addUsing('Qpid broker', 'Qpid broker', 'route-container', '', undefined, 0, 0, 5, 0, false, { + legendNodes.addUsing('Qpid broker', 'Qpid broker', 'route-container', '', undefined, 0, 0, 6, 0, false, { product: 'qpid-cpp' }); } if (!svg.selectAll('circle.artemis').empty()) { - legendNodes.addUsing('Artemis broker', 'Artemis broker', 'route-container', '', undefined, 0, 0, 6, 0, false, + legendNodes.addUsing('Artemis broker', 'Artemis broker', 'route-container', '', undefined, 0, 0, 7, 0, false, {product: 'apache-activemq-artemis'}); } if (!svg.selectAll('circle.route-container').empty()) { - legendNodes.addUsing('Service', 'Service', 'route-container', 'external-service', undefined, 0, 0, 7, 0, false, + legendNodes.addUsing('Service', 'Service', 'route-container', 'external-service', undefined, 0, 0, 8, 0, false, {product: ' External Service'}); } lsvg = lsvg.data(legendNodes.nodes, function(d) { - return d.key; + return d.key + d.name; }); + let cury = 0; let lg = lsvg.enter().append('svg:g') - .attr('transform', function(d, i) { - // 45px between lines and add 10px space after 1st line - return 'translate(0, ' + (45 * i + (i > 0 ? 10 : 0)) + ')'; + .attr('transform', function(d) { + let t = `translate(0, ${cury})`; + cury += (Nodes.radius(d.nodeType) * 2 + 10); + return t; }); appendCircle(lg); @@ -795,7 +828,7 @@ export class TopologyController { return g.append('svg:circle') .attr('class', 'node') .attr('r', function(d) { - return radii[d.nodeType]; + return Nodes.radius(d.nodeType); }) .attr('fill', function (d) { if (d.cdir === 'both' && !QDRService.utilities.isConsole(d)) { @@ -819,11 +852,14 @@ export class TopologyController { return d.cdir == 'both'; }) .classed('inter-router', function(d) { - return d.nodeType == 'inter-router'; + return d.nodeType == 'inter-router' || d.nodeType === '_topo'; }) .classed('on-demand', function(d) { return d.nodeType == 'on-demand'; }) + .classed('edge', function(d) { + return d.nodeType === 'edge' || d.nodeType === '_edge'; + }) .classed('console', function(d) { return QDRService.utilities.isConsole(d); }) @@ -854,6 +890,8 @@ export class TopologyController { y = 4; else if (d.nodeType === 'route-container') y = 5; + else if (d.nodeType === 'edge' || d.nodeType === '_edge') + y = 4; return y; }) .attr('class', 'id') @@ -866,6 +904,12 @@ export class TopologyController { .classed('on-demand', function(d) { return d.nodeType === 'on-demand'; }) + .classed('edge', function(d) { + return d.nodeType === 'edge'; + }) + .classed('edge', function(d) { + return d.nodeType === '_edge'; + }) .classed('artemis', function(d) { return QDRService.utilities.isArtemis(d); }) @@ -881,84 +925,20 @@ export class TopologyController { return '\ue901'; } else if (d.nodeType === 'route-container') { return d.properties.product ? d.properties.product[0].toUpperCase() : 'S'; - } else if (d.nodeType === 'normal') + } else if (d.nodeType === 'normal') { return '\uf109'; // icon-laptop for clients + } else if (d.nodeType === 'edge' || d.nodeType === '_edge') { + return 'Edge'; + } return d.name.length > 7 ? d.name.substr(0, 6) + '...' : d.name; }); }; let appendTitle = function(g) { g.append('svg:title').text(function(d) { - return generateTitle(d); + return d.title(); }); }; - let generateTitle = function (d) { - let x = ''; - if (d.normals && d.normals.length > 1) - x = ' x ' + d.normals.length; - if (QDRService.utilities.isConsole(d)) - return 'Dispatch console' + x; - else if (QDRService.utilities.isArtemis(d)) - return 'Broker - Artemis' + x; - else if (d.properties.product == 'qpid-cpp') - return 'Broker - qpid-cpp' + x; - else if (d.cdir === 'in') - return 'Sender' + x; - else if (d.cdir === 'out') - return 'Receiver' + x; - else if (d.cdir === 'both') - return 'Sender/Receiver' + x; - else if (d.nodeType === 'normal') - return 'client' + x; - else if (d.nodeType === 'on-demand') - return 'broker'; - else if (d.properties.product) { - return d.properties.product; - } - else { - return ''; - } - }; - - let showClientTooltip = function (d, event) { - let type = generateTitle(d); - let title = `<table class="popupTable"><tr><td>Type</td><td>${type}</td></tr>`; - if (!d.normals || d.normals.length < 2) - title += ('<tr><td>Host</td><td>' + d.host + '</td></tr>'); - title += '</table>'; - showToolTip(title, event); - }; - - let showRouterTooltip = function (d, event) { - QDRService.management.topology.ensureEntities(d.key, [ - {entity: 'listener', attrs: ['role', 'port', 'http']}, - {entity: 'router', attrs: ['name', 'version', 'hostName']} - ], function () { - // update all the router title text - let nodes = QDRService.management.topology.nodeInfo(); - let node = nodes[d.key]; - let listeners = node['listener']; - let router = node['router']; - let r = QDRService.utilities.flatten(router.attributeNames, router.results[0]); - let title = '<table class="popupTable">'; - title += ('<tr><td>Router</td><td>' + r.name + '</td></tr>'); - if (r.hostName) - title += ('<tr><td>Host Name</td><td>' + r.hostHame + '</td></tr>'); - title += ('<tr><td>Version</td><td>' + r.version + '</td></tr>'); - let ports = []; - for (let l=0; l<listeners.results.length; l++) { - let listener = QDRService.utilities.flatten(listeners.attributeNames, listeners.results[l]); - if (listener.role === 'normal') { - ports.push(listener.port+''); - } - } - if (ports.length > 0) { - title += ('<tr><td>Ports</td><td>' + ports.join(', ') + '</td></tr>'); - } - title += '</table>'; - showToolTip(title, event); - }); - }; let showToolTip = function (title, event) { // show the tooltip $timeout ( function () { @@ -1027,16 +1007,7 @@ export class TopologyController { savedKeys[key] = nodeInfo[key]['connection'].results.length; } } - // we are about to leave the page, save the node positions - $rootScope.$on('$locationChangeStart', function() { - //QDRLog.debug("locationChangeStart"); - nodes.savePositions(); - }); - // When the DOM element is removed from the page, - // AngularJS will trigger the $destroy event on - // the scope - $scope.$on('$destroy', function() { - //QDRLog.debug("scope on destroy"); + function destroy () { nodes.savePositions(); QDRService.management.topology.setUpdateEntities([]); QDRService.management.topology.stopUpdating(); @@ -1047,6 +1018,18 @@ export class TopologyController { d3.select('#SVG_ID').remove(); window.removeEventListener('resize', resize); traffic.stop(); + d3.select('#main_container') + .style('background-color', 'white'); + } + // When the DOM element is removed from the page, + // AngularJS will trigger the $destroy event on + // the scope + $scope.$on('$destroy', function() { + destroy(); + }); + // we are about to leave the page, save the node positions + $rootScope.$on('$locationChangeStart', function() { + destroy(); }); function handleInitialUpdate() { http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/3d79fda1/console/stand-alone/vendor-js.txt ---------------------------------------------------------------------- diff --git a/console/stand-alone/vendor-js.txt b/console/stand-alone/vendor-js.txt index 8fca75b..935d9a4 100644 --- a/console/stand-alone/vendor-js.txt +++ b/console/stand-alone/vendor-js.txt @@ -33,10 +33,14 @@ node_modules/angular-ui-bootstrap/dist/ui-bootstrap-tpls.js node_modules/angular-bootstrap-checkbox/angular-bootstrap-checkbox.js node_modules/bootstrap/dist/js/bootstrap.min.js node_modules/d3/d3.min.js +node_modules/d3-array/dist/d3-array.js +node_modules/d3-geo/dist/d3-geo.js +node_modules/d3-geo-projection/dist/d3-geo-projection.js node_modules/d3-queue/build/d3-queue.min.js node_modules/d3-time/build/d3-time.min.js node_modules/d3-time-format/build/d3-time-format.min.js node_modules/d3-path/build/d3-path.min.js +node_modules/topojson-client/dist/topojson-client.js node_modules/c3/c3.min.js node_modules/notifyjs-browser/dist/notify.js node_modules/patternfly/dist/js/patternfly.min.js --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@qpid.apache.org For additional commands, e-mail: commits-h...@qpid.apache.org