mirror of https://github.com/linkerd/linkerd2.git
224 lines
5.7 KiB
JavaScript
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'],
|
|
},
|
|
);
|