react-network-diagrams
Version:
459 lines (412 loc) • 15.5 kB
JavaScript
/**
* Copyright (c) 2018, The Regents of the University of California,
* through Lawrence Berkeley National Laboratory (subject to receipt
* of any required approvals from the U.S. Dept. of Energy).
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from "react";
import PropTypes from "prop-types";
import _ from "underscore";
import { BaseMap } from "./BaseMap";
import { Resizable } from "./Resizable";
/**
* A high level component for showing network topology, including visualizing
* network traffic as a heat map.
*/
export class TrafficMap extends React.Component {
bounds() {
if (this.props.bounds) {
return this.props.bounds;
}
const minX = _.min(this.props.topology.nodes, node => node.x).x;
const minY = _.min(this.props.topology.nodes, node => node.y).y;
const maxX = _.max(this.props.topology.nodes, node => node.x).x;
const maxY = _.max(this.props.topology.nodes, node => node.y).y;
return { x1: minX, x2: maxX, y1: minY, y2: maxY };
}
nodeSize(name) {
return this.props.nodeSizeMap[name] || 7;
}
nodeShape(name) {
return this.props.nodeShapeMap[name] || "circle";
}
edgeThickness(capacity) {
return this.props.edgeThicknessMap[capacity] || 5;
}
edgeShape(name) {
if (_.has(this.props.edgeShapeMap, name)) {
return this.props.edgeShapeMap[name].shape;
} else {
return "linear";
}
}
edgeCurveDirection(name) {
let direction;
if (_.has(this.props.edgeShapeMap, name)) {
if (this.props.edgeShapeMap[name].shape === "curved") {
return this.props.edgeShapeMap[name].direction;
}
}
return direction;
}
edgeCurveOffset(name) {
let offset;
if (_.has(this.props.edgeShapeMap, name)) {
if (this.props.edgeShapeMap[name].shape === "curved") {
return this.props.edgeShapeMap[name].offset;
}
}
return offset;
}
selectEdgeColor(bps) {
const gbps = bps / 1.0e9;
for (let i = 0; i < this.props.edgeColorMap.length; i++) {
const row = this.props.edgeColorMap[i];
if (gbps >= row.range[0]) {
return row.color;
}
}
return "#C9CACC";
}
filteredPaths() {
return _.filter(this.props.topology.paths, path => {
if (_.isArray(this.props.showPaths)) {
return _.contains(this.props.showPaths, path.name);
}
return true;
});
}
buildTopology() {
const topology = {};
if (_.isNull(this.props.topology)) {
return null;
}
const genericStyle = {
node: {
normal: { fill: "#B0B0B0", stroke: "#9E9E9E", cursor: "pointer" },
selected: {
fill: "#37B6D3",
stroke: "rgba(55, 182, 211, 0.22)",
strokeWidth: 10,
cursor: "pointer"
},
muted: {
fill: "#B0B0B0",
stroke: "#9E9E9E",
opacity: 0.6,
cursor: "pointer"
}
},
label: {
normal: { fill: "#696969", stroke: "none", fontSize: 9 },
selected: { fill: "#333", stroke: "none", fontSize: 11 },
muted: {
fill: "#696969",
stroke: "none",
fontSize: 8,
opacity: 0.6
}
}
};
// Create a node list
topology.nodes = _.map(this.props.topology.nodes, node => {
const n = _.clone(node);
// Radius is based on the type of node, given in the nodeSizeMap
n.radius = this.nodeSize(node.type);
n.labelPosition = node.label_position;
n.labelOffsetX = node.label_dx;
n.labelOffsetY = node.label_dy;
const styleMap = _.has(this.props.stylesMap, node.type)
? this.props.stylesMap[node.type]
: genericStyle;
n.style = styleMap.node;
n.labelStyle = styleMap.label;
n.shape = this.nodeShape(node.name);
return n;
});
// Create the edge list
topology.edges = _.map(this.props.topology.edges, edge => {
const edgeName = `${edge.source}--${edge.target}`;
return {
width: this.edgeThickness(edge.capacity),
classed: edge.capacity,
source: edge.source,
target: edge.target,
totalCapacity: edge.total_capacity,
ifaces: edge.ifaces,
name: edgeName,
shape: this.edgeShape(edgeName),
curveDirection: this.edgeCurveDirection(edgeName),
offset: this.edgeCurveOffset(edgeName)
};
});
// Create the path list, filtering based on what is in showPaths
if (this.props.showPaths) {
topology.paths = _.map(this.filteredPaths(), path => {
const color = _.has(this.props.pathColorMap, path.name)
? this.props.pathColorMap[path.name]
: "lightsteelblue";
const width = _.has(this.props.pathWidthMap, path.name)
? this.props.pathWidthMap[path.name]
: 4;
return {
name: path.name,
steps: path.steps,
color,
width
};
});
}
// Colorize the topology
if (this.props.traffic) {
if (!this.props.showPaths && this.props.edgeDrawingMethod === "bidirectionalArrow") {
_.each(topology.edges, edge => {
const sourceTargetName = `${edge.source}--${edge.target}`;
const targetSourceName = `${edge.target}--${edge.source}`;
const sourceTargetTraffic = this.props.traffic.get(sourceTargetName);
const targetSourceTraffic = this.props.traffic.get(targetSourceName);
edge.sourceTargetColor = this.selectEdgeColor(sourceTargetTraffic);
edge.targetSourceColor = this.selectEdgeColor(targetSourceTraffic);
});
} else {
const edgeMap = {};
_.each(this.filteredPaths(), path => {
const pathAtoZTraffic = this.props.traffic.get(`${path.name}--AtoZ`);
const pathZtoATraffic = this.props.traffic.get(`${path.name}--ZtoA`);
let prev = null;
_.each(path.steps, step => {
if (prev) {
const sourceTargetName = `${prev}--${step}`;
if (!_.has(edgeMap, sourceTargetName)) {
edgeMap[sourceTargetName] = 0;
}
edgeMap[sourceTargetName] += pathAtoZTraffic;
const targetSourceName = `${step}--${prev}`;
if (!_.has(edgeMap, targetSourceName)) {
edgeMap[targetSourceName] = 0;
}
edgeMap[targetSourceName] += pathZtoATraffic;
}
prev = step;
});
});
_.each(topology.edges, edge => {
edge.stroke = this.props.edgeColor ? this.props.edgeColor : "#DDD";
const sourceTargetName = `${edge.source}--${edge.target}`;
const targetSourceName = `${edge.target}--${edge.source}`;
if (_.has(edgeMap, sourceTargetName)) {
const sourceTargetTraffic = edgeMap[sourceTargetName];
edge.sourceTargetColor = this.selectEdgeColor(sourceTargetTraffic);
}
if (_.has(edgeMap, targetSourceName)) {
const targetSourceTraffic = edgeMap[targetSourceName];
edge.targetSourceColor = this.selectEdgeColor(targetSourceTraffic);
}
});
}
}
topology.name = this.props.topology.name;
topology.description = this.props.topology.description;
return topology;
}
handleSelectionChanged(selectionType, selection) {
if (this.props.onSelectionChange) {
this.props.onSelectionChange(selectionType, selection);
}
}
render() {
const topo = this.buildTopology();
const bounds = this.bounds();
const aspect = (bounds.x2 - bounds.x1) / (bounds.y2 - bounds.y1);
const autoSize = this.props.autoSize;
const defaultStyle = {
background: "#F6F6F6",
borderStyle: "solid",
borderWidth: "thin",
borderColor: "#E6E6E6"
};
const style = this.props.style ? this.props.style : defaultStyle;
if (autoSize) {
return (
<Resizable aspect={aspect} style={style}>
<BaseMap
topology={topo}
paths={topo.paths}
bounds={bounds}
width={this.props.width}
height={this.props.height}
margin={this.props.margin}
selection={this.props.selection}
edgeDrawingMethod={this.props.edgeDrawingMethod}
onSelectionChange={(selectionType, selection) =>
this.handleSelectionChanged(selectionType, selection)
}
/>
</Resizable>
);
} else {
return (
<div style={style}>
<BaseMap
topology={topo}
paths={topo.paths}
bounds={bounds}
width={this.props.width}
height={this.props.height}
margin={this.props.margin}
selection={this.props.selection}
edgeDrawingMethod={this.props.edgeDrawingMethod}
onSelectionChange={(selectionType, selection) =>
this.handleSelectionChanged(selectionType, selection)
}
/>
</div>
);
}
}
}
TrafficMap.defaultProps = {
edgeThicknessMap: {
"100G": 5,
"10G": 3,
"1G": 1.5,
subG: 1
},
edgeColor: "#DDD",
edgeColorMap: [],
nodeSizeMap: {},
nodeShapeMap: {},
edgeShapeMap: {},
selected: false,
shape: "circle",
stylesMap: {},
showPaths: false,
autoSize: true
};
TrafficMap.propTypes = {
/** The width of the circuit diagram */
width: PropTypes.number,
/**
* The topology structure, as detailed above. This contains the
* descriptions of nodes, edges and paths used to render the topology
*/
topology: PropTypes.object,
/**
* Specified as an object containing x1, y1 and x2, y2. This is the region
* to display on the map. If this isn't specified the bounds will be
* calculated from the nodes in the Map.
*/
bounds: PropTypes.shape({
x1: PropTypes.number,
y1: PropTypes.number,
x2: PropTypes.number,
y2: PropTypes.number
}),
/**
* The is the overall rendering style for the edge connections. Maybe
* one of the following strings:
*
* * "simple" - simple line connections between nodes
* * "bidirectionalArrow" - network traffic represented by bi-directional arrows
* * "pathBidirectionalArrow" - similar to "bidirectionalArrow", but only for
* edges that are used in the currently displayed path(s).
*/
edgeDrawingMethod: PropTypes.oneOf(["simple", "bidirectionalArrow", "pathBidirectionalArrow"]),
/**
* Either a boolean or a list of path names. If a bool, and true, then all
* paths will be shown. If a list then only the paths in that list will be
* shown. The default is to show no paths.
*/
showPaths: PropTypes.oneOfType([PropTypes.bool, PropTypes.arrayOf(PropTypes.string)]),
/**
* A mapping of the capacity field within the tologogy edge object
* to a line thickness for rendering the edges.
*
* Example:
*
* ```
* const edgeThicknessMap = {
* "100G": 5,
* "10G": 3,
* "1G": 1.5,
* "subG": 1
* };
* ```
*/
edgeThicknessMap: PropTypes.object,
/**
* The default color for an edge which isn't colored using the `edgeColorMap`.
*/
edgeColor: PropTypes.string,
/**
* A mapping of traffic on the link, in Gbps, to a color and label. The label is because the same
* mapping can be used to create a legend for the map.
*
* Example:
*
* ```
* const edgeColorMap = [
* { color: "#990000", label: ">=50 Gbps", range: [50, 100] },
* { color: "#bd0026", label: "20 - 50", range: [20, 50] },
* { color: "#cc4c02", label: "10 - 20", range: [10, 20] },
* { color: "#016c59", label: "5 - 10", range: [5, 10] },
* { color: "#238b45", label: "2 - 5", range: [2, 5] },
* { color: "#3690c0", label: "1 - 2", range: [1, 2] },
* { color: "#74a9cf", label: "0 - 1", range: [0, 1] }
* ];
* ```
*/
edgeColorMap: PropTypes.array,
/**
* A mapping from the type field in the node object to a size to draw the shape
*
* Example:
* ```
* const nodeSizeMap = {
* hub: 5.5,
* esnet_site: 7
* };
* ```
*/
nodeSizeMap: PropTypes.object,
/**
* Mapping of node name to shape (default is "circle", other options are
* "cloud" or "square", currently).
*
* Example:
* ```
* const nodeShapeMap = {
* DENV: "square"
* };
* ```
*/
nodeShapeMap: PropTypes.object,
/**
* A mapping of the edge name (which is source + "--" + target) to a
* dict of edge shape options:
* * `shape` (either "linear" or "curved")
* * `direction` (if shape is curved, either "left" or "right")
* * `offset` (if shape is curved, the amount of curve, which is
* pixel offset from a straight line between the source and target at the midpoint)
*
* Example:
* ```
* const edgeShapeMap = {
* "ALBQ--DENV": {
* "shape": "curved",
* "direction": "right",
* "offset": 15
* }
* ```
*/
edgeShapeMap: PropTypes.object,
/** Display the endpoint selected */
selected: PropTypes.bool,
/** The shape of the endpoint */
shape: PropTypes.oneOf(["circle", "square", "cloud"]),
stylesMap: PropTypes.object,
autoSize: PropTypes.bool
};