mirror of
https://github.com/yldio/copilot.git
synced 2024-11-28 14:10:04 +02:00
Add new topology to ui components
This commit is contained in:
parent
dc8954a110
commit
1f2f8a6965
@ -68,7 +68,6 @@
|
|||||||
"react/jsx-no-duplicate-props": 2,
|
"react/jsx-no-duplicate-props": 2,
|
||||||
"react/jsx-no-target-blank": 2,
|
"react/jsx-no-target-blank": 2,
|
||||||
"react/jsx-pascal-case": 2,
|
"react/jsx-pascal-case": 2,
|
||||||
"react/jsx-sort-props": 2,
|
|
||||||
"react/jsx-space-before-closing": 2,
|
"react/jsx-space-before-closing": 2,
|
||||||
"react/jsx-wrap-multilines": 2,
|
"react/jsx-wrap-multilines": 2,
|
||||||
"jsx-a11y/anchor-has-content": 2,
|
"jsx-a11y/anchor-has-content": 2,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const React = require('React');
|
const React = require('react');
|
||||||
const Styled = require('styled-components');
|
const Styled = require('styled-components');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const React = require('React');
|
const React = require('react');
|
||||||
const Styled = require('styled-components');
|
const Styled = require('styled-components');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const React = require('React');
|
const React = require('react');
|
||||||
const Styled = require('styled-components');
|
const Styled = require('styled-components');
|
||||||
const GraphNodeButton = require('./graph-node-button');
|
const GraphNodeButton = require('./graph-node-button');
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const React = require('React');
|
const React = require('react');
|
||||||
const Styled = require('styled-components');
|
const Styled = require('styled-components');
|
||||||
const GraphNodeButton = require('./graph-node-button');
|
const GraphNodeButton = require('./graph-node-button');
|
||||||
const GraphNodeMetrics = require('./graph-node-metrics');
|
const GraphNodeMetrics = require('./graph-node-metrics');
|
||||||
|
5
spikes/graphs-topology/d3/client/topology.js
vendored
5
spikes/graphs-topology/d3/client/topology.js
vendored
@ -7,11 +7,6 @@ const {
|
|||||||
default: styled
|
default: styled
|
||||||
} = Styled;
|
} = Styled;
|
||||||
|
|
||||||
const StyledSvg = styled.svg`
|
|
||||||
width: 1024px;
|
|
||||||
height: 860px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledForm = styled.form`
|
const StyledForm = styled.form`
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
`;
|
`;
|
||||||
|
@ -68,7 +68,6 @@
|
|||||||
"react/jsx-no-duplicate-props": 2,
|
"react/jsx-no-duplicate-props": 2,
|
||||||
"react/jsx-no-target-blank": 2,
|
"react/jsx-no-target-blank": 2,
|
||||||
"react/jsx-pascal-case": 2,
|
"react/jsx-pascal-case": 2,
|
||||||
"react/jsx-sort-props": 2,
|
|
||||||
"react/jsx-space-before-closing": 2,
|
"react/jsx-space-before-closing": 2,
|
||||||
"react/jsx-wrap-multilines": 2,
|
"react/jsx-wrap-multilines": 2,
|
||||||
"jsx-a11y/anchor-has-content": 2,
|
"jsx-a11y/anchor-has-content": 2,
|
||||||
|
@ -31,6 +31,7 @@ const Select = (props) => {
|
|||||||
id = rndId(),
|
id = rndId(),
|
||||||
label = '',
|
label = '',
|
||||||
multiple = false,
|
multiple = false,
|
||||||
|
name = '',
|
||||||
placeholder = '',
|
placeholder = '',
|
||||||
value = defaultValue,
|
value = defaultValue,
|
||||||
warning = ''
|
warning = ''
|
||||||
@ -67,6 +68,7 @@ const Select = (props) => {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
id={id}
|
id={id}
|
||||||
multiple={multiple}
|
multiple={multiple}
|
||||||
|
name={name}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={_placeholder ? value : undefined}
|
value={_placeholder ? value : undefined}
|
||||||
>
|
>
|
||||||
@ -84,6 +86,7 @@ Select.propTypes = {
|
|||||||
id: React.PropTypes.string,
|
id: React.PropTypes.string,
|
||||||
label: React.PropTypes.string,
|
label: React.PropTypes.string,
|
||||||
multiple: React.PropTypes.bool,
|
multiple: React.PropTypes.bool,
|
||||||
|
name: React.PropTypes.string,
|
||||||
placeholder: React.PropTypes.string,
|
placeholder: React.PropTypes.string,
|
||||||
value: React.PropTypes.string,
|
value: React.PropTypes.string,
|
||||||
warning: React.PropTypes.string
|
warning: React.PropTypes.string
|
||||||
|
425
ui/src/components/topology-old/index.js
Normal file
425
ui/src/components/topology-old/index.js
Normal file
@ -0,0 +1,425 @@
|
|||||||
|
const constants = require('../../shared/constants');
|
||||||
|
const d3 = require('d3');
|
||||||
|
const fns = require('../../shared/functions');
|
||||||
|
const React = require('react');
|
||||||
|
const Styled = require('styled-components');
|
||||||
|
|
||||||
|
const {
|
||||||
|
colors
|
||||||
|
} = constants;
|
||||||
|
|
||||||
|
const {
|
||||||
|
remcalc
|
||||||
|
} = fns;
|
||||||
|
|
||||||
|
const {
|
||||||
|
default: styled
|
||||||
|
} = Styled;
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
function rightRoundedRect(x, y, width, height, radius) {
|
||||||
|
return 'M' + x + ',' + y // Move to top left (absolute)
|
||||||
|
+ 'h ' + (width - 2 * radius) // Horizontal line to (relative)
|
||||||
|
+ 'a ' + radius + ',' + radius + ' 0 0 1 ' + radius + ',' + radius // Relative arc
|
||||||
|
+ 'v ' + (height - 2 * radius) // Vertical line to (relative)
|
||||||
|
+ 'a ' + radius + ',' + radius + ' 0 0 1 ' + -radius + ',' + radius // Relative arch
|
||||||
|
+ 'h ' + (2 * radius - width) // Horizontal lint to (relative)
|
||||||
|
+ 'z '; // path back to start
|
||||||
|
}
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
function leftRoundedRect(x, y, width, height, radius) {
|
||||||
|
return 'M' + (x + width) + ',' + y // Move to (absolute) start at top-right
|
||||||
|
+ 'v ' + height // Vertical line to (relative)
|
||||||
|
+ 'h ' + (2 * radius - width) // Horizontal line to (relative)
|
||||||
|
+ 'a ' + radius + ',' + radius + ' 0 0 1 ' + -radius + ',' + -radius // Relative arc
|
||||||
|
+ 'v ' + -(height - 2 * radius) // Vertical line to (relative)
|
||||||
|
+ 'a ' + radius + ',' + radius + ' 0 0 1 ' + radius + ',' + -radius // Relative arch
|
||||||
|
+ 'z '; // path back to start
|
||||||
|
}
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
function topRoundedRect(x, y, width, height, radius) {
|
||||||
|
return 'M' + x + ',' + -(y - height) // Move to (absolute) start at bottom-left
|
||||||
|
+ 'v ' + -(height - radius) // Vertical line to (relative)
|
||||||
|
+ 'a ' + radius + ',' + radius + ' 0 0 1 ' + radius + ',' + -radius // Relative arc
|
||||||
|
+ 'h ' + -(2 * radius - width) // Horizontal line to (relative)
|
||||||
|
+ 'a ' + radius + ',' + radius + ' 0 0 1 ' + radius + ',' + radius // Relative arc
|
||||||
|
+ 'v ' + (height - radius) // Vertical line to (relative)
|
||||||
|
+ 'h ' + (2 * radius - width) // Horizontal line to (relative)
|
||||||
|
+ 'z '; // path back to start
|
||||||
|
}
|
||||||
|
|
||||||
|
function bottomRoundedRect(x, y, width, height, radius) {
|
||||||
|
return 'M' + x + ',' + -(y - (height - 2 * radius)) // Move to (absolute) start at bottom-right
|
||||||
|
+ 'v ' + -(height - 2 * radius) // Vertical line to (relative)
|
||||||
|
+ 'h ' + (width) // Horizontal line to (relative)
|
||||||
|
+ 'v ' + (height - 2 * radius) // Vertical line to (relative)
|
||||||
|
+ 'a ' + -radius + ',' + radius + ' 0 0 1 ' + -radius + ',' + radius // Relative arc
|
||||||
|
+ 'h ' + (2 * radius - width) // Horizontal line to (relative)
|
||||||
|
+ 'a ' + radius + ',' + radius + ' 0 0 1 ' + -radius + ',' + -radius // Relative arc
|
||||||
|
+ 'z '; // path back to start
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
function rect(x, y, width, height) {
|
||||||
|
return 'M' + x + ',' + -(y - height) // Move to (absolute) start at bottom-right
|
||||||
|
+ 'v ' + -(height) // Vertical line to (relative)
|
||||||
|
+ 'h ' + width // Horizontal line to (relative)
|
||||||
|
+ 'v ' + height // Vertical line to (relative)
|
||||||
|
+ 'h ' + -(width) // Horizontal line to (relative)
|
||||||
|
+ 'z '; // path back to start
|
||||||
|
}
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
const StyledSVGContainer = styled.svg`
|
||||||
|
& {
|
||||||
|
.links line {
|
||||||
|
stroke: #343434;
|
||||||
|
stroke-opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health, .health_warn {
|
||||||
|
font-family: "LibreFranklin";
|
||||||
|
font-size: ${remcalc(12)};
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
font-stretch: normal;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health_warn {
|
||||||
|
font-size: ${remcalc(15)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
font-family: "LibreFranklin";
|
||||||
|
font-size: ${remcalc(12)};
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-stretch: normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node_statistics {
|
||||||
|
font-family: "LibreFranklin";
|
||||||
|
font-size: ${remcalc(12)};
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-stretch: normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node_statistics p {
|
||||||
|
margin: 0 0 0 0;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary, .secondary {
|
||||||
|
font-family: "LibreFranklin";
|
||||||
|
font-size: ${remcalc(12)};
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-stretch: normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info_text {
|
||||||
|
font-family: "LibreFranklin";
|
||||||
|
font-size: ${remcalc(16)};
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
font-stretch: normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
class TopologyGraph extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.svg = null;
|
||||||
|
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
this.simulation = d3.forceSimulation()
|
||||||
|
.force('charge', d3.forceManyBody()
|
||||||
|
.strength(() => -50)
|
||||||
|
.distanceMin(() => 30))
|
||||||
|
.force('link', d3.forceLink().distance(() => 200).id((d) => d.id))
|
||||||
|
// TODO manually handle looking for collisions in the tick, we then get the BBox
|
||||||
|
// and keep moving things for a while to try to get a fit.
|
||||||
|
.force('collide',
|
||||||
|
d3.forceCollide().radius((d) => 220 + 0.5).iterations(15))
|
||||||
|
.force('center', d3.forceCenter(width * 1/3, height * 1/3));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const component = this;
|
||||||
|
|
||||||
|
const {
|
||||||
|
simulation,
|
||||||
|
} = this;
|
||||||
|
|
||||||
|
const svg = d3.select(this._refs.svg);
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
graph = {
|
||||||
|
nodes: [],
|
||||||
|
links: []
|
||||||
|
},
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
// Drawing the links between nodes
|
||||||
|
const link = svg.append('g')
|
||||||
|
.attr('class', 'links')
|
||||||
|
.selectAll('line')
|
||||||
|
.data(graph.links)
|
||||||
|
.enter().append('line')
|
||||||
|
.attr('stroke-width', remcalc(12));
|
||||||
|
|
||||||
|
// And svg group, to contain all of the attributes in @antonas' first prototype
|
||||||
|
svg.selectAll('.node')
|
||||||
|
.data(graph.nodes)
|
||||||
|
.enter()
|
||||||
|
.append('g')
|
||||||
|
.attr('class', 'node_group');
|
||||||
|
|
||||||
|
svg.selectAll('.node_group').each(function(d) {
|
||||||
|
// Create different type of node for services with Primaries + Secondaries
|
||||||
|
// We could extend this further to allow us to have as many nested services
|
||||||
|
// as wanted.
|
||||||
|
// TODO handle this per prop
|
||||||
|
// if (d.id === 'Percona') {
|
||||||
|
// createExtendedNode(d3.select(this));
|
||||||
|
// } else {
|
||||||
|
component.createServiceNodes(d, d3.select(this));
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
|
||||||
|
simulation
|
||||||
|
.nodes(graph.nodes)
|
||||||
|
.on('tick', ticked);
|
||||||
|
|
||||||
|
simulation.force('link')
|
||||||
|
.links(graph.links);
|
||||||
|
|
||||||
|
function contrain(dimension, r, z) {
|
||||||
|
return Math.max(0, Math.min(dimension - r, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
function ticked() {
|
||||||
|
// TODO: Think of a common way of extracting the bounding boxes for each
|
||||||
|
// item and to grab the x{1,2} and y{1,2} values.
|
||||||
|
link
|
||||||
|
.attr('x1', function(d) {
|
||||||
|
let x;
|
||||||
|
svg.selectAll('.node_group').each(function(_, i) {
|
||||||
|
if (i !== d.source.index) return;
|
||||||
|
x = d3.select(this).node().getBBox().width;
|
||||||
|
});
|
||||||
|
return contrain(width, x, d.source.x) + 80;
|
||||||
|
})
|
||||||
|
.attr('y1', function(d) {
|
||||||
|
let y;
|
||||||
|
svg.selectAll('.node_group').each(function(_, i) {
|
||||||
|
if (i !== d.source.index) return;
|
||||||
|
y = d3.select(this).node().getBBox().height;
|
||||||
|
});
|
||||||
|
return contrain(height, y, d.source.y) + 24;
|
||||||
|
})
|
||||||
|
.attr('x2', function(d) {
|
||||||
|
let x;
|
||||||
|
svg.selectAll('.node_group').each(function(_, i) {
|
||||||
|
if (i !== d.target.index) return;
|
||||||
|
x = d3.select(this).node().getBBox().width;
|
||||||
|
});
|
||||||
|
return contrain(width, x, d.target.x) + 80;
|
||||||
|
})
|
||||||
|
.attr('y2', function(d) {
|
||||||
|
let y;
|
||||||
|
svg.selectAll('.node_group').each(function(_, i) {
|
||||||
|
if (i !== d.target.index) return;
|
||||||
|
y = d3.select(this).node().getBBox().height;
|
||||||
|
});
|
||||||
|
return contrain(height, y, d.target.y) + 24;
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.selectAll('.node_group')
|
||||||
|
.attr('transform', function(d) {
|
||||||
|
const x = d3.select(this).node().getBBox().width;
|
||||||
|
const y = d3.select(this).node().getBBox().height;
|
||||||
|
return 'translate(' + contrain(width, x, d.x) + ','
|
||||||
|
+ contrain(height, y, d.y) + ')';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createHealthCheckBadge(element, x, y) {
|
||||||
|
const paddingLeft = 30;
|
||||||
|
const health = element.append('g');
|
||||||
|
|
||||||
|
// TODO: replace these element with the designed SVG elements from
|
||||||
|
// @antonasdeduchovas' designs with full svg elements.
|
||||||
|
|
||||||
|
health.append('circle')
|
||||||
|
.attr('class', 'alert')
|
||||||
|
.attr('cx', function() {
|
||||||
|
return element
|
||||||
|
.node()
|
||||||
|
.getBBox()
|
||||||
|
.width + paddingLeft;
|
||||||
|
})
|
||||||
|
.attr('cy', '24')
|
||||||
|
.attr('stroke-width', 0)
|
||||||
|
.attr('fill', (d) =>
|
||||||
|
d.id === 'Memcached' ? 'rgb(217, 77, 68)' : 'rgb(0,175,102)')
|
||||||
|
.attr('r', remcalc(9));
|
||||||
|
|
||||||
|
// An icon or label that exists within the circle, inside the infobox
|
||||||
|
health.append('text')
|
||||||
|
.attr('class', 'health')
|
||||||
|
.attr('x', function() {
|
||||||
|
return element
|
||||||
|
.node()
|
||||||
|
.getBBox()
|
||||||
|
.width + 3;
|
||||||
|
})
|
||||||
|
.attr('y', '29')
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.attr('fill', colors.brandPrimaryColor)
|
||||||
|
.text((d) => d.id === 'Memcached' ? '!' : '❤');
|
||||||
|
}
|
||||||
|
|
||||||
|
createServiceNodeBody(data, element, d) {
|
||||||
|
const stats = element.append('g');
|
||||||
|
stats.append('path')
|
||||||
|
.attr('class', 'node')
|
||||||
|
.attr('d', d)
|
||||||
|
.attr('stroke', '#343434')
|
||||||
|
.attr('stroke-width', remcalc(1))
|
||||||
|
.attr('fill', '#464646');
|
||||||
|
|
||||||
|
const html = stats
|
||||||
|
.append('switch')
|
||||||
|
.append('foreignObject')
|
||||||
|
.attr('requiredFeatures',
|
||||||
|
'http://www.w3.org/TR/SVG11/feature#Extensibility')
|
||||||
|
.attr('x', 12)
|
||||||
|
.attr('y', 57)
|
||||||
|
.attr('width', 160)
|
||||||
|
.attr('height', 70)
|
||||||
|
// From here everything will be rendered with react using a ref.
|
||||||
|
// However for now these values are hard-coded.
|
||||||
|
.append('xhtml:div')
|
||||||
|
.attr('class', 'node_statistics');
|
||||||
|
// Remove with react + dyanmic data.
|
||||||
|
|
||||||
|
html.selectAll('.node_statistics').data(data.metrics).enter()
|
||||||
|
.append('p')
|
||||||
|
.text((d) => `${d.name}: ${d.stat}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
createServiceNodes(data, elm) {
|
||||||
|
const component = this;
|
||||||
|
|
||||||
|
const {
|
||||||
|
dragged,
|
||||||
|
dragstarted,
|
||||||
|
dragended,
|
||||||
|
} = this;
|
||||||
|
|
||||||
|
const width = 170;
|
||||||
|
const topHeight = 47;
|
||||||
|
const radius = 4;
|
||||||
|
|
||||||
|
// Box where label will live
|
||||||
|
elm.append('path')
|
||||||
|
.attr('class', 'node')
|
||||||
|
.attr('d', topRoundedRect('0', '0', width, topHeight, radius))
|
||||||
|
.attr('stroke', colors.topologyBackground)
|
||||||
|
.attr('stroke-width', remcalc(1))
|
||||||
|
.attr('fill', colors.brandSecondaryColor);
|
||||||
|
|
||||||
|
const text = elm.append('g');
|
||||||
|
|
||||||
|
text.append('text')
|
||||||
|
.attr('class', 'info_text')
|
||||||
|
.attr('x', '12')
|
||||||
|
.attr('y', '30')
|
||||||
|
.attr('text-anchor', 'start')
|
||||||
|
.attr('fill', colors.brandPrimaryColor)
|
||||||
|
.text(d => d.id);
|
||||||
|
|
||||||
|
// if (service is registered twice in the scheduler) {
|
||||||
|
// Do not show healthcheck in the header
|
||||||
|
// } else {
|
||||||
|
this.createHealthCheckBadge(text);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (service is registered twice in the scheduler) {
|
||||||
|
// this.createServiceNodeBody(data, elm, rect('0',`-${topHeight}`, width, 78, 4));
|
||||||
|
// } else {
|
||||||
|
this.createServiceNodeBody(data, elm,
|
||||||
|
bottomRoundedRect('0', `-${topHeight}`, width, 78, 4));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// <==== END ====>
|
||||||
|
|
||||||
|
// Set up movement for service nodes
|
||||||
|
elm.call(d3.drag()
|
||||||
|
.on('start', dragstarted.bind(component))
|
||||||
|
.on('drag', dragged.bind(component))
|
||||||
|
.on('end', dragended.bind(component)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
dragstarted(d) {
|
||||||
|
if (!d3.event.active) this.simulation.alphaTarget(0.3).restart();
|
||||||
|
d.fx = d.x;
|
||||||
|
d.fy = d.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragged(d) {
|
||||||
|
d.fx = d3.event.x;
|
||||||
|
d.fy = d3.event.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragended(d) {
|
||||||
|
if (!d3.event.active) this.simulation.alphaTarget(0);
|
||||||
|
d.fx = null;
|
||||||
|
d.fy = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref(name) {
|
||||||
|
this._refs = this._refs || {};
|
||||||
|
|
||||||
|
return (el) => {
|
||||||
|
this._refs[name] = el;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<StyledSVGContainer
|
||||||
|
innerRef={this.ref('svg')}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
TopologyGraph.propTypes = {
|
||||||
|
graph: React.PropTypes.object,
|
||||||
|
height: React.PropTypes.number,
|
||||||
|
width: React.PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = TopologyGraph;
|
154
ui/src/components/topology-old/story.js
Normal file
154
ui/src/components/topology-old/story.js
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
const React = require('react');
|
||||||
|
|
||||||
|
const {
|
||||||
|
storiesOf
|
||||||
|
} = require('@kadira/storybook');
|
||||||
|
|
||||||
|
const Base = require('../base');
|
||||||
|
const Topology = require('./');
|
||||||
|
const TopologyView = require('./view');
|
||||||
|
const services = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'Nginx',
|
||||||
|
attrs: {
|
||||||
|
dcs: 1,
|
||||||
|
instances: 2,
|
||||||
|
healthy: true,
|
||||||
|
},
|
||||||
|
metrics: [
|
||||||
|
{
|
||||||
|
name: 'CPU',
|
||||||
|
stat: '50%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Memory',
|
||||||
|
stat: '20%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Network',
|
||||||
|
stat: '5.9KB/sec',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'WordPress',
|
||||||
|
attrs: {
|
||||||
|
dcs: 1,
|
||||||
|
instances: 2,
|
||||||
|
healthy: true,
|
||||||
|
},
|
||||||
|
metrics: [
|
||||||
|
{
|
||||||
|
name: 'CPU',
|
||||||
|
stat: '50%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Memory',
|
||||||
|
stat: '20%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Network',
|
||||||
|
stat: '5.9KB/sec',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Memcached',
|
||||||
|
attrs: {
|
||||||
|
dcs: 1,
|
||||||
|
instances: 2,
|
||||||
|
healthy: true,
|
||||||
|
},
|
||||||
|
metrics: [
|
||||||
|
{
|
||||||
|
name: 'CPU',
|
||||||
|
stat: '50%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Memory',
|
||||||
|
stat: '20%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Network',
|
||||||
|
stat: '5.9KB/sec',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Percona',
|
||||||
|
attrs: {
|
||||||
|
dcs: 1,
|
||||||
|
instances: 2,
|
||||||
|
healthy: true,
|
||||||
|
},
|
||||||
|
metrics: [
|
||||||
|
{
|
||||||
|
name: 'CPU',
|
||||||
|
stat: '50%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Memory',
|
||||||
|
stat: '20%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Network',
|
||||||
|
stat: '5.9KB/sec',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'NFS',
|
||||||
|
attrs: {
|
||||||
|
dcs: 1,
|
||||||
|
instances: 2,
|
||||||
|
healthy: true,
|
||||||
|
},
|
||||||
|
metrics: [
|
||||||
|
{
|
||||||
|
name: 'CPU',
|
||||||
|
stat: '50%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Memory',
|
||||||
|
stat: '20%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Network',
|
||||||
|
stat: '5.9KB/sec',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
source: 'Nginx',
|
||||||
|
target: 'WordPress',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: 'WordPress',
|
||||||
|
target: 'Memcached',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: 'WordPress',
|
||||||
|
target: 'NFS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: 'WordPress',
|
||||||
|
target: 'Percona',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
storiesOf('Topology', module)
|
||||||
|
.add('5 services', () => (
|
||||||
|
<Base>
|
||||||
|
<TopologyView>
|
||||||
|
<Topology
|
||||||
|
graph={services}
|
||||||
|
height={500}
|
||||||
|
width={500}
|
||||||
|
/>
|
||||||
|
</TopologyView>
|
||||||
|
</Base>
|
||||||
|
));
|
154
ui/src/components/topology/graph-link.js
Normal file
154
ui/src/components/topology/graph-link.js
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const Styled = require('styled-components');
|
||||||
|
const PropTypes = require('./prop-types');
|
||||||
|
|
||||||
|
const {
|
||||||
|
default: styled
|
||||||
|
} = Styled;
|
||||||
|
|
||||||
|
const StyledLine = styled.line`
|
||||||
|
stroke: #343434;
|
||||||
|
stroke-width: 1.5;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledCircle = styled.circle`
|
||||||
|
stroke: #343434;
|
||||||
|
fill: #464646;
|
||||||
|
stroke-width: 1.5;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledArrow = styled.line`
|
||||||
|
stroke: white;
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-linecap: round;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const getAngleFromPoints = (source, target) => {
|
||||||
|
|
||||||
|
const lineAngle = Math.atan2(target.y-source.y, target.x - source.x);
|
||||||
|
const lineAngleDeg = lineAngle*180/Math.PI;
|
||||||
|
const zeroToThreeSixty = lineAngleDeg < 0 ? 360 + lineAngleDeg : lineAngleDeg;
|
||||||
|
|
||||||
|
return zeroToThreeSixty;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPosition = (angle, positions, position, noCorners=false) => {
|
||||||
|
const positionIndex = noCorners ?
|
||||||
|
Math.round(angle/90)*2 : Math.round(angle/45);
|
||||||
|
const offsetPosition = positions[positionIndex];
|
||||||
|
return {
|
||||||
|
id: offsetPosition.id,
|
||||||
|
x: position.x + offsetPosition.x,
|
||||||
|
y: position.y + offsetPosition.y
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPositions = (halfWidth, halfHeight, halfCorner=0) => ([{
|
||||||
|
id: 'r',
|
||||||
|
x: halfWidth,
|
||||||
|
y: 0
|
||||||
|
}, {
|
||||||
|
id: 'br',
|
||||||
|
x: halfWidth - halfCorner,
|
||||||
|
y: halfHeight - halfCorner
|
||||||
|
}, {
|
||||||
|
id: 'b',
|
||||||
|
x: 0,
|
||||||
|
y: halfHeight
|
||||||
|
}, {
|
||||||
|
id: 'bl',
|
||||||
|
x: -halfWidth + halfCorner,
|
||||||
|
y: halfHeight - halfCorner
|
||||||
|
}, {
|
||||||
|
id: 'l',
|
||||||
|
x: -halfWidth,
|
||||||
|
y: 0
|
||||||
|
}, {
|
||||||
|
id: 'tl',
|
||||||
|
x: -halfWidth + halfCorner,
|
||||||
|
y: -halfHeight + halfCorner
|
||||||
|
}, {
|
||||||
|
id: 't',
|
||||||
|
x: 0,
|
||||||
|
y: -halfHeight
|
||||||
|
}, {
|
||||||
|
id: 'tr',
|
||||||
|
x: halfWidth - halfCorner,
|
||||||
|
y: -halfHeight + halfCorner
|
||||||
|
},{
|
||||||
|
id: 'r',
|
||||||
|
x: halfWidth,
|
||||||
|
y: 0
|
||||||
|
}]);
|
||||||
|
|
||||||
|
const GraphLink = ({
|
||||||
|
data,
|
||||||
|
index,
|
||||||
|
nodeSize
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const {
|
||||||
|
source,
|
||||||
|
target
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
// actually, this will need to be got dynamically, in case them things are different sizes
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
} = nodeSize;
|
||||||
|
|
||||||
|
const halfWidth = width/2;
|
||||||
|
const halfHeight = height/2;
|
||||||
|
const halfCorner = 2;
|
||||||
|
|
||||||
|
const positions = getPositions(halfWidth, halfHeight, halfCorner);
|
||||||
|
const sourceAngle = getAngleFromPoints(source, target);
|
||||||
|
const sourcePosition = getPosition(sourceAngle, positions, source);
|
||||||
|
const targetAngle = getAngleFromPoints(target, source);
|
||||||
|
const targetPosition = getPosition(targetAngle, positions, target, true);
|
||||||
|
const arrowAngle = getAngleFromPoints(sourcePosition, targetPosition);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g>
|
||||||
|
<StyledLine
|
||||||
|
x1={sourcePosition.x}
|
||||||
|
x2={targetPosition.x}
|
||||||
|
y1={sourcePosition.y}
|
||||||
|
y2={targetPosition.y}
|
||||||
|
/>
|
||||||
|
<g
|
||||||
|
transform={
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
`translate(${targetPosition.x}, ${targetPosition.y}) rotate(${arrowAngle})`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledCircle
|
||||||
|
cx={0}
|
||||||
|
cy={0}
|
||||||
|
r={9}
|
||||||
|
/>
|
||||||
|
<StyledArrow
|
||||||
|
x1={-1}
|
||||||
|
x2={2}
|
||||||
|
y1={-3}
|
||||||
|
y2={0}
|
||||||
|
/>
|
||||||
|
<StyledArrow
|
||||||
|
x1={-1}
|
||||||
|
x2={2}
|
||||||
|
y1={3}
|
||||||
|
y2={0}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
GraphLink.propTypes = {
|
||||||
|
data: React.PropTypes.object.isRequired,
|
||||||
|
index: React.PropTypes.number.isRequired,
|
||||||
|
nodeSize: PropTypes.Size
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = GraphLink;
|
57
ui/src/components/topology/graph-node-button.js
Normal file
57
ui/src/components/topology/graph-node-button.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const Styled = require('styled-components');
|
||||||
|
|
||||||
|
const {
|
||||||
|
default: styled
|
||||||
|
} = Styled;
|
||||||
|
|
||||||
|
const StyledButton = styled.rect`
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledButtonCircle = styled.circle`
|
||||||
|
fill: white;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GraphNodeButton = ({
|
||||||
|
buttonRect,
|
||||||
|
onButtonClick
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const buttonCircleRadius = 2;
|
||||||
|
const buttonCircleSpacing = 2;
|
||||||
|
const buttonCircleY =
|
||||||
|
(buttonRect.height - buttonCircleRadius*4 - buttonCircleSpacing*2)/2;
|
||||||
|
const buttonCircles = [1,2,3].map((item, index) => (
|
||||||
|
<StyledButtonCircle
|
||||||
|
cx={buttonRect.width/2}
|
||||||
|
cy={buttonCircleY + (buttonCircleRadius*2 + buttonCircleSpacing)*index}
|
||||||
|
key={index}
|
||||||
|
r={2}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g transform={`translate(${buttonRect.x}, ${buttonRect.y})`}>
|
||||||
|
<StyledButton
|
||||||
|
height={buttonRect.height}
|
||||||
|
onClick={onButtonClick}
|
||||||
|
width={buttonRect.width}
|
||||||
|
/>
|
||||||
|
{buttonCircles}
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
GraphNodeButton.propTypes = {
|
||||||
|
buttonRect: React.PropTypes.shape({
|
||||||
|
x: React.PropTypes.number,
|
||||||
|
y: React.PropTypes.number,
|
||||||
|
width: React.PropTypes.number,
|
||||||
|
height: React.PropTypes.number
|
||||||
|
}).isRequired,
|
||||||
|
onButtonClick: React.PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = GraphNodeButton;
|
55
ui/src/components/topology/graph-node-info.js
Normal file
55
ui/src/components/topology/graph-node-info.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const Styled = require('styled-components');
|
||||||
|
const DataCentresIcon =
|
||||||
|
require(
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
'!babel-loader!svg-react-loader!./icon-data-centers.svg?name=DataCentresIcon'
|
||||||
|
);
|
||||||
|
const InstancesIcon =
|
||||||
|
require(
|
||||||
|
'!babel-loader!svg-react-loader!./icon-instances.svg?name=InstancesIcon'
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
default: styled
|
||||||
|
} = Styled;
|
||||||
|
|
||||||
|
const StyledText = styled.text`
|
||||||
|
fill: white;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GraphNodeInfo = ({
|
||||||
|
attrs,
|
||||||
|
infoPosition
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const {
|
||||||
|
dcs,
|
||||||
|
instances
|
||||||
|
} = attrs;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g transform={`translate(${infoPosition.x}, ${infoPosition.y})`}>
|
||||||
|
<DataCentresIcon />
|
||||||
|
<StyledText>{`${dcs} inst.`}</StyledText>
|
||||||
|
<InstancesIcon />
|
||||||
|
<StyledText>{`${instances} DCs`}</StyledText>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
GraphNodeInfo.propTypes = {
|
||||||
|
attrs: React.PropTypes.shape({
|
||||||
|
dcs: React.PropTypes.number,
|
||||||
|
instances: React.PropTypes.number,
|
||||||
|
healthy: React.PropTypes.bool
|
||||||
|
}),
|
||||||
|
infoPosition: React.PropTypes.shape({
|
||||||
|
x: React.PropTypes.number,
|
||||||
|
y: React.PropTypes.number
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = GraphNodeInfo;
|
48
ui/src/components/topology/graph-node-metrics.js
Normal file
48
ui/src/components/topology/graph-node-metrics.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const Styled = require('styled-components');
|
||||||
|
|
||||||
|
const {
|
||||||
|
default: styled
|
||||||
|
} = Styled;
|
||||||
|
|
||||||
|
const StyledText = styled.text`
|
||||||
|
fill: white;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GraphNodeMetrics = ({
|
||||||
|
metrics,
|
||||||
|
metricsPosition
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const metricSpacing = 18;
|
||||||
|
const metricsText = metrics.map((metric, index) => (
|
||||||
|
<StyledText
|
||||||
|
key={index}
|
||||||
|
x={0}
|
||||||
|
y={12 + metricSpacing*index}
|
||||||
|
>
|
||||||
|
{`${metric.name}: ${metric.stat}`}
|
||||||
|
</StyledText>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g transform={`translate(${metricsPosition.x}, ${metricsPosition.y})`}>
|
||||||
|
{metricsText}
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
GraphNodeMetrics.propTypes = {
|
||||||
|
metrics: React.PropTypes.arrayOf(React.PropTypes.shape({
|
||||||
|
name: React.PropTypes.string,
|
||||||
|
stat: React.PropTypes.string
|
||||||
|
})),
|
||||||
|
metricsPosition: React.PropTypes.shape({
|
||||||
|
x: React.PropTypes.number,
|
||||||
|
y: React.PropTypes.number
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = GraphNodeMetrics;
|
130
ui/src/components/topology/graph-node.js
Normal file
130
ui/src/components/topology/graph-node.js
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const Styled = require('styled-components');
|
||||||
|
const PropTypes = require('./prop-types');
|
||||||
|
const GraphNodeButton = require('./graph-node-button');
|
||||||
|
const GraphNodeMetrics = require('./graph-node-metrics');
|
||||||
|
const HeartIcon =
|
||||||
|
require(
|
||||||
|
'!babel-loader!svg-react-loader!./icon-heart.svg?name=HeartIcon'
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
default: styled
|
||||||
|
} = Styled;
|
||||||
|
|
||||||
|
const StyledRect = styled.rect`
|
||||||
|
stroke: #343434;
|
||||||
|
fill: #464646;
|
||||||
|
stroke-width: 1.5;
|
||||||
|
rx: 4;
|
||||||
|
ry: 4;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledShadowRect = styled.rect`
|
||||||
|
fill: #464646;
|
||||||
|
opacity: 0.33;
|
||||||
|
rx: 4;
|
||||||
|
ry: 4;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledLine = styled.line`
|
||||||
|
stroke: #343434;
|
||||||
|
stroke-width: 1.5;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledText = styled.text`
|
||||||
|
fill: white;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HeartCircle = styled.circle`
|
||||||
|
fill: #00af66;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GraphNode = ({
|
||||||
|
data,
|
||||||
|
size
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
} = size;
|
||||||
|
|
||||||
|
const halfWidth = width/2;
|
||||||
|
const halfHeight = height/2;
|
||||||
|
const lineY = 48 - halfHeight;
|
||||||
|
const lineX = 140 - halfWidth;
|
||||||
|
const buttonRect = {
|
||||||
|
x: lineX,
|
||||||
|
y: -halfHeight,
|
||||||
|
width: width - 140,
|
||||||
|
height: 48
|
||||||
|
};
|
||||||
|
|
||||||
|
const onButtonClick = (evt) => {
|
||||||
|
// console.log('Rect clicked!!!');
|
||||||
|
};
|
||||||
|
|
||||||
|
const paddingLeft = 18-halfWidth;
|
||||||
|
const metricsPosition = {
|
||||||
|
x: paddingLeft,
|
||||||
|
y: 89 - halfHeight
|
||||||
|
};
|
||||||
|
|
||||||
|
// const titleBbBox = {x:100, y: 30 - halfHeight};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g transform={`translate(${data.x}, ${data.y})`}>
|
||||||
|
<StyledShadowRect
|
||||||
|
x={-halfWidth}
|
||||||
|
y={3-halfHeight}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
/>
|
||||||
|
<StyledRect
|
||||||
|
x={-halfWidth}
|
||||||
|
y={-halfHeight}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
/>
|
||||||
|
<StyledLine
|
||||||
|
x1={-halfWidth}
|
||||||
|
y1={lineY}
|
||||||
|
x2={halfWidth}
|
||||||
|
y2={lineY}
|
||||||
|
/>
|
||||||
|
<StyledLine
|
||||||
|
x1={lineX}
|
||||||
|
y1={-halfHeight}
|
||||||
|
x2={lineX}
|
||||||
|
y2={lineY}
|
||||||
|
/>
|
||||||
|
<StyledText x={paddingLeft} y={30 - halfHeight}>{data.id}</StyledText>
|
||||||
|
<g transform={`translate(${25}, ${15 - halfHeight})`}>
|
||||||
|
<HeartCircle
|
||||||
|
cx={9}
|
||||||
|
cy={9}
|
||||||
|
r={9}
|
||||||
|
/>
|
||||||
|
<HeartIcon />
|
||||||
|
</g>
|
||||||
|
<GraphNodeButton
|
||||||
|
buttonRect={buttonRect}
|
||||||
|
onButtonClick={onButtonClick}
|
||||||
|
/>
|
||||||
|
<GraphNodeMetrics
|
||||||
|
metrics={data.metrics}
|
||||||
|
metricsPosition={metricsPosition}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
GraphNode.propTypes = {
|
||||||
|
data: React.PropTypes.object.isRequired,
|
||||||
|
size: PropTypes.Size,
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = GraphNode;
|
20
ui/src/components/topology/icon-data-centers.svg
Normal file
20
ui/src/components/topology/icon-data-centers.svg
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="9px" height="13px" viewBox="0 0 9 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>icon: data center</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Project:-topology-of-services" transform="translate(-575.000000, -487.000000)" fill="#FFFFFF">
|
||||||
|
<g id="services-copy" transform="translate(213.000000, 426.000000)">
|
||||||
|
<g id="service:-nginx" transform="translate(263.000000, 0.000000)">
|
||||||
|
<g id="metric">
|
||||||
|
<g id="data-centers-&-instanecs" transform="translate(18.000000, 59.000000)">
|
||||||
|
<path d="M81,15 L90,15 L90,2 L81,2 L81,15 Z M83,13 L88,13 L88,4 L83,4 L83,13 Z M84,6 L87.001,6 L87.001,5 L84,5 L84,6 Z M84,8 L87.001,8 L87.001,7 L84,7 L84,8 Z M84,10 L87.001,10 L87.001,9 L84,9 L84,10 Z" id="icon:--data-center"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
18
ui/src/components/topology/icon-heart.svg
Normal file
18
ui/src/components/topology/icon-heart.svg
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="80px" height="70px" viewBox="0 0 80 70" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>icon: health</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Project:-topology-of-services" transform="translate(0, 0)" fill="#FFFFFF">
|
||||||
|
<g id="services-copy" transform="translate(0, 0)">
|
||||||
|
<g id="service:-nginx" transform="translate(0, 0)">
|
||||||
|
<g id="icon:-state" transform="translate(0, 0)">
|
||||||
|
<path d="M9.47745233,6.60270759 L8.95496861,7.04565311 L8.51133742,6.60270759 C7.70841297,5.79909747 6.40563205,5.79909747 5.60270759,6.60270759 C4.79909747,7.40631772 4.79909747,8.70841297 5.60270759,9.5120231 L8.95496861,12.8642841 L12.3668833,9.5120231 C13.1698077,8.70841297 13.2301471,7.40631772 12.4265369,6.60270759 C11.6229268,5.79909747 10.2810625,5.79909747 9.47745233,6.60270759 Z" id="icon:-health"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
20
ui/src/components/topology/icon-instances.svg
Normal file
20
ui/src/components/topology/icon-instances.svg
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="18px" height="9px" viewBox="0 0 18 9" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>icon: instances</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Project:-topology-of-services" transform="translate(-494.000000, -489.000000)" fill="#FFFFFF">
|
||||||
|
<g id="services-copy" transform="translate(213.000000, 426.000000)">
|
||||||
|
<g id="service:-nginx" transform="translate(263.000000, 0.000000)">
|
||||||
|
<g id="metric">
|
||||||
|
<g id="data-centers-&-instanecs" transform="translate(18.000000, 59.000000)">
|
||||||
|
<path d="M4.5,4 C2.015,4 0,6.015 0,8.5 C0,10.985 2.015,13 4.5,13 C6.985,13 9,10.985 9,8.5 C9,6.015 6.985,4 4.5,4 M13.0909091,4 C12.7145455,4 12.3512727,4.047 12,4.12 C14.184,4.576 15.8181818,6.359 15.8181818,8.5 C15.8181818,10.641 14.184,12.424 12,12.88 C12.3512727,12.953 12.7145455,13 13.0909091,13 C15.8018182,13 18,10.985 18,8.5 C18,6.015 15.8018182,4 13.0909091,4 M14,8.5 C14,10.985 11.8018182,13 9.09090909,13 C8.71454545,13 8.35127273,12.953 8,12.88 C10.184,12.424 11.8181818,10.641 11.8181818,8.5 C11.8181818,6.359 10.184,4.576 8,4.12 C8.35127273,4.047 8.71454545,4 9.09090909,4 C11.8018182,4 14,6.015 14,8.5" id="icon:-instances"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
@ -1,425 +1,7 @@
|
|||||||
const constants = require('../../shared/constants');
|
module.exports = {
|
||||||
const d3 = require('d3');
|
TopologyGraph: require('./topology-graph'),
|
||||||
const fns = require('../../shared/functions');
|
TopologyGraphNode: require('./graph-node'),
|
||||||
const React = require('react');
|
TopologyGraphLink: require('./graph-link'),
|
||||||
const Styled = require('styled-components');
|
TopologyGraphNodeButton: require('./graph-node-button'),
|
||||||
|
TopologyGraphNodeMetrics: require('./graph-node-metrics'),
|
||||||
const {
|
|
||||||
colors
|
|
||||||
} = constants;
|
|
||||||
|
|
||||||
const {
|
|
||||||
remcalc
|
|
||||||
} = fns;
|
|
||||||
|
|
||||||
const {
|
|
||||||
default: styled
|
|
||||||
} = Styled;
|
|
||||||
|
|
||||||
/* eslint-disable */
|
|
||||||
function rightRoundedRect(x, y, width, height, radius) {
|
|
||||||
return 'M' + x + ',' + y // Move to top left (absolute)
|
|
||||||
+ 'h ' + (width - 2 * radius) // Horizontal line to (relative)
|
|
||||||
+ 'a ' + radius + ',' + radius + ' 0 0 1 ' + radius + ',' + radius // Relative arc
|
|
||||||
+ 'v ' + (height - 2 * radius) // Vertical line to (relative)
|
|
||||||
+ 'a ' + radius + ',' + radius + ' 0 0 1 ' + -radius + ',' + radius // Relative arch
|
|
||||||
+ 'h ' + (2 * radius - width) // Horizontal lint to (relative)
|
|
||||||
+ 'z '; // path back to start
|
|
||||||
}
|
|
||||||
/* eslint-enable */
|
|
||||||
|
|
||||||
/* eslint-disable */
|
|
||||||
function leftRoundedRect(x, y, width, height, radius) {
|
|
||||||
return 'M' + (x + width) + ',' + y // Move to (absolute) start at top-right
|
|
||||||
+ 'v ' + height // Vertical line to (relative)
|
|
||||||
+ 'h ' + (2 * radius - width) // Horizontal line to (relative)
|
|
||||||
+ 'a ' + radius + ',' + radius + ' 0 0 1 ' + -radius + ',' + -radius // Relative arc
|
|
||||||
+ 'v ' + -(height - 2 * radius) // Vertical line to (relative)
|
|
||||||
+ 'a ' + radius + ',' + radius + ' 0 0 1 ' + radius + ',' + -radius // Relative arch
|
|
||||||
+ 'z '; // path back to start
|
|
||||||
}
|
|
||||||
/* eslint-enable */
|
|
||||||
|
|
||||||
function topRoundedRect(x, y, width, height, radius) {
|
|
||||||
return 'M' + x + ',' + -(y - height) // Move to (absolute) start at bottom-left
|
|
||||||
+ 'v ' + -(height - radius) // Vertical line to (relative)
|
|
||||||
+ 'a ' + radius + ',' + radius + ' 0 0 1 ' + radius + ',' + -radius // Relative arc
|
|
||||||
+ 'h ' + -(2 * radius - width) // Horizontal line to (relative)
|
|
||||||
+ 'a ' + radius + ',' + radius + ' 0 0 1 ' + radius + ',' + radius // Relative arc
|
|
||||||
+ 'v ' + (height - radius) // Vertical line to (relative)
|
|
||||||
+ 'h ' + (2 * radius - width) // Horizontal line to (relative)
|
|
||||||
+ 'z '; // path back to start
|
|
||||||
}
|
|
||||||
|
|
||||||
function bottomRoundedRect(x, y, width, height, radius) {
|
|
||||||
return 'M' + x + ',' + -(y - (height - 2 * radius)) // Move to (absolute) start at bottom-right
|
|
||||||
+ 'v ' + -(height - 2 * radius) // Vertical line to (relative)
|
|
||||||
+ 'h ' + (width) // Horizontal line to (relative)
|
|
||||||
+ 'v ' + (height - 2 * radius) // Vertical line to (relative)
|
|
||||||
+ 'a ' + -radius + ',' + radius + ' 0 0 1 ' + -radius + ',' + radius // Relative arc
|
|
||||||
+ 'h ' + (2 * radius - width) // Horizontal line to (relative)
|
|
||||||
+ 'a ' + radius + ',' + radius + ' 0 0 1 ' + -radius + ',' + -radius // Relative arc
|
|
||||||
+ 'z '; // path back to start
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-disable */
|
|
||||||
function rect(x, y, width, height) {
|
|
||||||
return 'M' + x + ',' + -(y - height) // Move to (absolute) start at bottom-right
|
|
||||||
+ 'v ' + -(height) // Vertical line to (relative)
|
|
||||||
+ 'h ' + width // Horizontal line to (relative)
|
|
||||||
+ 'v ' + height // Vertical line to (relative)
|
|
||||||
+ 'h ' + -(width) // Horizontal line to (relative)
|
|
||||||
+ 'z '; // path back to start
|
|
||||||
}
|
|
||||||
/* eslint-enable */
|
|
||||||
|
|
||||||
const StyledSVGContainer = styled.svg`
|
|
||||||
& {
|
|
||||||
.links line {
|
|
||||||
stroke: #343434;
|
|
||||||
stroke-opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.health, .health_warn {
|
|
||||||
font-family: "LibreFranklin";
|
|
||||||
font-size: ${remcalc(12)};
|
|
||||||
font-weight: bold;
|
|
||||||
font-style: normal;
|
|
||||||
font-stretch: normal;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.health_warn {
|
|
||||||
font-size: ${remcalc(15)};
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat {
|
|
||||||
font-family: "LibreFranklin";
|
|
||||||
font-size: ${remcalc(12)};
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
font-stretch: normal;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node_statistics {
|
|
||||||
font-family: "LibreFranklin";
|
|
||||||
font-size: ${remcalc(12)};
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
font-stretch: normal;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node_statistics p {
|
|
||||||
margin: 0 0 0 0;
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary, .secondary {
|
|
||||||
font-family: "LibreFranklin";
|
|
||||||
font-size: ${remcalc(12)};
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
font-stretch: normal;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info_text {
|
|
||||||
font-family: "LibreFranklin";
|
|
||||||
font-size: ${remcalc(16)};
|
|
||||||
font-weight: 600;
|
|
||||||
font-style: normal;
|
|
||||||
font-stretch: normal;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
class TopologyGraph extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.svg = null;
|
|
||||||
|
|
||||||
const {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
this.simulation = d3.forceSimulation()
|
|
||||||
.force('charge', d3.forceManyBody()
|
|
||||||
.strength(() => -50)
|
|
||||||
.distanceMin(() => 30))
|
|
||||||
.force('link', d3.forceLink().distance(() => 200).id((d) => d.id))
|
|
||||||
// TODO manually handle looking for collisions in the tick, we then get the BBox
|
|
||||||
// and keep moving things for a while to try to get a fit.
|
|
||||||
.force('collide',
|
|
||||||
d3.forceCollide().radius((d) => 220 + 0.5).iterations(15))
|
|
||||||
.force('center', d3.forceCenter(width * 1/3, height * 1/3));
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const component = this;
|
|
||||||
|
|
||||||
const {
|
|
||||||
simulation,
|
|
||||||
} = this;
|
|
||||||
|
|
||||||
const svg = d3.select(this._refs.svg);
|
|
||||||
const {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
graph = {
|
|
||||||
nodes: [],
|
|
||||||
links: []
|
|
||||||
},
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
// Drawing the links between nodes
|
|
||||||
const link = svg.append('g')
|
|
||||||
.attr('class', 'links')
|
|
||||||
.selectAll('line')
|
|
||||||
.data(graph.links)
|
|
||||||
.enter().append('line')
|
|
||||||
.attr('stroke-width', remcalc(12));
|
|
||||||
|
|
||||||
// And svg group, to contain all of the attributes in @antonas' first prototype
|
|
||||||
svg.selectAll('.node')
|
|
||||||
.data(graph.nodes)
|
|
||||||
.enter()
|
|
||||||
.append('g')
|
|
||||||
.attr('class', 'node_group');
|
|
||||||
|
|
||||||
svg.selectAll('.node_group').each(function(d) {
|
|
||||||
// Create different type of node for services with Primaries + Secondaries
|
|
||||||
// We could extend this further to allow us to have as many nested services
|
|
||||||
// as wanted.
|
|
||||||
// TODO handle this per prop
|
|
||||||
// if (d.id === 'Percona') {
|
|
||||||
// createExtendedNode(d3.select(this));
|
|
||||||
// } else {
|
|
||||||
component.createServiceNodes(d, d3.select(this));
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
|
|
||||||
simulation
|
|
||||||
.nodes(graph.nodes)
|
|
||||||
.on('tick', ticked);
|
|
||||||
|
|
||||||
simulation.force('link')
|
|
||||||
.links(graph.links);
|
|
||||||
|
|
||||||
function contrain(dimension, r, z) {
|
|
||||||
return Math.max(0, Math.min(dimension - r, z));
|
|
||||||
}
|
|
||||||
|
|
||||||
function ticked() {
|
|
||||||
// TODO: Think of a common way of extracting the bounding boxes for each
|
|
||||||
// item and to grab the x{1,2} and y{1,2} values.
|
|
||||||
link
|
|
||||||
.attr('x1', function(d) {
|
|
||||||
let x;
|
|
||||||
svg.selectAll('.node_group').each(function(_, i) {
|
|
||||||
if (i !== d.source.index) return;
|
|
||||||
x = d3.select(this).node().getBBox().width;
|
|
||||||
});
|
|
||||||
return contrain(width, x, d.source.x) + 80;
|
|
||||||
})
|
|
||||||
.attr('y1', function(d) {
|
|
||||||
let y;
|
|
||||||
svg.selectAll('.node_group').each(function(_, i) {
|
|
||||||
if (i !== d.source.index) return;
|
|
||||||
y = d3.select(this).node().getBBox().height;
|
|
||||||
});
|
|
||||||
return contrain(height, y, d.source.y) + 24;
|
|
||||||
})
|
|
||||||
.attr('x2', function(d) {
|
|
||||||
let x;
|
|
||||||
svg.selectAll('.node_group').each(function(_, i) {
|
|
||||||
if (i !== d.target.index) return;
|
|
||||||
x = d3.select(this).node().getBBox().width;
|
|
||||||
});
|
|
||||||
return contrain(width, x, d.target.x) + 80;
|
|
||||||
})
|
|
||||||
.attr('y2', function(d) {
|
|
||||||
let y;
|
|
||||||
svg.selectAll('.node_group').each(function(_, i) {
|
|
||||||
if (i !== d.target.index) return;
|
|
||||||
y = d3.select(this).node().getBBox().height;
|
|
||||||
});
|
|
||||||
return contrain(height, y, d.target.y) + 24;
|
|
||||||
});
|
|
||||||
|
|
||||||
svg.selectAll('.node_group')
|
|
||||||
.attr('transform', function(d) {
|
|
||||||
const x = d3.select(this).node().getBBox().width;
|
|
||||||
const y = d3.select(this).node().getBBox().height;
|
|
||||||
return 'translate(' + contrain(width, x, d.x) + ','
|
|
||||||
+ contrain(height, y, d.y) + ')';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createHealthCheckBadge(element, x, y) {
|
|
||||||
const paddingLeft = 30;
|
|
||||||
const health = element.append('g');
|
|
||||||
|
|
||||||
// TODO: replace these element with the designed SVG elements from
|
|
||||||
// @antonasdeduchovas' designs with full svg elements.
|
|
||||||
|
|
||||||
health.append('circle')
|
|
||||||
.attr('class', 'alert')
|
|
||||||
.attr('cx', function() {
|
|
||||||
return element
|
|
||||||
.node()
|
|
||||||
.getBBox()
|
|
||||||
.width + paddingLeft;
|
|
||||||
})
|
|
||||||
.attr('cy', '24')
|
|
||||||
.attr('stroke-width', 0)
|
|
||||||
.attr('fill', (d) =>
|
|
||||||
d.id === 'Memcached' ? 'rgb(217, 77, 68)' : 'rgb(0,175,102)')
|
|
||||||
.attr('r', remcalc(9));
|
|
||||||
|
|
||||||
// An icon or label that exists within the circle, inside the infobox
|
|
||||||
health.append('text')
|
|
||||||
.attr('class', 'health')
|
|
||||||
.attr('x', function() {
|
|
||||||
return element
|
|
||||||
.node()
|
|
||||||
.getBBox()
|
|
||||||
.width + 3;
|
|
||||||
})
|
|
||||||
.attr('y', '29')
|
|
||||||
.attr('text-anchor', 'middle')
|
|
||||||
.attr('fill', colors.brandPrimaryColor)
|
|
||||||
.text((d) => d.id === 'Memcached' ? '!' : '❤');
|
|
||||||
}
|
|
||||||
|
|
||||||
createServiceNodeBody(data, element, d) {
|
|
||||||
const stats = element.append('g');
|
|
||||||
stats.append('path')
|
|
||||||
.attr('class', 'node')
|
|
||||||
.attr('d', d)
|
|
||||||
.attr('stroke', '#343434')
|
|
||||||
.attr('stroke-width', remcalc(1))
|
|
||||||
.attr('fill', '#464646');
|
|
||||||
|
|
||||||
const html = stats
|
|
||||||
.append('switch')
|
|
||||||
.append('foreignObject')
|
|
||||||
.attr('requiredFeatures',
|
|
||||||
'http://www.w3.org/TR/SVG11/feature#Extensibility')
|
|
||||||
.attr('x', 12)
|
|
||||||
.attr('y', 57)
|
|
||||||
.attr('width', 160)
|
|
||||||
.attr('height', 70)
|
|
||||||
// From here everything will be rendered with react using a ref.
|
|
||||||
// However for now these values are hard-coded.
|
|
||||||
.append('xhtml:div')
|
|
||||||
.attr('class', 'node_statistics');
|
|
||||||
// Remove with react + dyanmic data.
|
|
||||||
|
|
||||||
html.selectAll('.node_statistics').data(data.metrics).enter()
|
|
||||||
.append('p')
|
|
||||||
.text((d) => `${d.name}: ${d.stat}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
createServiceNodes(data, elm) {
|
|
||||||
const component = this;
|
|
||||||
|
|
||||||
const {
|
|
||||||
dragged,
|
|
||||||
dragstarted,
|
|
||||||
dragended,
|
|
||||||
} = this;
|
|
||||||
|
|
||||||
const width = 170;
|
|
||||||
const topHeight = 47;
|
|
||||||
const radius = 4;
|
|
||||||
|
|
||||||
// Box where label will live
|
|
||||||
elm.append('path')
|
|
||||||
.attr('class', 'node')
|
|
||||||
.attr('d', topRoundedRect('0', '0', width, topHeight, radius))
|
|
||||||
.attr('stroke', colors.topologyBackground)
|
|
||||||
.attr('stroke-width', remcalc(1))
|
|
||||||
.attr('fill', colors.brandSecondaryColor);
|
|
||||||
|
|
||||||
const text = elm.append('g');
|
|
||||||
|
|
||||||
text.append('text')
|
|
||||||
.attr('class', 'info_text')
|
|
||||||
.attr('x', '12')
|
|
||||||
.attr('y', '30')
|
|
||||||
.attr('text-anchor', 'start')
|
|
||||||
.attr('fill', colors.brandPrimaryColor)
|
|
||||||
.text(d => d.id);
|
|
||||||
|
|
||||||
// if (service is registered twice in the scheduler) {
|
|
||||||
// Do not show healthcheck in the header
|
|
||||||
// } else {
|
|
||||||
this.createHealthCheckBadge(text);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (service is registered twice in the scheduler) {
|
|
||||||
// this.createServiceNodeBody(data, elm, rect('0',`-${topHeight}`, width, 78, 4));
|
|
||||||
// } else {
|
|
||||||
this.createServiceNodeBody(data, elm,
|
|
||||||
bottomRoundedRect('0', `-${topHeight}`, width, 78, 4));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// <==== END ====>
|
|
||||||
|
|
||||||
// Set up movement for service nodes
|
|
||||||
elm.call(d3.drag()
|
|
||||||
.on('start', dragstarted.bind(component))
|
|
||||||
.on('drag', dragged.bind(component))
|
|
||||||
.on('end', dragended.bind(component)));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
dragstarted(d) {
|
|
||||||
if (!d3.event.active) this.simulation.alphaTarget(0.3).restart();
|
|
||||||
d.fx = d.x;
|
|
||||||
d.fy = d.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
dragged(d) {
|
|
||||||
d.fx = d3.event.x;
|
|
||||||
d.fy = d3.event.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
dragended(d) {
|
|
||||||
if (!d3.event.active) this.simulation.alphaTarget(0);
|
|
||||||
d.fx = null;
|
|
||||||
d.fy = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ref(name) {
|
|
||||||
this._refs = this._refs || {};
|
|
||||||
|
|
||||||
return (el) => {
|
|
||||||
this._refs[name] = el;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<StyledSVGContainer
|
|
||||||
innerRef={this.ref('svg')}
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
TopologyGraph.propTypes = {
|
|
||||||
graph: React.PropTypes.object,
|
|
||||||
height: React.PropTypes.number,
|
|
||||||
width: React.PropTypes.number,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = TopologyGraph;
|
|
||||||
|
30
ui/src/components/topology/prop-types.js
Normal file
30
ui/src/components/topology/prop-types.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
const React = require('react');
|
||||||
|
|
||||||
|
const p = {
|
||||||
|
x: React.PropTypes.number.isRequired,
|
||||||
|
y: React.PropTypes.number.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const s = {
|
||||||
|
width: React.PropTypes.number,
|
||||||
|
height: React.PropTypes.number
|
||||||
|
};
|
||||||
|
|
||||||
|
const Point = React.PropTypes.shape({
|
||||||
|
...p
|
||||||
|
});
|
||||||
|
|
||||||
|
const Size = React.PropTypes.shape({
|
||||||
|
...s
|
||||||
|
});
|
||||||
|
|
||||||
|
const Rect = React.PropTypes.shape({
|
||||||
|
...p,
|
||||||
|
...s
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Point,
|
||||||
|
Rect,
|
||||||
|
Size
|
||||||
|
};
|
109
ui/src/components/topology/story-helper.js
Normal file
109
ui/src/components/topology/story-helper.js
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const Styled = require('styled-components');
|
||||||
|
const Input = require('../input');
|
||||||
|
const Select = require('../select');
|
||||||
|
const Topology = require('./');
|
||||||
|
const data = require('./data');
|
||||||
|
|
||||||
|
const {
|
||||||
|
default: styled
|
||||||
|
} = Styled;
|
||||||
|
|
||||||
|
const {
|
||||||
|
TopologyGraph
|
||||||
|
} = Topology;
|
||||||
|
|
||||||
|
const StyledForm = styled.form`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin: 5px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const nodeSize = {
|
||||||
|
width: 180,
|
||||||
|
height: 156
|
||||||
|
};
|
||||||
|
|
||||||
|
class StoryHelper extends React.Component {
|
||||||
|
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
data: data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
|
||||||
|
const data = this.state.data;
|
||||||
|
|
||||||
|
const linkOptions = (nodes) => {
|
||||||
|
return nodes.map((node, index) => (
|
||||||
|
<option key={node.id} value={node.id}>{node.id}</option>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddService = (evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
|
||||||
|
const service = evt.target.service.value;
|
||||||
|
const target = evt.target.target.value;
|
||||||
|
const source = evt.target.source.value;
|
||||||
|
|
||||||
|
const links = [];
|
||||||
|
if(target) {
|
||||||
|
links.push({
|
||||||
|
target: target,
|
||||||
|
source: service
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if(source) {
|
||||||
|
links.push({
|
||||||
|
target: service,
|
||||||
|
source: source
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if(links.length) {
|
||||||
|
const data = this.state.data;
|
||||||
|
this.setState({
|
||||||
|
data: {
|
||||||
|
nodes: [
|
||||||
|
...data.nodes,
|
||||||
|
{
|
||||||
|
...data.nodes[0],
|
||||||
|
id: service
|
||||||
|
}
|
||||||
|
],
|
||||||
|
links: [
|
||||||
|
...data.links,
|
||||||
|
...links
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<StyledForm onSubmit={onAddService}>
|
||||||
|
<Input name='service' placeholder='Service name' />
|
||||||
|
<Select name='target'>
|
||||||
|
<option value=''>Select a service to link to (optional)</option>
|
||||||
|
{linkOptions(data.nodes)}
|
||||||
|
</Select>
|
||||||
|
<Select name='source'>
|
||||||
|
<option value=''>Select a service to link from (optional)</option>
|
||||||
|
{linkOptions(data.nodes)}
|
||||||
|
</Select>
|
||||||
|
<Input name='Add service' type='submit' />
|
||||||
|
</StyledForm>
|
||||||
|
<TopologyGraph data={data} nodeSize={nodeSize} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = StoryHelper;
|
@ -1,154 +1,11 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
const StoryHelper = require('./story-helper');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
storiesOf
|
storiesOf
|
||||||
} = require('@kadira/storybook');
|
} = require('@kadira/storybook');
|
||||||
|
|
||||||
const Base = require('../base');
|
|
||||||
const Topology = require('./');
|
|
||||||
const TopologyView = require('./view');
|
|
||||||
const services = {
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
id: 'Nginx',
|
|
||||||
attrs: {
|
|
||||||
dcs: 1,
|
|
||||||
instances: 2,
|
|
||||||
healthy: true,
|
|
||||||
},
|
|
||||||
metrics: [
|
|
||||||
{
|
|
||||||
name: 'CPU',
|
|
||||||
stat: '50%',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Memory',
|
|
||||||
stat: '20%',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Network',
|
|
||||||
stat: '5.9KB/sec',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'WordPress',
|
|
||||||
attrs: {
|
|
||||||
dcs: 1,
|
|
||||||
instances: 2,
|
|
||||||
healthy: true,
|
|
||||||
},
|
|
||||||
metrics: [
|
|
||||||
{
|
|
||||||
name: 'CPU',
|
|
||||||
stat: '50%',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Memory',
|
|
||||||
stat: '20%',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Network',
|
|
||||||
stat: '5.9KB/sec',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Memcached',
|
|
||||||
attrs: {
|
|
||||||
dcs: 1,
|
|
||||||
instances: 2,
|
|
||||||
healthy: true,
|
|
||||||
},
|
|
||||||
metrics: [
|
|
||||||
{
|
|
||||||
name: 'CPU',
|
|
||||||
stat: '50%',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Memory',
|
|
||||||
stat: '20%',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Network',
|
|
||||||
stat: '5.9KB/sec',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Percona',
|
|
||||||
attrs: {
|
|
||||||
dcs: 1,
|
|
||||||
instances: 2,
|
|
||||||
healthy: true,
|
|
||||||
},
|
|
||||||
metrics: [
|
|
||||||
{
|
|
||||||
name: 'CPU',
|
|
||||||
stat: '50%',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Memory',
|
|
||||||
stat: '20%',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Network',
|
|
||||||
stat: '5.9KB/sec',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'NFS',
|
|
||||||
attrs: {
|
|
||||||
dcs: 1,
|
|
||||||
instances: 2,
|
|
||||||
healthy: true,
|
|
||||||
},
|
|
||||||
metrics: [
|
|
||||||
{
|
|
||||||
name: 'CPU',
|
|
||||||
stat: '50%',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Memory',
|
|
||||||
stat: '20%',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Network',
|
|
||||||
stat: '5.9KB/sec',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
links: [
|
|
||||||
{
|
|
||||||
source: 'Nginx',
|
|
||||||
target: 'WordPress',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
source: 'WordPress',
|
|
||||||
target: 'Memcached',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
source: 'WordPress',
|
|
||||||
target: 'NFS',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
source: 'WordPress',
|
|
||||||
target: 'Percona',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
storiesOf('Topology', module)
|
storiesOf('Topology', module)
|
||||||
.add('5 services', () => (
|
.add('5 services', () => (
|
||||||
<Base>
|
<StoryHelper />
|
||||||
<TopologyView>
|
|
||||||
<Topology
|
|
||||||
graph={services}
|
|
||||||
height={500}
|
|
||||||
width={500}
|
|
||||||
/>
|
|
||||||
</TopologyView>
|
|
||||||
</Base>
|
|
||||||
));
|
));
|
||||||
|
144
ui/src/components/topology/topology-graph.js
Normal file
144
ui/src/components/topology/topology-graph.js
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const d3 = require('d3');
|
||||||
|
const Styled = require('styled-components');
|
||||||
|
const GraphNode = require('./graph-node');
|
||||||
|
const GraphLink = require('./graph-link');
|
||||||
|
|
||||||
|
const {
|
||||||
|
default: styled
|
||||||
|
} = Styled;
|
||||||
|
|
||||||
|
const StyledSvg = styled.svg`
|
||||||
|
width: 1024px;
|
||||||
|
height: 860px;
|
||||||
|
border: 1px solid #ff0000;
|
||||||
|
`;
|
||||||
|
|
||||||
|
/*const nodeSize = {
|
||||||
|
width: 180,
|
||||||
|
height: 156
|
||||||
|
};*/
|
||||||
|
|
||||||
|
const mapData = (data, withIndex=false) => {
|
||||||
|
return data.map((d, index) => {
|
||||||
|
const r = {
|
||||||
|
...d
|
||||||
|
};
|
||||||
|
if(withIndex) {
|
||||||
|
r.index = index;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
class TopologyGraph extends React.Component {
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
nodeSize
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
this.setState(
|
||||||
|
this.createSimulation(data, nodeSize)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
nodeSize
|
||||||
|
} = nextProps;
|
||||||
|
|
||||||
|
this.setState(
|
||||||
|
this.createSimulation(data, nodeSize)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
createSimulation(data, nodeSize) {
|
||||||
|
|
||||||
|
const dataNodes = mapData(data.nodes, true);
|
||||||
|
const dataLinks = mapData(data.links);
|
||||||
|
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
} = nodeSize;
|
||||||
|
const nodeRadius = Math.round(Math.sqrt(width*width + height*height)/2);
|
||||||
|
// const linkDistance = nodeRadius*2 + 20;
|
||||||
|
// console.log('nodeRadius = ', nodeRadius);
|
||||||
|
// console.log('linkDistance = ', linkDistance);
|
||||||
|
const simulation = d3.forceSimulation(dataNodes)
|
||||||
|
.force('charge', d3.forceManyBody())
|
||||||
|
.force('link', d3.forceLink(dataLinks)
|
||||||
|
/*.distance(() => linkDistance)*/
|
||||||
|
.id(d => d.id))
|
||||||
|
.force('collide', d3.forceCollide(nodeRadius))
|
||||||
|
.force('center', d3.forceCenter(1024/2, 860/2))
|
||||||
|
.on('tick', () => {
|
||||||
|
this.forceUpdate();
|
||||||
|
})
|
||||||
|
.on('end', () => {
|
||||||
|
// console.log('SIMULATION END');
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
dataNodes,
|
||||||
|
dataLinks,
|
||||||
|
simulation
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNodes(nodeSize) {
|
||||||
|
return this.state.dataNodes.map((n, index) => (
|
||||||
|
<GraphNode
|
||||||
|
key={index}
|
||||||
|
data={n}
|
||||||
|
index={index}
|
||||||
|
size={nodeSize}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLinks(nodeSize) {
|
||||||
|
return this.state.dataLinks.map((l, index) => (
|
||||||
|
<GraphLink
|
||||||
|
key={index}
|
||||||
|
data={l}
|
||||||
|
index={index}
|
||||||
|
nodeSize={nodeSize}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
nodeSize
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledSvg>
|
||||||
|
<g>
|
||||||
|
{this.renderNodes(nodeSize)}
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
{this.renderLinks(nodeSize)}
|
||||||
|
</g>
|
||||||
|
</StyledSvg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TopologyGraph.propTypes = {
|
||||||
|
data: React.PropTypes.shape({
|
||||||
|
nodes: React.PropTypes.array,
|
||||||
|
links: React.PropTypes.array
|
||||||
|
}),
|
||||||
|
nodeSize: React.PropTypes.shape({
|
||||||
|
width: React.PropTypes.number,
|
||||||
|
height: React.PropTypes.number
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = TopologyGraph;
|
Loading…
Reference in New Issue
Block a user