http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/81e58b46/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 54168ec..4ada081 100644 --- a/console/stand-alone/plugin/js/topology/qdrTopology.js +++ b/console/stand-alone/plugin/js/topology/qdrTopology.js @@ -21,60 +21,95 @@ under the License. /** * @module QDR */ -import { QDRLogger, QDRRedirectWhenConnected, QDRTemplatePath } from '../qdrGlobals.js'; -import { Traffic } from './traffic.js'; -import { separateAddresses } from '../chord/filters.js'; -import { Nodes } from './nodes.js'; -import { Links } from './links.js'; -import { nextHop, connectionPopupHTML, addStyles } from './topoUtils.js'; -import { BackgroundMap } from './map.js'; +import { QDRLogger, QDRRedirectWhenConnected, QDRTemplatePath } from "../qdrGlobals.js"; +import { Traffic } from "./traffic.js"; +import { separateAddresses } from "../chord/filters.js"; +import { Nodes } from "./nodes.js"; +import { Links } from "./links.js"; +import { nextHop, connectionPopupHTML, getSizes } from "./topoUtils.js"; +import { BackgroundMap } from "./map.js"; +import { utils } from "../amqp/utilities.js"; +import { Legend } from "./legend.js"; +import { appendCircle, appendContent, addGradient, addDefs, updateState } from "./svgUtils.js"; /** * @module QDR */ export class TopologyController { - constructor(QDRService, $scope, $log, $rootScope, $location, $timeout, $uibModal, $sce) { - this.controllerName = 'QDR.TopologyController'; - - let QDRLog = new QDRLogger($log, 'TopologyController'); - const TOPOOPTIONSKEY = 'topoOptions'; + constructor( + QDRService, + $scope, + $log, + $rootScope, + $location, + $timeout, + $uibModal, + $sce + ) { + this.controllerName = "QDR.TopologyController"; + + let QDRLog = new QDRLogger($log, "TopologyController"); + const TOPOOPTIONSKEY = "topoOptions"; // - 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(QDRLog); - let links = new Links(QDRLog); - let forceData = {nodes: nodes, links: links}; + let forceData = { + nodes: new Nodes(QDRLog), + links: new Links(QDRLog) + }; - $scope.legendOptions = angular.fromJson(localStorage[TOPOOPTIONSKEY]) || {showTraffic: false, trafficType: 'dots', mapOpen: false, legendOpen: true}; - if (typeof $scope.legendOptions.mapOpen == 'undefined') + // restore the state of the legend sections + $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') + if (typeof $scope.legendOptions.legendOpen == "undefined") $scope.legendOptions.legendOpen = false; - let backgroundMap = new BackgroundMap($scope, + 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(); + forceData.nodes.setXY(backgroundMap); + forceData.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]; + urlPrefix = urlPrefix.split("#")[0]; if (!$scope.legendOptions.trafficType) - $scope.legendOptions.trafficType = 'dots'; - $scope.legend = {status: {legendOpen: true, optionsOpen: true, mapOpen: false}}; + $scope.legendOptions.trafficType = "dots"; + $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, - Nodes.radius('inter-router'), forceData, $scope.legendOptions.trafficType, urlPrefix); + let traffic = new Traffic( + $scope, + $timeout, + QDRService, + separateAddresses, + Nodes.radius("inter-router"), + forceData, + $scope.legendOptions.trafficType, + urlPrefix + ); // the showTraaffic checkbox was just toggled (or initialized) - $scope.$watch('legend.status.optionsOpen', function () { + $scope.$watch("legend.status.optionsOpen", function () { $scope.legendOptions.showTraffic = $scope.legend.status.optionsOpen; localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions); if ($scope.legend.status.optionsOpen) { @@ -85,15 +120,21 @@ export class TopologyController { restart(); } }); - $scope.$watch('legendOptions.trafficType', function () { + // the traffic type was just changed or initialized + $scope.$watch("legendOptions.trafficType", function () { localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions); if ($scope.legendOptions.showTraffic) { restart(); - traffic.setAnimationType($scope.legendOptions.trafficType, separateAddresses, Nodes.radius('inter-router')); + traffic.setAnimationType( + $scope.legendOptions.trafficType, + separateAddresses, + Nodes.radius("inter-router") + ); traffic.start(); } }); - $scope.$watch('legend.status.mapOpen', function (newvalue, oldvalue) { + // the background map was shown or hidden + $scope.$watch("legend.status.mapOpen", function (newvalue, oldvalue) { $scope.legendOptions.mapOpen = $scope.legend.status.mapOpen; localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions); // map was shown @@ -102,16 +143,12 @@ export class TopologyController { backgroundMap.restartZoom(); // set the main_container div's background color to the ocean color backgroundMap.updateOceanColor(); - d3.select('g.geo') - .style('opacity', 1); + d3.select("g.geo").style("opacity", 1); } else { - if (newvalue !== oldvalue) - backgroundMap.cancelZoom(); + 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'); + d3.select("g.geo").style("opacity", 0); + d3.select("#main_container").style("background-color", "#FFF"); } }); @@ -121,37 +158,38 @@ export class TopologyController { mouseup_node = null, initial_mouse_down_position = null; - $scope.schema = 'Not connected'; - $scope.current_node = null, - $scope.mousedown_node = 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) { + $scope.isRight = function (mode) { return mode.right; }; + // show the details dialog for a client or group of clients function doDialog(d) { - $uibModal.open({ - backdrop: true, - keyboard: true, - backdropClick: true, - templateUrl: QDRTemplatePath + 'tmplClientDetail.html', - controller: 'QDR.DetailDialogController', - resolve: { - d: function() { - return d; + $uibModal + .open({ + backdrop: true, + keyboard: true, + backdropClick: true, + templateUrl: QDRTemplatePath + "tmplClientDetail.html", + controller: "QDR.DetailDialogController", + resolve: { + d: function () { + return d; + } } - } - }).result.then(function() { - }); + }) + .result.then(function () { }); } // called from the html page's popup menu - $scope.setFixed = function(b) { + $scope.setFixed = function (b) { if ($scope.contextNode) { - $scope.contextNode.setFixed(b); - nodes.savePositions(); - nodes.saveLonLat(backgroundMap, $scope.contextNode); + forceData.nodes.setFixed($scope.contextNode, b); + forceData.nodes.savePositions(); + forceData.nodes.saveLonLat(backgroundMap, $scope.contextNode); } // redraw the circles/links restart(); @@ -159,20 +197,25 @@ export class TopologyController { if (!b) { // let the nodes move to a new position animate = true; - force.start(); + force.start(); } }; - $scope.isFixed = function() { - if (!$scope.contextNode) - return false; - return ($scope.contextNode.fixed); + $scope.isFixed = function () { + if (!$scope.contextNode) return false; + return $scope.contextNode.fixed; + }; + $scope.addressStyle = function (address) { + return { + "background-color": $scope.addressColors[address] + }; }; let mouseX, mouseY; var relativeMouse = function () { - let offset = $('#main_container').offset(); - return {left: (mouseX + $(document).scrollLeft()) - 1, - top: (mouseY + $(document).scrollTop()) - 1, + let offset = $("#main_container").offset(); + return { + left: mouseX + $(document).scrollLeft() - 1, + top: mouseY + $(document).scrollTop() - 1, offset: offset }; }; @@ -182,66 +225,50 @@ export class TopologyController { mouseY = e.clientY; }); $(document).mousemove(); - $(document).click(function() { + $(document).click(function () { $scope.contextNode = null; - $('.contextMenu').fadeOut(200); + $(".contextMenu").fadeOut(200); }); - let svg, lsvg; // main svg and legend svg + let svg; // main svg let force; let animate = false; // should the force graph organize itself when it is displayed - let path, circle; - let savedKeys = {}; + let path, circle; // the d3 selections for links and nodes respectively + let savedKeys = {}; // so we can redraw the svg if the topology changes let width = 0; let height = 0; - var getSizes = function() { - const gap = 5; - let legendWidth = 194; - let topoWidth = $('#topology').width(); - if (topoWidth < 768) - legendWidth = 0; - let width = $('#topology').width() - gap - legendWidth; - let top = $('#topology').offset().top; - let height = window.innerHeight - top - gap; - if (width < 10) { - QDRLog.info(`page width and height are abnormal w: ${width} h: ${height}`); - return [0, 0]; - } - return [width, height]; - }; - var resize = function() { - if (!svg) - return; - let sizes = getSizes(); + var resize = function () { + if (!svg) return; + let sizes = getSizes(QDRLog); width = sizes[0]; height = sizes[1]; if (width > 0) { // set attrs and 'resume' force - svg.attr('width', width); - svg.attr('height', height); + svg.attr("width", width); + svg.attr("height", height); force.size(sizes).resume(); } - $timeout(createLegend); + $timeout(updateLegend); }; // the window is narrow and the page menu icon was clicked. // Re-create the legend - $scope.$on('pageMenuClicked', function () { - $timeout(createLegend); + $scope.$on("pageMenuClicked", function () { + $timeout(updateLegend); }); - window.addEventListener('resize', resize); - let sizes = getSizes(); + window.addEventListener("resize", resize); + let sizes = getSizes(QDRLog); width = sizes[0]; height = sizes[1]; - if (width <= 0 || height <= 0) - return; + if (width <= 0 || height <= 0) return; // initialize the nodes and links array from the QDRService.topology._nodeInfo object - var initForceGraph = function() { - forceData.nodes = nodes = new Nodes(QDRLog); - forceData.links = links = new Links(QDRLog); + var initForceGraph = function () { + if (width < 768) { + $scope.legend.status.mapOpen = false; + } let nodeInfo = QDRService.management.topology.nodeInfo(); let nodeCount = Object.keys(nodeInfo).length; @@ -250,110 +277,78 @@ export class TopologyController { mouseover_node = null; selected_node = null; - d3.select('#SVG_ID').remove(); - svg = d3.select('#topology') - .append('svg') - .attr('id', 'SVG_ID') - .attr('width', width) - .attr('height', height) - .on('click', function () { - clearPopups(); + if (d3.select("#SVG_ID").empty()) { + svg = d3 + .select("#topology") + .append("svg") + .attr("id", "SVG_ID") + .attr("width", width) + .attr("height", height) + .on("click", function () { + clearPopups(); + }); + // read the map data from the data file and build the map layer + backgroundMap.init($scope, svg, width, height).then(function () { + forceData.nodes.saveLonLat(backgroundMap); + backgroundMap.setMapOpacity($scope.legend.status.mapOpen); }); - - // the legend - //d3.select('#topo_svg_legend svg').remove(); - if (d3.select('#svglegend').empty()) { - lsvg = d3.select('#topo_svg_legend') - .append('svg') - .attr('id', 'svglegend'); - lsvg = lsvg.append('svg:g') - .attr('transform', `translate(${Nodes.maxRadius()}, ${Nodes.maxRadius()})`) - .selectAll('g'); + addDefs(svg); + addGradient(svg); + // handles to link and node element groups + path = svg.append("svg:g").attr("class", "links").selectAll("g"); + circle = svg.append("svg:g").attr("class", "nodes").selectAll("g"); } - // mouse event vars $scope.mousedown_node = null; mouseup_node = null; // initialize the list of nodes - 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); - }); + animate = forceData.nodes.initialize(nodeInfo, width, height, localStorage); + forceData.nodes.savePositions(); // initialize the list of links let unknowns = []; - if (links.initialize(nodeInfo, nodes, unknowns, localStorage, height)) { + if (forceData.links.initialize(nodeInfo, + forceData.nodes, + unknowns, + height, + localStorage)) animate = true; - } $scope.schema = QDRService.management.schema(); // init D3 force layout - force = d3.layout.force() - .nodes(nodes.nodes) - .links(links.links) + force = d3.layout + .force() + .nodes(forceData.nodes.nodes) + .links(forceData.links.links) .size([width, height]) - .linkDistance(function(d) { return nodes.linkDistance(d, nodeCount); }) - .charge(function(d) { return nodes.charge(d, nodeCount); }) - .friction(.10) - .gravity(function(d) { return nodes.gravity(d, nodeCount); }) - .on('tick', tick) - .on('end', function () {nodes.savePositions(); nodes.saveLonLat(backgroundMap);}) + .linkDistance(function (d) { + return forceData.nodes.linkDistance(d, nodeCount); + }) + .charge(function (d) { + return forceData.nodes.charge(d, nodeCount); + }) + .friction(0.1) + .gravity(function (d) { + return forceData.nodes.gravity(d, nodeCount); + }) + .on("tick", tick) + .on("end", function () { + forceData.nodes.savePositions(); + forceData.nodes.saveLonLat(backgroundMap); + }) .start(); - // This section adds in the arrows - // Generate a marker for each combination of: - // start|end, ''|selected highlighted, and each possible node radius - { - let sten = ['start', 'end']; - let states = ['', 'selected', 'highlighted', 'unknown']; - let radii = Nodes.discrete(); - let defs = []; - for (let isten=0; isten<sten.length; isten++) { - for (let istate=0; istate<states.length; istate++) { - for (let iradii=0; iradii<radii.length; iradii++) { - defs.push({sten: sten[isten], state: states[istate], r: radii[iradii]}); - } - } - } - svg.append('svg:defs').attr('class', 'marker-defs').selectAll('marker') - .data(defs) - .enter().append('svg:marker') - .attr('id', function (d) { return [d.sten, d.state, d.r].join('-'); }) - .attr('viewBox', '0 -5 10 10') - .attr('refX', function (d) { return Nodes.refX(d.sten, d.r); }) - .attr('markerWidth', 14) - .attr('markerHeight', 14) - .attr('markerUnits', 'userSpaceOnUse') - .attr('orient', 'auto') - .append('svg:path') - .attr('d', function (d) { - return d.sten === 'end' ? 'M 0 -5 L 10 0 L 0 5 z' : 'M 10 -5 L 0 0 L 10 5 z'; - }); - addStyles (sten, {selected: '#33F', highlighted: '#6F6', unknown: '#888'}, radii); - } - // gradient for sender/receiver client - let grad = svg.append('svg:defs').append('linearGradient') - .attr('id', 'half-circle') - .attr('x1', '0%') - .attr('x2', '0%') - .attr('y1', '100%') - .attr('y2', '0%'); - grad.append('stop').attr('offset', '50%').style('stop-color', '#C0F0C0'); - grad.append('stop').attr('offset', '50%').style('stop-color', '#F0F000'); - - // handles to link and node element groups - path = svg.append('svg:g').attr('class', 'links').selectAll('g'), - circle = svg.append('svg:g').attr('class', 'nodes').selectAll('g'); - // app starts here - //if (unknowns.length === 0) - restart(); + if (unknowns.length === 0) + restart(); + // the legend + // call updateLegend 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(updateLegend); + if (oldSelectedNode) { - d3.selectAll('circle.inter-router').classed('selected', function (d) { + d3.selectAll("circle.inter-router").classed("selected", function (d) { if (d.key === oldSelectedNode.key) { selected_node = d; return true; @@ -362,13 +357,19 @@ export class TopologyController { }); } if (oldMouseoverNode && selected_node) { - d3.selectAll('circle.inter-router').each(function (d) { + d3.selectAll("circle.inter-router").each(function (d) { if (d.key === oldMouseoverNode.key) { mouseover_node = d; - QDRService.management.topology.ensureAllEntities([{entity: 'router.node', attrs: ['id','nextHop']}], function () { - nextHopHighlight(selected_node, d); - restart(); - }); + QDRService.management.topology.ensureAllEntities( + [{ + entity: "router.node", + attrs: ["id", "nextHop"] + }], + function () { + nextHopHighlight(selected_node, d); + restart(); + } + ); } }); } @@ -384,38 +385,45 @@ export class TopologyController { setTimeout(continueForce, 100, extra); } }; - continueForce(Nodes.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. - // That means we don't have the router.link info when links.initialize() is first called. + // That means we don't have the router.link info when links.initialize() is first called + // and the initial graph is drawn. // The router.link info is needed to determine which direction the arrows between routers // and client should point. (Direction between interior routers is determined by connection.dir) - // So, the first time through links.initialize() we keep track of the nodes for which we + // So, the first time through links.initialize() we keep track of the nodes for which we // need router.link info and fill in that info here. var resolveUnknowns = function (nodeInfo, unknowns) { let unknownNodes = {}; // collapse the unknown nodes using an object - for (let i=0; i<unknowns.length; ++i) { + for (let i = 0; i < unknowns.length; ++i) { unknownNodes[unknowns[i]] = 1; } unknownNodes = Object.keys(unknownNodes); - QDRService.management.topology.ensureEntities(unknownNodes, - [{entity: 'router.link', - attrs: ['linkType','connectionId','linkDir','owningAddr'], - force: true}], + QDRService.management.topology.ensureEntities( + unknownNodes, + [{ + entity: "router.link", + attrs: ["linkType", "connectionId", "linkDir", "owningAddr"], + force: true + }], function () { let nodeInfo = QDRService.management.topology.nodeInfo(); - forceData.nodes = nodes = new Nodes(QDRLog); - nodes.initialize(nodeInfo, localStorage, width, height); - forceData.links = links = new Links(QDRLog); + forceData.nodes.initialize(nodeInfo, width, height, localStorage); let edgeUnknowns = []; - links.initialize(nodeInfo, nodes, edgeUnknowns, localStorage, height); + forceData.links.initialize(nodeInfo, forceData.nodes, edgeUnknowns, height, localStorage); animate = true; - force.nodes(nodes.nodes).links(links.links).start(); - nodes.saveLonLat(backgroundMap); + force + .nodes(forceData.nodes.nodes) + .links(forceData.links.links) + .start(); + forceData.nodes.saveLonLat(backgroundMap); restart(); - }); + updateLegend(); + } + ); }; function resetMouseVars() { @@ -427,7 +435,7 @@ export class TopologyController { // update force layout (called automatically each iteration) function tick() { // move the circles - circle.attr('transform', function(d) { + circle.attr("transform", function (d) { // don't let the edges of the circle go beyond the edges of the svg let r = Nodes.radius(d.nodeType); d.x = Math.max(Math.min(d.x, width - r), r); @@ -436,10 +444,9 @@ export class TopologyController { }); // draw lines from node centers - path.selectAll('path') - .attr('d', function(d) { - return `M${d.source.x},${d.source.y}L${d.target.x},${d.target.y}`; - }); + path.selectAll("path").attr("d", function (d) { + return `M${d.source.x},${d.source.y}L${d.target.x},${d.target.y}`; + }); if (!animate) { animate = true; @@ -448,218 +455,259 @@ export class TopologyController { } function nextHopHighlight(selected_node, d) { - nextHop(selected_node, d, nodes, links, QDRService, selected_node, function (hlLink, hnode) { - hlLink.highlighted = true; - hnode.highlighted = true; - }); - let hnode = nodes.nodeFor(d.name); + nextHop( + selected_node, + d, + forceData.nodes, + forceData.links, + QDRService.management.topology.nodeInfo(), + selected_node, + function (hlLink, hnode) { + hlLink.highlighted = true; + hnode.highlighted = true; + } + ); + let hnode = forceData.nodes.nodeFor(d.name); hnode.highlighted = true; } function clearPopups() { - d3.select('#crosssection').style('display', 'none'); - $('.hastip').empty(); - d3.select('#multiple_details').style('display', 'none'); - d3.select('#link_details').style('display', 'none'); - d3.select('#node_context_menu').style('display', 'none'); - d3.select('#popover-div').style('display', 'none'); + d3.select("#crosssection").style("display", "none"); + $(".hastip").empty(); + d3.select("#multiple_details").style("display", "none"); + d3.select("#link_details").style("display", "none"); + d3.select("#node_context_menu").style("display", "none"); + d3.select("#popover-div").style("display", "none"); } function clearAllHighlights() { - links.clearHighlighted(); - nodes.clearHighlighted(); + forceData.links.clearHighlighted(); + forceData.nodes.clearHighlighted(); } // Takes the forceData.nodes and forceData.links array and creates svg elements // Also updates any existing svg elements based on the updated values in forceData.nodes // and forceData.links function restart() { - if (!circle) - return; + if (!circle) return; circle.call(force.drag); // path is a selection of all g elements under the g.links svg:group // here we associate the links.links array with the {g.links g} selection // based on the link.uid - path = path.data(links.links, function(d) {return d.uid;}); + path = path.data(forceData.links.links, function (d) { + return d.uid; + }); // update each existing {g.links g.link} element - path.select('.link') - .classed('selected', function(d) { + path + .select(".link") + .classed("selected", function (d) { return d.selected; }) - .classed('highlighted', function(d) { + .classed("highlighted", function (d) { return d.highlighted; }) - .classed('unknown', function (d) { + .classed("unknown", function (d) { return !d.right && !d.left; }); // reset the markers based on current highlighted/selected - if (!$scope.legend.status.optionsOpen || $scope.legendOptions.trafficType === 'dots') { - path.select('.link') - .attr('marker-end', function(d) { - return d.right ? `url(${urlPrefix}#end${d.markerId('end')})` : null; + if ( + !$scope.legend.status.optionsOpen || + $scope.legendOptions.trafficType === "dots" + ) { + path + .select(".link") + .attr("marker-end", function (d) { + return d.right ? `url(${urlPrefix}#end${d.markerId("end")})` : null; }) - .attr('marker-start', function(d) { - return (d.left || (!d.left && !d.right)) ? `url(${urlPrefix}#start${d.markerId('start')})` : null; + .attr("marker-start", function (d) { + return d.left || (!d.left && !d.right) ? + `url(${urlPrefix}#start${d.markerId("start")})` : + null; }); } // add new links. if a link with a new uid is found in the data, add a new path - let enterpath = path.enter().append('g') - .on('mouseover', function(d) { // mouse over a path + let enterpath = path + .enter() + .append("g") + .on("mouseover", function (d) { + // mouse over a path let event = d3.event; d.selected = true; let updateTooltip = function () { $timeout(function () { if (d.selected) { - $scope.trustedpopoverContent = $sce.trustAsHtml(connectionPopupHTML(d, QDRService)); + $scope.trustedpopoverContent = $sce.trustAsHtml( + connectionPopupHTML( + d, + QDRService.management.topology.nodeInfo() + ) + ); displayTooltip(event); } }); }; // update the contents of the popup tooltip each time the data is polled - QDRService.management.topology.addUpdatedAction('connectionPopupHTML', updateTooltip); + QDRService.management.topology.addUpdatedAction( + "connectionPopupHTML", + updateTooltip + ); // request the data and update the tooltip as soon as it arrives QDRService.management.topology.ensureAllEntities( - [{ entity: 'router.link', force: true},{entity: 'connection'}], function () { + [{ + entity: "router.link", + force: true + }, { + entity: "connection" + }], + function () { updateTooltip(); - }); + } + ); // just show the tooltip with whatever data we have updateTooltip(); restart(); - }) - .on('mouseout', function(d) { // mouse out of a path - QDRService.management.topology.delUpdatedAction('connectionPopupHTML'); - d3.select('#popover-div') - .style('display', 'none'); + .on("mouseout", function (d) { + // mouse out of a path + QDRService.management.topology.delUpdatedAction( + "connectionPopupHTML" + ); + d3.select("#popover-div").style("display", "none"); d.selected = false; connectionPopupHTML(); restart(); }) // left click a path - .on('click', function () { + .on("click", function () { d3.event.stopPropagation(); clearPopups(); }); - enterpath.append('path') - .attr('class', 'link') - .attr('marker-end', function(d) { - return d.right ? `url(${urlPrefix}#end${d.markerId('end')})` : null; + enterpath + .append("path") + .attr("class", "link") + .attr("marker-end", function (d) { + return d.right ? `url(${urlPrefix}#end${d.markerId("end")})` : null; }) - .attr('marker-start', function(d) { - return (d.left || (!d.left && !d.right)) ? `url(${urlPrefix}#start${d.markerId('start')})` : null; + .attr("marker-start", function (d) { + return d.left || (!d.left && !d.right) ? + `url(${urlPrefix}#start${d.markerId("start")})` : + null; }) - .attr('id', function (d) { - const si = d.source.uid(QDRService); - const ti = d.target.uid(QDRService); - return ['path', si, ti].join('-'); + .attr("id", function (d) { + const si = d.source.uid(); + const ti = d.target.uid(); + return ["path", si, ti].join("-"); }) - .classed('unknown', function (d) { + .classed("unknown", function (d) { return !d.right && !d.left; }); - enterpath.append('path') - .attr('class', 'hittarget'); + enterpath.append("path").attr("class", "hittarget"); // remove old links path.exit().remove(); - // circle (node) group - // nodes are known by router id, or for groups, by the router id + 1st connectionId - circle = circle.data(nodes.nodes, function(d) { - return d.uid(); - }); + circle = d3.select("g.nodes").selectAll("g") + .data(forceData.nodes.nodes, function (d) { + return d.uid(); + }); // update existing nodes visual states - circle.selectAll('circle') - .classed('highlighted', function(d) { - return d.highlighted; - }) - .classed('selected', function(d) { - return (d === selected_node); - }) - .classed('fixed', function(d) { - return d.fixed; - }); - circle - .classed('multiple', function (d) { - return (d.normals && d.normals.length > 1); - }); + updateState(circle, selected_node); // add new circle nodes - let g = circle.enter().append('svg:g') - .classed('multiple', function(d) { - return (d.normals && d.normals.length > 1); - }) - .attr('id', function (d) { return (d.nodeType !== 'normal' ? 'router' : 'client') + '-' + d.index; }); + let enterCircle = circle + .enter() + .append("g") + .classed("multiple", function (d) { + return d.normals && d.normals.length > 1; + }) + .attr("id", function (d) { + return ( + (d.nodeType !== "normal" ? "router" : "client") + "-" + d.index + ); + }); - appendCircle(g) - .on('mouseover', function(d) { // mouseover a circle + appendCircle(enterCircle) + .on("mouseover", function (d) { + // mouseover a circle $scope.current_node = d; - QDRService.management.topology.delUpdatedAction('connectionPopupHTML'); + QDRService.management.topology.delUpdatedAction( + "connectionPopupHTML" + ); let e = d3.event; - d.toolTip(QDRService.management.topology) - .then( function (toolTip) { - showToolTip(toolTip, e); - }); - if (d === $scope.mousedown_node) - return; + d.toolTip(QDRService.management.topology).then(function (toolTip) { + showToolTip(toolTip, e); + }); + if (d === $scope.mousedown_node) return; // enlarge target node - d3.select(this).attr('transform', 'scale(1.1)'); + d3.select(this).attr("transform", "scale(1.1)"); if (!selected_node) { return; } // highlight the next-hop route from the selected node to this node clearAllHighlights(); // we need .router.node info to highlight hops - QDRService.management.topology.ensureAllEntities([{entity: 'router.node', attrs: ['id','nextHop']}], function () { - mouseover_node = d; // save this node in case the topology changes so we can restore the highlights - nextHopHighlight(selected_node, d); - restart(); - }); + QDRService.management.topology.ensureAllEntities( + [{ + entity: "router.node", + attrs: ["id", "nextHop"] + }], + function () { + mouseover_node = d; // save this node in case the topology changes so we can restore the highlights + nextHopHighlight(selected_node, d); + restart(); + } + ); }) - .on('mouseout', function() { // mouse out for a circle + .on("mouseout", function () { + // mouse out for a circle $scope.current_node = null; // unenlarge target node - d3.select('#popover-div') - .style('display', 'none'); - d3.select(this).attr('transform', ''); + d3.select("#popover-div").style("display", "none"); + d3.select(this).attr("transform", ""); clearAllHighlights(); mouseover_node = null; restart(); }) - .on('mousedown', function(d) { // mouse down for circle + .on("mousedown", function (d) { + // mouse down for circle backgroundMap.cancelZoom(); $scope.current_node = d; - if (d3.event.button !== 0) { // ignore all but left button + if (d3.event.button !== 0) { + // ignore all but left button return; } $scope.mousedown_node = d; // mouse position relative to svg - initial_mouse_down_position = d3.mouse(this.parentNode.parentNode.parentNode).slice(); + initial_mouse_down_position = d3 + .mouse(this.parentNode.parentNode.parentNode) + .slice(); }) - .on('mouseup', function(d) { // mouse up for circle + .on("mouseup", function (d) { + // mouse up for circle backgroundMap.restartZoom(); - if (!$scope.mousedown_node) - return; + if (!$scope.mousedown_node) return; // unenlarge target node - d3.select(this).attr('transform', ''); + d3.select(this).attr("transform", ""); // check for drag mouseup_node = d; - let mySvg = d3.select('#SVG_ID').node(); // if we dragged the node, make it fixed - let cur_mouse = d3.mouse(mySvg); - if (cur_mouse[0] != initial_mouse_down_position[0] || - cur_mouse[1] != initial_mouse_down_position[1]) { - d.setFixed(true); - nodes.savePositions(); - nodes.saveLonLat(backgroundMap); + let cur_mouse = d3.mouse(svg.node()); + if ( + cur_mouse[0] != initial_mouse_down_position[0] || + cur_mouse[1] != initial_mouse_down_position[1] + ) { + forceData.nodes.setFixed(d, true); + forceData.nodes.savePositions(); + forceData.nodes.saveLonLat(backgroundMap); resetMouseVars(); restart(); return; @@ -669,10 +717,12 @@ export class TopologyController { if ($scope.mousedown_node === selected_node) { selected_node = null; } else { - if (d.nodeType !== 'normal' && - d.nodeType !== 'on-demand' && - d.nodeType !== 'edge' && - d.nodeTYpe !== '_edge') + if ( + d.nodeType !== "normal" && + d.nodeType !== "on-demand" && + d.nodeType !== "edge" && + d.nodeTYpe !== "_edge" + ) selected_node = $scope.mousedown_node; } clearAllHighlights(); @@ -684,314 +734,115 @@ export class TopologyController { } // apply any data changes to the interface restart(); - }) - .on('dblclick', function(d) { // circle + .on("dblclick", function (d) { + // circle d3.event.preventDefault(); if (d.fixed) { - d.setFixed(false); + forceData.nodes.setFixed(d, false); restart(); // redraw the node without a dashed line force.start(); // let the nodes move to a new position } }) - .on('contextmenu', function(d) { // circle + .on("contextmenu", function (d) { + // circle $(document).click(); d3.event.preventDefault(); let rm = relativeMouse(); - d3.select('#node_context_menu') - .style({ - display: 'block', - left: rm.left + 'px', - top: (rm.top - rm.offset.top) + 'px' - }); - $timeout( function () { + d3.select("#node_context_menu").style({ + display: "block", + left: rm.left + "px", + top: rm.top - rm.offset.top + "px" + }); + $timeout(function () { $scope.contextNode = d; }); }) - .on('click', function(d) { // circle - if (!mouseup_node) - return; + .on("click", function (d) { + // circle + if (!mouseup_node) return; // clicked on a circle clearPopups(); if (!d.normals) { // circle was a router or a broker - if (QDRService.utilities.isArtemis(d)) { - const artemisPath = '/jmx/attributes?tab=artemis&con=Artemis'; - window.location = $location.protocol() + '://localhost:8161/hawtio' + artemisPath; + if (utils.isArtemis(d)) { + const artemisPath = "/jmx/attributes?tab=artemis&con=Artemis"; + window.location = + $location.protocol() + "://localhost:8161/hawtio" + artemisPath; } return; } d3.event.stopPropagation(); }); - appendContent(g); - //appendTitle(g); + appendContent(enterCircle); // remove old nodes circle.exit().remove(); // add text to client circles if there are any that represent multiple clients - svg.selectAll('.subtext').remove(); - let multiples = svg.selectAll('.multiple'); - multiples.each(function(d) { + svg.selectAll(".subtext").remove(); + 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', r + 4) - .attr('y', Math.floor((r / 2) - 4)) - .attr('class', 'subtext') - .text('* ' + d.normals.length); + g.append("svg:text") + .attr("x", r + 4) + .attr("y", Math.floor(r / 2 - 4)) + .attr("class", "subtext") + .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 (!$scope.mousedown_node || !selected_node) return; // set the graph in motion - //QDRLog.debug("mousedown_node is " + mousedown_node); force.start(); - } - let createLegend = function () { - // dynamically create the legend based on which node types are present - 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(${Nodes.maxRadius()}, ${Nodes.maxRadius()})`) - .selectAll('g'); - let legendNodes = new Nodes(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, 2, 0, false, { - console_identifier: 'Dispatch console' - }); - } - if (!svg.selectAll('circle.client.in').empty()) { - 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, 4, 0, false, {}).cdir = 'out'; - } - if (!svg.selectAll('circle.client.inout').empty()) { - 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, 6, 0, false, { - product: 'qpid-cpp' - }); - } - if (!svg.selectAll('circle.artemis').empty()) { - 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', undefined, 0, 0, 8, 0, false, - {product: ' External Service'}); - } - lsvg = lsvg.data(legendNodes.nodes, function(d) { - return d.key + d.name; - }); - let cury = 0; - let lg = lsvg.enter().append('svg:g') - .attr('transform', function(d) { - let t = `translate(0, ${cury})`; - cury += (Nodes.radius(d.nodeType) * 2 + 10); - return t; - }); - appendCircle(lg); - appendContent(lg); - appendTitle(lg); - lg.append('svg:text') - .attr('x', 35) - .attr('y', 6) - .attr('class', 'label') - .text(function(d) { - return d.key; - }); - lsvg.exit().remove(); - let svgEl = document.getElementById('svglegend'); - if (svgEl) { - let bb; - // firefox can throw an exception on getBBox on an svg element - try { - bb = svgEl.getBBox(); - } catch (e) { - bb = { - y: 0, - height: 200, - x: 0, - width: 200 - }; - } - svgEl.style.height = (bb.y + bb.height) + 'px'; - svgEl.style.width = (bb.x + bb.width) + 'px'; - } - }; - let 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 Nodes.radius(d.nodeType); - }) - .attr('fill', function (d) { - if (d.cdir === 'both' && !QDRService.utilities.isConsole(d)) { - return 'url(' + urlPrefix + '#half-circle)'; - } - return null; - }) - .classed('fixed', function(d) { - return d.fixed; - }) - .classed('normal', function(d) { - return d.nodeType == 'normal' || QDRService.utilities.isConsole(d); - }) - .classed('in', function(d) { - return d.cdir == 'in'; - }) - .classed('out', function(d) { - return d.cdir == 'out'; - }) - .classed('inout', function(d) { - return d.cdir == 'both'; - }) - .classed('inter-router', function(d) { - return d.nodeType == 'inter-router' || 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); - }) - .classed('artemis', function(d) { - return QDRService.utilities.isArtemis(d); - }) - .classed('qpid-cpp', function(d) { - return QDRService.utilities.isQpid(d); - }) - .classed('route-container', function (d) { - return (!QDRService.utilities.isArtemis(d) && !QDRService.utilities.isQpid(d) && d.nodeType === 'route-container'); - }) - .classed('client', function(d) { - return d.nodeType === 'normal' && !d.properties.console_identifier; - }); - }; - let appendContent = function(g) { - // show node IDs - g.append('svg:text') - .attr('x', 0) - .attr('y', function(d) { - let y = 7; - if (QDRService.utilities.isArtemis(d)) - y = 8; - else if (QDRService.utilities.isQpid(d)) - y = 9; - else if (d.nodeType === 'inter-router') - y = 4; - else if (d.nodeType === 'route-container') - y = 5; - else if (d.nodeType === 'edge' || d.nodeType === '_edge') - y = 4; - return y; - }) - .attr('class', 'id') - .classed('console', function(d) { - return QDRService.utilities.isConsole(d); - }) - .classed('normal', function(d) { - return d.nodeType === 'normal'; - }) - .classed('on-demand', function(d) { - return d.nodeType === 'on-demand'; - }) - .classed('edge', function(d) { - return d.nodeType === 'edge'; - }) - .classed('edge', function(d) { - return d.nodeType === '_edge'; - }) - .classed('artemis', function(d) { - return QDRService.utilities.isArtemis(d); - }) - .classed('qpid-cpp', function(d) { - return QDRService.utilities.isQpid(d); - }) - .text(function(d) { - if (QDRService.utilities.isConsole(d)) { - return '\uf108'; // icon-desktop for this console - } else if (QDRService.utilities.isArtemis(d)) { - return '\ue900'; - } else if (QDRService.utilities.isQpid(d)) { - return '\ue901'; - } else if (d.nodeType === 'route-container') { - return d.properties.product ? d.properties.product[0].toUpperCase() : 'S'; - } else if (d.nodeType === 'normal') { - return '\uf109'; // icon-laptop for clients - } else if (d.nodeType === 'edge' || d.nodeType === '_edge') { - return 'Edge'; - } - return d.name.length > 7 ? - d.name.substr(0, 3) + '...' + d.name.substr(d.name.length-3, 3) : - d.name; - }); - }; - let appendTitle = function(g) { - g.append('svg:title').text(function(d) { - return d.title(); - }); - }; + function updateLegend() { + // dynamically create/update the legend based on which node types are present + let lsvg = new Legend(svg); + lsvg.update(svg, QDRLog, urlPrefix); + } - let showToolTip = function (title, event) { + function showToolTip(title, event) { // show the tooltip - $timeout ( function () { + $timeout(function () { $scope.trustedpopoverContent = $sce.trustAsHtml(title); displayTooltip(event); }); - }; + } - let displayTooltip = function (event) { - $timeout( function () { - let top = $('#topology').offset().top - 5; - let width = $('#topology').width(); - d3.select('#popover-div') - .style('visibility', 'hidden') - .style('display', 'block') - .style('left', (event.pageX+5)+'px') - .style('top', (event.pageY-top)+'px'); - let pwidth = $('#popover-div').width(); - d3.select('#popover-div') - .style('visibility', 'visible') - .style('left',(Math.min(width-pwidth, event.pageX+5) + 'px')) - .on('mouseout', function () { - d3.select(this) - .style('display', 'none'); + function displayTooltip(event) { + $timeout(function () { + let top = $("#topology").offset().top - 5; + let width = $("#topology").width(); + d3.select("#popover-div") + .style("visibility", "hidden") + .style("display", "block") + .style("left", event.pageX + 5 + "px") + .style("top", event.pageY - top + "px"); + let pwidth = $("#popover-div").width(); + d3.select("#popover-div") + .style("visibility", "visible") + .style("left", Math.min(width - pwidth, event.pageX + 5) + "px") + .on("mouseout", function () { + d3.select(this).style("display", "none"); }); }); - }; + } function hasChanged() { // Don't update the underlying topology diagram if we are adding a new node. // Once adding is completed, the topology will update automatically if it has changed let nodeInfo = QDRService.management.topology.nodeInfo(); // don't count the nodes without connection info - let cnodes = Object.keys(nodeInfo).filter ( function (node) { - return (nodeInfo[node]['connection']); + let cnodes = Object.keys(nodeInfo).filter(function (node) { + return nodeInfo[node]["connection"]; }); - let routers = nodes.nodes.filter( function (node) { - return node.nodeType === '_topo'; + let routers = forceData.nodes.nodes.filter(function (node) { + return node.nodeType === "_topo"; }); if (routers.length > cnodes.length) { return -1; @@ -1000,14 +851,13 @@ export class TopologyController { return cnodes.length > Object.keys(savedKeys).length ? 1 : -1; } // we may have dropped a node and added a different node in the same update cycle - for (let i=0; i<cnodes.length; i++) { + for (let i = 0; i < cnodes.length; i++) { let key = cnodes[i]; // if this node isn't in the saved node list - if (!savedKeys.hasOwnProperty(key)) - return 1; + if (!savedKeys.hasOwnProperty(key)) return 1; // if the number of connections for this node chaanged - if (nodeInfo[key]['connection'].results.length !== savedKeys[key]) - return nodeInfo[key]['connection'].results.length - savedKeys[key]; + if (nodeInfo[key]["connection"].results.length !== savedKeys[key]) + return nodeInfo[key]["connection"].results.length - savedKeys[key]; } return 0; } @@ -1017,52 +867,53 @@ export class TopologyController { let nodeInfo = QDRService.management.topology.nodeInfo(); // save the number of connections per node for (let key in nodeInfo) { - if (nodeInfo[key]['connection']) - savedKeys[key] = nodeInfo[key]['connection'].results.length; + if (nodeInfo[key]["connection"]) + savedKeys[key] = nodeInfo[key]["connection"].results.length; } } - function destroy () { - nodes.savePositions(); + + function destroy() { + forceData.nodes.savePositions(); QDRService.management.topology.setUpdateEntities([]); QDRService.management.topology.stopUpdating(); - QDRService.management.topology.delUpdatedAction('normalsStats'); - QDRService.management.topology.delUpdatedAction('topology'); - QDRService.management.topology.delUpdatedAction('connectionPopupHTML'); + QDRService.management.topology.delUpdatedAction("normalsStats"); + QDRService.management.topology.delUpdatedAction("topology"); + QDRService.management.topology.delUpdatedAction("connectionPopupHTML"); - d3.select('#SVG_ID').remove(); - window.removeEventListener('resize', resize); + d3.select("#SVG_ID").remove(); + window.removeEventListener("resize", resize); traffic.stop(); - d3.select('#main_container') - .style('background-color', 'white'); + 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() { + $scope.$on("$destroy", function () { destroy(); }); // we are about to leave the page, save the node positions - $rootScope.$on('$locationChangeStart', function() { + $rootScope.$on("$locationChangeStart", function () { destroy(); }); function handleInitialUpdate() { // we only need to update connections during steady-state - QDRService.management.topology.setUpdateEntities(['connection']); + QDRService.management.topology.setUpdateEntities(["connection"]); // we currently have all entities available on all routers initForceGraph(); saveChanged(); // after the graph is displayed fetch all .router.node info. This is done so highlighting between nodes // doesn't incur a delay - QDRService.management.topology.addUpdateEntities([ - {entity: 'router.node', attrs: ['id','nextHop']} - ]); + QDRService.management.topology.addUpdateEntities([{ + entity: "router.node", + attrs: ["id", "nextHop"] + }]); // call this function every time a background update is done - QDRService.management.topology.addUpdatedAction('topology', function() { + QDRService.management.topology.addUpdatedAction("topology", function () { let changed = hasChanged(); // there is a new node, we need to get all of it's entities before drawing the graph if (changed > 0) { - QDRService.management.topology.delUpdatedAction('topology'); + QDRService.management.topology.delUpdatedAction("topology"); animate = true; setupInitialUpdate(); } else if (changed === -1) { @@ -1073,27 +924,36 @@ export class TopologyController { } else { //QDRLog.debug("topology didn't change") } - }); } + function setupInitialUpdate() { // make sure all router nodes have .connection info. if not then fetch any missing info QDRService.management.topology.ensureAllEntities( - [{entity: 'connection'}], - handleInitialUpdate); + [{ + entity: "connection" + }], + handleInitialUpdate + ); } if (!QDRService.management.connection.is_connected()) { // we are not connected. we probably got here from a bookmark or manual page reload - QDRRedirectWhenConnected($location, 'topology'); + QDRRedirectWhenConnected($location, "topology"); return; } - - animate = true; setupInitialUpdate(); QDRService.management.topology.startUpdating(true); - } } -TopologyController.$inject = ['QDRService', '$scope', '$log', '$rootScope', '$location', '$timeout', '$uibModal', '$sce']; +TopologyController.$inject = [ + "QDRService", + "$scope", + "$log", + "$rootScope", + "$location", + "$timeout", + "$uibModal", + "$sce" +]; \ No newline at end of file
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/81e58b46/console/stand-alone/plugin/js/topology/svgUtils.js ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/js/topology/svgUtils.js b/console/stand-alone/plugin/js/topology/svgUtils.js new file mode 100644 index 0000000..999bcf5 --- /dev/null +++ b/console/stand-alone/plugin/js/topology/svgUtils.js @@ -0,0 +1,249 @@ +/* + * Copyright 2018 Red Hat Inc. + * + * 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. + */ + +import { Nodes } from "./nodes.js"; +import { utils } from "../amqp/utilities.js"; + +export function updateState(circle, selected_node) { + circle + .selectAll("circle") + .classed("highlighted", function (d) { + return d.highlighted; + }) + .classed("selected", function (d) { + return d === selected_node; + }) + .classed("fixed", function (d) { + return d.fixed ? d.fixed & 1 : false; + }) + .classed("multiple", function (d) { + return d.normals && d.normals.length > 1; + }); +} + +export function appendCircle(g, urlPrefix) { + // add new circles and set their attr/class/behavior + return g + .append("svg:circle") + .attr("class", "node") + .attr("r", function (d) { + return Nodes.radius(d.nodeType); + }) + .attr("fill", function (d) { + if (d.cdir === "both" && !utils.isConsole(d)) { + return "url(" + urlPrefix + "#half-circle)"; + } + return null; + }) + .classed("fixed", function (d) { + return d.fixed ? d.fixed & 1 : false; + }) + .classed("normal", function (d) { + return d.nodeType == "normal" || utils.isConsole(d); + }) + .classed("in", function (d) { + return d.cdir == "in"; + }) + .classed("out", function (d) { + return d.cdir == "out"; + }) + .classed("inout", function (d) { + return d.cdir == "both"; + }) + .classed("inter-router", function (d) { + return d.nodeType == "inter-router" || 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 utils.isConsole(d); + }) + .classed("artemis", function (d) { + return utils.isArtemis(d); + }) + .classed("qpid-cpp", function (d) { + return utils.isQpid(d); + }) + .classed("route-container", function (d) { + return ( + !utils.isArtemis(d) && + !utils.isQpid(d) && + d.nodeType === "route-container" + ); + }) + .classed("client", function (d) { + return d.nodeType === "normal" && !d.properties.console_identifier; + }); +} + +export function appendContent(g) { + // show node IDs + g.append("svg:text") + .attr("x", 0) + .attr("y", function (d) { + let y = 7; + if (utils.isArtemis(d)) y = 8; + else if (utils.isQpid(d)) y = 9; + else if (d.nodeType === "inter-router") 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") + .classed("console", function (d) { + return utils.isConsole(d); + }) + .classed("normal", function (d) { + return d.nodeType === "normal"; + }) + .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 utils.isArtemis(d); + }) + .classed("qpid-cpp", function (d) { + return utils.isQpid(d); + }) + .text(function (d) { + if (utils.isConsole(d)) { + return "\uf108"; // icon-desktop for a console + } else if (utils.isArtemis(d)) { + return "\ue900"; // custom font character + } else if (utils.isQpid(d)) { + return "\ue901"; // custom font character + } else if (d.nodeType === "route-container") { + return d.properties.product ? + d.properties.product[0].toUpperCase() : + "S"; + } else if (d.nodeType === "normal") { + return "\uf109"; // icon-laptop for clients + } else if (d.nodeType === "edge" || d.nodeType === "_edge") { + return "Edge"; + } + return d.name.length > 7 ? + d.name.substr(0, 3) + "..." + d.name.substr(d.name.length - 3, 3) : + d.name; + }); +} + +export function appendTitle(g) { + g.append("svg:title").text(function (d) { + return d.title(); + }); +} + +// Generate a marker for each combination of: +// start|end, ''|selected highlighted, and each possible node radius +export function addDefs(svg) { + let sten = ["start", "end"]; + let states = ["", "selected", "highlighted", "unknown"]; + let radii = Nodes.discrete(); + let defs = []; + for (let isten = 0; isten < sten.length; isten++) { + for (let istate = 0; istate < states.length; istate++) { + for (let iradii = 0; iradii < radii.length; iradii++) { + defs.push({ + sten: sten[isten], + state: states[istate], + r: radii[iradii] + }); + } + } + } + svg + .append("svg:defs") + .attr("class", "marker-defs") + .selectAll("marker") + .data(defs) + .enter() + .append("svg:marker") + .attr("id", function (d) { + return [d.sten, d.state, d.r].join("-"); + }) + .attr("viewBox", "0 -5 10 10") + .attr("refX", function (d) { + return Nodes.refX(d.sten, d.r); + }) + .attr("markerWidth", 14) + .attr("markerHeight", 14) + .attr("markerUnits", "userSpaceOnUse") + .attr("orient", "auto") + .append("svg:path") + .attr("d", function (d) { + return d.sten === "end" ? + "M 0 -5 L 10 0 L 0 5 z" : + "M 10 -5 L 0 0 L 10 5 z"; + }); + addStyles( + sten, { + selected: "#33F", + highlighted: "#6F6", + unknown: "#888" + }, + radii + ); +} +export function addGradient(svg) { + // gradient for sender/receiver client + let grad = svg + .append("svg:defs") + .append("linearGradient") + .attr("id", "half-circle") + .attr("x1", "0%") + .attr("x2", "0%") + .attr("y1", "100%") + .attr("y2", "0%"); + grad + .append("stop") + .attr("offset", "50%") + .style("stop-color", "#C0F0C0"); + grad + .append("stop") + .attr("offset", "50%") + .style("stop-color", "#F0F000"); +} + +function addStyles(stend, stateColor, radii) { + // the <style> + let element = document.querySelector("style"); + // Reference to the stylesheet + let sheet = element.sheet; + + let states = Object.keys(stateColor); + // create styles for each combo of 'stend-state-radii' + for (let istend = 0; istend < stend.length; istend++) { + for (let istate = 0; istate < states.length; istate++) { + let selectors = []; + for (let iradii = 0; iradii < radii.length; iradii++) { + selectors.push(`#${stend[istend]}-${states[istate]}-${radii[iradii]}`); + } + let color = stateColor[states[istate]]; + let sels = `${selectors.join(",")} {fill: ${color}; stroke: ${color};}`; + sheet.insertRule(sels, 0); + } + } +} http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/81e58b46/console/stand-alone/plugin/js/topology/topoUtils.js ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/js/topology/topoUtils.js b/console/stand-alone/plugin/js/topology/topoUtils.js index 71e5e4c..99e2c9a 100644 --- a/console/stand-alone/plugin/js/topology/topoUtils.js +++ b/console/stand-alone/plugin/js/topology/topoUtils.js @@ -18,36 +18,44 @@ under the License. */ /* global Set */ +import { + utils +} from "../amqp/utilities.js"; // highlight the paths between the selected node and the hovered node -function findNextHopNode(from, d, QDRService, selected_node, nodes) { +function findNextHopNode(from, d, nodeInfo, selected_node, nodes) { // d is the node that the mouse is over // from is the selected_node .... - if (!from) - return null; + if (!from) return null; - if (from == d) - return selected_node; + if (from == d) return selected_node; - let sInfo = QDRService.management.topology.nodeInfo()[from.key]; + let sInfo = nodeInfo[from.key]; // find the hovered name in the selected name's .router.node results - if (!sInfo['router.node']) - return null; - let aAr = sInfo['router.node'].attributeNames; - let vAr = sInfo['router.node'].results; + if (!sInfo["router.node"]) return null; + let aAr = sInfo["router.node"].attributeNames; + let vAr = sInfo["router.node"].results; for (let hIdx = 0; hIdx < vAr.length; ++hIdx) { - let addrT = QDRService.utilities.valFor(aAr, vAr[hIdx], 'id'); - if (d.name && (addrT == d.name)) { - let next = QDRService.utilities.valFor(aAr, vAr[hIdx], 'nextHop'); - return (next == null) ? nodes.nodeFor(addrT) : nodes.nodeFor(next); + let addrT = utils.valFor(aAr, vAr[hIdx], "id"); + if (d.name && addrT == d.name) { + let next = utils.valFor(aAr, vAr[hIdx], "nextHop"); + return next == null ? nodes.nodeFor(addrT) : nodes.nodeFor(next); } } return null; } -export function nextHop(thisNode, d, nodes, links, QDRService, selected_node, cb) { - if ((thisNode) && (thisNode != d)) { - let target = findNextHopNode(thisNode, d, QDRService, selected_node, nodes); +export function nextHop( + thisNode, + d, + nodes, + links, + nodeInfo, + selected_node, + cb +) { + if (thisNode && thisNode != d) { + let target = findNextHopNode(thisNode, d, nodeInfo, selected_node, nodes); if (target) { let hnode = nodes.nodeFor(thisNode.name); let hlLink = links.linkFor(hnode, target); @@ -55,164 +63,177 @@ export function nextHop(thisNode, d, nodes, links, QDRService, selected_node, cb if (cb) { cb(hlLink, hnode, target); } - } - else - target = null; + } else target = null; } - nextHop(target, d, nodes, links, QDRService, selected_node, cb); + nextHop(target, d, nodes, links, nodeInfo, selected_node, cb); } } let linkRateHistory = {}; -export function connectionPopupHTML (d, QDRService) { +export function connectionPopupHTML(d, nodeInfo) { if (!d) { linkRateHistory = {}; return; } - let utils = QDRService.utilities; // return all of onode's connections that connecto to right let getConnsArray = function (onode, key, right) { if (right.normals) { // if we want connections between a router and a client[s] let connIds = new Set(); - let connIndex = onode.connection.attributeNames.indexOf('identity'); - for (let n=0; n<right.normals.length; n++) { + let connIndex = onode.connection.attributeNames.indexOf("identity"); + for (let n = 0; n < right.normals.length; n++) { let normal = right.normals[n]; if (normal.key === key) { connIds.add(normal.connectionId); } else if (normal.alsoConnectsTo) { - normal.alsoConnectsTo.forEach( function (ac2) { - if (ac2.key === key) - connIds.add(ac2.connectionId); + normal.alsoConnectsTo.forEach(function (ac2) { + if (ac2.key === key) connIds.add(ac2.connectionId); }); } } - return onode.connection.results.filter( function (result) { - return connIds.has(result[connIndex]); - }).map( function (c) { - return utils.flatten(onode.connection.attributeNames, c); - }); - } - else { - // we want the connection between two routers + return onode.connection.results + .filter(function (result) { + return connIds.has(result[connIndex]); + }) + .map(function (c) { + return utils.flatten(onode.connection.attributeNames, c); + }); + } else { + // we want the connection between two routers let container = utils.nameFromId(right.key); - let containerIndex = onode.connection.attributeNames.indexOf('container'); - let roleIndex = onode.connection.attributeNames.indexOf('role'); - return onode.connection.results.filter( function (conn) { - return conn[containerIndex] === container && conn[roleIndex] === 'inter-router'; - }).map( function (c) { - return utils.flatten(onode.connection.attributeNames, c); - }); + let containerIndex = onode.connection.attributeNames.indexOf("container"); + let roleIndex = onode.connection.attributeNames.indexOf("role"); + return onode.connection.results + .filter(function (conn) { + return ( + conn[containerIndex] === container && + conn[roleIndex] === "inter-router" + ); + }) + .map(function (c) { + return utils.flatten(onode.connection.attributeNames, c); + }); } }; // construct HTML to be used in a popup when the mouse is moved over a link. // The HTML is sanitized elsewhere before it is displayed let linksHTML = function (onode, conns) { const max_links = 10; - const fields = ['deliveryCount', 'undeliveredCount', 'unsettledCount', 'rejectedCount', 'releasedCount', 'modifiedCount']; + const fields = [ + "deliveryCount", + "undeliveredCount", + "unsettledCount", + "rejectedCount", + "releasedCount", + "modifiedCount" + ]; // local function to determine if a link's connectionId is in any of the connections let isLinkFor = function (connectionId, conns) { - for (let c=0; c<conns.length; c++) { - if (conns[c].identity === connectionId) - return true; + for (let c = 0; c < conns.length; c++) { + if (conns[c].identity === connectionId) return true; } return false; }; let fnJoin = function (ar, sepfn) { - let out = ''; + let out = ""; out = ar[0]; - for (let i=1; i<ar.length; i++) { - let sep = sepfn(ar[i], i===ar.length-1); - out += (sep[0] + sep[1]); + for (let i = 1; i < ar.length; i++) { + let sep = sepfn(ar[i], i === ar.length - 1); + out += sep[0] + sep[1]; } return out; }; // if the data for the line is from a client (small circle), we may have multiple connections // loop through all links for this router and accumulate those belonging to the connection(s) - let nodeLinks = onode['router.link']; - if (!nodeLinks) - return ''; + let nodeLinks = onode["router.link"]; + if (!nodeLinks) return ""; let links = []; let hasAddress = false; - for (let n=0; n<nodeLinks.results.length; n++) { + for (let n = 0; n < nodeLinks.results.length; n++) { let link = utils.flatten(nodeLinks.attributeNames, nodeLinks.results[n]); let allZero = true; - if (link.linkType !== 'router-control') { + if (link.linkType !== "router-control") { if (isLinkFor(link.connectionId, conns)) { - if (link.owningAddr) - hasAddress = true; + if (link.owningAddr) hasAddress = true; if (link.name) { - let rates = utils.rates(link, fields, linkRateHistory, link.name, 1); + let rates = utils.rates( + link, + fields, + linkRateHistory, + link.name, + 1 + ); // replace the raw value with the rate - for (let i=0; i<fields.length; i++) { - if (rates[fields[i]] > 0) - allZero = false; + for (let i = 0; i < fields.length; i++) { + if (rates[fields[i]] > 0) allZero = false; link[fields[i]] = rates[fields[i]]; } } - if (!allZero) - links.push(link); + if (!allZero) links.push(link); } } } // we may need to limit the number of links displayed, so sort descending by the sum of the field values let sum = function (a) { let s = 0; - for (let i=0; i<fields.length; i++) { + for (let i = 0; i < fields.length; i++) { s += a[fields[i]]; } return s; }; - links.sort( function (a, b) { + links.sort(function (a, b) { let asum = sum(a); let bsum = sum(b); return asum < bsum ? 1 : asum > bsum ? -1 : 0; }); - let HTMLHeading = '<h5>Rates (per second) for links</h5>'; + let HTMLHeading = "<h5>Rates (per second) for links</h5>"; let HTML = '<table class="popupTable">'; // copy of fields since we may be prepending an address let th = fields.slice(); let td = fields; - th.unshift('dir'); - td.unshift('linkDir'); - th.push('priority'); - td.push('priority'); + th.unshift("dir"); + td.unshift("linkDir"); + th.push("priority"); + td.push("priority"); // add an address field if any of the links had an owningAddress if (hasAddress) { - th.unshift('address'); - td.unshift('owningAddr'); + th.unshift("address"); + td.unshift("owningAddr"); } let rate_th = function (th) { - let rth = th.map( function (t) { - if (t.endsWith('Count')) - t = t.replace('Count', 'Rate'); + let rth = th.map(function (t) { + if (t.endsWith("Count")) t = t.replace("Count", "Rate"); return utils.humanify(t); }); return rth; }; - HTML += ('<tr class="header"><td>' + rate_th(th).join('</td><td>') + '</td></tr>'); + HTML += + '<tr class="header"><td>' + rate_th(th).join("</td><td>") + "</td></tr>"; // add rows to the table for each link - for (let l=0; l<links.length; l++) { - if (l>=max_links) { + for (let l = 0; l < links.length; l++) { + if (l >= max_links) { HTMLHeading = `<h5>Rates (per second) for top ${max_links} links</h5>`; break; } let link = links[l]; - let vals = td.map( function (f) { - if (f === 'owningAddr') { + let vals = td.map(function (f) { + if (f === "owningAddr") { let identity = utils.identity_clean(link.owningAddr); return utils.addr_text(identity); } return link[f]; }); let joinedVals = fnJoin(vals, function (v1, last) { - return [`</td><td${(isNaN(+v1) ? '': ' align="right"')}>`, last ? v1 : utils.pretty(v1 || '0', ',.2f')]; + return [ + `</td><td${isNaN(+v1) ? "" : ' align="right"'}>`, + last ? v1 : utils.pretty(v1 || "0", ",.2f") + ]; }); HTML += `<tr><td> ${joinedVals} </td></tr>`; } - return links.length > 0 ? `${HTMLHeading}${HTML}</table>` : ''; + return links.length > 0 ? `${HTMLHeading}${HTML}</table>` : ""; }; let left, right; @@ -228,43 +249,40 @@ export function connectionPopupHTML (d, QDRService) { [left, right] = [right, left]; } // left is a router. right is either a router or a client[s] - let onode = QDRService.management.topology.nodeInfo()[left.key]; + let onode = nodeInfo[left.key]; // find all the connections for left that go to right let conns = getConnsArray(onode, left.key, right); - let HTML = ''; - HTML += '<h5>Connection'+(conns.length > 1 ? 's' : '')+'</h5>'; - HTML += '<table class="popupTable"><tr class="header"><td>Security</td><td>Authentication</td><td>Tenant</td><td>Host</td>'; + let HTML = ""; + HTML += "<h5>Connection" + (conns.length > 1 ? "s" : "") + "</h5>"; + HTML += + '<table class="popupTable"><tr class="header"><td>Security</td><td>Authentication</td><td>Tenant</td><td>Host</td>'; - for (let c=0; c<Math.min(conns.length, 10); c++) { - HTML += ('<tr><td>' + utils.connSecurity(conns[c]) + '</td>'); - HTML += ('<td>' + utils.connAuth(conns[c]) + '</td>'); - HTML += ('<td>' + (utils.connTenant(conns[c]) || '--') + '</td>'); - HTML += ('<td>' + conns[c].host + '</td>'); - HTML += '</tr>'; + for (let c = 0; c < Math.min(conns.length, 10); c++) { + HTML += "<tr><td>" + utils.connSecurity(conns[c]) + "</td>"; + HTML += "<td>" + utils.connAuth(conns[c]) + "</td>"; + HTML += "<td>" + (utils.connTenant(conns[c]) || "--") + "</td>"; + HTML += "<td>" + conns[c].host + "</td>"; + HTML += "</tr>"; } - HTML += '</table>'; + HTML += "</table>"; HTML += linksHTML(onode, conns); return HTML; } -export function addStyles (stend, stateColor, radii) { - // the <style> - let element = document.querySelector('style'); - // Reference to the stylesheet - let sheet = element.sheet; - - let states = Object.keys(stateColor); - // create styles for each combo of 'stend-state-radii' - for (let istend=0; istend<stend.length; istend++) { - for (let istate=0; istate<states.length; istate++) { - let selectors = []; - for (let iradii=0; iradii<radii.length; iradii++) { - selectors.push(`#${stend[istend]}-${states[istate]}-${radii[iradii]}`); - } - let color = stateColor[states[istate]]; - let sels = `${selectors.join(',')} {fill: ${color}; stroke: ${color};}`; - sheet.insertRule(sels, 0); - } +export function getSizes(QDRLog) { + const gap = 5; + let legendWidth = 194; + let topoWidth = $("#topology").width(); + if (topoWidth < 768) legendWidth = 0; + let width = $("#topology").width() - gap - legendWidth; + let top = $("#topology").offset().top; + let height = window.innerHeight - top - gap; + if (width < 10) { + QDRLog.info( + `page width and height are abnormal w: ${width} h: ${height}` + ); + return [0, 0]; } + return [width, height]; } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
