blob: 998c85b0ce19e09a96f3976a3b662922db5ea91e [file] [log] [blame]
/*
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.
*/
import React, { Component } from "react";
import * as d3 from "d3";
import {
TopologyView,
TopologyControlBar,
createTopologyControlButtons,
TopologySideBar,
} from "@patternfly/react-topology";
import { Traffic } from "./traffic.js";
import { separateAddresses } from "../chord/filters.js";
import { Nodes } from "./nodes.js";
import { Links } from "./links.js";
import {
connectionPopupHTML,
getSizes,
reconcileArrays,
reconcileLinks,
nextHopHighlight,
} from "./topoUtils.js";
import { BackgroundMap } from "./map.js";
import { utils } from "../common/amqp/utilities.js";
import { Legend } from "./legend.js";
import RouterInfoComponent from "./routerInfoComponent";
import ClientInfoComponent from "./clientInfoComponent";
import ContextMenu from "./contextMenu";
import TopologyToolbar from "./topologyToolbar";
import LegendComponent from "./legendComponent";
import {
appendCircle,
appendContent,
addGradient,
addDefs,
updateState,
} from "./svgUtils.js";
import { QDRLogger } from "../common/qdrGlobals";
const TOPOOPTIONSKEY = "topologyLegendOptionsKey";
const SEPARATES = "topoSeparates";
class TopologyViewer extends Component {
constructor(props) {
super(props);
this.topology = this.props.service.management.topology;
// restore the state of the legend sections
let savedOptions = localStorage.getItem(TOPOOPTIONSKEY);
savedOptions = savedOptions
? JSON.parse(savedOptions)
: {
traffic: {
open: false,
dots: false,
congestion: false,
},
legend: {
open: true,
},
map: {
open: false,
show: false,
},
arrows: {
open: false,
routerArrows: false,
clientArrows: true,
},
};
// previous version read from storage didn't have show attribute
if (typeof savedOptions.map.show === "undefined") {
savedOptions.map.show = false;
}
this.state = {
legendOptions: savedOptions,
showRouterInfo: false,
showClientInfo: false,
showContextMenu: false,
showLegend: false,
mapOptions: { areaColor: "#000000", oceanColor: "#FFFFFF" },
addressColors: {},
};
this.QDRLog = new QDRLogger(console, "Topology");
this.popupCancelled = true;
// - 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
this.forceData = {
nodes: new Nodes(this.QDRLog),
links: new Links(this.QDRLog),
edges: {},
};
this.force = null;
this.currentScale = 1;
let seps = localStorage.getItem(SEPARATES);
seps = seps ? JSON.parse(seps) : [];
this.separateContainers = new Set(seps);
this.traffic = new Traffic(
this,
this.props.service,
separateAddresses,
Nodes.radius("inter-router"),
this.forceData,
[],
this.addressesChanged
);
this.traffic.remove();
}
// called only once when the component is initialized
componentDidMount = () => {
this.mounted = true;
this.backgroundMap = new BackgroundMap(
this,
this.state.legendOptions.map,
// notify: called each time a pan/zoom is performed
() => {
if (this.state.legendOptions.map.show) {
// set all the nodes' x,y position based on their saved lon,lat
this.forceData.nodes.setXY(this.backgroundMap);
this.forceData.nodes.savePositions();
// redraw the nodes in their x,y position and let non-fixed nodes bungie
this.force.start();
this.clearPopups();
}
}
);
window.addEventListener("resize", this.resize);
// we need to get data for connections and links
this.topology.setUpdateEntities(["connection", "router.link"]);
this.traffic
.getAddressColors(this.props.service, separateAddresses)
.then(addressColors => {
if (!this.mounted) return;
this.setState({
addressColors,
mapOptions: this.backgroundMap.mapOptions,
});
// poll the routers for their latest data
this.topology.get().then(() => {
if (!this.mounted) return;
// create the svg
this.topology.startUpdating(null, this.getEdgeNodes()).then(() => {
this.init().then(() => {
if (!this.mounted) return;
// get notified when a router is added/dropped and when
// the number of connections for a router changes
this.topology.addChangedAction("topology", this.topologyChanged);
});
});
});
});
};
topologyChanged = changed => {
let message;
let silent = true;
if (changed.connections.length > 0) {
// see if any routers have stale info
const nodeInfo = this.topology.nodeInfo();
for (let id in nodeInfo) {
const ds = this.forceData.nodes.nodes.filter(n => n.key === id);
ds.forEach(d => {
d.dropped = nodeInfo[id].connection.stale;
});
const links = this.forceData.links.links.filter(
l => l.source.key === id || l.target.key === id
);
links.forEach(l => {
l.dropped = nodeInfo[id].connection.stale;
});
this.restart();
}
message = `${
changed.connections[0].from > changed.connections[0].to ? "Lost" : "New"
} connection for ${utils.nameFromId(changed.connections[0].router)}`;
this.reInit();
} else {
silent = false;
if (changed.lostRouters.length > 0) {
message = `Lost connection to ${utils.nameFromId(changed.lostRouters[0])}`;
} else if (changed.newRouters.length > 0) {
message = `New router discovered: ${utils.nameFromId(changed.newRouters[0])}`;
}
this.reInit();
}
this.props.handleAddNotification("event", message, new Date(), "info", silent);
};
componentWillUnmount = () => {
// set this flag so we can abandon the results from any
// pending management calls
this.mounted = false;
this.topology.setUpdateEntities([]);
this.topology.stopUpdating();
this.topology.delChangedAction("topology");
this.topology.delUpdatedAction("connectionPopupHTML");
d3.select("#SVG_ID .links").remove();
d3.select("#SVG_ID .nodes").remove();
d3.select("#SVG_ID circle.flow").remove();
d3.select("#SVG_ID").remove();
this.traffic.remove();
delete this.traffic;
this.forceData.nodes.savePositions();
window.removeEventListener("resize", this.resize);
d3.select(".pf-c-page__main").style("background-color", "white");
};
getEdgeNodes = () => {
// get list of edge routers per interior router
const edgesPerRouter = this.topology.edgesPerRouter();
const edgeList = new Set(this.topology.edgeList.map(e => utils.nameFromId(e)));
const visibleEdgeIds = [];
for (const routerId in edgesPerRouter) {
let notSeparate = [];
edgesPerRouter[routerId].forEach(name => {
if (!edgeList.has(name)) {
if (this.separateContainers.has(name)) {
visibleEdgeIds.push(utils.idFromName(name, "_edge"));
} else {
notSeparate.push(name);
}
}
});
/*
// if only 1 edge router is not separate, add it to the list
if (notSeparate.length === 1) {
visibleEdgeIds.push(utils.idFromName(notSeparate[0], "_edge"));
}
*/
}
return visibleEdgeIds;
};
// called by traffic when a new address is detected or
// when an existing address is dropped
addressesChanged = () => {
this.setState({ addressColors: this.traffic.addressColors() });
};
resize = () => {
if (!this.svg) return;
const { width, height } = getSizes("topology");
this.width = width;
this.height = height;
if (this.width > 0) {
// set attrs and 'resume' force
this.svg.attr("width", this.width);
this.svg.attr("height", this.height);
if (this.backgroundMap) this.backgroundMap.setWidthHeight(width, height);
this.force.size([width, height]).resume();
}
};
// called from contextMenu
setSelected = (item, data) => {
// remove the selected attr from each node
this.circle.each(function (d) {
d.selected = false;
});
// set the selected attr for this node
data.selected = item.title === "Select";
this.selected_node = data.selected ? data : null;
this.restart();
};
canExpandAll = data =>
this.forceData.nodes.nodes.find(
node => node.key === data.key && node.nodeType === "edge"
);
canCollapseAll = data => {
// get all possible edges for this router
const edgeNames = new Set(this.topology.edgesPerRouter()[data.key]);
// difference = edgeNames - expanded
const difference = new Set(
[...edgeNames].filter(x => !this.separateContainers.has(x))
);
return difference.size < edgeNames.size;
};
// called from contextMenu
separateAllEdges = (item, data) => {
// find the node for the group of edges for this router
const edgeGroup = this.forceData.nodes.nodes.find(
node => node.key === data.key && node.nodeType === "edge"
);
if (edgeGroup) {
edgeGroup.normals.forEach(n => {
this.separateContainers.add(n.container);
});
localStorage.setItem(SEPARATES, JSON.stringify([...this.separateContainers]));
this.reInit();
}
};
// called from contextMenu
collapseAllEdges = (item, data) => {
// find all the separated edge nodes for this router
const edgeNames = this.topology.edgesPerRouter()[data.key];
if (edgeNames) {
this.topology.removeTheseEdgeNames(edgeNames);
edgeNames.forEach(name => this.separateContainers.delete(name));
localStorage.setItem(SEPARATES, JSON.stringify([...this.separateContainers]));
this.reInit();
}
};
updateLegend = () => {
this.legend.update();
};
clearPopups = () => {};
createSvg = () => {
d3.select("#SVG_ID .links").remove();
d3.select("#SVG_ID .nodes").remove();
d3.select("#SVG_ID circle.flow").remove();
d3.select("#SVG_ID").remove();
this.svg = d3
.select("#topology")
.append("svg")
.attr("id", "SVG_ID")
.attr("xmlns", "http://www.w3.org/2000/svg")
.attr("width", this.width)
.attr("height", this.height)
.attr("aria-label", "topology-svg")
.on("click", this.clearPopups);
// append the map layer before the nodes/links
if (this.backgroundMap) {
this.backgroundMap.setSvg(this.svg, this.width, this.height);
}
addDefs(this.svg);
addGradient(this.svg);
// handles to link and node element groups
this.path = this.svg.append("svg:g").attr("class", "links").selectAll("g");
this.circle = this.svg.append("svg:g").attr("class", "nodes").selectAll("g");
};
// initialize the nodes and links array from the QDRService.topology._nodeInfo object
init = () => {
return new Promise((resolve, reject) => {
const { width, height } = getSizes("topology");
this.width = width;
this.height = height;
if (this.width < 768) {
const legendOptions = this.state.legendOptions;
legendOptions.map.open = false;
legendOptions.map.show = false;
this.setState({ legendOptions });
}
let nodeInfo = this.topology.nodeInfo();
let nodeCount = Object.keys(nodeInfo).length;
this.mouseover_node = null;
this.selected_node = null;
this.createSvg();
// read the map data from the data file and build the map layer
this.backgroundMap.init(this, this.svg, this.width, this.height).then(() => {
this.backgroundMap.setMapOpacity(this.state.legendOptions.map.show);
if (this.state.legendOptions.map.show) this.backgroundMap.restartZoom();
this.forceData.nodes.saveLonLat(this.backgroundMap);
this.forceData.nodes.savePositions();
});
if (this.traffic) this.traffic.remove();
if (this.state.legendOptions.traffic.dots)
this.traffic.addAnimationType(
"dots",
separateAddresses,
Nodes.radius("inter-router")
);
if (this.state.legendOptions.traffic.congestion)
this.traffic.addAnimationType(
"congestion",
separateAddresses,
Nodes.radius("inter-router")
);
// mouse event vars
this.mousedown_node = null;
this.forceData.nodes.initialize(nodeInfo, this.width, this.height, localStorage);
this.forceData.links.initialize(
nodeInfo,
this.forceData.nodes,
this.separateContainers,
[],
this.height,
localStorage
);
this.force = d3.layout
.force()
.nodes(this.forceData.nodes.nodes)
.links(this.forceData.links.links)
.size([this.width, this.height])
.linkDistance(d => {
return this.forceData.nodes.linkDistance(d, nodeCount);
})
.charge(d => {
return this.forceData.nodes.charge(d, nodeCount);
})
.friction(0.1)
.gravity(d => {
return this.forceData.nodes.gravity(d, nodeCount);
})
.on("tick", this.tick)
.on("end", () => {
this.forceData.nodes.savePositions();
if (this.backgroundMap) this.forceData.nodes.saveLonLat(this.backgroundMap);
});
//.start();
this.force.stop();
this.force.start();
if (this.backgroundMap) this.forceData.nodes.saveLonLat(this.backgroundMap);
this.restart();
this.circle.call(this.force.drag);
this.legend = new Legend(this.forceData.nodes, this.QDRLog);
this.updateLegend();
if (this.oldSelectedNode) {
d3.selectAll("circle.inter-router").classed("selected", function (d) {
if (d.key === this.oldSelectedNode.key) {
this.selected_node = d;
return true;
}
return false;
});
}
if (this.oldMouseoverNode && this.selected_node) {
d3.selectAll("circle.inter-router").each(function (d) {
if (d.key === this.oldMouseoverNode.key) {
this.mouseover_node = d;
this.topology.ensureAllEntities(
[
{
entity: "router.node",
attrs: ["id", "nextHop"],
},
],
() => {
nextHopHighlight(
this.selected_node,
d,
this.forceData.nodes,
this.forceData.links,
this.topology.nodeInfo()
);
this.restart();
}
);
}
});
}
resolve();
});
};
resetMouseVars = () => {
this.mousedown_node = null;
this.mouseover_node = null;
this.mouseup_node = null;
};
handleMouseOutPath = d => {
// mouse out of a path
this.popupCancelled = true;
this.topology.delUpdatedAction("connectionPopupHTML");
this.hideTooltip();
d.selected = false;
connectionPopupHTML();
};
showMarker = d => {
if (d.source.nodeType === "normal" || d.target.nodeType === "normal") {
// link between router and client
return this.state.legendOptions.arrows.clientArrows;
} else {
// link between routers or edge routers
return this.state.legendOptions.arrows.routerArrows;
}
};
// Takes the forceData.nodes and forceData.links array and creates svg elements
// Also updates any existing svg elements based on the updated values in forceData.nodes
// and forceData.links
restart = () => {
if (!this.circle) return;
this.circle.call(this.force.drag);
// path is a selection of all g elements under the g.links svg:group
// here we associate the links.links array with the {g.links g} selection
// based on the link.uid
this.path = this.path.data(this.forceData.links.links, d => {
return d.uid();
});
// add new links. if a link with a new uid is found in the data, add a new path
let enterpath = this.path
.enter()
.append("g")
.on("mouseover", d => {
// mouse over a path
let event = d3.event;
d.selected = true;
this.popupCancelled = false;
let updateTooltip = () => {
if (this.popupCancelled) return;
if (d.selected) {
const popupContent = connectionPopupHTML(d, this.topology._nodeInfo);
this.displayTooltip(popupContent, { x: event.pageX, y: event.pageY });
} else {
this.handleMouseOutPath(d);
}
};
// update the contents of the popup tooltip each time the data is polled
this.topology.addUpdatedAction("connectionPopupHTML", updateTooltip);
// request the data and update the tooltip as soon as it arrives
this.topology.ensureAllEntities(
[
{
entity: "router.link",
force: true,
},
{
entity: "connection",
},
],
updateTooltip
);
// just show the tooltip with whatever data we have
updateTooltip();
this.restart();
})
.on("mouseout", d => {
this.handleMouseOutPath(d);
this.restart();
})
// left click a path
.on("click", () => {
d3.event.stopPropagation();
this.clearPopups();
});
enterpath
.append("path")
.attr("class", "link")
.attr("id", d => `path-${d.source.uid()}-${d.target.uid()}`);
enterpath
.append("path")
.attr("class", "hittarget")
.attr("id", d => `hitpath-${d.source.uid()}-${d.target.uid()}`);
// remove old links
this.path.exit().remove();
// update each {g.links g.link} element
this.path
.select(".link")
.classed("selected", d => d.selected)
.classed("highlighted", d => d.highlighted)
.classed("unknown", d => !d.right && !d.left)
.classed("dropped", d => d.dropped)
// reset the markers based on current highlighted/selected
.attr("marker-end", d => {
if (!this.showMarker(d)) return null;
return d.right ? `url(#end${d.markerId("end")})` : null;
})
.attr("marker-start", d => {
if (!this.showMarker(d)) return null;
return d.left || (!d.left && !d.right)
? `url(#start${d.markerId("start")})`
: null;
});
// circle (node) group
this.circle = d3
.select("g.nodes")
.selectAll("g")
.data(this.forceData.nodes.nodes, function (d) {
return d.uid();
});
// add new circle nodes
let enterCircle = this.circle
.enter()
.append("g")
.attr("id", function (d) {
return (d.nodeType !== "normal" ? "router" : "client") + "-" + d.index;
});
let self = this;
appendCircle(enterCircle)
.on("mouseover", function (d) {
// mouseover a circle
self.current_node = d;
self.props.service.management.topology.delUpdatedAction("connectionPopupHTML");
let e = d3.event;
self.popupCancelled = false;
if (!self.state.showContextMenu) {
d.toolTip(self.props.service.management.topology).then(function (toolTip) {
if (self.popupCancelled) return;
self.displayTooltip(toolTip, { x: e.pageX, y: e.pageY });
});
}
if (d === self.mousedown_node) return;
// enlarge target node
d3.select(this).attr("transform", "scale(1.1)");
if (!self.selected_node) {
return;
}
// highlight the next-hop route from the selected node to this node
self.clearAllHighlights();
// we need .router.node info to highlight hops
self.topology.ensureAllEntities(
[
{
entity: "router.node",
attrs: ["id", "nextHop"],
},
],
() => {
// the mouse left the circle before the data came in
if (!self.current_node) return;
self.mouseover_node = d; // save this node in case the topology changes so we can restore the highlights
nextHopHighlight(
self.selected_node,
d,
self.forceData.nodes,
self.forceData.links,
self.topology.nodeInfo()
);
self.restart();
}
);
self.restart();
})
.on("mouseout", function () {
// mouse out for a circle
self.current_node = null;
// unenlarge target node
d3.select(this).attr("transform", "");
self.popupCancelled = true;
self.hideTooltip();
self.clearAllHighlights();
self.mouseover_node = null;
self.restart();
})
.on("mousedown", d => {
// mouse down for circle
if (this.backgroundMap) this.backgroundMap.cancelZoom();
this.current_node = d;
if (d3.event && d3.event.button !== 0) {
// ignore all but left button
return;
}
this.mousedown_node = d;
// mouse position relative to svg
this.initial_mouse_down_position = d3.mouse(this.svg.node());
this.hideTooltip();
})
.on("mouseup", function (d) {
// mouse up for circle
if (self.backgroundMap) self.backgroundMap.restartZoom();
if (!self.mousedown_node) return;
// unenlarge target node
d3.select(this).attr("transform", "");
// check for drag
self.mouseup_node = d;
// if we dragged the node, make it fixed
let cur_mouse = d3.mouse(self.svg.node());
if (
cur_mouse[0] !== self.initial_mouse_down_position[0] ||
cur_mouse[1] !== self.initial_mouse_down_position[1]
) {
self.forceData.nodes.setFixed(d, true);
self.forceData.nodes.saveLonLat(self.backgroundMap);
self.forceData.nodes.savePositions();
self.restart();
self.resetMouseVars();
return;
}
self.clearAllHighlights();
self.mousedown_node = null;
// handle clicking on nodes that represent multiple sub-nodes
if (d.normals && !d.isArtemis && !d.isQpid && d.nodeType !== "_edge") {
self.doDialog(d, "client");
} else if (d.nodeType === "_topo" || d.nodeType === "_edge") {
self.doDialog(d, "router");
}
// apply any data changes to the interface
self.restart();
})
.on("dblclick", d => {
// circle
d3.event.preventDefault();
if (d.fixed) {
this.forceData.nodes.setFixed(d, false);
this.restart(); // redraw the node without a dashed line
this.force.start(); // let the nodes move to a new position
}
})
.on("contextmenu", d => {
// circle
if (d3.event) {
d3.event.preventDefault();
this.contextEventPosition = [d3.event.pageX, d3.event.pageY];
} else {
this.contextEventPosition = [100, 100];
}
this.contextEventData = d;
this.setState({ showContextMenu: true });
return false;
})
.on("click", d => {
// circle
if (!this.mouseup_node) return;
// clicked on a circle
this.clearPopups();
// circle was a broker
if (utils.isArtemis(d)) {
const host = d.container === "0.0.0.0" ? "localhost" : d.container;
const artemis = `${window.location.protocol}//${host}:8161/console`;
window.open(
artemis,
"artemis",
"fullscreen=yes, toolbar=yes,location = yes, directories = yes, status = yes, menubar = yes, scrollbars = yes, copyhistory = yes, resizable = yes"
);
return;
}
d3.event.stopPropagation();
});
appendContent(enterCircle);
// remove old nodes
this.circle.exit().remove();
// update all nodes visual states
updateState(this.circle);
// add text to client circles if there are any that represent multiple clients
this.svg.selectAll(".subtext").remove();
let multiples = this.svg.selectAll(".multiple");
multiples.each(function (d) {
let g = d3.select(this.parentNode);
let r = Nodes.radius(d.nodeType);
if (d.nodeType === "edge" || d.normals.length > 1) {
g.append("svg:text")
.attr("x", r + 4)
.attr("y", Math.floor(r / 2 - 8))
.attr("class", "subtext")
.text(`* ${d.normals.length}`);
}
});
if (!this.mousedown_node || !this.selected_node) return;
// set the graph in motion
//this.force.start();
};
// update force layout (called automatically each iteration)
tick = () => {
// move the circles
this.circle.attr("transform", d => {
// don't let the edges of the circle go beyond the edges of the svg
let r = Nodes.radius(d.nodeType);
d.x = Math.max(Math.min(d.x, this.width - r), r);
d.y = Math.max(Math.min(d.y, this.height - r), r);
return `translate(${d.x},${d.y})`;
});
// draw lines from node centers
// There are 2 paths under each this.path selection.
this.path
.selectAll("path")
.attr("d", d => `M${d.source.x},${d.source.y}L${d.target.x},${d.target.y}`);
};
// show the details dialog for a client or group of clients
doDialog = (d, type) => {
this.d = d;
if (type === "router") {
this.setState({ showRouterInfo: true });
} else if (type === "client") {
this.setState({ showClientInfo: true });
}
};
handleCloseRouterInfo = type => {
this.setState({ showRouterInfo: false });
};
handleCloseClientInfo = () => {
this.setState({ showClientInfo: false });
};
handleSeparate = container => {
return new Promise(resolve => {
const oldKey = this.d.key;
this.separateContainers.add(container);
localStorage.setItem(SEPARATES, JSON.stringify([...this.separateContainers]));
this.reInit().then(() => {
// find the node with oldKey that has a list of edge normals
const newD = this.forceData.nodes.nodes.find(
node => node.key === oldKey && node.nodeType === "edge" && node.normals
);
resolve(newD);
});
});
};
reInit = () => {
return new Promise(resolve => {
this.topology.startUpdating(null, this.getEdgeNodes()).then(() => {
const nodeInfo = this.topology.nodeInfo();
const newNodes = new Nodes(this.QDRLog);
const newLinks = new Links(this.QDRLog);
newNodes.initialize(nodeInfo, this.width, this.height, localStorage);
newLinks.initialize(
nodeInfo,
newNodes,
this.separateContainers,
[],
this.height,
localStorage
);
reconcileArrays(this.forceData.nodes.nodes, newNodes.nodes);
reconcileLinks(
this.forceData.links.links,
newLinks.links,
this.forceData.nodes.nodes
);
this.force.nodes(this.forceData.nodes.nodes).links(this.forceData.links.links);
this.force.stop();
this.force.start();
this.restart();
this.circle.call(this.force.drag);
delete this.legend;
this.legend = new Legend(this.forceData.nodes, this.QDRLog);
this.updateLegend();
resolve();
});
});
};
displayTooltip = (content, xy) => {
if (this.popupCancelled) {
return this.hideTooltip();
}
const top = Math.max(Math.min(window.innerHeight - 100, xy.y), 0);
const left = Math.max(Math.min(window.innerWidth - 200, xy.x), 0);
// position the popup
d3.select("#popover-div")
.style("left", `${left + 5}px`)
.style("top", `${top}px`)
.style("display", "block")
.html(content);
};
hideTooltip = () => {
d3.select("#popover-div").style("display", "none");
};
clearAllHighlights = () => {
this.forceData.links.clearHighlighted();
this.forceData.nodes.clearHighlighted();
d3.selectAll(".hittarget").classed("highlighted", false);
};
saveLegendOptions = legendOptions => {
localStorage.setItem(TOPOOPTIONSKEY, JSON.stringify(legendOptions));
};
handleLegendOptionsChange = (legendOptions, callback) => {
this.saveLegendOptions(legendOptions);
this.setState({ legendOptions }, () => {
if (callback) {
callback();
}
this.restart();
});
};
handleOpenChange = (section, open) => {
const { legendOptions } = this.state;
legendOptions[section].open = open;
if (section === "legend" && open) {
this.legend.update();
}
this.handleLegendOptionsChange(this.state.legendOptions);
};
handleChangeArrows = (checked, event) => {
const { legendOptions } = this.state;
legendOptions.arrows[event.target.name] = checked;
this.handleLegendOptionsChange(legendOptions);
};
// checking and unchecking of which traffic animation to show
handleChangeTrafficAnimation = (checked, event) => {
const { legendOptions } = this.state;
const name = event.target.name;
legendOptions.traffic[name] = checked;
if (!checked) {
this.traffic.remove(name);
} else {
this.traffic.addAnimationType(
name,
separateAddresses,
Nodes.radius("inter-router")
);
}
this.handleLegendOptionsChange(legendOptions);
};
addressFilterChanged = () => {
//this.traffic.remove("dots");
this.traffic.addAnimationType(
"dots",
separateAddresses,
Nodes.radius("inter-router")
);
};
handleChangeTrafficFlowAddress = (address, checked) => {
this.traffic.updateAddressColors(address, checked);
this.setState({ addressColors: this.traffic.addressColors() });
};
// called from traffic
// the list of addresses has changed. set new addresses to true
handleUpdatedAddresses = addresses => {
const { legendOptions } = this.state;
let changed = false;
// set any new keys to the passed in value
Object.keys(addresses).forEach(address => {
if (typeof legendOptions.traffic.addresses[address] === "undefined") {
legendOptions.traffic.addresses[address] = addresses[address];
changed = true;
}
});
// remove any old keys that were not passed in
Object.keys(legendOptions.traffic.addresses).forEach(address => {
if (typeof addresses[address] === "undefined") {
delete legendOptions.traffic.addresses[address];
changed = true;
}
});
if (changed) {
this.handleLegendOptionsChange(legendOptions, this.addressFilterChanged);
}
};
handleUpdateMapColor = (which, color) => {
if (this.backgroundMap) {
let mapOptions = this.backgroundMap.updateMapColor(which, color);
this.setState({ mapOptions });
}
};
// the mouse was hovered over one of the addresses in the legend
handleHoverAddress = (address, over) => {
// this.enterLegend and this.leaveLegend are defined in traffic.js
if (over) {
this.enterLegend(address);
} else {
this.leaveLegend();
}
};
handleUpdateMapShown = checked => {
const { legendOptions } = this.state;
legendOptions.map.show = checked;
if (this.backgroundMap) {
this.setState({ legendOptions }, () => {
this.backgroundMap.setMapOpacity(checked);
this.backgroundMap.setBackgroundColor();
if (checked) {
this.backgroundMap.restartZoom();
} else {
this.backgroundMap.cancelZoom();
}
this.saveLegendOptions(legendOptions);
});
}
};
handleContextHide = () => {
this.setState({ showContextMenu: false });
};
// clicked on the Legend button in the control bar
handleLegendClick = id => {
this.setState({ showLegend: !this.state.showLegend });
};
// clicked on the x button on the legend
handleCloseLegend = () => {
this.setState({ showLegend: false });
};
scaleSVG = () => {
this.svg.attr("transform", `scale(${this.currentScale})`);
};
zoomInCallback = () => {
this.currentScale += 0.1;
this.scaleSVG();
};
zoomOutCallback = () => {
this.currentScale -= 0.1;
this.scaleSVG();
};
resetViewCallback = () => {
this.currentScale = 1;
this.scaleSVG();
};
render() {
const controlButtons = createTopologyControlButtons({
zoomInCallback: this.zoomInCallback,
zoomOutCallback: this.zoomOutCallback,
resetViewCallback: this.resetViewCallback,
fitToScreenHidden: true,
legendCallback: this.handleLegendClick,
legendAriaLabel: "topology-legend",
});
return (
<TopologyView
aria-label="topology-viewer"
viewToolbar={
<TopologyToolbar
legendOptions={this.state.legendOptions}
addressColors={this.state.addressColors}
mapOptions={this.state.mapOptions}
handleOpenChange={this.handleOpenChange}
handleChangeArrows={this.handleChangeArrows}
handleChangeTrafficAnimation={this.handleChangeTrafficAnimation}
handleChangeTrafficFlowAddress={this.handleChangeTrafficFlowAddress}
handleUpdateMapColor={this.handleUpdateMapColor}
handleUpdateMapShown={this.handleUpdateMapShown}
handleHoverAddress={this.handleHoverAddress}
/>
}
controlBar={<TopologyControlBar controlButtons={controlButtons} />}
sideBar={<TopologySideBar show={false}></TopologySideBar>}
sideBarOpen={false}
className="qdrTopology"
>
<div className="diagram" aria-label="topology-diagram" id="topology"></div>
{this.state.showContextMenu && (
<ContextMenu
contextEventPosition={this.contextEventPosition}
contextEventData={this.contextEventData}
handleContextHide={this.handleContextHide}
setSelected={this.setSelected}
separateAllEdges={this.separateAllEdges}
collapseAllEdges={this.collapseAllEdges}
canExpandAll={this.canExpandAll(this.contextEventData)}
canCollapseAll={this.canCollapseAll(this.contextEventData)}
/>
)}
{this.state.showLegend && (
<LegendComponent
nodes={this.forceData.nodes}
handleCloseLegend={this.handleCloseLegend}
/>
)}
<div id="popover-div" className="qdrPopup"></div>
{this.state.showRouterInfo && (
<RouterInfoComponent
d={this.d}
topology={this.topology}
handleCloseRouterInfo={this.handleCloseRouterInfo}
/>
)}
{this.state.showClientInfo && (
<ClientInfoComponent
d={this.d}
topology={this.topology}
handleCloseClientInfo={this.handleCloseClientInfo}
handleSeparate={this.handleSeparate}
/>
)}
</TopologyView>
);
}
}
export default TopologyViewer;