You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by ea...@apache.org on 2018/05/17 20:55:08 UTC
[1/4] qpid-dispatch git commit: DISPATCH-1002 Added optional message
flow animation to topology page
Repository: qpid-dispatch
Updated Branches:
refs/heads/master e9f8a852a -> 61df4890b
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/61df4890/console/stand-alone/plugin/js/topology/traffic.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/topology/traffic.js b/console/stand-alone/plugin/js/topology/traffic.js
new file mode 100644
index 0000000..883cb2f
--- /dev/null
+++ b/console/stand-alone/plugin/js/topology/traffic.js
@@ -0,0 +1,278 @@
+/*
+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.
+*/
+'use strict';
+
+/* global d3 ChordData MIN_CHORD_THRESHOLD */
+
+/* Create animated dots moving along the links between routers
+ to show that there is message flow between routers.
+ */
+const transitionDuration = 1000;
+const CHORDFILTERKEY = 'chordFilter';
+
+function Traffic ($scope, $timeout, QDRService, converter, radius, topology, nextHop, excluded) {
+ this.QDRService = QDRService;
+ this.radius = radius; // the radius of a router circle
+ this.topology = topology; // contains the list of router nodes
+ this.nextHop = nextHop; // fn that returns the route through the network between two routers
+ this.$scope = $scope;
+ this.$timeout = $timeout;
+ $scope.addressColors = {};
+ this.excludedAddresses = JSON.parse(localStorage[CHORDFILTERKEY]) || [];
+
+ // internal variables
+ this.interval = null; // setInterval handle
+ this.lastFlows = {}; // the number of dots animated between routers
+ this.chordData = new ChordData(this.QDRService, true, converter); // gets ingressHistogram data
+ this.chordData.setFilter(this.excludedAddresses);
+ this.$scope.addresses = {};
+ this.chordData.getMatrix().then( function () {
+ $timeout( function () {
+ this.$scope.addresses = this.chordData.getAddresses();
+ for (let address in this.$scope.addresses) {
+ this.fillColor(address);
+ }
+ }.bind(this));
+ }.bind(this));
+}
+
+/* Public methods on Traffic object */
+
+// stop updating the traffic data
+Traffic.prototype.stop = function () {
+ if (this.interval) {
+ clearInterval(this.interval);
+ this.interval = null;
+ }
+};
+// start updating the traffic data
+Traffic.prototype.start = function () {
+ this.doUpdate(this);
+ this.interval = setInterval(this.doUpdate.bind(this), transitionDuration);
+};
+// remove any animationions that are in progress
+Traffic.prototype.remove = function () {
+ for (let id in this.lastFlows) {
+ d3.select('#SVG_ID').selectAll('circle.flow' + id).remove();
+ }
+ this.lastFlows = {};
+};
+// called when one of the address checkboxes is toggled
+Traffic.prototype.updateAddresses = function () {
+ this.excludedAddresses = [];
+ for (let address in this.$scope.addresses) {
+ if (!this.$scope.addresses[address])
+ this.excludedAddresses.push(address);
+ }
+ localStorage[CHORDFILTERKEY] = JSON.stringify(this.excludedAddresses);
+ if (this.chordData)
+ this.chordData.setFilter(this.excludedAddresses);
+ // don't wait for the next polling cycle. update now
+ this.stop();
+ this.start();
+};
+Traffic.prototype.toggleAddress = function (address) {
+ this.$scope.addresses[address] = !this.$scope.addresses[address];
+ this.updateAddresses();
+};
+Traffic.prototype.fadeOtherAddresses = function (address) {
+ d3.selectAll('circle.flow').classed('fade', function(d) {
+ return d.address !== address;
+ });
+};
+Traffic.prototype.unFadeAll = function () {
+ d3.selectAll('circle.flow').classed('fade', false);
+};
+
+/* The following don't need to be public, but they are for simplicity sake */
+
+// called periodically to refresh the traffic flow
+Traffic.prototype.doUpdate = function () {
+ let self = this;
+ // we need the nextHop data to show traffic between routers that are connected by intermediaries
+ this.QDRService.management.topology.ensureAllEntities([{entity: 'router.node', attrs: ['id','nextHop']}],
+ function () {
+ // get the ingressHistogram data for all routers
+ self.chordData.getMatrix().then(self.render.bind(self), function (e) {
+ console.log('Could not get message histogram' + e);
+ });
+ });
+};
+
+// calculate the translation for each dot along the path
+let translateDots = function (radius, path, count, back) {
+ let pnode = path.node();
+ // will be called for each element in the flow selection (for each dot)
+ return function(d) {
+ // will be called with t going from 0 to 1 for each dot
+ return function(t) {
+ // start the points at different positions depending on their value (d)
+ let tt = t * 1000;
+ let f = ((tt + (d.i*1000/count)) % 1000)/1000;
+ if (back)
+ f = 1 - f;
+ // l needs to be calculated each tick because the path's length might be changed during the animation
+ let l = pnode.getTotalLength();
+ let p = pnode.getPointAtLength(f * l);
+ return 'translate(' + p.x + ',' + p.y + ')';
+ };
+ };
+};
+// animate the d3 selection (flow) along the given path
+Traffic.prototype.animateFlow = function (flow, path, count, back, rate) {
+ let self = this;
+ let l = path.node().getTotalLength();
+ flow.transition()
+ .ease('easeLinear')
+ .duration(l*10/rate)
+ .attrTween('transform', translateDots(self.radius, path, count, back))
+ .each('end', function () {self.animateFlow(flow, path, count, back, rate);});
+};
+
+Traffic.prototype.render = function (matrix) {
+ this.$timeout(
+ function () {
+ this.$scope.addresses = this.chordData.getAddresses();
+ }.bind(this)
+ );
+ // get the rate of message flow between routers
+ let hops = {}; // every hop between routers that is involved in message flow
+ let matrixMessages = matrix.matrixMessages();
+ // the fastest traffic rate gets 3 times as many dots as the slowest
+ let minmax = matrix.getMinMax();
+ let flowScale = d3.scale.linear().domain(minmax).range([1,1.75]);
+
+ // row is ingress router, col is egress router. Value at [row][col] is the rate
+ matrixMessages.forEach( function (row, r) {
+ row.forEach(function (val, c) {
+ if (val > MIN_CHORD_THRESHOLD) {
+ // translate between matrix row/col and node index
+ let f = nodeIndexFor(this.topology.nodes, matrix.rows[r].egress);
+ let t = nodeIndexFor(this.topology.nodes, matrix.rows[r].ingress);
+ let address = matrix.getAddress(r, c);
+
+ if (r !== c) {
+ // move the dots along the links between the routers
+ this.nextHop(this.topology.nodes[f], this.topology.nodes[t], function (link, fnode, tnode) {
+ let key = '-' + link.uid;
+ let back = fnode.index < tnode.index;
+ if (!hops[key])
+ hops[key] = [];
+ hops[key].push({val: val, back: back, address: address});
+ });
+ }
+ // Find the senders connected to nodes[f] and the receivers connected to nodes[t]
+ // and add their links to the animation
+ addClients(hops, this.topology.nodes, f, val, true, address);
+ addClients(hops, this.topology.nodes, t, val, false, address);
+ }
+ }.bind(this));
+ }.bind(this));
+ // for each link between routers that has traffic, start an animation
+ let keep = {};
+ for (let id in hops) {
+ let hop = hops[id];
+ for (let h=0; h<hop.length; h++) {
+ let ahop = hop[h];
+ let flowId = id + '-' + addressIndex(this, ahop.address) + (ahop.back ? 'b' : '');
+ let path = d3.select('#path' + id);
+ // start the animation. If the animation is already running, this will have no effect
+ this.startDots(path, flowId, ahop, flowScale(ahop.val));
+ keep[flowId] = true;
+ }
+ }
+ // remove any existing animations that we don't have data for anymore
+ for (let id in this.lastFlows) {
+ if (this.lastFlows[id] && !keep[id]) {
+ this.lastFlows[id] = 0;
+ d3.select('#SVG_ID').selectAll('circle.flow' + id).remove();
+ }
+ }
+};
+
+// create dots along the path between routers
+Traffic.prototype.startDots = function (path, id, hop, rate) {
+ let back = hop.back, address = hop.address;
+ if (!path.node())
+ return;
+ // the density of dots is determined by the rate of this traffic relative to the other traffic
+ let len = Math.max(Math.floor(path.node().getTotalLength() / 50), 1);
+ let dots = [];
+ for (let i=0, offset=addressIndex(this, address); i<len; ++i) {
+ dots[i] = {i: i + 10 * offset, address: address};
+ }
+ // keep track of the number of dots for each link. If the length of the link is changed,
+ // re-create the animation
+ if (!this.lastFlows[id])
+ this.lastFlows[id] = len;
+ else {
+ if (this.lastFlows[id] !== len) {
+ this.lastFlows[id] = len;
+ d3.select('#SVG_ID').selectAll('circle.flow' + id).remove();
+ }
+ }
+ let flow = d3.select('#SVG_ID').selectAll('circle.flow' + id)
+ .data(dots, function (d) { return d.i + d.address; });
+
+ let circles = flow
+ .enter().append('circle')
+ .attr('class', 'flow flow' + id)
+ .attr('fill', this.fillColor(address))
+ .attr('r', 5);
+
+ this.animateFlow(circles, path, dots.length, back, rate);
+
+ flow.exit()
+ .remove();
+};
+
+// colors
+let colorGen = d3.scale.category10();
+Traffic.prototype.fillColor = function (n) {
+ if (!(n in this.$scope.addressColors)) {
+ let ci = Object.keys(this.$scope.addressColors).length;
+ this.$scope.addressColors[n] = colorGen(ci);
+ }
+ return this.$scope.addressColors[n];
+};
+// return the node index for a router name
+let nodeIndexFor = function (nodes, name) {
+ for (let i=0; i<nodes.length; i++) {
+ let node = nodes[i];
+ if (node.routerId === name)
+ return i;
+ }
+ return -1;
+};
+let addClients = function (hops, nodes, f, val, sender, address) {
+ let cdir = sender ? 'out' : 'in';
+ for (let n=0; n<nodes.length; n++) {
+ let node = nodes[n];
+ if (node.normals && node.key === nodes[f].key && node.cdir === cdir) {
+ let key = ['',f,n].join('-');
+ if (!hops[key])
+ hops[key] = [];
+ hops[key].push({val: val, back: node.cdir === 'in', address: address});
+ return;
+ }
+ }
+};
+let addressIndex = function (traffic, address) {
+ return Object.keys(traffic.$scope.addresses).indexOf(address);
+};
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org
[3/4] qpid-dispatch git commit: DISPATCH-1002 Added optional message
flow animation to topology page
Posted by ea...@apache.org.
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/61df4890/console/stand-alone/plugin/js/qdrTopology.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/qdrTopology.js b/console/stand-alone/plugin/js/qdrTopology.js
deleted file mode 100644
index cd0cdf2..0000000
--- a/console/stand-alone/plugin/js/qdrTopology.js
+++ /dev/null
@@ -1,2098 +0,0 @@
-/*
-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.
-*/
-'use strict';
-
-/* global angular d3 */
-/**
- * @module QDR
- */
-var QDR = (function(QDR) {
-
- QDR.module.controller('QDR.TopologyFormController', function($scope, $timeout) {
-
- $scope.attributes = [];
- $scope.attributesConnections = [];
-
- $scope.form = 'Router';
- $scope.$on('showEntityForm', function(event, args) {
- let attributes = args.attributes;
- // capitalize 1st letter
- $scope.form = args.entity.charAt(0).toUpperCase() + args.entity.slice(1);
-
- let H = '<div class="infoGrid">';
- attributes.forEach( function (a, i) {
- let even = (i % 2) ? 'even' : 'odd';
- if (a.attributeName === 'Listening on')
- even += ' listening-on';
- H += ('<div class="'+ even +'"><span title="'+a.attributeName+'">' + a.attributeName + '</span><span title="'+a.attributeValue+'">' + a.attributeValue + '</span></div>');
- });
- H += '</div>';
- $('#formInfo').html(H);
-
- if (!$scope.$$phase) $scope.$apply();
-
- });
- $scope.infoStyle = function () {
- return {
- height: (Math.max($scope.attributes.length, 15) * 30 + 46) + 'px'
- };
- };
-
- $scope.panelVisible = true; // show/hide the panel on the left
- $scope.hideLeftPane = function () {
- d3.select('.page-menu')
- .style('left' , '-360px')
- .style('z-index', '1');
-
- d3.select('.diagram')
- .transition().duration(300).ease('sin-in')
- .style('margin-left', '-360px')
- .each('end', function () {
- $timeout(function () {$scope.panelVisible = false;});
- let div = d3.select(this);
- div.style('margin-left', '0');
- div.style('padding-left', 0);
- });
- };
- $scope.showLeftPane = function () {
- d3.select('.page-menu')
- .style('left' , '0px');
-
- $timeout(function () {$scope.panelVisible = true;});
- d3.select('.diagram')
- .style('margin-left', '0px')
- .transition().duration(300).ease('sin-out')
- .style('margin-left', '300px')
- .each('end', function () {
- let div = d3.select(this);
- div.style('margin-left', '0');
- div.style('padding-left', '300px');
- });
- };
-
-
- });
- /**
- * @method TopologyController
- *
- * Controller that handles the QDR topology page
- */
- QDR.module.controller('QDR.TopologyController', ['$scope', '$rootScope', 'QDRService', '$location', '$timeout', '$uibModal', '$sce',
- function($scope, $rootScope, QDRService, $location, $timeout, $uibModal, $sce) {
-
- $scope.multiData = [];
- $scope.quiesceState = {};
- let dontHide = false;
- $scope.crosshtml = $sce.trustAsHtml('');
-
- $scope.quiesceConnection = function(row) {
- let entity = row.entity;
- let state = $scope.quiesceState[entity.connectionId].state;
- if (state === 'enabled') {
- // start quiescing all links
- $scope.quiesceState[entity.connectionId].state = 'quiescing';
- } else if (state === 'quiesced') {
- // start reviving all links
- $scope.quiesceState[entity.connectionId].state = 'reviving';
- }
- $scope.multiDetails.updateState(entity);
- dontHide = true;
- $scope.multiDetails.selectRow(row.rowIndex, true);
- $scope.multiDetails.showLinksList(row);
- };
- $scope.quiesceDisabled = function(row) {
- return $scope.quiesceState[row.entity.connectionId].buttonDisabled;
- };
- $scope.quiesceText = function(row) {
- return $scope.quiesceState[row.entity.connectionId].buttonText;
- };
- $scope.quiesceClass = function(row) {
- const stateClassMap = {
- enabled: 'btn-primary',
- quiescing: 'btn-warning',
- reviving: 'btn-warning',
- quiesced: 'btn-danger'
- };
- return stateClassMap[$scope.quiesceState[row.entity.connectionId].state];
- };
-
- // This is the grid that shows each connection when a client node that represents multiple connections is clicked
- $scope.multiData = [];
- $scope.multiDetails = {
- data: 'multiData',
- enableColumnResize: true,
- enableHorizontalScrollbar: 0,
- enableVerticalScrollbar: 0,
- jqueryUIDraggable: true,
- enablePaging: false,
- multiSelect: false,
- enableSelectAll: false,
- enableSelectionBatchEvent: false,
- enableRowHeaderSelection: false,
- noUnselect: true,
- onRegisterApi: function (gridApi) {
- if (gridApi.selection) {
- gridApi.selection.on.rowSelectionChanged($scope, function(row){
- let detailsDiv = d3.select('#link_details');
- let isVis = detailsDiv.style('display') === 'block';
- if (!dontHide && isVis && $scope.connectionId === row.entity.connectionId) {
- hideLinkDetails();
- return;
- }
- dontHide = false;
- $scope.multiDetails.showLinksList(row);
- });
- }
- },
- showLinksList: function(obj) {
- $scope.linkData = obj.entity.linkData;
- $scope.connectionId = obj.entity.connectionId;
- let visibleLen = Math.min(obj.entity.linkData.length, 10);
- //QDR.log.debug("visibleLen is " + visibleLen)
- let left = parseInt(d3.select('#multiple_details').style('left'), 10);
- let offset = $('#topology').offset();
- let detailsDiv = d3.select('#link_details');
- detailsDiv
- .style({
- display: 'block',
- opacity: 1,
- left: (left + 20) + 'px',
- top: (mouseY - offset.top + 20 + $(document).scrollTop()) + 'px',
- height: ((visibleLen + 1) * 30) + 40 + 'px', // +1 for the header row
- 'overflow-y': obj.entity.linkData > 10 ? 'scroll' : 'hidden'
- });
- },
- updateState: function(entity) {
- let state = $scope.quiesceState[entity.connectionId].state;
-
- // count enabled and disabled links for this connection
- let enabled = 0,
- disabled = 0;
- entity.linkData.forEach(function(link) {
- if (link.adminStatus === 'enabled')
- ++enabled;
- if (link.adminStatus === 'disabled')
- ++disabled;
- });
-
- let linkCount = entity.linkData.length;
- // if state is quiescing and any links are enabled, button should say 'Quiescing' and be disabled
- if (state === 'quiescing' && (enabled > 0)) {
- $scope.quiesceState[entity.connectionId].buttonText = 'Quiescing';
- $scope.quiesceState[entity.connectionId].buttonDisabled = true;
- } else
- // if state is enabled and all links are disabled, button should say Revive and be enabled. set state to quisced
- // if state is quiescing and all links are disabled, button should say 'Revive' and be enabled. set state to quiesced
- if ((state === 'quiescing' || state === 'enabled') && (disabled === linkCount)) {
- $scope.quiesceState[entity.connectionId].buttonText = 'Revive';
- $scope.quiesceState[entity.connectionId].buttonDisabled = false;
- $scope.quiesceState[entity.connectionId].state = 'quiesced';
- } else
- // if state is reviving and any links are disabled, button should say 'Reviving' and be disabled
- if (state === 'reviving' && (disabled > 0)) {
- $scope.quiesceState[entity.connectionId].buttonText = 'Reviving';
- $scope.quiesceState[entity.connectionId].buttonDisabled = true;
- } else
- // if state is reviving or quiesced and all links are enabled, button should say 'Quiesce' and be enabled. set state to enabled
- if ((state === 'reviving' || state === 'quiesced') && (enabled === linkCount)) {
- $scope.quiesceState[entity.connectionId].buttonText = 'Quiesce';
- $scope.quiesceState[entity.connectionId].buttonDisabled = false;
- $scope.quiesceState[entity.connectionId].state = 'enabled';
- }
- },
- columnDefs: [{
- field: 'host',
- cellTemplate: 'titleCellTemplate.html',
- //headerCellTemplate: 'titleHeaderCellTemplate.html',
- displayName: 'Connection host'
- }, {
- field: 'user',
- cellTemplate: 'titleCellTemplate.html',
- //headerCellTemplate: 'titleHeaderCellTemplate.html',
- displayName: 'User'
- }, {
- field: 'properties',
- cellTemplate: 'titleCellTemplate.html',
- //headerCellTemplate: 'titleHeaderCellTemplate.html',
- displayName: 'Properties'
- }
- /*,
- {
- cellClass: 'gridCellButton',
- cellTemplate: '<button title="{{quiesceText(row)}} the links" type="button" ng-class="quiesceClass(row)" class="btn" ng-click="$event.stopPropagation();quiesceConnection(row)" ng-disabled="quiesceDisabled(row)">{{quiesceText(row)}}</button>'
- }*/
- ]
- };
- $scope.quiesceLinkClass = function(row) {
- const stateClassMap = {
- enabled: 'btn-primary',
- disabled: 'btn-danger'
- };
- return stateClassMap[row.entity.adminStatus];
- };
- $scope.quiesceLink = function(row) {
- QDRService.management.topology.quiesceLink(row.entity.nodeId, row.entity.name)
- .then( function (results, context) {
- let statusCode = context.message.application_properties.statusCode;
- if (statusCode < 200 || statusCode >= 300) {
- QDR.Core.notification('error', context.message.statusDescription);
- QDR.log.info('Error ' + context.message.statusDescription);
- }
- });
- };
- $scope.quiesceLinkDisabled = function(row) {
- return (row.entity.operStatus !== 'up' && row.entity.operStatus !== 'down');
- };
- $scope.quiesceLinkText = function(row) {
- return row.entity.operStatus === 'down' ? 'Revive' : 'Quiesce';
- };
- $scope.linkData = [];
- $scope.linkDetails = {
- data: 'linkData',
- jqueryUIDraggable: true,
- columnDefs: [{
- field: 'adminStatus',
- cellTemplate: 'titleCellTemplate.html',
- headerCellTemplate: 'titleHeaderCellTemplate.html',
- displayName: 'Admin state'
- }, {
- field: 'operStatus',
- cellTemplate: 'titleCellTemplate.html',
- headerCellTemplate: 'titleHeaderCellTemplate.html',
- displayName: 'Oper state'
- }, {
- field: 'dir',
- cellTemplate: 'titleCellTemplate.html',
- headerCellTemplate: 'titleHeaderCellTemplate.html',
- displayName: 'dir'
- }, {
- field: 'owningAddr',
- cellTemplate: 'titleCellTemplate.html',
- headerCellTemplate: 'titleHeaderCellTemplate.html',
- displayName: 'Address'
- }, {
- field: 'deliveryCount',
- displayName: 'Delivered',
- headerCellTemplate: 'titleHeaderCellTemplate.html',
- cellClass: 'grid-values'
-
- }, {
- field: 'uncounts',
- displayName: 'Outstanding',
- headerCellTemplate: 'titleHeaderCellTemplate.html',
- cellClass: 'grid-values'
- }
- /*,
- {
- cellClass: 'gridCellButton',
- cellTemplate: '<button title="{{quiesceLinkText(row)}} this link" type="button" ng-class="quiesceLinkClass(row)" class="btn" ng-click="quiesceLink(row)" ng-disabled="quiesceLinkDisabled(row)">{{quiesceLinkText(row)}}</button>'
- }*/
- ]
- };
-
- let urlPrefix = $location.absUrl();
- urlPrefix = urlPrefix.split('#')[0];
- QDR.log.debug('started QDR.TopologyController with urlPrefix: ' + urlPrefix);
-
- // 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.contextNode = null; // node that is associated with the current context menu
- $scope.isRight = function(mode) {
- return mode.right;
- };
-
- var setNodesFixed = function (name, b) {
- nodes.some(function (n) {
- if (n.name === name) {
- n.fixed = b;
- return true;
- }
- });
- };
- $scope.setFixed = function(b) {
- if ($scope.contextNode) {
- $scope.contextNode.fixed = b;
- setNodesFixed($scope.contextNode.name, b);
- savePositions();
- }
- restart();
- };
- $scope.isFixed = function() {
- if (!$scope.contextNode)
- return false;
- return ($scope.contextNode.fixed & 1);
- };
-
- let mouseX, mouseY;
- var relativeMouse = function () {
- let offset = $('#main_container').offset();
- return {left: (mouseX + $(document).scrollLeft()) - 1,
- top: (mouseY + $(document).scrollTop()) - 1,
- offset: offset
- };
- };
- // event handlers for popup context menu
- $(document).mousemove(function(e) {
- mouseX = e.clientX;
- mouseY = e.clientY;
- });
- $(document).mousemove();
- $(document).click(function() {
- $scope.contextNode = null;
- $('.contextMenu').fadeOut(200);
- });
-
- const radii = {
- 'inter-router': 25,
- 'normal': 15,
- 'on-demand': 15,
- 'route-container': 15,
- };
- let radius = 25;
- let radiusNormal = 15;
- let svg, lsvg;
- let force;
- let animate = false; // should the force graph organize itself when it is displayed
- let path, circle;
- let savedKeys = {};
- let width = 0;
- let height = 0;
-
- var getSizes = function() {
- let legendWidth = 143;
- let display = $('#svg_legend').css('display');
- if (display === 'none')
- legendWidth = 0;
- const gap = 5;
- let width = $('#topology').width() - gap - legendWidth;
- let top = $('#topology').offset().top;
- let tpformHeight = $('#topologyForm').height();
- let height = Math.max(window.innerHeight, tpformHeight + top) - top - gap;
- if (width < 10) {
- QDR.log.info('page width and height are abnormal w:' + width + ' height:' + height);
- return [0, 0];
- }
- return [width, height];
- };
- var resize = function() {
- if (!svg)
- return;
- let sizes = getSizes();
- width = sizes[0];
- height = sizes[1];
- if (width > 0) {
- // set attrs and 'resume' force
- svg.attr('width', width);
- svg.attr('height', height);
- force.size(sizes).resume();
- }
- };
-
- $scope.$on('panel-resized', function () {
- resize();
- });
- window.addEventListener('resize', resize);
- let sizes = getSizes();
- width = sizes[0];
- height = sizes[1];
- if (width <= 0 || height <= 0)
- return;
-
- // set up initial nodes and links
- // - nodes are known by 'id', not by index in array.
- // - selected edges are indicated on the node (as a bold red circle).
- // - links are always source < target; edge directions are set by 'left' and 'right'.
- let nodes = [];
- let links = [];
-
- var nodeExists = function (connectionContainer) {
- return nodes.findIndex( function (node) {
- return node.container === connectionContainer;
- });
- };
- var normalExists = function (connectionContainer) {
- let normalInfo = {};
- for (let i=0; i<nodes.length; ++i) {
- if (nodes[i].normals) {
- if (nodes[i].normals.some(function (normal, j) {
- if (normal.container === connectionContainer && i !== j) {
- normalInfo = {nodesIndex: i, normalsIndex: j};
- return true;
- }
- return false;
- }))
- break;
- }
- }
- return normalInfo;
- };
- var getLinkSource = function (nodesIndex) {
- for (let i=0; i<links.length; ++i) {
- if (links[i].target === nodesIndex)
- return i;
- }
- return -1;
- };
- var aNode = function(id, name, nodeType, nodeInfo, nodeIndex, x, y, connectionContainer, resultIndex, fixed, properties) {
- properties = properties || {};
- for (let i=0; i<nodes.length; ++i) {
- if (nodes[i].name === name || nodes[i].container === connectionContainer) {
- if (properties.product)
- nodes[i].properties = properties;
- return nodes[i];
- }
- }
- let routerId = QDRService.management.topology.nameFromId(id);
- return {
- key: id,
- name: name,
- nodeType: nodeType,
- properties: properties,
- routerId: routerId,
- x: x,
- y: y,
- id: nodeIndex,
- resultIndex: resultIndex,
- fixed: !!+fixed,
- cls: '',
- container: connectionContainer
- };
- };
-
- var getLinkDir = function (id, connection, onode) {
- let links = onode['router.link'];
- if (!links) {
- return 'unknown';
- }
- let inCount = 0, outCount = 0;
- links.results.forEach( function (linkResult) {
- let link = QDRService.utilities.flatten(links.attributeNames, linkResult);
- if (link.linkType === 'endpoint' && link.connectionId === connection.identity)
- if (link.linkDir === 'in')
- ++inCount;
- else
- ++outCount;
- });
- if (inCount > 0 && outCount > 0)
- return 'both';
- if (inCount > 0)
- return 'in';
- if (outCount > 0)
- return 'out';
- return 'unknown';
- };
-
- var savePositions = function () {
- nodes.forEach( function (d) {
- localStorage[d.name] = angular.toJson({
- x: Math.round(d.x),
- y: Math.round(d.y),
- fixed: (d.fixed & 1) ? 1 : 0,
- });
- });
- };
-
- var initializeNodes = function (nodeInfo) {
- let nodeCount = Object.keys(nodeInfo).length;
- let yInit = 50;
- nodes = [];
- for (let id in nodeInfo) {
- let name = QDRService.management.topology.nameFromId(id);
- // if we have any new nodes, animate the force graph to position them
- let position = angular.fromJson(localStorage[name]);
- if (!angular.isDefined(position)) {
- animate = true;
- position = {
- x: Math.round(width / 4 + ((width / 2) / nodeCount) * nodes.length),
- y: Math.round(height / 2 + Math.sin(nodes.length / (Math.PI*2.0)) * height / 4),
- fixed: false,
- };
- //QDR.log.debug("new node pos (" + position.x + ", " + position.y + ")")
- }
- if (position.y > height) {
- position.y = 200 - yInit;
- yInit *= -1;
- }
- nodes.push(aNode(id, name, 'inter-router', nodeInfo, nodes.length, position.x, position.y, name, undefined, position.fixed));
- }
- };
-
- var initializeLinks = function (nodeInfo, unknowns) {
- links = [];
- let source = 0;
- let client = 1.0;
- for (let id in nodeInfo) {
- let onode = nodeInfo[id];
- if (!onode['connection'])
- continue;
- let conns = onode['connection'].results;
- let attrs = onode['connection'].attributeNames;
- //QDR.log.debug("external client parent is " + parent);
- let normalsParent = {}; // 1st normal node for this parent
-
- for (let j = 0; j < conns.length; j++) {
- let connection = QDRService.utilities.flatten(attrs, conns[j]);
- let role = connection.role;
- let properties = connection.properties || {};
- let dir = connection.dir;
- if (role == 'inter-router') {
- let connId = connection.container;
- let target = getContainerIndex(connId, nodeInfo);
- if (target >= 0) {
- 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 = QDRService.management.topology.nameFromId(id) + '.' + connection.identity;
-
- // if we have any new clients, animate the force graph to position them
- let position = angular.fromJson(localStorage[name]);
- if (!angular.isDefined(position)) {
- animate = true;
- position = {
- x: Math.round(nodes[source].x + 40 * Math.sin(client / (Math.PI * 2.0))),
- y: Math.round(nodes[source].y + 40 * Math.cos(client / (Math.PI * 2.0))),
- fixed: false
- };
- //QDR.log.debug("new client pos (" + position.x + ", " + position.y + ")")
- }// else QDR.log.debug("using previous location")
- if (position.y > height) {
- position.y = Math.round(nodes[source].y + 40 + Math.cos(client / (Math.PI * 2.0)));
- }
- let existingNodeIndex = nodeExists(connection.container);
- let normalInfo = normalExists(connection.container);
- let node = aNode(id, name, role, nodeInfo, nodes.length, position.x, position.y, connection.container, j, position.fixed, properties);
- let nodeType = QDRService.utilities.isAConsole(properties, connection.identity, role, node.key) ? 'console' : 'client';
- let cdir = getLinkDir(id, connection, onode);
- if (existingNodeIndex >= 0) {
- // make a link between the current router (source) and the existing node
- 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 = getLinkSource(normalInfo.nodesIndex);
- if (normalSource >= 0) {
- if (cdir === 'unknown')
- cdir = dir;
- node.cdir = cdir;
- nodes.push(node);
- // create link from original node to the new node
- getLink(links[normalSource].source, nodes.length-1, cdir, 'small', connection.name);
- // create link from this router to the new node
- getLink(source, nodes.length-1, cdir, 'small', connection.name);
- // remove the old node from the normals list
- nodes[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.push(node);
- node.normals = [node];
- // now add a link
- getLink(source, nodes.length - 1, cdir, 'small', connection.name);
- client++;
- } else {
- normalsParent[nodeType+cdir].normals.push(node);
- }
- } else {
- node.id = nodes.length - 1 + unknowns.length;
- unknowns.push(node);
- }
- } else {
- nodes.push(node);
- // now add a link
- getLink(source, nodes.length - 1, dir, 'small', connection.name);
- client++;
- }
- }
- }
- source++;
- }
- };
-
- // 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 = nodeCount;
- if (nodeCount < 6) count = 6;
- if (nodeCount > 80) count = 80;
- let x = d3.scale.linear()
- .domain([6,80])
- .range([min, max]);
- //QDR.log.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() {
- nodes = [];
- links = [];
- let nodeInfo = QDRService.management.topology.nodeInfo();
- let nodeCount = Object.keys(nodeInfo).length;
-
- let oldSelectedNode = selected_node;
- let oldMouseoverNode = mouseover_node;
- mouseover_node = null;
- selected_node = null;
- selected_link = null;
-
- savePositions();
- d3.select('#SVG_ID').remove();
- svg = d3.select('#topology')
- .append('svg')
- .attr('id', 'SVG_ID')
- .attr('width', width)
- .attr('height', height)
- .on('click', function() {
- removeCrosssection();
- });
-
- $(document).keyup(function(e) {
- if (e.keyCode === 27) {
- removeCrosssection();
- }
- });
-
- // the legend
- d3.select('#svg_legend svg').remove();
- lsvg = d3.select('#svg_legend')
- .append('svg')
- .attr('id', 'svglegend');
- lsvg = lsvg.append('svg:g')
- .attr('transform', 'translate(' + (radii['inter-router'] + 2) + ',' + (radii['inter-router'] + 2) + ')')
- .selectAll('g');
-
- // mouse event vars
- mousedown_link = null;
- mousedown_node = null;
- mouseup_node = null;
-
- // initialize the list of nodes
- initializeNodes(nodeInfo);
- savePositions();
-
- // initialize the list of links
- let unknowns = [];
- initializeLinks(nodeInfo, unknowns);
- $scope.schema = QDRService.management.schema();
- // init D3 force layout
- force = d3.layout.force()
- .nodes(nodes)
- .links(links)
- .size([width, height])
- .linkDistance(function(d) { return linkDistance(d, nodeCount); })
- .charge(function(d) { return charge(d, nodeCount); })
- .friction(.10)
- .gravity(function(d) { return gravity(d, nodeCount); })
- .on('tick', tick)
- .on('end', function () {savePositions();})
- .start();
-
- svg.append('svg:defs').selectAll('marker')
- .data(['end-arrow', 'end-arrow-selected', 'end-arrow-small', 'end-arrow-highlighted']) // Different link/path types can be defined here
- .enter().append('svg:marker') // This section adds in the arrows
- .attr('id', String)
- .attr('viewBox', '0 -5 10 10')
- .attr('markerWidth', 4)
- .attr('markerHeight', 4)
- .attr('orient', 'auto')
- .classed('small', function (d) {return d.indexOf('small') > -1;})
- .append('svg:path')
- .attr('d', 'M 0 -5 L 10 0 L 0 5 z');
-
- svg.append('svg:defs').selectAll('marker')
- .data(['start-arrow', 'start-arrow-selected', 'start-arrow-small', 'start-arrow-highlighted']) // Different link/path types can be defined here
- .enter().append('svg:marker') // This section adds in the arrows
- .attr('id', String)
- .attr('viewBox', '0 -5 10 10')
- .attr('refX', 5)
- .attr('markerWidth', 4)
- .attr('markerHeight', 4)
- .attr('orient', 'auto')
- .append('svg:path')
- .attr('d', 'M 10 -5 L 0 0 L 10 5 z');
-
- 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').selectAll('path'),
- circle = svg.append('svg:g').selectAll('g');
-
- // app starts here
- restart(false);
- force.start();
- if (oldSelectedNode) {
- d3.selectAll('circle.inter-router').classed('selected', function (d) {
- if (d.key === oldSelectedNode.key) {
- selected_node = d;
- return true;
- }
- return false;
- });
- }
- if (oldMouseoverNode && selected_node) {
- 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 () {
- nextHop(selected_node, d);
- restart();
- });
- }
- });
- }
- setTimeout(function () {
- updateForm(Object.keys(QDRService.management.topology.nodeInfo())[0], 'router', 0);
- });
-
- // if any clients don't yet have link directions, get the links for those nodes and restart the graph
- if (unknowns.length > 0)
- setTimeout(resolveUnknowns, 10, nodeInfo, unknowns);
-
- var continueForce = function (extra) {
- if (extra > 0) {
- --extra;
- force.start();
- setTimeout(continueForce, 100, extra);
- }
- };
- continueForce(forceScale(nodeCount, 0, 200)); // give graph time to settle down
- };
-
- var resolveUnknowns = function (nodeInfo, unknowns) {
- let unknownNodes = {};
- // collapse the unknown node.keys using an object
- for (let i=0; i<unknowns.length; ++i) {
- unknownNodes[unknowns[i].key] = 1;
- }
- unknownNodes = Object.keys(unknownNodes);
- //QDR.log.info("-- resolveUnknowns: ensuring .connection and .router.link are present for each node")
- QDRService.management.topology.ensureEntities(unknownNodes, [{entity: 'connection', force: true}, {entity: 'router.link', attrs: ['linkType','connectionId','linkDir'], force: true}], function () {
- nodeInfo = QDRService.management.topology.nodeInfo();
- initializeLinks(nodeInfo, []);
- // collapse any router-container nodes that are duplicates
- animate = true;
- force.nodes(nodes).links(links).start();
- restart(false);
- });
- };
-
- function updateForm(key, entity, resultIndex) {
- if (!angular.isDefined(resultIndex))
- return;
- let nodeList = QDRService.management.topology.nodeIdList();
- if (nodeList.indexOf(key) > -1) {
- QDRService.management.topology.fetchEntities(key, [
- {entity: entity},
- {entity: 'listener', attrs: ['role', 'port']}], function (results) {
- let onode = results[key];
- if (!onode[entity]) {
- console.log('requested ' + entity + ' but didn\'t get it');
- return;
- }
- let nodeResults = onode[entity].results[resultIndex];
- let nodeAttributes = onode[entity].attributeNames;
- let attributes = nodeResults.map(function(row, i) {
- return {
- attributeName: nodeAttributes[i],
- attributeValue: row
- };
- });
- // sort by attributeName
- attributes.sort(function(a, b) {
- return a.attributeName.localeCompare(b.attributeName);
- });
-
- // move the Name first
- let nameIndex = attributes.findIndex(function(attr) {
- return attr.attributeName === 'name';
- });
- if (nameIndex >= 0)
- attributes.splice(0, 0, attributes.splice(nameIndex, 1)[0]);
-
- // get the list of ports this router is listening on
- if (entity === 'router') {
- let listeners = onode['listener'].results;
- let listenerAttributes = onode['listener'].attributeNames;
- let normals = listeners.filter(function(listener) {
- return QDRService.utilities.valFor(listenerAttributes, listener, 'role') === 'normal';
- });
- let ports = [];
- normals.forEach(function(normalListener) {
- ports.push(QDRService.utilities.valFor(listenerAttributes, normalListener, 'port'));
- });
- // add as 2nd row
- if (ports.length) {
- attributes.splice(1, 0, {
- attributeName: 'Listening on',
- attributeValue: ports,
- description: 'The port(s) on which this router is listening for connections'
- });
- }
- }
- $rootScope.$broadcast('showEntityForm', {
- entity: entity,
- attributes: attributes
- });
- if (!$scope.$$phase) $scope.$apply();
- });
- }
- }
-
- function getContainerIndex(_id, nodeInfo) {
- let nodeIndex = 0;
- for (let id in nodeInfo) {
- if (QDRService.management.topology.nameFromId(id) === _id)
- return nodeIndex;
- ++nodeIndex;
- }
- return -1;
- }
-
- function getLink(_source, _target, dir, cls, uid) {
- for (let i = 0; i < links.length; i++) {
- let s = links[i].source,
- t = links[i].target;
- if (typeof links[i].source == 'object') {
- s = s.id;
- t = t.id;
- }
- if (s == _source && t == _target) {
- return i;
- }
- // same link, just reversed
- if (s == _target && t == _source) {
- return -i;
- }
- }
- //QDR.log.debug("creating new link (" + (links.length) + ") between " + nodes[_source].name + " and " + nodes[_target].name);
- if (links.some( function (l) { return l.uid === uid;}))
- uid = uid + '.' + links.length;
- let link = {
- source: _source,
- target: _target,
- left: dir != 'out',
- right: (dir == 'out' || dir == 'both'),
- cls: cls,
- uid: uid,
- };
- return links.push(link) - 1;
- }
-
-
- function resetMouseVars() {
- mousedown_node = null;
- mouseover_node = null;
- mouseup_node = null;
- mousedown_link = null;
- }
-
- // 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));
- return 'translate(' + d.x + ',' + d.y + ')';
- });
-
- // draw directed edges with proper padding from node centers
- path.attr('d', function(d) {
- let sourcePadding, targetPadding, r;
-
- if (d.target.nodeType == 'inter-router') {
- r = radius;
- // right arrow left line start
- sourcePadding = d.left ? radius + 8 : radius;
- // left arrow right line start
- targetPadding = d.right ? radius + 16 : radius;
- } else {
- r = radiusNormal - 18;
- sourcePadding = d.left ? radiusNormal + 18 : radiusNormal;
- targetPadding = d.right ? radiusNormal + 16 : radiusNormal;
- }
- let dtx = Math.max(targetPadding, Math.min(width - r, d.target.x)),
- dty = Math.max(targetPadding, Math.min(height - r, d.target.y)),
- dsx = Math.max(sourcePadding, Math.min(width - r, d.source.x)),
- dsy = Math.max(sourcePadding, Math.min(height - r, d.source.y));
-
- let deltaX = dtx - dsx,
- deltaY = dty - dsy,
- dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
- if (dist == 0)
- dist = 0.001;
- let normX = deltaX / dist,
- normY = deltaY / dist;
- let sourceX = dsx + (sourcePadding * normX),
- sourceY = dsy + (sourcePadding * normY),
- targetX = dtx - (targetPadding * normX),
- targetY = dty - (targetPadding * normY);
- sourceX = Math.max(0, sourceX);
- sourceY = Math.max(0, sourceY);
- targetX = Math.max(0, targetX);
- targetY = Math.max(0, targetY);
-
- return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY;
- });
-
- if (!animate) {
- animate = true;
- force.stop();
- }
- }
-
- // highlight the paths between the selected node and the hovered node
- function findNextHopNode(from, d) {
- // d is the node that the mouse is over
- // from is the selected_node ....
- if (!from)
- return null;
-
- if (from == d)
- return selected_node;
-
- //QDR.log.debug("finding nextHop from: " + from.name + " to " + d.name);
- let sInfo = QDRService.management.topology.nodeInfo()[from.key];
-
- if (!sInfo) {
- QDR.log.warn('unable to find topology node info for ' + from.key);
- return null;
- }
-
- // find the hovered name in the selected name's .router.node results
- if (!sInfo['router.node'])
- return null;
- 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 (addrT == d.name) {
- //QDR.log.debug("found " + d.name + " at " + hIdx);
- let nextHop = QDRService.utilities.valFor(aAr, vAr[hIdx], 'nextHop');
- //QDR.log.debug("nextHop was " + nextHop);
- return (nextHop == null) ? nodeFor(addrT) : nodeFor(nextHop);
- }
- }
- return null;
- }
-
- function nodeFor(name) {
- for (let i = 0; i < nodes.length; ++i) {
- if (nodes[i].name == name)
- return nodes[i];
- }
- return null;
- }
-
- function linkFor(source, target) {
- for (let i = 0; i < links.length; ++i) {
- if ((links[i].source == source) && (links[i].target == target))
- return links[i];
- if ((links[i].source == target) && (links[i].target == source))
- return links[i];
- }
- // the selected node was a client/broker
- //QDR.log.debug("failed to find a link between ");
- //console.dump(source);
- //QDR.log.debug(" and ");
- //console.dump(target);
- return null;
- }
-
- function clearPopups() {
- d3.select('#crosssection').style('display', 'none');
- $('.hastip').empty();
- d3.select('#multiple_details').style('display', 'none');
- d3.select('#link_details').style('display', 'none');
- d3.select('#node_context_menu').style('display', 'none');
-
- }
-
- function removeCrosssection() {
- d3.select('#crosssection svg g').transition()
- .duration(1000)
- .attr('transform', 'scale(0)')
- .style('opacity', 0)
- .each('end', function () {
- d3.select('#crosssection svg').remove();
- d3.select('#crosssection').style('display','none');
- });
- d3.select('#multiple_details').transition()
- .duration(500)
- .style('opacity', 0)
- .each('end', function() {
- d3.select('#multiple_details').style('display', 'none');
- stopUpdateConnectionsGrid();
- });
- hideLinkDetails();
- }
-
- function hideLinkDetails() {
- d3.select('#link_details').transition()
- .duration(500)
- .style('opacity', 0)
- .each('end', function() {
- d3.select('#link_details').style('display', 'none');
- });
- }
-
- function clerAllHighlights() {
- for (let i = 0; i < links.length; ++i) {
- links[i]['highlighted'] = false;
- }
- for (let i = 0; i<nodes.length; ++i) {
- nodes[i]['highlighted'] = false;
- }
- }
- // takes the nodes and links array of objects and adds svg elements for everything that hasn't already
- // been added
- function restart(start) {
- circle.call(force.drag);
-
- // path (link) group
- path = path.data(links, function(d) {return d.uid;});
-
- // update existing links
- path.classed('selected', function(d) {
- return d === selected_link;
- })
- .classed('highlighted', function(d) {
- return d.highlighted;
- })
- .attr('marker-start', function(d) {
- let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
- if (d.highlighted)
- sel = '-highlighted';
- return d.left ? 'url(' + urlPrefix + '#start-arrow' + sel + ')' : '';
- })
- .attr('marker-end', function(d) {
- let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
- if (d.highlighted)
- sel = '-highlighted';
- return d.right ? 'url(' + urlPrefix + '#end-arrow' + sel + ')' : '';
- });
- // add new links. if a link with a new uid is found in the data, add a new path
- path.enter().append('svg:path')
- .attr('class', 'link')
- .attr('marker-start', function(d) {
- let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
- return d.left ? 'url(' + urlPrefix + '#start-arrow' + sel + ')' : '';
- })
- .attr('marker-end', function(d) {
- let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
- return d.right ? 'url(' + urlPrefix + '#end-arrow' + sel + ')' : '';
- })
- .classed('small', function(d) {
- return d.cls == 'small';
- })
- .on('mouseover', function(d) { // mouse over a path
- let resultIndex = 0; // the connection to use
- let left = d.left ? d.target : d.source;
- // right is the node that the arrow points to, left is the other node
- let right = d.left ? d.source : d.target;
- let onode = QDRService.management.topology.nodeInfo()[left.key];
- // loop through all the connections for left, and find the one for right
- if (!onode || !onode['connection'])
- return;
- // update the info dialog for the link the mouse is over
- if (!selected_node && !selected_link) {
- for (resultIndex = 0; resultIndex < onode['connection'].results.length; ++resultIndex) {
- let conn = onode['connection'].results[resultIndex];
- /// find the connection whose container is the right's name
- let name = QDRService.utilities.valFor(onode['connection'].attributeNames, conn, 'container');
- if (name == right.routerId) {
- break;
- }
- }
- // did not find connection. this is a connection to a non-interrouter node
- if (resultIndex === onode['connection'].results.length) {
- // use the non-interrouter node's connection info
- left = d.target;
- resultIndex = left.resultIndex;
- }
- updateForm(left.key, 'connection', resultIndex);
- }
-
- mousedown_link = d;
- selected_link = mousedown_link;
- restart();
- })
- .on('mousemove', function (d) {
- let top = $('#topology').offset().top - 5;
- $timeout(function () {
- $scope.trustedpopoverContent = $sce.trustAsHtml(connectionPopupHTML(d));
- });
- d3.select('#popover-div')
- .style('display', 'block')
- .style('left', (d3.event.pageX+5)+'px')
- .style('top', (d3.event.pageY-top)+'px');
- })
- .on('mouseout', function() { // mouse out of a path
- d3.select('#popover-div')
- .style('display', 'none');
- selected_link = null;
- restart();
- })
- // left click a path
- .on('click', function (d) {
- let clickPos = d3.mouse(this);
- d3.event.stopPropagation();
- clearPopups();
- var showCrossSection = function() {
- const diameter = 400;
- let pack = d3.layout.pack()
- .size([diameter - 4, diameter - 4])
- .padding(-10)
- .value(function(d) { return d.size; });
-
- d3.select('#crosssection svg').remove();
- let svg = d3.select('#crosssection').append('svg')
- .attr('width', diameter)
- .attr('height', diameter);
-
- let rg = svg.append('svg:defs')
- .append('radialGradient')
- .attr('id', 'cross-gradient')
- .attr('gradientTransform', 'scale(2.0) translate(-0.5,-0.5)');
-
- rg
- .append('stop')
- .attr('offset', '0%')
- .attr('stop-color', '#feffff');
- rg
- .append('stop')
- .attr('offset', '40%')
- .attr('stop-color', '#cfe2f3');
-
- let svgg = svg.append('g')
- .attr('transform', 'translate(2,2)');
-
- svgg
- .append('rect')
- .attr('x', 0)
- .attr('y', 0)
- .attr('width', 200)
- .attr('height', 200)
- .attr('class', 'cross-rect')
- .attr('fill', 'url('+urlPrefix+'#cross-gradient)');
-
- svgg
- .append('line')
- .attr('class', 'cross-line')
- .attr({x1: 2, y1: 0, x2: 200, y2: 0});
- svgg
- .append('line')
- .attr('class', 'cross-line')
- .attr({x1: 2, y1: 0, x2: 0, y2: 200});
-
- /*
- let simpleLine = d3.svg.line();
- svgg
- .append('path')
- .attr({
- d: simpleLine([[0,0],[0,200]]),
- stroke: '#000',
- 'stroke-width': '4px'
- });
- svgg
- .append('path')
- .attr({
- d: simpleLine([[0,0],[200,0]]),
- stroke: '#000',
- 'stroke-width': '4px'
- });
-*/
- let root = {
- name: ' Links between ' + d.source.name + ' and ' + d.target.name,
- children: []
- };
- let nodeInfo = QDRService.management.topology.nodeInfo();
- let connections = nodeInfo[d.source.key]['connection'];
- let containerIndex = connections.attributeNames.indexOf('container');
- connections.results.some ( function (connection) {
- if (connection[containerIndex] == d.target.routerId) {
- root.attributeNames = connections.attributeNames;
- root.obj = connection;
- root.desc = 'Connection';
- return true; // stop looping after 1 match
- }
- return false;
- });
-
- // find router.links where link.remoteContainer is d.source.name
- let links = nodeInfo[d.source.key]['router.link'];
- let identityIndex = connections.attributeNames.indexOf('identity');
- let roleIndex = connections.attributeNames.indexOf('role');
- let connectionIdIndex = links.attributeNames.indexOf('connectionId');
- let linkTypeIndex = links.attributeNames.indexOf('linkType');
- let nameIndex = links.attributeNames.indexOf('name');
- let linkDirIndex = links.attributeNames.indexOf('linkDir');
-
- if (roleIndex < 0 || identityIndex < 0 || connectionIdIndex < 0
- || linkTypeIndex < 0 || nameIndex < 0 || linkDirIndex < 0)
- return;
- links.results.forEach ( function (link) {
- if (root.obj && link[connectionIdIndex] == root.obj[identityIndex] && link[linkTypeIndex] == root.obj[roleIndex])
- root.children.push (
- { name: ' ' + link[linkDirIndex] + ' ',
- size: 100,
- obj: link,
- desc: 'Link',
- attributeNames: links.attributeNames
- });
- });
- if (root.children.length == 0)
- return;
- let node = svgg.datum(root).selectAll('.node')
- .data(pack.nodes)
- .enter().append('g')
- .attr('class', function(d) { return d.children ? 'parent node hastip' : 'leaf node hastip'; })
- .attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')' + (!d.children ? 'scale(0.9)' : ''); });
- node.append('circle')
- .attr('r', function(d) { return d.r; });
-
- node.on('mouseenter', function (d) {
- let title = '<h4>' + d.desc + '</h4><table class=\'tiptable\'><tbody>';
- if (d.attributeNames)
- d.attributeNames.forEach( function (n, i) {
- title += '<tr><td>' + n + '</td><td>';
- title += d.obj[i] != null ? d.obj[i] : '';
- title += '</td></tr>';
- });
- title += '</tbody></table>';
- $timeout( (function () {
- $scope.crosshtml = $sce.trustAsHtml(title);
- $('#crosshtml').show();
- let parent = $('#crosssection');
- let ppos = parent.position();
- let mleft = ppos.left + parent.width();
- $('#crosshtml').css({left: mleft, top: ppos.top});
- }).bind(this));
- });
- node.on('mouseout', function () {
- $('#crosshtml').hide();
- });
-
- node.append('text')
- .attr('dy', function (d) { return d.children ? '-10em' : '.5em';})
- .style('text-anchor', 'middle')
- .text(function(d) {
- return d.name.substring(0, d.r / 3);
- });
- svgg.attr('transform', 'translate(2,2) scale(0.01)');
-
- let bounds = $('#topology').position();
- d3.select('#crosssection')
- .style('display', 'block')
- .style('left', (clickPos[0] + bounds.left) + 'px')
- .style('top', (clickPos[1] + bounds.top) + 'px');
-
- svgg.transition()
- .attr('transform', 'translate(2,2) scale(1)')
- .each('end', function () {
- d3.selectAll('#crosssection g.leaf text').attr('dy', '.3em');
- });
- };
- QDRService.management.topology.ensureEntities(d.source.key, {entity: 'router.link', force: true}, showCrossSection);
- });
- // remove old links
- path.exit().remove();
-
-
- // circle (node) group
- // nodes are known by id
- circle = circle.data(nodes, function(d) {
- return d.name;
- });
-
- // 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 & 1;
- });
-
- // add new circle nodes. if nodes[] is longer than the existing paths, add a new path for each new element
- let g = circle.enter().append('svg:g')
- .classed('multiple', function(d) {
- return (d.normals && d.normals.length > 1);
- });
-
- var appendCircle = function(g) {
- // add new circles and set their attr/class/behavior
- return g.append('svg:circle')
- .attr('class', 'node')
- .attr('r', function(d) {
- return radii[d.nodeType];
- })
- .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 & 1;
- })
- .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';
- })
- .classed('on-demand', function(d) {
- return d.nodeType == 'on-demand';
- })
- .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;
- });
- };
- appendCircle(g)
- .on('mouseover', function(d) { // mouseover a circle
- if (!selected_node && !mousedown_node) {
- if (d.nodeType === 'inter-router') {
- //QDR.log.debug("showing general form");
- updateForm(d.key, 'router', 0);
- } else if (d.nodeType === 'normal' || d.nodeType === 'on-demand' || d.nodeType === 'route-container') {
- //QDR.log.debug("showing connections form");
- updateForm(d.key, 'connection', d.resultIndex);
- }
- }
-
- if (d === mousedown_node)
- return;
- //if (d === selected_node)
- // return;
- // enlarge target node
- d3.select(this).attr('transform', 'scale(1.1)');
- // highlight the next-hop route from the selected node to this node
- //mousedown_node = null;
-
- if (!selected_node) {
- return;
- }
- clerAllHighlights();
- // 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
- nextHop(selected_node, d);
- restart();
- });
- })
- .on('mouseout', function() { // mouse out for a circle
- // unenlarge target node
- d3.select(this).attr('transform', '');
- clerAllHighlights();
- mouseover_node = null;
- restart();
- })
- .on('mousedown', function(d) { // mouse down for circle
- if (d3.event.button !== 0) { // ignore all but left button
- return;
- }
- mousedown_node = d;
- // mouse position relative to svg
- initial_mouse_down_position = d3.mouse(this.parentNode.parentNode.parentNode).slice();
- })
- .on('mouseup', function(d) { // mouse up for circle
- if (!mousedown_node)
- return;
-
- selected_link = null;
- // unenlarge target node
- d3.select(this).attr('transform', '');
-
- // check for drag
- mouseup_node = d;
-
- let mySvg = this.parentNode.parentNode.parentNode;
- // 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.fixed = true;
- setNodesFixed(d.name, true);
- resetMouseVars();
- restart();
- return;
- }
-
- // if this node was selected, unselect it
- if (mousedown_node === selected_node) {
- selected_node = null;
- } else {
- if (d.nodeType !== 'normal' && d.nodeType !== 'on-demand')
- selected_node = mousedown_node;
- }
- clerAllHighlights();
- mousedown_node = null;
- if (!$scope.$$phase) $scope.$apply();
- restart(false);
-
- })
- .on('dblclick', function(d) { // circle
- if (d.fixed) {
- d.fixed = false;
- setNodesFixed(d.name, false);
- restart(); // redraw the node without a dashed line
- force.start(); // let the nodes move to a new position
- }
- })
- .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 () {
- $scope.contextNode = d;
- });
- })
- .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';
- if (QDR.isStandalone)
- window.location = $location.protocol() + '://localhost:8161/hawtio' + artemisPath;
- else
- $location.path(artemisPath);
- }
- return;
- }
- d3.event.stopPropagation();
- startUpdateConnectionsGrid(d);
- });
- //.attr("transform", function (d) {return "scale(" + (d.nodeType === 'normal' ? .5 : 1) + ")"})
- //.transition().duration(function (d) {return d.nodeType === 'normal' ? 3000 : 0}).ease("elastic").attr("transform", "scale(1)")
-
- var 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;
- 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('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
- return d.name.length > 7 ? d.name.substr(0, 6) + '...' : d.name;
- });
- };
-
- appendContent(g);
-
- var appendTitle = function(g) {
- g.append('svg:title').text(function(d) {
- 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.properties.product) {
- return d.properties.product;
- } 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;
- return d.nodeType == 'normal' ? 'client' + x : (d.nodeType == 'on-demand' ? 'broker' : 'Router ' + d.name);
- });
- };
- appendTitle(g);
-
- // remove old nodes
- circle.exit().remove();
-
- // add subcircles
- svg.selectAll('.subcircle').remove();
- let multiples = svg.selectAll('.multiple');
- multiples.each(function(d) {
- d.normals.forEach(function(n, i) {
- if (i < d.normals.length - 1 && i < 3) // only show a few shadow circles
- this.insert('svg:circle', ':first-child')
- .attr('class', 'subcircle node')
- .attr('r', 15 - i)
- .attr('transform', 'translate(' + 4 * (i + 1) + ', 0)');
- }, d3.select(this));
- });
-
- // dynamically create the legend based on which node types are present
- // the legend
- d3.select('#svg_legend svg').remove();
- lsvg = d3.select('#svg_legend')
- .append('svg')
- .attr('id', 'svglegend');
- lsvg = lsvg.append('svg:g')
- .attr('transform', 'translate(' + (radii['inter-router'] + 2) + ',' + (radii['inter-router'] + 2) + ')')
- .selectAll('g');
- let legendNodes = [];
- legendNodes.push(aNode('Router', '', 'inter-router', '', undefined, 0, 0, 0, 0, false, {}));
-
- if (!svg.selectAll('circle.console').empty()) {
- legendNodes.push(aNode('Console', '', 'normal', '', undefined, 1, 0, 0, 0, false, {
- console_identifier: 'Dispatch console'
- }));
- }
- if (!svg.selectAll('circle.client.in').empty()) {
- let node = aNode('Sender', '', 'normal', '', undefined, 2, 0, 0, 0, false, {});
- node.cdir = 'in';
- legendNodes.push(node);
- }
- if (!svg.selectAll('circle.client.out').empty()) {
- let node = aNode('Receiver', '', 'normal', '', undefined, 3, 0, 0, 0, false, {});
- node.cdir = 'out';
- legendNodes.push(node);
- }
- if (!svg.selectAll('circle.client.inout').empty()) {
- let node = aNode('Sender/Receiver', '', 'normal', '', undefined, 4, 0, 0, 0, false, {});
- node.cdir = 'both';
- legendNodes.push(node);
- }
- if (!svg.selectAll('circle.qpid-cpp').empty()) {
- legendNodes.push(aNode('Qpid broker', '', 'route-container', '', undefined, 5, 0, 0, 0, false, {
- product: 'qpid-cpp'
- }));
- }
- if (!svg.selectAll('circle.artemis').empty()) {
- legendNodes.push(aNode('Artemis broker', '', 'route-container', '', undefined, 6, 0, 0, 0, false,
- {product: 'apache-activemq-artemis'}));
- }
- if (!svg.selectAll('circle.route-container').empty()) {
- legendNodes.push(aNode('Service', '', 'route-container', 'external-service', undefined, 7, 0, 0, 0, false,
- {product: ' External Service'}));
- }
- lsvg = lsvg.data(legendNodes, function(d) {
- return d.key;
- });
- 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)) + ')';
- });
-
- 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';
- }
-
- if (!mousedown_node || !selected_node)
- return;
-
- if (!start)
- return;
- // set the graph in motion
- //QDR.log.debug("mousedown_node is " + mousedown_node);
- force.start();
-
- }
-
- var startUpdateConnectionsGrid = function(d) {
- // called after each topology update
- var extendConnections = function() {
- // force a fetch of the links for this node
- QDRService.management.topology.ensureEntities(d.key, {entity: 'router.link', force: true}, function () {
- // the links for this node are now available
- $scope.multiData = [];
- let normals = d.normals;
- // find updated normals for d
- d3.selectAll('.normal')
- .each(function(newd) {
- if (newd.id == d.id && newd.name == d.name) {
- normals = newd.normals;
- }
- });
- if (normals) {
- normals.forEach(function(n) {
- let nodeInfo = QDRService.management.topology.nodeInfo();
- let links = nodeInfo[n.key]['router.link'];
- let linkTypeIndex = links.attributeNames.indexOf('linkType');
- let connectionIdIndex = links.attributeNames.indexOf('connectionId');
- n.linkData = [];
- links.results.forEach(function(link) {
- if (link[linkTypeIndex] === 'endpoint' && link[connectionIdIndex] === n.connectionId) {
- let l = {};
- let ll = QDRService.utilities.flatten(links.attributeNames, link);
- l.owningAddr = ll.owningAddr;
- l.dir = ll.linkDir;
- if (l.owningAddr && l.owningAddr.length > 2)
- if (l.owningAddr[0] === 'M')
- l.owningAddr = l.owningAddr.substr(2);
- else
- l.owningAddr = l.owningAddr.substr(1);
-
- l.deliveryCount = ll.deliveryCount;
- l.uncounts = QDRService.utilities.pretty(ll.undeliveredCount + ll.unsettledCount);
- l.adminStatus = ll.adminStatus;
- l.operStatus = ll.operStatus;
- l.identity = ll.identity;
- l.connectionId = ll.connectionId;
- l.nodeId = n.key;
- l.type = ll.type;
- l.name = ll.name;
-
- // TODO: remove this fake quiescing/reviving logic when the routers do the work
- initConnState(n.connectionId);
- if ($scope.quiesceState[n.connectionId].linkStates[l.identity])
- l.adminStatus = $scope.quiesceState[n.connectionId].linkStates[l.identity];
- if ($scope.quiesceState[n.connectionId].state == 'quiescing') {
- if (l.adminStatus === 'enabled') {
- // 25% chance of switching
- let chance = Math.floor(Math.random() * 2);
- if (chance == 1) {
- l.adminStatus = 'disabled';
- $scope.quiesceState[n.connectionId].linkStates[l.identity] = 'disabled';
- }
- }
- }
- if ($scope.quiesceState[n.connectionId].state == 'reviving') {
- if (l.adminStatus === 'disabled') {
- // 25% chance of switching
- let chance = Math.floor(Math.random() * 2);
- if (chance == 1) {
- l.adminStatus = 'enabled';
- $scope.quiesceState[n.connectionId].linkStates[l.identity] = 'enabled';
- }
- }
- }
- QDR.log.debug('pushing link state for ' + l.owningAddr + ' status: ' + l.adminStatus);
-
- n.linkData.push(l);
- }
- });
- $scope.multiData.push(n);
- if (n.connectionId == $scope.connectionId)
- $scope.linkData = n.linkData;
- initConnState(n.connectionId);
- $scope.multiDetails.updateState(n);
- });
- }
- $scope.$apply();
-
- d3.select('#multiple_details')
- .style({
- height: ((normals.length + 1) * 30) + 40 + 'px',
- 'overflow-y': normals.length > 10 ? 'scroll' : 'hidden'
- });
- });
- };
- $scope.connectionsStyle = function () {
- return {
- height: ($scope.multiData.length * 30 + 40) + 'px'
- };
- };
- $scope.linksStyle = function () {
- return {
- height: ($scope.linkData.length * 30 + 40) + 'px'
- };
- };
- // register a notification function for when the topology is updated
- QDRService.management.topology.addUpdatedAction('normalsStats', extendConnections);
- // call the function that gets the links right now
- extendConnections();
- clearPopups();
- let display = 'block';
- if (d.normals.length === 1) {
- display = 'none';
- mouseY = mouseY - 20;
- }
- let rm = relativeMouse();
- d3.select('#multiple_details')
- .style({
- display: display,
- opacity: 1,
- left: rm.left + 'px',
- top: (rm.top - rm.offset.top) + 'px'
- });
- if (d.normals.length === 1) {
- // simulate a click on the connection to popup the link details
- QDRService.management.topology.ensureEntities(d.key, {entity: 'router.link', force: true}, function () {
- $scope.multiDetails.showLinksList({
- entity: d
- });
- });
- }
- };
- var stopUpdateConnectionsGrid = function() {
- QDRService.management.topology.delUpdatedAction('normalsStats');
- };
-
- var initConnState = function(id) {
- if (!angular.isDefined($scope.quiesceState[id])) {
- $scope.quiesceState[id] = {
- state: 'enabled',
- buttonText: 'Quiesce',
- buttonDisabled: false,
- linkStates: {}
- };
- }
- };
-
- function nextHop(thisNode, d) {
- if ((thisNode) && (thisNode != d)) {
- let target = findNextHopNode(thisNode, d);
- //QDR.log.debug("highlight link from node ");
- //console.dump(nodeFor(selected_node.name));
- //console.dump(target);
- if (target) {
- let hnode = nodeFor(thisNode.name);
- let hlLink = linkFor(hnode, target);
- //QDR.log.debug("need to highlight");
- //console.dump(hlLink);
- if (hlLink) {
- hlLink['highlighted'] = true;
- hnode['highlighted'] = true;
- }
- else
- target = null;
- }
- nextHop(target, d);
- }
- if (thisNode == d) {
- let hnode = nodeFor(thisNode.name);
- hnode['highlighted'] = true;
- }
- }
-
- 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 routers = nodes.filter( function (node) {
- return node.nodeType === 'inter-router';
- });
- if (routers.length > cnodes.length) {
- return -1;
- }
-
-
- if (cnodes.length != Object.keys(savedKeys).length) {
- 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++) {
- let key = cnodes[i];
- // if this node isn't in the saved node list
- if (!savedKeys.hasOwnProperty(key))
- return 1;
- // if the number of connections for this node chaanged
- if (!nodeInfo[key]['connection'])
- return -1;
- if (nodeInfo[key]['connection'].results.length != savedKeys[key]) {
- return -1;
- }
- }
- return 0;
- }
-
- function saveChanged() {
- savedKeys = {};
- 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;
- }
- }
- // we are about to leave the page, save the node positions
- $rootScope.$on('$locationChangeStart', function() {
- //QDR.log.debug("locationChangeStart");
- savePositions();
- });
- // When the DOM element is removed from the page,
- // AngularJS will trigger the $destroy event on
- // the scope
- $scope.$on('$destroy', function() {
- //QDR.log.debug("scope on destroy");
- savePositions();
- QDRService.management.topology.setUpdateEntities([]);
- QDRService.management.topology.stopUpdating();
- QDRService.management.topology.delUpdatedAction('normalsStats');
- QDRService.management.topology.delUpdatedAction('topology');
-
- d3.select('#SVG_ID').remove();
- window.removeEventListener('resize', resize);
- });
-
- function handleInitialUpdate() {
- // we only need to update connections during steady-state
- QDRService.management.topology.setUpdateEntities(['connection']);
- // we currently have all entities available on all routers
- saveChanged();
- initForceGraph();
- // 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']});
- // call this function every time a background update is done
- 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');
- animate = true;
- setupInitialUpdate();
- } else if (changed === -1) {
- // we lost a node (or a client), we can draw the new svg immediately
- animate = false;
- saveChanged();
- let nodeInfo = QDRService.management.topology.nodeInfo();
- initializeNodes(nodeInfo);
-
- let unknowns = [];
- initializeLinks(nodeInfo, unknowns);
- if (unknowns.length > 0) {
- resolveUnknowns(nodeInfo, unknowns);
- }
- else {
- force.nodes(nodes).links(links).start();
- restart();
- }
-
- //initForceGraph();
- } else {
- //QDR.log.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"}, {entity: ".router.lin.router.link", attrs: ["linkType","connectionId","linkDir"]}],
- [{entity: 'connection'}],
- //[{entity: ".connection"}],
- handleInitialUpdate);
- }
- if (!QDRService.management.connection.is_connected()) {
- // we are not connected. we probably got here from a bookmark or manual page reload
- QDR.redirectWhenConnected($location, 'topology');
- return;
- }
-
- let connectionPopupHTML = function (d) {
- let left = d.left ? d.source : d.target;
- // left is the connection with dir 'in'
- let right = d.left ? d.target : d.source;
- let onode = QDRService.management.topology.nodeInfo()[left.key];
- let connSecurity = function (conn) {
- if (!conn.isEncrypted)
- return 'no-security';
- if (conn.sasl === 'GSSAPI')
- return 'Kerberos';
- return conn.sslProto + '(' + conn.sslCipher + ')';
- };
- let connAuth = function (conn) {
- if (!conn.isAuthenticated)
- return 'no-auth';
- let sasl = conn.sasl;
- if (sasl === 'GSSAPI')
- sasl = 'Kerberos';
- else if (sasl === 'EXTERNAL')
- sasl = 'x.509';
- else if (sasl === 'ANONYMOUS')
- return 'anonymous-user';
- if (!conn.user)
- return sasl;
- return conn.user + '(' + sasl + ')';
- };
- let connTenant = function (conn) {
- if (!conn.tenant) {
- return '';
- }
- if (conn.tenant.length > 1)
- return conn.tenant.replace(/\/$/, '');
- };
- // loop through all the connections for left, and find the one for right
- let rightIndex = onode['connection'].results.findIndex( function (conn) {
- return QDRService.utilities.valFor(onode['connection'].attributeNames, conn, 'container') === right.routerId;
- });
- if (rightIndex < 0) {
- // we have a connection to a client/service
- rightIndex = +left.connectionId;
- }
- if (isNaN(rightIndex)) {
- // we have a connection to a console
- rightIndex = +right.connectionId;
- }
- let HTML = '';
- if (rightIndex >= 0) {
- let conn = onode['connection'].results[rightIndex];
- conn = QDRService.utilities.flatten(onode['connection'].attributeNames, conn);
- HTML += '<table class="popupTable">';
- HTML += ('<tr><td>Security</td><td>' + connSecurity(conn) + '</td></tr>');
- HTML += ('<tr><td>Authentication</td><td>' + connAuth(conn) + '</td></tr>');
- HTML += ('<tr><td>Tenant</td><td>' + connTenant(conn) + '</td></tr>');
- HTML += '</table>';
- }
- return HTML;
- };
-
- animate = true;
- setupInitialUpdate();
- QDRService.management.topology.startUpdating(false);
-
- }
- ]);
-
- return QDR;
-
-}(QDR || {}));
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org
[2/4] qpid-dispatch git commit: DISPATCH-1002 Added optional message
flow animation to topology page
Posted by ea...@apache.org.
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/61df4890/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
new file mode 100644
index 0000000..82af88a
--- /dev/null
+++ b/console/stand-alone/plugin/js/topology/qdrTopology.js
@@ -0,0 +1,2125 @@
+/*
+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.
+*/
+'use strict';
+
+/* global angular d3 separateAddresses Traffic */
+/**
+ * @module QDR
+ */
+var QDR = (function(QDR) {
+
+ QDR.module.controller('QDR.TopologyFormController', function($scope, $timeout) {
+
+ $scope.attributes = [];
+ $scope.attributesConnections = [];
+
+ $scope.form = 'Router';
+ $scope.$on('showEntityForm', function(event, args) {
+ let attributes = args.attributes;
+ // capitalize 1st letter
+ $scope.form = args.entity.charAt(0).toUpperCase() + args.entity.slice(1);
+
+ let H = '<div class="infoGrid">';
+ attributes.forEach( function (a, i) {
+ let even = (i % 2) ? 'even' : 'odd';
+ if (a.attributeName === 'Listening on')
+ even += ' listening-on';
+ H += ('<div class="'+ even +'"><span title="'+a.attributeName+'">' + a.attributeName + '</span><span title="'+a.attributeValue+'">' + a.attributeValue + '</span></div>');
+ });
+ H += '</div>';
+ $('#formInfo').html(H);
+
+ if (!$scope.$$phase) $scope.$apply();
+
+ });
+
+ $scope.panelVisible = true; // show/hide the panel on the left
+ $scope.hideLeftPane = function () {
+ d3.select('.page-menu')
+ .style('left' , '-360px')
+ .style('z-index', '1');
+
+ d3.select('.diagram')
+ .transition().duration(300).ease('sin-in')
+ .style('margin-left', '-360px')
+ .each('end', function () {
+ $timeout(function () {$scope.panelVisible = false;});
+ let div = d3.select(this);
+ div.style('margin-left', '0');
+ div.style('padding-left', 0);
+ });
+ };
+ $scope.showLeftPane = function () {
+ d3.select('.page-menu')
+ .style('left' , '0px');
+
+ $timeout(function () {$scope.panelVisible = true;});
+ d3.select('.diagram')
+ .style('margin-left', '0px')
+ .transition().duration(300).ease('sin-out')
+ .style('margin-left', '300px')
+ .each('end', function () {
+ let div = d3.select(this);
+ div.style('margin-left', '0');
+ div.style('padding-left', '300px');
+ });
+ };
+ });
+ /**
+ * @method TopologyController
+ *
+ * Controller that handles the QDR topology page
+ */
+ QDR.module.controller('QDR.TopologyController', ['$scope', '$rootScope', 'QDRService', '$location', '$timeout', '$uibModal', '$sce',
+ function($scope, $rootScope, QDRService, $location, $timeout, $uibModal, $sce) {
+
+ 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 = [];
+ let links = [];
+ let forceData = {nodes: nodes, links: links};
+
+ $scope.multiData = [];
+ $scope.quiesceState = {};
+ let dontHide = false;
+ $scope.crosshtml = $sce.trustAsHtml('');
+ $scope.legendOptions = angular.fromJson(localStorage[TOPOOPTIONSKEY]) || {showTraffic: false};
+ $scope.legend = {status: {legendOpen: true, optionsOpen: true}};
+ let traffic = new Traffic($scope, $timeout, QDRService, separateAddresses,
+ radius, forceData, nextHop);
+
+ // the showTraaffic checkbox was just toggled (or initialized)
+ $scope.$watch('legendOptions.showTraffic', function () {
+ localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions);
+ if ($scope.legendOptions.showTraffic) {
+ traffic.start();
+ } else {
+ traffic.stop();
+ traffic.remove();
+ }
+ });
+ // event notification that an address checkbox has changed
+ $scope.addressFilterChanged = function () {
+ traffic.updateAddresses();
+ };
+
+ // called by angular when mouse enters one of the address legends
+ $scope.enterLegend = function (address) {
+ // fade all flows that aren't for this address
+ traffic.fadeOtherAddresses(address);
+ };
+ // called when the mouse leaves one of the address legends
+ $scope.leaveLegend = function () {
+ traffic.unFadeAll();
+ };
+ // clicked on the address name. toggle the address checkbox
+ $scope.addressClick = function (address) {
+ traffic.toggleAddress(address);
+ };
+
+ $scope.quiesceConnection = function(row) {
+ let entity = row.entity;
+ let state = $scope.quiesceState[entity.connectionId].state;
+ if (state === 'enabled') {
+ // start quiescing all links
+ $scope.quiesceState[entity.connectionId].state = 'quiescing';
+ } else if (state === 'quiesced') {
+ // start reviving all links
+ $scope.quiesceState[entity.connectionId].state = 'reviving';
+ }
+ $scope.multiDetails.updateState(entity);
+ dontHide = true;
+ $scope.multiDetails.selectRow(row.rowIndex, true);
+ $scope.multiDetails.showLinksList(row);
+ };
+ $scope.quiesceDisabled = function(row) {
+ return $scope.quiesceState[row.entity.connectionId].buttonDisabled;
+ };
+ $scope.quiesceText = function(row) {
+ return $scope.quiesceState[row.entity.connectionId].buttonText;
+ };
+ $scope.quiesceClass = function(row) {
+ const stateClassMap = {
+ enabled: 'btn-primary',
+ quiescing: 'btn-warning',
+ reviving: 'btn-warning',
+ quiesced: 'btn-danger'
+ };
+ return stateClassMap[$scope.quiesceState[row.entity.connectionId].state];
+ };
+
+ // This is the grid that shows each connection when a client node that represents multiple connections is clicked
+ $scope.multiData = [];
+ $scope.multiDetails = {
+ data: 'multiData',
+ enableColumnResize: true,
+ enableHorizontalScrollbar: 0,
+ enableVerticalScrollbar: 0,
+ jqueryUIDraggable: true,
+ enablePaging: false,
+ multiSelect: false,
+ enableSelectAll: false,
+ enableSelectionBatchEvent: false,
+ enableRowHeaderSelection: false,
+ noUnselect: true,
+ onRegisterApi: function (gridApi) {
+ if (gridApi.selection) {
+ gridApi.selection.on.rowSelectionChanged($scope, function(row){
+ let detailsDiv = d3.select('#link_details');
+ let isVis = detailsDiv.style('display') === 'block';
+ if (!dontHide && isVis && $scope.connectionId === row.entity.connectionId) {
+ hideLinkDetails();
+ return;
+ }
+ dontHide = false;
+ $scope.multiDetails.showLinksList(row);
+ });
+ }
+ },
+ showLinksList: function(obj) {
+ $scope.linkData = obj.entity.linkData;
+ $scope.connectionId = obj.entity.connectionId;
+ let visibleLen = Math.min(obj.entity.linkData.length, 10);
+ //QDR.log.debug("visibleLen is " + visibleLen)
+ let left = parseInt(d3.select('#multiple_details').style('left'), 10);
+ let offset = $('#topology').offset();
+ let detailsDiv = d3.select('#link_details');
+ detailsDiv
+ .style({
+ display: 'block',
+ opacity: 1,
+ left: (left + 20) + 'px',
+ top: (mouseY - offset.top + 20 + $(document).scrollTop()) + 'px',
+ height: ((visibleLen + 1) * 30) + 40 + 'px', // +1 for the header row
+ 'overflow-y': obj.entity.linkData > 10 ? 'scroll' : 'hidden'
+ });
+ },
+ updateState: function(entity) {
+ let state = $scope.quiesceState[entity.connectionId].state;
+
+ // count enabled and disabled links for this connection
+ let enabled = 0,
+ disabled = 0;
+ entity.linkData.forEach(function(link) {
+ if (link.adminStatus === 'enabled')
+ ++enabled;
+ if (link.adminStatus === 'disabled')
+ ++disabled;
+ });
+
+ let linkCount = entity.linkData.length;
+ // if state is quiescing and any links are enabled, button should say 'Quiescing' and be disabled
+ if (state === 'quiescing' && (enabled > 0)) {
+ $scope.quiesceState[entity.connectionId].buttonText = 'Quiescing';
+ $scope.quiesceState[entity.connectionId].buttonDisabled = true;
+ } else
+ // if state is enabled and all links are disabled, button should say Revive and be enabled. set state to quisced
+ // if state is quiescing and all links are disabled, button should say 'Revive' and be enabled. set state to quiesced
+ if ((state === 'quiescing' || state === 'enabled') && (disabled === linkCount)) {
+ $scope.quiesceState[entity.connectionId].buttonText = 'Revive';
+ $scope.quiesceState[entity.connectionId].buttonDisabled = false;
+ $scope.quiesceState[entity.connectionId].state = 'quiesced';
+ } else
+ // if state is reviving and any links are disabled, button should say 'Reviving' and be disabled
+ if (state === 'reviving' && (disabled > 0)) {
+ $scope.quiesceState[entity.connectionId].buttonText = 'Reviving';
+ $scope.quiesceState[entity.connectionId].buttonDisabled = true;
+ } else
+ // if state is reviving or quiesced and all links are enabled, button should say 'Quiesce' and be enabled. set state to enabled
+ if ((state === 'reviving' || state === 'quiesced') && (enabled === linkCount)) {
+ $scope.quiesceState[entity.connectionId].buttonText = 'Quiesce';
+ $scope.quiesceState[entity.connectionId].buttonDisabled = false;
+ $scope.quiesceState[entity.connectionId].state = 'enabled';
+ }
+ },
+ columnDefs: [{
+ field: 'host',
+ cellTemplate: 'titleCellTemplate.html',
+ //headerCellTemplate: 'titleHeaderCellTemplate.html',
+ displayName: 'Connection host'
+ }, {
+ field: 'user',
+ cellTemplate: 'titleCellTemplate.html',
+ //headerCellTemplate: 'titleHeaderCellTemplate.html',
+ displayName: 'User'
+ }, {
+ field: 'properties',
+ cellTemplate: 'titleCellTemplate.html',
+ //headerCellTemplate: 'titleHeaderCellTemplate.html',
+ displayName: 'Properties'
+ }
+ /*,
+ {
+ cellClass: 'gridCellButton',
+ cellTemplate: '<button title="{{quiesceText(row)}} the links" type="button" ng-class="quiesceClass(row)" class="btn" ng-click="$event.stopPropagation();quiesceConnection(row)" ng-disabled="quiesceDisabled(row)">{{quiesceText(row)}}</button>'
+ }*/
+ ]
+ };
+ $scope.quiesceLinkClass = function(row) {
+ const stateClassMap = {
+ enabled: 'btn-primary',
+ disabled: 'btn-danger'
+ };
+ return stateClassMap[row.entity.adminStatus];
+ };
+ $scope.quiesceLink = function(row) {
+ QDRService.management.topology.quiesceLink(row.entity.nodeId, row.entity.name)
+ .then( function (results, context) {
+ let statusCode = context.message.application_properties.statusCode;
+ if (statusCode < 200 || statusCode >= 300) {
+ QDR.Core.notification('error', context.message.statusDescription);
+ QDR.log.info('Error ' + context.message.statusDescription);
+ }
+ });
+ };
+ $scope.quiesceLinkDisabled = function(row) {
+ return (row.entity.operStatus !== 'up' && row.entity.operStatus !== 'down');
+ };
+ $scope.quiesceLinkText = function(row) {
+ return row.entity.operStatus === 'down' ? 'Revive' : 'Quiesce';
+ };
+ $scope.linkData = [];
+ $scope.linkDetails = {
+ data: 'linkData',
+ jqueryUIDraggable: true,
+ columnDefs: [{
+ field: 'adminStatus',
+ cellTemplate: 'titleCellTemplate.html',
+ headerCellTemplate: 'titleHeaderCellTemplate.html',
+ displayName: 'Admin state'
+ }, {
+ field: 'operStatus',
+ cellTemplate: 'titleCellTemplate.html',
+ headerCellTemplate: 'titleHeaderCellTemplate.html',
+ displayName: 'Oper state'
+ }, {
+ field: 'dir',
+ cellTemplate: 'titleCellTemplate.html',
+ headerCellTemplate: 'titleHeaderCellTemplate.html',
+ displayName: 'dir'
+ }, {
+ field: 'owningAddr',
+ cellTemplate: 'titleCellTemplate.html',
+ headerCellTemplate: 'titleHeaderCellTemplate.html',
+ displayName: 'Address'
+ }, {
+ field: 'deliveryCount',
+ displayName: 'Delivered',
+ headerCellTemplate: 'titleHeaderCellTemplate.html',
+ cellClass: 'grid-values'
+
+ }, {
+ field: 'uncounts',
+ displayName: 'Outstanding',
+ headerCellTemplate: 'titleHeaderCellTemplate.html',
+ cellClass: 'grid-values'
+ }
+ /*,
+ {
+ cellClass: 'gridCellButton',
+ cellTemplate: '<button title="{{quiesceLinkText(row)}} this link" type="button" ng-class="quiesceLinkClass(row)" class="btn" ng-click="quiesceLink(row)" ng-disabled="quiesceLinkDisabled(row)">{{quiesceLinkText(row)}}</button>'
+ }*/
+ ]
+ };
+
+ let urlPrefix = $location.absUrl();
+ urlPrefix = urlPrefix.split('#')[0];
+ QDR.log.debug('started QDR.TopologyController with urlPrefix: ' + urlPrefix);
+
+ // 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.contextNode = null; // node that is associated with the current context menu
+ $scope.isRight = function(mode) {
+ return mode.right;
+ };
+
+ var setNodesFixed = function (name, b) {
+ nodes.some(function (n) {
+ if (n.name === name) {
+ n.fixed = b;
+ return true;
+ }
+ });
+ };
+ $scope.setFixed = function(b) {
+ if ($scope.contextNode) {
+ $scope.contextNode.fixed = b;
+ setNodesFixed($scope.contextNode.name, b);
+ savePositions();
+ }
+ restart();
+ };
+ $scope.isFixed = function() {
+ if (!$scope.contextNode)
+ return false;
+ return ($scope.contextNode.fixed & 1);
+ };
+
+ let mouseX, mouseY;
+ var relativeMouse = function () {
+ let offset = $('#main_container').offset();
+ return {left: (mouseX + $(document).scrollLeft()) - 1,
+ top: (mouseY + $(document).scrollTop()) - 1,
+ offset: offset
+ };
+ };
+ // event handlers for popup context menu
+ $(document).mousemove(function(e) {
+ mouseX = e.clientX;
+ mouseY = e.clientY;
+ });
+ $(document).mousemove();
+ $(document).click(function() {
+ $scope.contextNode = null;
+ $('.contextMenu').fadeOut(200);
+ });
+
+ const radii = {
+ 'inter-router': 25,
+ 'normal': 15,
+ 'on-demand': 15,
+ 'route-container': 15,
+ };
+ let svg, lsvg;
+ let force;
+ let animate = false; // should the force graph organize itself when it is displayed
+ let path, circle;
+ let savedKeys = {};
+ let width = 0;
+ let height = 0;
+
+ var getSizes = function() {
+ let legendWidth = 143;
+ let display = $('#topo_svg_legend').css('display');
+ if (display === 'none')
+ legendWidth = 0;
+ const gap = 5;
+ let width = $('#topology').width() - gap - legendWidth;
+ let top = $('#topology').offset().top;
+ let tpformHeight = $('#topologyForm').height();
+ let height = Math.max(window.innerHeight, tpformHeight + top) - top - gap;
+ if (width < 10) {
+ QDR.log.info('page width and height are abnormal w:' + width + ' height:' + height);
+ return [0, 0];
+ }
+ return [width, height];
+ };
+ var resize = function() {
+ if (!svg)
+ return;
+ let sizes = getSizes();
+ width = sizes[0];
+ height = sizes[1];
+ if (width > 0) {
+ // set attrs and 'resume' force
+ svg.attr('width', width);
+ svg.attr('height', height);
+ force.size(sizes).resume();
+ }
+ };
+
+ $scope.$on('panel-resized', function () {
+ resize();
+ });
+ window.addEventListener('resize', resize);
+ let sizes = getSizes();
+ width = sizes[0];
+ height = sizes[1];
+ if (width <= 0 || height <= 0)
+ return;
+
+ var nodeExists = function (connectionContainer) {
+ return nodes.findIndex( function (node) {
+ return node.container === connectionContainer;
+ });
+ };
+ var normalExists = function (connectionContainer) {
+ let normalInfo = {};
+ for (let i=0; i<nodes.length; ++i) {
+ if (nodes[i].normals) {
+ if (nodes[i].normals.some(function (normal, j) {
+ if (normal.container === connectionContainer && i !== j) {
+ normalInfo = {nodesIndex: i, normalsIndex: j};
+ return true;
+ }
+ return false;
+ }))
+ break;
+ }
+ }
+ return normalInfo;
+ };
+ var getLinkSource = function (nodesIndex) {
+ for (let i=0; i<links.length; ++i) {
+ if (links[i].target === nodesIndex)
+ return i;
+ }
+ return -1;
+ };
+ var aNode = function(id, name, nodeType, nodeInfo, nodeIndex, x, y, connectionContainer, resultIndex, fixed, properties) {
+ properties = properties || {};
+ for (let i=0; i<nodes.length; ++i) {
+ if (nodes[i].name === name || nodes[i].container === connectionContainer) {
+ if (properties.product)
+ nodes[i].properties = properties;
+ return nodes[i];
+ }
+ }
+ let routerId = QDRService.management.topology.nameFromId(id);
+ return {
+ key: id,
+ name: name,
+ nodeType: nodeType,
+ properties: properties,
+ routerId: routerId,
+ x: x,
+ y: y,
+ id: nodeIndex,
+ resultIndex: resultIndex,
+ fixed: !!+fixed,
+ cls: '',
+ container: connectionContainer
+ };
+ };
+
+ var getLinkDir = function (id, connection, onode) {
+ let links = onode['router.link'];
+ if (!links) {
+ return 'unknown';
+ }
+ let inCount = 0, outCount = 0;
+ links.results.forEach( function (linkResult) {
+ let link = QDRService.utilities.flatten(links.attributeNames, linkResult);
+ if (link.linkType === 'endpoint' && link.connectionId === connection.identity)
+ if (link.linkDir === 'in')
+ ++inCount;
+ else
+ ++outCount;
+ });
+ if (inCount > 0 && outCount > 0)
+ return 'both';
+ if (inCount > 0)
+ return 'in';
+ if (outCount > 0)
+ return 'out';
+ return 'unknown';
+ };
+
+ var savePositions = function () {
+ nodes.forEach( function (d) {
+ localStorage[d.name] = angular.toJson({
+ x: Math.round(d.x),
+ y: Math.round(d.y),
+ fixed: (d.fixed & 1) ? 1 : 0,
+ });
+ });
+ };
+
+ var initializeNodes = function (nodeInfo) {
+ let nodeCount = Object.keys(nodeInfo).length;
+ let yInit = 50;
+ forceData.nodes = nodes = [];
+ for (let id in nodeInfo) {
+ let name = QDRService.management.topology.nameFromId(id);
+ // if we have any new nodes, animate the force graph to position them
+ let position = angular.fromJson(localStorage[name]);
+ if (!angular.isDefined(position)) {
+ animate = true;
+ position = {
+ x: Math.round(width / 4 + ((width / 2) / nodeCount) * nodes.length),
+ y: Math.round(height / 2 + Math.sin(nodes.length / (Math.PI*2.0)) * height / 4),
+ fixed: false,
+ };
+ //QDR.log.debug("new node pos (" + position.x + ", " + position.y + ")")
+ }
+ if (position.y > height) {
+ position.y = 200 - yInit;
+ yInit *= -1;
+ }
+ nodes.push(aNode(id, name, 'inter-router', nodeInfo, nodes.length, position.x, position.y, name, undefined, position.fixed));
+ }
+ };
+
+ var initializeLinks = function (nodeInfo, unknowns) {
+ forceData.links = links = [];
+ let source = 0;
+ let client = 1.0;
+ for (let id in nodeInfo) {
+ let onode = nodeInfo[id];
+ if (!onode['connection'])
+ continue;
+ let conns = onode['connection'].results;
+ let attrs = onode['connection'].attributeNames;
+ //QDR.log.debug("external client parent is " + parent);
+ let normalsParent = {}; // 1st normal node for this parent
+
+ for (let j = 0; j < conns.length; j++) {
+ let connection = QDRService.utilities.flatten(attrs, conns[j]);
+ let role = connection.role;
+ let properties = connection.properties || {};
+ let dir = connection.dir;
+ if (role == 'inter-router') {
+ let connId = connection.container;
+ let target = getContainerIndex(connId, nodeInfo);
+ if (target >= 0) {
+ 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 = QDRService.management.topology.nameFromId(id) + '.' + connection.identity;
+
+ // if we have any new clients, animate the force graph to position them
+ let position = angular.fromJson(localStorage[name]);
+ if (!angular.isDefined(position)) {
+ animate = true;
+ position = {
+ x: Math.round(nodes[source].x + 40 * Math.sin(client / (Math.PI * 2.0))),
+ y: Math.round(nodes[source].y + 40 * Math.cos(client / (Math.PI * 2.0))),
+ fixed: false
+ };
+ //QDR.log.debug("new client pos (" + position.x + ", " + position.y + ")")
+ }// else QDR.log.debug("using previous location")
+ if (position.y > height) {
+ position.y = Math.round(nodes[source].y + 40 + Math.cos(client / (Math.PI * 2.0)));
+ }
+ let existingNodeIndex = nodeExists(connection.container);
+ let normalInfo = normalExists(connection.container);
+ let node = aNode(id, name, role, nodeInfo, nodes.length, position.x, position.y, connection.container, j, position.fixed, properties);
+ let nodeType = QDRService.utilities.isAConsole(properties, connection.identity, role, node.key) ? 'console' : 'client';
+ let cdir = getLinkDir(id, connection, onode);
+ if (existingNodeIndex >= 0) {
+ // make a link between the current router (source) and the existing node
+ 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 = getLinkSource(normalInfo.nodesIndex);
+ if (normalSource >= 0) {
+ if (cdir === 'unknown')
+ cdir = dir;
+ node.cdir = cdir;
+ nodes.push(node);
+ // create link from original node to the new node
+ getLink(links[normalSource].source, nodes.length-1, cdir, 'small', connection.name);
+ // create link from this router to the new node
+ getLink(source, nodes.length-1, cdir, 'small', connection.name);
+ // remove the old node from the normals list
+ nodes[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.push(node);
+ node.normals = [node];
+ // now add a link
+ getLink(source, nodes.length - 1, cdir, 'small', connection.name);
+ client++;
+ } else {
+ normalsParent[nodeType+cdir].normals.push(node);
+ }
+ } else {
+ node.id = nodes.length - 1 + unknowns.length;
+ unknowns.push(node);
+ }
+ } else {
+ nodes.push(node);
+ // now add a link
+ getLink(source, nodes.length - 1, dir, 'small', connection.name);
+ client++;
+ }
+ }
+ }
+ source++;
+ }
+ };
+
+ // 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 = nodeCount;
+ if (nodeCount < 6) count = 6;
+ if (nodeCount > 80) count = 80;
+ let x = d3.scale.linear()
+ .domain([6,80])
+ .range([min, max]);
+ //QDR.log.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 = [];
+ forceData.links = links = [];
+ let nodeInfo = QDRService.management.topology.nodeInfo();
+ let nodeCount = Object.keys(nodeInfo).length;
+
+ let oldSelectedNode = selected_node;
+ let oldMouseoverNode = mouseover_node;
+ mouseover_node = null;
+ selected_node = null;
+ selected_link = null;
+
+ savePositions();
+ d3.select('#SVG_ID').remove();
+ svg = d3.select('#topology')
+ .append('svg')
+ .attr('id', 'SVG_ID')
+ .attr('width', width)
+ .attr('height', height)
+ .on('click', function() {
+ removeCrosssection();
+ });
+
+ $(document).keyup(function(e) {
+ if (e.keyCode === 27) {
+ removeCrosssection();
+ }
+ });
+
+ // 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) + ')')
+ .selectAll('g');
+
+ // mouse event vars
+ mousedown_link = null;
+ mousedown_node = null;
+ mouseup_node = null;
+
+ // initialize the list of nodes
+ initializeNodes(nodeInfo);
+ savePositions();
+
+ // initialize the list of links
+ let unknowns = [];
+ initializeLinks(nodeInfo, unknowns);
+ $scope.schema = QDRService.management.schema();
+ // init D3 force layout
+ force = d3.layout.force()
+ .nodes(nodes)
+ .links(links)
+ .size([width, height])
+ .linkDistance(function(d) { return linkDistance(d, nodeCount); })
+ .charge(function(d) { return charge(d, nodeCount); })
+ .friction(.10)
+ .gravity(function(d) { return gravity(d, nodeCount); })
+ .on('tick', tick)
+ .on('end', function () {savePositions();})
+ .start();
+
+ svg.append('svg:defs').selectAll('marker')
+ .data(['end-arrow', 'end-arrow-selected', 'end-arrow-small', 'end-arrow-highlighted']) // Different link/path types can be defined here
+ .enter().append('svg:marker') // This section adds in the arrows
+ .attr('id', String)
+ .attr('viewBox', '0 -5 10 10')
+ .attr('refX', 24)
+ .attr('markerWidth', 4)
+ .attr('markerHeight', 4)
+ .attr('orient', 'auto')
+ .classed('small', function (d) {return d.indexOf('small') > -1;})
+ .append('svg:path')
+ .attr('d', 'M 0 -5 L 10 0 L 0 5 z');
+
+ svg.append('svg:defs').selectAll('marker')
+ .data(['start-arrow', 'start-arrow-selected', 'start-arrow-small', 'start-arrow-highlighted']) // Different link/path types can be defined here
+ .enter().append('svg:marker') // This section adds in the arrows
+ .attr('id', String)
+ .attr('viewBox', '0 -5 10 10')
+ .attr('refX', function (d) { return d !== 'start-arrow-small' ? -14 : -24;})
+ .attr('markerWidth', 4)
+ .attr('markerHeight', 4)
+ .attr('orient', 'auto')
+ .append('svg:path')
+ .attr('d', 'M 10 -5 L 0 0 L 10 5 z');
+
+ 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').selectAll('path'),
+ circle = svg.append('svg:g').selectAll('g');
+
+ // app starts here
+ restart(false);
+ force.start();
+ if (oldSelectedNode) {
+ d3.selectAll('circle.inter-router').classed('selected', function (d) {
+ if (d.key === oldSelectedNode.key) {
+ selected_node = d;
+ return true;
+ }
+ return false;
+ });
+ }
+ if (oldMouseoverNode && selected_node) {
+ 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 () {
+ nextHop(selected_node, d);
+ restart();
+ });
+ }
+ });
+ }
+ setTimeout(function () {
+ updateForm(Object.keys(QDRService.management.topology.nodeInfo())[0], 'router', 0);
+ });
+
+ // if any clients don't yet have link directions, get the links for those nodes and restart the graph
+ if (unknowns.length > 0)
+ setTimeout(resolveUnknowns, 10, nodeInfo, unknowns);
+
+ var continueForce = function (extra) {
+ if (extra > 0) {
+ --extra;
+ force.start();
+ setTimeout(continueForce, 100, extra);
+ }
+ };
+ continueForce(forceScale(nodeCount, 0, 200)); // give graph time to settle down
+ };
+
+ var resolveUnknowns = function (nodeInfo, unknowns) {
+ let unknownNodes = {};
+ // collapse the unknown node.keys using an object
+ for (let i=0; i<unknowns.length; ++i) {
+ unknownNodes[unknowns[i].key] = 1;
+ }
+ unknownNodes = Object.keys(unknownNodes);
+ //QDR.log.info("-- resolveUnknowns: ensuring .connection and .router.link are present for each node")
+ QDRService.management.topology.ensureEntities(unknownNodes, [{entity: 'connection', force: true}, {entity: 'router.link', attrs: ['linkType','connectionId','linkDir'], force: true}], function () {
+ nodeInfo = QDRService.management.topology.nodeInfo();
+ initializeLinks(nodeInfo, []);
+ // collapse any router-container nodes that are duplicates
+ animate = true;
+ force.nodes(nodes).links(links).start();
+ restart(false);
+ });
+ };
+
+ function updateForm(key, entity, resultIndex) {
+ if (!angular.isDefined(resultIndex))
+ return;
+ let nodeList = QDRService.management.topology.nodeIdList();
+ if (nodeList.indexOf(key) > -1) {
+ QDRService.management.topology.fetchEntities(key, [
+ {entity: entity},
+ {entity: 'listener', attrs: ['role', 'port']}], function (results) {
+ let onode = results[key];
+ if (!onode[entity]) {
+ console.log('requested ' + entity + ' but didn\'t get it');
+ return;
+ }
+ let nodeResults = onode[entity].results[resultIndex];
+ let nodeAttributes = onode[entity].attributeNames;
+ let attributes = nodeResults.map(function(row, i) {
+ return {
+ attributeName: nodeAttributes[i],
+ attributeValue: row
+ };
+ });
+ // sort by attributeName
+ attributes.sort(function(a, b) {
+ return a.attributeName.localeCompare(b.attributeName);
+ });
+
+ // move the Name first
+ let nameIndex = attributes.findIndex(function(attr) {
+ return attr.attributeName === 'name';
+ });
+ if (nameIndex >= 0)
+ attributes.splice(0, 0, attributes.splice(nameIndex, 1)[0]);
+
+ // get the list of ports this router is listening on
+ if (entity === 'router') {
+ let listeners = onode['listener'].results;
+ let listenerAttributes = onode['listener'].attributeNames;
+ let normals = listeners.filter(function(listener) {
+ return QDRService.utilities.valFor(listenerAttributes, listener, 'role') === 'normal';
+ });
+ let ports = [];
+ normals.forEach(function(normalListener) {
+ ports.push(QDRService.utilities.valFor(listenerAttributes, normalListener, 'port'));
+ });
+ // add as 2nd row
+ if (ports.length) {
+ attributes.splice(1, 0, {
+ attributeName: 'Listening on',
+ attributeValue: ports,
+ description: 'The port(s) on which this router is listening for connections'
+ });
+ }
+ }
+ $rootScope.$broadcast('showEntityForm', {
+ entity: entity,
+ attributes: attributes
+ });
+ if (!$scope.$$phase) $scope.$apply();
+ });
+ }
+ }
+
+ function getContainerIndex(_id, nodeInfo) {
+ let nodeIndex = 0;
+ for (let id in nodeInfo) {
+ if (QDRService.management.topology.nameFromId(id) === _id)
+ return nodeIndex;
+ ++nodeIndex;
+ }
+ return -1;
+ }
+
+ function getLink(_source, _target, dir, cls, uid) {
+ for (let i = 0; i < links.length; i++) {
+ let s = links[i].source,
+ t = links[i].target;
+ if (typeof links[i].source == 'object') {
+ s = s.id;
+ t = t.id;
+ }
+ if (s == _source && t == _target) {
+ return i;
+ }
+ // same link, just reversed
+ if (s == _target && t == _source) {
+ return -i;
+ }
+ }
+ //QDR.log.debug("creating new link (" + (links.length) + ") between " + nodes[_source].name + " and " + nodes[_target].name);
+ if (links.some( function (l) { return l.uid === uid;}))
+ uid = uid + '.' + links.length;
+ let link = {
+ source: _source,
+ target: _target,
+ left: dir != 'out',
+ right: (dir == 'out' || dir == 'both'),
+ cls: cls,
+ uid: uid,
+ };
+ return links.push(link) - 1;
+ }
+
+
+ function resetMouseVars() {
+ mousedown_node = null;
+ mouseover_node = null;
+ mouseup_node = null;
+ mousedown_link = null;
+ }
+
+ // 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));
+ return 'translate(' + d.x + ',' + d.y + ')';
+ });
+
+ // draw directed edges with proper padding from node centers
+ path.attr('d', function(d) {
+ let sourcePadding, targetPadding, r;
+
+ r = d.target.nodeType === 'inter-router' ? radius : radiusNormal - 18;
+ 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)),
+ dsx = Math.max(sourcePadding, Math.min(width - r, d.source.x)),
+ dsy = Math.max(sourcePadding, Math.min(height - r, d.source.y));
+
+ let deltaX = dtx - dsx,
+ deltaY = dty - dsy,
+ dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
+ if (dist == 0)
+ dist = 0.001;
+ let normX = deltaX / dist,
+ normY = deltaY / dist;
+ let sourceX = dsx + (sourcePadding * normX),
+ sourceY = dsy + (sourcePadding * normY),
+ targetX = dtx - (targetPadding * normX),
+ targetY = dty - (targetPadding * normY);
+ sourceX = Math.max(0, sourceX);
+ sourceY = Math.max(0, sourceY);
+ targetX = Math.max(0, targetX);
+ targetY = Math.max(0, targetY);
+
+ return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY;
+ })
+ .attr('id', function (d) {
+ return ['path', d.source.index, d.target.index].join('-');
+ });
+
+ if (!animate) {
+ animate = true;
+ force.stop();
+ }
+ }
+
+ // highlight the paths between the selected node and the hovered node
+ function findNextHopNode(from, d) {
+ // d is the node that the mouse is over
+ // from is the selected_node ....
+ if (!from)
+ return null;
+
+ if (from == d)
+ return selected_node;
+
+ //QDR.log.debug("finding nextHop from: " + from.name + " to " + d.name);
+ let sInfo = QDRService.management.topology.nodeInfo()[from.key];
+
+ if (!sInfo) {
+ QDR.log.warn('unable to find topology node info for ' + from.key);
+ return null;
+ }
+
+ // find the hovered name in the selected name's .router.node results
+ if (!sInfo['router.node'])
+ return null;
+ 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 (addrT == d.name) {
+ //QDR.log.debug("found " + d.name + " at " + hIdx);
+ let nextHop = QDRService.utilities.valFor(aAr, vAr[hIdx], 'nextHop');
+ //QDR.log.debug("nextHop was " + nextHop);
+ return (nextHop == null) ? nodeFor(addrT) : nodeFor(nextHop);
+ }
+ }
+ return null;
+ }
+
+ function nodeFor(name) {
+ for (let i = 0; i < nodes.length; ++i) {
+ if (nodes[i].name == name)
+ return nodes[i];
+ }
+ return null;
+ }
+
+ function linkFor(source, target) {
+ for (let i = 0; i < links.length; ++i) {
+ if ((links[i].source == source) && (links[i].target == target))
+ return links[i];
+ if ((links[i].source == target) && (links[i].target == source))
+ return links[i];
+ }
+ // the selected node was a client/broker
+ //QDR.log.debug("failed to find a link between ");
+ //console.dump(source);
+ //QDR.log.debug(" and ");
+ //console.dump(target);
+ return null;
+ }
+
+ function clearPopups() {
+ d3.select('#crosssection').style('display', 'none');
+ $('.hastip').empty();
+ d3.select('#multiple_details').style('display', 'none');
+ d3.select('#link_details').style('display', 'none');
+ d3.select('#node_context_menu').style('display', 'none');
+
+ }
+
+ function removeCrosssection() {
+ d3.select('#crosssection svg g').transition()
+ .duration(1000)
+ .attr('transform', 'scale(0)')
+ .style('opacity', 0)
+ .each('end', function () {
+ d3.select('#crosssection svg').remove();
+ d3.select('#crosssection').style('display','none');
+ });
+ d3.select('#multiple_details').transition()
+ .duration(500)
+ .style('opacity', 0)
+ .each('end', function() {
+ d3.select('#multiple_details').style('display', 'none');
+ stopUpdateConnectionsGrid();
+ });
+ hideLinkDetails();
+ }
+
+ function hideLinkDetails() {
+ d3.select('#link_details').transition()
+ .duration(500)
+ .style('opacity', 0)
+ .each('end', function() {
+ d3.select('#link_details').style('display', 'none');
+ });
+ }
+
+ function clerAllHighlights() {
+ for (let i = 0; i < links.length; ++i) {
+ links[i]['highlighted'] = false;
+ }
+ for (let i = 0; i<nodes.length; ++i) {
+ nodes[i]['highlighted'] = false;
+ }
+ }
+ // takes the nodes and links array of objects and adds svg elements for everything that hasn't already
+ // been added
+ function restart(start) {
+ circle.call(force.drag);
+
+ // path (link) group
+ path = path.data(links, function(d) {return d.uid;});
+
+ // update existing links
+ path.classed('selected', function(d) {
+ return d === selected_link;
+ })
+ .classed('highlighted', function(d) {
+ return d.highlighted;
+ })
+ .attr('marker-start', function(d) {
+ let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
+ if (d.highlighted)
+ sel = '-highlighted';
+ return d.left ? 'url(' + urlPrefix + '#start-arrow' + sel + ')' : '';
+ })
+ .attr('marker-end', function(d) {
+ let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
+ if (d.highlighted)
+ sel = '-highlighted';
+ return d.right ? 'url(' + urlPrefix + '#end-arrow' + sel + ')' : '';
+ });
+ // add new links. if a link with a new uid is found in the data, add a new path
+ path.enter().append('svg:path')
+ .attr('class', 'link')
+ .attr('marker-start', function(d) {
+ let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
+ return d.left ? 'url(' + urlPrefix + '#start-arrow' + sel + ')' : '';
+ })
+ .attr('marker-end', function(d) {
+ let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
+ return d.right ? 'url(' + urlPrefix + '#end-arrow' + sel + ')' : '';
+ })
+ .classed('small', function(d) {
+ return d.cls == 'small';
+ })
+ .on('mouseover', function(d) { // mouse over a path
+ let resultIndex = 0; // the connection to use
+ let left = d.left ? d.target : d.source;
+ // right is the node that the arrow points to, left is the other node
+ let right = d.left ? d.source : d.target;
+ let onode = QDRService.management.topology.nodeInfo()[left.key];
+ // loop through all the connections for left, and find the one for right
+ if (!onode || !onode['connection'])
+ return;
+ // update the info dialog for the link the mouse is over
+ if (!selected_node && !selected_link) {
+ for (resultIndex = 0; resultIndex < onode['connection'].results.length; ++resultIndex) {
+ let conn = onode['connection'].results[resultIndex];
+ /// find the connection whose container is the right's name
+ let name = QDRService.utilities.valFor(onode['connection'].attributeNames, conn, 'container');
+ if (name == right.routerId) {
+ break;
+ }
+ }
+ // did not find connection. this is a connection to a non-interrouter node
+ if (resultIndex === onode['connection'].results.length) {
+ // use the non-interrouter node's connection info
+ left = d.target;
+ resultIndex = left.resultIndex;
+ }
+ updateForm(left.key, 'connection', resultIndex);
+ }
+
+ mousedown_link = d;
+ selected_link = mousedown_link;
+ restart();
+ })
+ .on('mousemove', function (d) {
+ let top = $('#topology').offset().top - 5;
+ $timeout(function () {
+ $scope.trustedpopoverContent = $sce.trustAsHtml(connectionPopupHTML(d));
+ });
+ d3.select('#popover-div')
+ .style('display', 'block')
+ .style('left', (d3.event.pageX+5)+'px')
+ .style('top', (d3.event.pageY-top)+'px');
+ })
+ .on('mouseout', function() { // mouse out of a path
+ d3.select('#popover-div')
+ .style('display', 'none');
+ selected_link = null;
+ restart();
+ })
+ // left click a path
+ .on('click', function (d) {
+ let clickPos = d3.mouse(this);
+ d3.event.stopPropagation();
+ clearPopups();
+ var showCrossSection = function() {
+ const diameter = 400;
+ let pack = d3.layout.pack()
+ .size([diameter - 4, diameter - 4])
+ .padding(-10)
+ .value(function(d) { return d.size; });
+
+ d3.select('#crosssection svg').remove();
+ let svg = d3.select('#crosssection').append('svg')
+ .attr('width', diameter)
+ .attr('height', diameter);
+
+ let rg = svg.append('svg:defs')
+ .append('radialGradient')
+ .attr('id', 'cross-gradient')
+ .attr('gradientTransform', 'scale(2.0) translate(-0.5,-0.5)');
+
+ rg
+ .append('stop')
+ .attr('offset', '0%')
+ .attr('stop-color', '#feffff');
+ rg
+ .append('stop')
+ .attr('offset', '40%')
+ .attr('stop-color', '#cfe2f3');
+
+ let svgg = svg.append('g')
+ .attr('transform', 'translate(2,2)');
+
+ svgg
+ .append('rect')
+ .attr('x', 0)
+ .attr('y', 0)
+ .attr('width', 200)
+ .attr('height', 200)
+ .attr('class', 'cross-rect')
+ .attr('fill', 'url('+urlPrefix+'#cross-gradient)');
+
+ svgg
+ .append('line')
+ .attr('class', 'cross-line')
+ .attr({x1: 2, y1: 0, x2: 200, y2: 0});
+ svgg
+ .append('line')
+ .attr('class', 'cross-line')
+ .attr({x1: 2, y1: 0, x2: 0, y2: 200});
+
+ /*
+ let simpleLine = d3.svg.line();
+ svgg
+ .append('path')
+ .attr({
+ d: simpleLine([[0,0],[0,200]]),
+ stroke: '#000',
+ 'stroke-width': '4px'
+ });
+ svgg
+ .append('path')
+ .attr({
+ d: simpleLine([[0,0],[200,0]]),
+ stroke: '#000',
+ 'stroke-width': '4px'
+ });
+*/
+ let root = {
+ name: ' Links between ' + d.source.name + ' and ' + d.target.name,
+ children: []
+ };
+ let nodeInfo = QDRService.management.topology.nodeInfo();
+ let connections = nodeInfo[d.source.key]['connection'];
+ let containerIndex = connections.attributeNames.indexOf('container');
+ connections.results.some ( function (connection) {
+ if (connection[containerIndex] == d.target.routerId) {
+ root.attributeNames = connections.attributeNames;
+ root.obj = connection;
+ root.desc = 'Connection';
+ return true; // stop looping after 1 match
+ }
+ return false;
+ });
+
+ // find router.links where link.remoteContainer is d.source.name
+ let links = nodeInfo[d.source.key]['router.link'];
+ let identityIndex = connections.attributeNames.indexOf('identity');
+ let roleIndex = connections.attributeNames.indexOf('role');
+ let connectionIdIndex = links.attributeNames.indexOf('connectionId');
+ let linkTypeIndex = links.attributeNames.indexOf('linkType');
+ let nameIndex = links.attributeNames.indexOf('name');
+ let linkDirIndex = links.attributeNames.indexOf('linkDir');
+
+ if (roleIndex < 0 || identityIndex < 0 || connectionIdIndex < 0
+ || linkTypeIndex < 0 || nameIndex < 0 || linkDirIndex < 0)
+ return;
+ links.results.forEach ( function (link) {
+ if (root.obj && link[connectionIdIndex] == root.obj[identityIndex] && link[linkTypeIndex] == root.obj[roleIndex])
+ root.children.push (
+ { name: ' ' + link[linkDirIndex] + ' ',
+ size: 100,
+ obj: link,
+ desc: 'Link',
+ attributeNames: links.attributeNames
+ });
+ });
+ if (root.children.length == 0)
+ return;
+ let node = svgg.datum(root).selectAll('.node')
+ .data(pack.nodes)
+ .enter().append('g')
+ .attr('class', function(d) { return d.children ? 'parent node hastip' : 'leaf node hastip'; })
+ .attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')' + (!d.children ? 'scale(0.9)' : ''); });
+ node.append('circle')
+ .attr('r', function(d) { return d.r; });
+
+ node.on('mouseenter', function (d) {
+ let title = '<h4>' + d.desc + '</h4><table class=\'tiptable\'><tbody>';
+ if (d.attributeNames)
+ d.attributeNames.forEach( function (n, i) {
+ title += '<tr><td>' + n + '</td><td>';
+ title += d.obj[i] != null ? d.obj[i] : '';
+ title += '</td></tr>';
+ });
+ title += '</tbody></table>';
+ $timeout( (function () {
+ $scope.crosshtml = $sce.trustAsHtml(title);
+ $('#crosshtml').show();
+ let parent = $('#crosssection');
+ let ppos = parent.position();
+ let mleft = ppos.left + parent.width();
+ $('#crosshtml').css({left: mleft, top: ppos.top});
+ }).bind(this));
+ });
+ node.on('mouseout', function () {
+ $('#crosshtml').hide();
+ });
+
+ node.append('text')
+ .attr('dy', function (d) { return d.children ? '-10em' : '.5em';})
+ .style('text-anchor', 'middle')
+ .text(function(d) {
+ return d.name.substring(0, d.r / 3);
+ });
+ svgg.attr('transform', 'translate(2,2) scale(0.01)');
+
+ let bounds = $('#topology').position();
+ d3.select('#crosssection')
+ .style('display', 'block')
+ .style('left', (clickPos[0] + bounds.left) + 'px')
+ .style('top', (clickPos[1] + bounds.top) + 'px');
+
+ svgg.transition()
+ .attr('transform', 'translate(2,2) scale(1)')
+ .each('end', function () {
+ d3.selectAll('#crosssection g.leaf text').attr('dy', '.3em');
+ });
+ };
+ QDRService.management.topology.ensureEntities(d.source.key, {entity: 'router.link', force: true}, showCrossSection);
+ });
+ // remove old links
+ path.exit().remove();
+
+
+ // circle (node) group
+ // nodes are known by id
+ circle = circle.data(nodes, function(d) {
+ return d.name;
+ });
+
+ // 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 & 1;
+ });
+
+ // add new circle nodes. if nodes[] is longer than the existing paths, add a new path for each new element
+ 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; });
+
+ appendCircle(g)
+ .on('mouseover', function(d) { // mouseover a circle
+ if (!selected_node && !mousedown_node) {
+ if (d.nodeType === 'inter-router') {
+ //QDR.log.debug("showing general form");
+ updateForm(d.key, 'router', 0);
+ } else if (d.nodeType === 'normal' || d.nodeType === 'on-demand' || d.nodeType === 'route-container') {
+ //QDR.log.debug("showing connections form");
+ updateForm(d.key, 'connection', d.resultIndex);
+ }
+ }
+
+ if (d === mousedown_node)
+ return;
+ //if (d === selected_node)
+ // return;
+ // enlarge target node
+ d3.select(this).attr('transform', 'scale(1.1)');
+ // highlight the next-hop route from the selected node to this node
+ //mousedown_node = null;
+
+ if (!selected_node) {
+ return;
+ }
+ clerAllHighlights();
+ // 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
+ nextHop(selected_node, d);
+ restart();
+ });
+ })
+ .on('mouseout', function() { // mouse out for a circle
+ // unenlarge target node
+ d3.select(this).attr('transform', '');
+ clerAllHighlights();
+ mouseover_node = null;
+ restart();
+ })
+ .on('mousedown', function(d) { // mouse down for circle
+ if (d3.event.button !== 0) { // ignore all but left button
+ return;
+ }
+ mousedown_node = d;
+ // mouse position relative to svg
+ initial_mouse_down_position = d3.mouse(this.parentNode.parentNode.parentNode).slice();
+ })
+ .on('mouseup', function(d) { // mouse up for circle
+ if (!mousedown_node)
+ return;
+
+ selected_link = null;
+ // unenlarge target node
+ d3.select(this).attr('transform', '');
+
+ // check for drag
+ mouseup_node = d;
+
+ let mySvg = this.parentNode.parentNode.parentNode;
+ // 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.fixed = true;
+ setNodesFixed(d.name, true);
+ resetMouseVars();
+ restart();
+ return;
+ }
+
+ // if this node was selected, unselect it
+ if (mousedown_node === selected_node) {
+ selected_node = null;
+ } else {
+ if (d.nodeType !== 'normal' && d.nodeType !== 'on-demand')
+ selected_node = mousedown_node;
+ }
+ clerAllHighlights();
+ mousedown_node = null;
+ if (!$scope.$$phase) $scope.$apply();
+ restart(false);
+
+ })
+ .on('dblclick', function(d) { // circle
+ if (d.fixed) {
+ d.fixed = false;
+ setNodesFixed(d.name, false);
+ restart(); // redraw the node without a dashed line
+ force.start(); // let the nodes move to a new position
+ }
+ })
+ .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 () {
+ $scope.contextNode = d;
+ });
+ })
+ .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';
+ if (QDR.isStandalone)
+ window.location = $location.protocol() + '://localhost:8161/hawtio' + artemisPath;
+ else
+ $location.path(artemisPath);
+ }
+ return;
+ }
+ d3.event.stopPropagation();
+ startUpdateConnectionsGrid(d);
+ });
+
+ appendContent(g);
+ appendTitle(g);
+
+ // remove old nodes
+ circle.exit().remove();
+
+ // add subcircles
+ svg.selectAll('.subcircle').remove();
+ let multiples = svg.selectAll('.multiple');
+ multiples.each(function(d) {
+ d.normals.forEach(function(n, i) {
+ if (i < d.normals.length - 1 && i < 3) // only show a few shadow circles
+ this.insert('svg:circle', ':first-child')
+ .attr('class', 'subcircle node')
+ .attr('r', 15 - i)
+ .attr('transform', 'translate(' + 4 * (i + 1) + ', 0)');
+ }, d3.select(this));
+ });
+ // 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)
+ return;
+
+ if (!start)
+ return;
+ // set the graph in motion
+ //QDR.log.debug("mousedown_node is " + mousedown_node);
+ force.start();
+
+ }
+ 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) + ')')
+ .selectAll('g');
+ let legendNodes = [];
+ legendNodes.push(aNode('Router', '', 'inter-router', '', undefined, 0, 0, 0, 0, false, {}));
+
+ if (!svg.selectAll('circle.console').empty()) {
+ legendNodes.push(aNode('Console', '', 'normal', '', undefined, 1, 0, 0, 0, false, {
+ console_identifier: 'Dispatch console'
+ }));
+ }
+ if (!svg.selectAll('circle.client.in').empty()) {
+ let node = aNode('Sender', '', 'normal', '', undefined, 2, 0, 0, 0, false, {});
+ node.cdir = 'in';
+ legendNodes.push(node);
+ }
+ if (!svg.selectAll('circle.client.out').empty()) {
+ let node = aNode('Receiver', '', 'normal', '', undefined, 3, 0, 0, 0, false, {});
+ node.cdir = 'out';
+ legendNodes.push(node);
+ }
+ if (!svg.selectAll('circle.client.inout').empty()) {
+ let node = aNode('Sender/Receiver', '', 'normal', '', undefined, 4, 0, 0, 0, false, {});
+ node.cdir = 'both';
+ legendNodes.push(node);
+ }
+ if (!svg.selectAll('circle.qpid-cpp').empty()) {
+ legendNodes.push(aNode('Qpid broker', '', 'route-container', '', undefined, 5, 0, 0, 0, false, {
+ product: 'qpid-cpp'
+ }));
+ }
+ if (!svg.selectAll('circle.artemis').empty()) {
+ legendNodes.push(aNode('Artemis broker', '', 'route-container', '', undefined, 6, 0, 0, 0, false,
+ {product: 'apache-activemq-artemis'}));
+ }
+ if (!svg.selectAll('circle.route-container').empty()) {
+ legendNodes.push(aNode('Service', '', 'route-container', 'external-service', undefined, 7, 0, 0, 0, false,
+ {product: ' External Service'}));
+ }
+ lsvg = lsvg.data(legendNodes, function(d) {
+ return d.key;
+ });
+ 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)) + ')';
+ });
+
+ 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 radii[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 & 1;
+ })
+ .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';
+ })
+ .classed('on-demand', function(d) {
+ return d.nodeType == 'on-demand';
+ })
+ .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;
+ 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('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
+ return d.name.length > 7 ? d.name.substr(0, 6) + '...' : d.name;
+ });
+ };
+ let appendTitle = function(g) {
+ g.append('svg:title').text(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.properties.product) {
+ return d.properties.product;
+ } 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;
+ return d.nodeType == 'normal' ? 'client' + x : (d.nodeType == 'on-demand' ? 'broker' : 'Router ' + d.name);
+ });
+ };
+
+ var startUpdateConnectionsGrid = function(d) {
+ // called after each topology update
+ var extendConnections = function() {
+ // force a fetch of the links for this node
+ QDRService.management.topology.ensureEntities(d.key, {entity: 'router.link', force: true}, function () {
+ // the links for this node are now available
+ $scope.multiData = [];
+ let normals = d.normals;
+ // find updated normals for d
+ d3.selectAll('.normal')
+ .each(function(newd) {
+ if (newd.id == d.id && newd.name == d.name) {
+ normals = newd.normals;
+ }
+ });
+ if (normals) {
+ normals.forEach(function(n) {
+ let nodeInfo = QDRService.management.topology.nodeInfo();
+ let links = nodeInfo[n.key]['router.link'];
+ let linkTypeIndex = links.attributeNames.indexOf('linkType');
+ let connectionIdIndex = links.attributeNames.indexOf('connectionId');
+ n.linkData = [];
+ links.results.forEach(function(link) {
+ if (link[linkTypeIndex] === 'endpoint' && link[connectionIdIndex] === n.connectionId) {
+ let l = {};
+ let ll = QDRService.utilities.flatten(links.attributeNames, link);
+ l.owningAddr = ll.owningAddr;
+ l.dir = ll.linkDir;
+ if (l.owningAddr && l.owningAddr.length > 2)
+ if (l.owningAddr[0] === 'M')
+ l.owningAddr = l.owningAddr.substr(2);
+ else
+ l.owningAddr = l.owningAddr.substr(1);
+
+ l.deliveryCount = ll.deliveryCount;
+ l.uncounts = QDRService.utilities.pretty(ll.undeliveredCount + ll.unsettledCount);
+ l.adminStatus = ll.adminStatus;
+ l.operStatus = ll.operStatus;
+ l.identity = ll.identity;
+ l.connectionId = ll.connectionId;
+ l.nodeId = n.key;
+ l.type = ll.type;
+ l.name = ll.name;
+
+ // TODO: remove this fake quiescing/reviving logic when the routers do the work
+ initConnState(n.connectionId);
+ if ($scope.quiesceState[n.connectionId].linkStates[l.identity])
+ l.adminStatus = $scope.quiesceState[n.connectionId].linkStates[l.identity];
+ if ($scope.quiesceState[n.connectionId].state == 'quiescing') {
+ if (l.adminStatus === 'enabled') {
+ // 25% chance of switching
+ let chance = Math.floor(Math.random() * 2);
+ if (chance == 1) {
+ l.adminStatus = 'disabled';
+ $scope.quiesceState[n.connectionId].linkStates[l.identity] = 'disabled';
+ }
+ }
+ }
+ if ($scope.quiesceState[n.connectionId].state == 'reviving') {
+ if (l.adminStatus === 'disabled') {
+ // 25% chance of switching
+ let chance = Math.floor(Math.random() * 2);
+ if (chance == 1) {
+ l.adminStatus = 'enabled';
+ $scope.quiesceState[n.connectionId].linkStates[l.identity] = 'enabled';
+ }
+ }
+ }
+ QDR.log.debug('pushing link state for ' + l.owningAddr + ' status: ' + l.adminStatus);
+
+ n.linkData.push(l);
+ }
+ });
+ $scope.multiData.push(n);
+ if (n.connectionId == $scope.connectionId)
+ $scope.linkData = n.linkData;
+ initConnState(n.connectionId);
+ $scope.multiDetails.updateState(n);
+ });
+ }
+ $scope.$apply();
+
+ d3.select('#multiple_details')
+ .style({
+ height: ((normals.length + 1) * 30) + 40 + 'px',
+ 'overflow-y': normals.length > 10 ? 'scroll' : 'hidden'
+ });
+ });
+ };
+ $scope.connectionsStyle = function () {
+ return {
+ height: ($scope.multiData.length * 30 + 40) + 'px'
+ };
+ };
+ $scope.linksStyle = function () {
+ return {
+ height: ($scope.linkData.length * 30 + 40) + 'px'
+ };
+ };
+ // register a notification function for when the topology is updated
+ QDRService.management.topology.addUpdatedAction('normalsStats', extendConnections);
+ // call the function that gets the links right now
+ extendConnections();
+ clearPopups();
+ let display = 'block';
+ if (d.normals.length === 1) {
+ display = 'none';
+ mouseY = mouseY - 20;
+ }
+ let rm = relativeMouse();
+ d3.select('#multiple_details')
+ .style({
+ display: display,
+ opacity: 1,
+ left: rm.left + 'px',
+ top: (rm.top - rm.offset.top) + 'px'
+ });
+ if (d.normals.length === 1) {
+ // simulate a click on the connection to popup the link details
+ QDRService.management.topology.ensureEntities(d.key, {entity: 'router.link', force: true}, function () {
+ $scope.multiDetails.showLinksList({entity: d});
+ });
+ }
+ };
+ var stopUpdateConnectionsGrid = function() {
+ QDRService.management.topology.delUpdatedAction('normalsStats');
+ };
+
+ var initConnState = function(id) {
+ if (!angular.isDefined($scope.quiesceState[id])) {
+ $scope.quiesceState[id] = {
+ state: 'enabled',
+ buttonText: 'Quiesce',
+ buttonDisabled: false,
+ linkStates: {}
+ };
+ }
+ };
+
+ function nextHop(thisNode, d, cb) {
+ if ((thisNode) && (thisNode != d)) {
+ let target = findNextHopNode(thisNode, d);
+ //QDR.log.debug("highlight link from node ");
+ //console.dump(nodeFor(selected_node.name));
+ //console.dump(target);
+ if (target) {
+ let hnode = nodeFor(thisNode.name);
+ let hlLink = linkFor(hnode, target);
+ //QDR.log.debug("need to highlight");
+ //console.dump(hlLink);
+ if (hlLink) {
+ if (cb) {
+ cb(hlLink, hnode, target);
+ } else {
+ hlLink['highlighted'] = true;
+ hnode['highlighted'] = true;
+ }
+ }
+ else
+ target = null;
+ }
+ nextHop(target, d, cb);
+ }
+ if (thisNode == d && !cb) {
+ let hnode = nodeFor(thisNode.name);
+ hnode['highlighted'] = true;
+ }
+ }
+
+ 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 routers = nodes.filter( function (node) {
+ return node.nodeType === 'inter-router';
+ });
+ if (routers.length > cnodes.length) {
+ return -1;
+ }
+
+
+ if (cnodes.length != Object.keys(savedKeys).length) {
+ 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++) {
+ let key = cnodes[i];
+ // if this node isn't in the saved node list
+ if (!savedKeys.hasOwnProperty(key))
+ return 1;
+ // if the number of connections for this node chaanged
+ if (!nodeInfo[key]['connection'])
+ return -1;
+ if (nodeInfo[key]['connection'].results.length != savedKeys[key]) {
+ return -1;
+ }
+ }
+ return 0;
+ }
+
+ function saveChanged() {
+ savedKeys = {};
+ 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;
+ }
+ }
+ // we are about to leave the page, save the node positions
+ $rootScope.$on('$locationChangeStart', function() {
+ //QDR.log.debug("locationChangeStart");
+ savePositions();
+ });
+ // When the DOM element is removed from the page,
+ // AngularJS will trigger the $destroy event on
+ // the scope
+ $scope.$on('$destroy', function() {
+ //QDR.log.debug("scope on destroy");
+ savePositions();
+ QDRService.management.topology.setUpdateEntities([]);
+ QDRService.management.topology.stopUpdating();
+ QDRService.management.topology.delUpdatedAction('normalsStats');
+ QDRService.management.topology.delUpdatedAction('topology');
+
+ d3.select('#SVG_ID').remove();
+ window.removeEventListener('resize', resize);
+ traffic.stop();
+ });
+
+ function handleInitialUpdate() {
+ // we only need to update connections during steady-state
+ QDRService.management.topology.setUpdateEntities(['connection']);
+ // we currently have all entities available on all routers
+ saveChanged();
+ initForceGraph();
+ // 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']});
+ // call this function every time a background update is done
+ 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');
+ animate = true;
+ setupInitialUpdate();
+ } else if (changed === -1) {
+ // we lost a node (or a client), we can draw the new svg immediately
+ animate = false;
+ saveChanged();
+ let nodeInfo = QDRService.management.topology.nodeInfo();
+ initializeNodes(nodeInfo);
+
+ let unknowns = [];
+ initializeLinks(nodeInfo, unknowns);
+ if (unknowns.length > 0) {
+ resolveUnknowns(nodeInfo, unknowns);
+ }
+ else {
+ force.nodes(nodes).links(links).start();
+ restart();
+ }
+
+ //initForceGraph();
+ } else {
+ //QDR.log.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"}, {entity: ".router.lin.router.link", attrs: ["linkType","connectionId","linkDir"]}],
+ [{entity: 'connection'}],
+ //[{entity: ".connection"}],
+ handleInitialUpdate);
+ }
+ if (!QDRService.management.connection.is_connected()) {
+ // we are not connected. we probably got here from a bookmark or manual page reload
+ QDR.redirectWhenConnected($location, 'topology');
+ return;
+ }
+
+ let connectionPopupHTML = function (d) {
+ let left = d.left ? d.source : d.target;
+ // left is the connection with dir 'in'
+ let right = d.left ? d.target : d.source;
+ let onode = QDRService.management.topology.nodeInfo()[left.key];
+ let connSecurity = function (conn) {
+ if (!conn.isEncrypted)
+ return 'no-security';
+ if (conn.sasl === 'GSSAPI')
+ return 'Kerberos';
+ return conn.sslProto + '(' + conn.sslCipher + ')';
+ };
+ let connAuth = function (conn) {
+ if (!conn.isAuthenticated)
+ return 'no-auth';
+ let sasl = conn.sasl;
+ if (sasl === 'GSSAPI')
+ sasl = 'Kerberos';
+ else if (sasl === 'EXTERNAL')
+ sasl = 'x.509';
+ else if (sasl === 'ANONYMOUS')
+ return 'anonymous-user';
+ if (!conn.user)
+ return sasl;
+ return conn.user + '(' + sasl + ')';
+ };
+ let connTenant = function (conn) {
+ if (!conn.tenant) {
+ return '';
+ }
+ if (conn.tenant.length > 1)
+ return conn.tenant.replace(/\/$/, '');
+ };
+ // loop through all the connections for left, and find the one for right
+ let rightIndex = onode['connection'].results.findIndex( function (conn) {
+ return QDRService.utilities.valFor(onode['connection'].attributeNames, conn, 'container') === right.routerId;
+ });
+ if (rightIndex < 0) {
+ // we have a connection to a client/service
+ rightIndex = +left.connectionId;
+ }
+ if (isNaN(rightIndex)) {
+ // we have a connection to a console
+ rightIndex = +right.connectionId;
+ }
+ let HTML = '';
+ if (rightIndex >= 0) {
+ let conn = onode['connection'].results[rightIndex];
+ conn = QDRService.utilities.flatten(onode['connection'].attributeNames, conn);
+ HTML += '<table class="popupTable">';
+ HTML += ('<tr><td>Security</td><td>' + connSecurity(conn) + '</td></tr>');
+ HTML += ('<tr><td>Authentication</td><td>' + connAuth(conn) + '</td></tr>');
+ HTML += ('<tr><td>Tenant</td><td>' + connTenant(conn) + '</td></tr>');
+ HTML += '</table>';
+ }
+ return HTML;
+ };
+
+ animate = true;
+ setupInitialUpdate();
+ QDRService.management.topology.startUpdating(false);
+
+ }
+ ]);
+
+ return QDR;
+
+}(QDR || {}));
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org
[4/4] qpid-dispatch git commit: DISPATCH-1002 Added optional message
flow animation to topology page
Posted by ea...@apache.org.
DISPATCH-1002 Added optional message flow animation to topology page
Project: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/repo
Commit: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/commit/61df4890
Tree: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/tree/61df4890
Diff: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/diff/61df4890
Branch: refs/heads/master
Commit: 61df4890ba6b1ed240b0c00accf57170115c219c
Parents: e9f8a85
Author: Ernest Allen <ea...@redhat.com>
Authored: Thu May 17 16:54:52 2018 -0400
Committer: Ernest Allen <ea...@redhat.com>
Committed: Thu May 17 16:54:52 2018 -0400
----------------------------------------------------------------------
console/stand-alone/plugin/css/dispatch.css | 3 -
console/stand-alone/plugin/html/qdrSchema.html | 6 +-
.../stand-alone/plugin/html/qdrTopology.html | 107 +-
console/stand-alone/plugin/js/chord/matrix.js | 19 +-
console/stand-alone/plugin/js/chord/qdrChord.js | 2 +-
console/stand-alone/plugin/js/qdrList.js | 2 +
console/stand-alone/plugin/js/qdrTopology.js | 2098 -----------------
.../plugin/js/topology/qdrTopology.js | 2125 ++++++++++++++++++
.../stand-alone/plugin/js/topology/traffic.js | 278 +++
9 files changed, 2527 insertions(+), 2113 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/61df4890/console/stand-alone/plugin/css/dispatch.css
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/css/dispatch.css b/console/stand-alone/plugin/css/dispatch.css
index 0096f95..a815ca1 100644
--- a/console/stand-alone/plugin/css/dispatch.css
+++ b/console/stand-alone/plugin/css/dispatch.css
@@ -263,9 +263,6 @@ li.currentStep {
font-weight: bold;
}
-.qdrTopology div.panel {
- position: absolute;
-}
/*
.ui-dialog-titlebar {
border: 0;
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/61df4890/console/stand-alone/plugin/html/qdrSchema.html
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/html/qdrSchema.html b/console/stand-alone/plugin/html/qdrSchema.html
index 5bd9207..6b86689 100644
--- a/console/stand-alone/plugin/html/qdrSchema.html
+++ b/console/stand-alone/plugin/html/qdrSchema.html
@@ -24,10 +24,8 @@ under the License.
span.fancytree-title {
color: black;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- width: 16em;
+ width: auto;
+ white-space: normal;
}
span.fancytree-node:hover {
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/61df4890/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 2b82e34..e355413 100644
--- a/console/stand-alone/plugin/html/qdrTopology.html
+++ b/console/stand-alone/plugin/html/qdrTopology.html
@@ -93,15 +93,88 @@ button.page-menu-button {
.page-menu {
width: 300px;
}
-
+
+ .legend-container {
+ position: absolute;
+ top: 1em;
+ right: 1em;
+ }
+
+ .legend-container ul {
+ list-style: none;
+ padding-left: 0;
+ margin-bottom: 0.5em;
+ }
+
+ span.legend-text {
+ padding-left: 0.25em;
+ font-weight: bold;
+ }
+
+ span.legend-text:disabled {
+ color: darkgray;
+ }
+
+ li:disabled * {
+ color: darkgray;
+ }
+ circle.flow {
+ opacity: 1;
+ pointer-events: none;
+ /*fill: green; */
+ }
+
+ circle.flow.fade {
+ opacity: 0.1;
+ }
+
+ #topo_legend ul.addresses li, #topo_legend ul.options li {
+ margin-top: 0.5em;
+ margin-bottom: 1em;
+ }
+
+#topo_legend ul.addresses {
+ margin-bottom: 1.5em;
+}
+
+ /* the checkboxes for the addresses */
+ #topo_legend ul li input[type=checkbox]:checked + label::before {
+ content:'\2713';
+ font-weight: bold;
+ font-size: 16px;
+ display:inline-block;
+ /* padding:0 6px 0 0; */
+ color: black;
+ position: absolute;
+ top: -8px;
+ left: -1px;
+ }
+ /* The aggregate addresses need a black checkbox on the white background */
+ #topo_legend ul li input[type=checkbox]:checked + label.aggregate::before {
+ color: black;
+ /* left: 1px; */
+ }
+#topo_legend ul.addresses button.btn-default {
+ background-image: none;
+ color: white;
+ border-radius: 10px;
+}
+
+#topo_legend li.legend-sublist ul {
+ margin-bottom: 0.5em;
+ }
+
+#topo_legend li.legend-sublist ul.addresses{
+ max-height: 11.6em; /* show up to 4 addresses */
+ overflow-y: auto;
+}
</style>
-<div class="qdrTopology row" ng-controller="QDR.TopologyController">
+<div class="qdrTopology" ng-controller="QDR.TopologyController">
<div ng-controller="QDR.TopologyFormController">
<div class="page-menu navbar-collapse collapse">
<div id="topologyForm">
<div>
<h4>{{form}} Info</h4>
- <!-- <div id="routerInfo" class="grid" ui-grid="topoGridRouter" ng-style="infoStyle()"></div> -->
<div id="formInfo"></div>
</div>
</div>
@@ -110,7 +183,32 @@ button.page-menu-button {
<button ng-if="!panelVisible" ng-click="showLeftPane()" class="showLeft" title="Show"><i class="icon-step-forward"></i></button>
</div>
- <div class="diagram col-xs-12">
+ <div class="legend-container hidden-xs">
+ <uib-accordion id="topo_legend" close-others="false">
+ <div uib-accordion-group class="panel-default" is-open="legend.status.optionsOpen" heading="Options">
+ <ul class="options">
+ <li class="legend-line">
+ <checkbox title="Show message traffic" ng-model="legendOptions.showTraffic"></checkbox>
+ <span class="legend-text" title="Select to show message traffic">Show traffic</span>
+ </li>
+ <li class="legend-sublist" ng-hide="!legendOptions.showTraffic">
+ <ul class="addresses">
+ <li ng-repeat="(address, color) in addresses" class="legend-line">
+ <checkbox style="background-color: {{addressColors[address]}};"
+ title="{{address}}" ng-change="addressFilterChanged()"
+ ng-model="addresses[address]"></checkbox>
+ <span class="legend-text" ng-mouseenter="enterLegend(address)" ng-mouseleave="leaveLegend()" ng-click="addressClick(address)" title="{{address}}">{{address | limitTo : 15}}{{address.length>15 ? '…' : ''}}</span>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <div uib-accordion-group class="panel-default" is-open="legend.status.legendOpen" heading="Legend">
+ <div id="topo_svg_legend"></div>
+ </div>
+ </uib-accordion>
+ </div>
+ <div class="diagram">
<div id="topology"><!-- d3 toplogy here --></div>
<div id="crosssection"></div><div id="crosshtml" ng-bind-html="crosshtml"></div>
@@ -120,7 +218,6 @@ button.page-menu-button {
<li class="na" ng-class="{'force-display': isFixed()}" ng-click="setFixed(false)">Unfreeze</li>
</ul>
</div>
- <div id="svg_legend" class="hidden-xs"></div>
<div id="multiple_details">
<h4 class="grid-title">Connections</h4>
<div class="grid" ui-grid="multiDetails" ui-grid-selection ui-grid-auto-resize ng-style="connectionsStyle()"></div>
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/61df4890/console/stand-alone/plugin/js/chord/matrix.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/chord/matrix.js b/console/stand-alone/plugin/js/chord/matrix.js
index d8b9456..3f10ddc 100644
--- a/console/stand-alone/plugin/js/chord/matrix.js
+++ b/console/stand-alone/plugin/js/chord/matrix.js
@@ -71,7 +71,18 @@ valuesMatrix.prototype.hasValues = function () {
});
});
};
-
+valuesMatrix.prototype.getMinMax = function () {
+ let min = Number.MAX_VALUE, max = Number.MIN_VALUE;
+ this.rows.forEach( function (row) {
+ row.cols.forEach( function (col) {
+ if (col.messages > MIN_CHORD_THRESHOLD) {
+ max = Math.max(max, col.messages);
+ min = Math.min(min, col.messages);
+ }
+ });
+ });
+ return [min, max];
+};
// extract a square matrix with just the values from the object matrix
valuesMatrix.prototype.matrixMessages = function () {
let m = emptyMatrix(this.rows.length);
@@ -130,7 +141,11 @@ valuesMatrix.prototype.getAddresses = function (r) {
let getAttribute = function (self, attr, i) {
if (self.aggregate)
return self.rows[i][attr];
- return self.rows[self.getGroupBy().indexOf(i)][attr];
+ let groupByIndex = self.getGroupBy().indexOf(i);
+ if (groupByIndex < 0) {
+ groupByIndex = i;
+ }
+ return self.rows[groupByIndex][attr];
};
valuesMatrix.prototype.addRow = function (chordName, ingress, egress, address) {
let rowIndex = this.rows.length;
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/61df4890/console/stand-alone/plugin/js/chord/qdrChord.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/chord/qdrChord.js b/console/stand-alone/plugin/js/chord/qdrChord.js
index 5ce7aa1..91ee96a 100644
--- a/console/stand-alone/plugin/js/chord/qdrChord.js
+++ b/console/stand-alone/plugin/js/chord/qdrChord.js
@@ -520,7 +520,7 @@ var QDR = (function (QDR) {
.transition()
.duration(duration/2)
.attrTween('d', arcTweenExit)
- .each('end', function (d) {d3.select(this).node().parentNode.remove();});
+ .each('end', function () {d3.select(this).node().parentNode.remove();});
// decorate the chord layout's .chord() data with key, color, and orgIndex
rechord.chordData = decorateChordData(rechord, matrix);
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/61df4890/console/stand-alone/plugin/js/qdrList.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/qdrList.js b/console/stand-alone/plugin/js/qdrList.js
index 056e686..8044400 100644
--- a/console/stand-alone/plugin/js/qdrList.js
+++ b/console/stand-alone/plugin/js/qdrList.js
@@ -376,6 +376,7 @@ var QDR = (function(QDR) {
});
let h = $scope.detailFields.length * 30 + 46;
$('.ui-grid-viewport').height(h);
+ $scope.details.excessRows = $scope.detailFields.length;
};
$(window).resize(resizer);
@@ -731,6 +732,7 @@ var QDR = (function(QDR) {
enableVerticalScrollbar: 0,
multiSelect: false,
jqueryUIDraggable: true,
+ excessRows: 20,
onRegisterApi: function(gridApi) {
$scope.gridApi = gridApi;
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org