linkerd2/web/app/js/components/NetworkGraph.jsx

224 lines
5.7 KiB
JavaScript

import 'whatwg-fetch';
import { forceCenter, forceLink, forceManyBody, forceSimulation } from 'd3-force';
import { select, selectAll } from 'd3-selection';
import PropTypes from 'prop-types';
import React from 'react';
import _get from 'lodash/get';
import _isEmpty from 'lodash/isEmpty';
import _map from 'lodash/map';
import _uniq from 'lodash/uniq';
import { drag } from 'd3-drag';
import { format } from 'd3-format';
import { metricsPropType } from './util/MetricUtils.jsx';
import { withContext } from './util/AppContext.jsx';
import withREST from './util/withREST.jsx';
// create a Object with only the subset of functions/submodules/plugins that we need
const d3 = {
drag,
forceCenter,
forceLink,
forceManyBody,
forceSimulation,
select,
selectAll,
format,
};
const defaultSvgWidth = 524;
const defaultSvgHeight = 325;
const defaultNodeRadius = 15;
const margin = { top: 0, right: 0, bottom: 10, left: 0 };
const simulation = d3.forceSimulation()
.force('link',
d3.forceLink()
.id(d => d.id)
.distance(140))
.force('charge', d3.forceManyBody().strength(-20))
.force('center', d3.forceCenter(defaultSvgWidth / 2, defaultSvgHeight / 2));
export class NetworkGraphBase extends React.Component {
constructor(props) {
super(props);
// https://github.com/d3/d3-zoom/issues/32
d3.getEvent = (() => require('d3-selection').event); // eslint-disable-line global-require
}
componentDidMount() {
const container = document.getElementsByClassName('network-graph-container')[0];
const width = !container ? defaultSvgWidth : container.getBoundingClientRect().width;
this.svg = d3.select('.network-graph-container')
.append('svg')
.attr('class', 'network-graph')
.attr('width', width)
.attr('height', width)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
}
componentDidUpdate() {
simulation.alpha(1).restart();
this.drawGraph();
}
getGraphData() {
const { data, deployments } = this.props;
const links = [];
const nodeList = [];
_map(data, (resp, i) => {
const rows = _get(resp, ['ok', 'statTables', 0, 'podGroup', 'rows']);
const dst = deployments[i].name;
_map(rows, row => {
links.push({
source: row.resource.name,
target: dst,
});
nodeList.push(row.resource.name);
nodeList.push(dst);
});
});
const nodes = _map(_uniq(nodeList), n => ({ id: n }));
return {
links,
nodes,
};
}
drawGraph() {
const graphData = this.getGraphData();
// check if graph is present to prevent drawing of multiple graphs
if (this.svg.select('circle')._groups[0][0]) {
return;
}
this.drawGraphComponents(graphData.links, graphData.nodes);
}
drawGraphComponents(links, nodes) {
if (_isEmpty(nodes)) {
d3.select('.network-graph-container').select('svg').attr('height', 0);
return;
} else {
d3.select('.network-graph-container').select('svg').attr('height', defaultSvgHeight);
}
this.svg.append('svg:defs').selectAll('marker')
.data(links) // Different link/path types can be defined here
.enter()
.append('svg:marker') // This section adds in the arrows
.attr('id', node => `${node.source}/${node.target}`)
.attr('viewBox', '0 -5 10 10')
.attr('refX', 24)
.attr('refY', -0.25)
.attr('markerWidth', 3)
.attr('markerHeight', 3)
.attr('fill', '#454242')
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5');
// add the links and the arrows
const path = this.svg.append('svg:g').selectAll('path')
.data(links)
.enter()
.append('svg:path')
.attr('stroke-width', 3)
.attr('stroke', '#454242')
.attr('marker-end', node => `url(#${node.source}/${node.target})`);
const nodeElements = this.svg.append('g')
.selectAll('circle')
.data(nodes)
.enter()
.append('circle')
.attr('r', defaultNodeRadius)
.attr('fill', 'steelblue')
.call(d3.drag()
.on('start', this.dragstarted)
.on('drag', this.dragged)
.on('end', this.dragended));
const textElements = this.svg.append('g')
.selectAll('text')
.data(nodes)
.enter()
.append('text')
.text(node => node.id)
.attr('font-size', 15)
.attr('dx', 20)
.attr('dy', 4);
simulation.nodes(nodes).on('tick', () => {
path
.attr('d', node => `M${node.source.x} ${node.source.y} L ${node.target.x} ${node.target.y}`);
nodeElements
.attr('cx', node => node.x)
.attr('cy', node => node.y);
textElements
.attr('x', node => node.x)
.attr('y', node => node.y);
});
simulation.force('link')
.links(links);
}
dragstarted = d => {
if (!d3.getEvent().active) {
simulation.alphaTarget(0.3).restart();
}
d.fx = d.x;
d.fy = d.y;
}
dragged = d => {
d.fx = d3.getEvent().x;
d.fy = d3.getEvent().y;
}
dragended = d => {
if (!d3.getEvent().active) {
simulation.alphaTarget(0);
}
d.fx = null;
d.fy = null;
}
render() {
return (
<div>
<div className="network-graph-container" />
</div>
);
}
}
NetworkGraphBase.defaultProps = {
deployments: [],
};
NetworkGraphBase.propTypes = {
data: PropTypes.arrayOf(metricsPropType.isRequired).isRequired,
deployments: PropTypes.arrayOf(PropTypes.object),
};
export default withREST(
withContext(NetworkGraphBase),
({ api, namespace, deployments }) => {
return _map(deployments, d => {
return api.fetchMetrics(`${api.urlsForResource('deployment', namespace)}&to_name=${d.name}`);
});
},
{
poll: false,
resetProps: ['deployment'],
},
);