diff --git a/frontend/.eslintrc b/frontend/.eslintrc
index 1271c186..04619ce8 100644
--- a/frontend/.eslintrc
+++ b/frontend/.eslintrc
@@ -68,7 +68,6 @@
"react/jsx-no-duplicate-props": 2,
"react/jsx-no-target-blank": 2,
"react/jsx-pascal-case": 2,
- "react/jsx-sort-props": 2,
"react/jsx-space-before-closing": 2,
"react/jsx-wrap-multilines": 2,
"jsx-a11y/anchor-has-content": 2,
diff --git a/spikes/graphs-topology/d3/client/graph/graph-link.js b/spikes/graphs-topology/d3/client/graph/graph-link.js
index fbcad8b1..e18a4ef8 100644
--- a/spikes/graphs-topology/d3/client/graph/graph-link.js
+++ b/spikes/graphs-topology/d3/client/graph/graph-link.js
@@ -1,4 +1,4 @@
-const React = require('React');
+const React = require('react');
const Styled = require('styled-components');
const {
diff --git a/spikes/graphs-topology/d3/client/graph/graph-node-button.js b/spikes/graphs-topology/d3/client/graph/graph-node-button.js
index bfef16c6..35227d39 100644
--- a/spikes/graphs-topology/d3/client/graph/graph-node-button.js
+++ b/spikes/graphs-topology/d3/client/graph/graph-node-button.js
@@ -1,4 +1,4 @@
-const React = require('React');
+const React = require('react');
const Styled = require('styled-components');
const {
diff --git a/spikes/graphs-topology/d3/client/graph/graph-node-metrics.js b/spikes/graphs-topology/d3/client/graph/graph-node-metrics.js
index 538d1874..ec1afb6a 100644
--- a/spikes/graphs-topology/d3/client/graph/graph-node-metrics.js
+++ b/spikes/graphs-topology/d3/client/graph/graph-node-metrics.js
@@ -1,4 +1,4 @@
-const React = require('React');
+const React = require('react');
const Styled = require('styled-components');
const GraphNodeButton = require('./graph-node-button');
diff --git a/spikes/graphs-topology/d3/client/graph/graph-node.js b/spikes/graphs-topology/d3/client/graph/graph-node.js
index 0bbe9685..cb7d8e63 100644
--- a/spikes/graphs-topology/d3/client/graph/graph-node.js
+++ b/spikes/graphs-topology/d3/client/graph/graph-node.js
@@ -1,4 +1,4 @@
-const React = require('React');
+const React = require('react');
const Styled = require('styled-components');
const GraphNodeButton = require('./graph-node-button');
const GraphNodeMetrics = require('./graph-node-metrics');
diff --git a/spikes/graphs-topology/d3/client/topology.js b/spikes/graphs-topology/d3/client/topology.js
index a4575255..c84979ec 100644
--- a/spikes/graphs-topology/d3/client/topology.js
+++ b/spikes/graphs-topology/d3/client/topology.js
@@ -7,11 +7,6 @@ const {
default: styled
} = Styled;
-const StyledSvg = styled.svg`
- width: 1024px;
- height: 860px;
-`;
-
const StyledForm = styled.form`
margin: 20px;
`;
diff --git a/ui/.eslintrc b/ui/.eslintrc
index 9f1ef695..4d27b6dd 100644
--- a/ui/.eslintrc
+++ b/ui/.eslintrc
@@ -68,7 +68,6 @@
"react/jsx-no-duplicate-props": 2,
"react/jsx-no-target-blank": 2,
"react/jsx-pascal-case": 2,
- "react/jsx-sort-props": 2,
"react/jsx-space-before-closing": 2,
"react/jsx-wrap-multilines": 2,
"jsx-a11y/anchor-has-content": 2,
diff --git a/ui/src/components/select/index.js b/ui/src/components/select/index.js
index e4f514e5..475353d8 100644
--- a/ui/src/components/select/index.js
+++ b/ui/src/components/select/index.js
@@ -31,6 +31,7 @@ const Select = (props) => {
id = rndId(),
label = '',
multiple = false,
+ name = '',
placeholder = '',
value = defaultValue,
warning = ''
@@ -67,6 +68,7 @@ const Select = (props) => {
disabled={disabled}
id={id}
multiple={multiple}
+ name={name}
placeholder={placeholder}
value={_placeholder ? value : undefined}
>
@@ -84,6 +86,7 @@ Select.propTypes = {
id: React.PropTypes.string,
label: React.PropTypes.string,
multiple: React.PropTypes.bool,
+ name: React.PropTypes.string,
placeholder: React.PropTypes.string,
value: React.PropTypes.string,
warning: React.PropTypes.string
diff --git a/ui/src/components/topology-old/index.js b/ui/src/components/topology-old/index.js
new file mode 100644
index 00000000..c4f9d7d4
--- /dev/null
+++ b/ui/src/components/topology-old/index.js
@@ -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 (
+
+ );
+ }
+
+}
+
+TopologyGraph.propTypes = {
+ graph: React.PropTypes.object,
+ height: React.PropTypes.number,
+ width: React.PropTypes.number,
+};
+
+module.exports = TopologyGraph;
diff --git a/ui/src/components/topology-old/story.js b/ui/src/components/topology-old/story.js
new file mode 100644
index 00000000..f6710b78
--- /dev/null
+++ b/ui/src/components/topology-old/story.js
@@ -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', () => (
+
+
+
+
+
+ ));
diff --git a/ui/src/components/topology/view.js b/ui/src/components/topology-old/view.js
similarity index 100%
rename from ui/src/components/topology/view.js
rename to ui/src/components/topology-old/view.js
diff --git a/spikes/graphs-topology/d3/client/data.js b/ui/src/components/topology/data.js
similarity index 100%
rename from spikes/graphs-topology/d3/client/data.js
rename to ui/src/components/topology/data.js
diff --git a/ui/src/components/topology/graph-link.js b/ui/src/components/topology/graph-link.js
new file mode 100644
index 00000000..69c93646
--- /dev/null
+++ b/ui/src/components/topology/graph-link.js
@@ -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 (
+
+
+
+
+
+
+
+
+ );
+};
+
+GraphLink.propTypes = {
+ data: React.PropTypes.object.isRequired,
+ index: React.PropTypes.number.isRequired,
+ nodeSize: PropTypes.Size
+};
+
+module.exports = GraphLink;
diff --git a/ui/src/components/topology/graph-node-button.js b/ui/src/components/topology/graph-node-button.js
new file mode 100644
index 00000000..62679ed9
--- /dev/null
+++ b/ui/src/components/topology/graph-node-button.js
@@ -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) => (
+
+ ));
+
+ return (
+
+
+ {buttonCircles}
+
+ );
+};
+
+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;
diff --git a/ui/src/components/topology/graph-node-info.js b/ui/src/components/topology/graph-node-info.js
new file mode 100644
index 00000000..a715030e
--- /dev/null
+++ b/ui/src/components/topology/graph-node-info.js
@@ -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 (
+
+
+ {`${dcs} inst.`}
+
+ {`${instances} DCs`}
+
+ );
+};
+
+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;
diff --git a/ui/src/components/topology/graph-node-metrics.js b/ui/src/components/topology/graph-node-metrics.js
new file mode 100644
index 00000000..42e032a3
--- /dev/null
+++ b/ui/src/components/topology/graph-node-metrics.js
@@ -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) => (
+
+ {`${metric.name}: ${metric.stat}`}
+
+ ));
+
+ return (
+
+ {metricsText}
+
+ );
+};
+
+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;
diff --git a/ui/src/components/topology/graph-node.js b/ui/src/components/topology/graph-node.js
new file mode 100644
index 00000000..30d4ee25
--- /dev/null
+++ b/ui/src/components/topology/graph-node.js
@@ -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 (
+
+
+
+
+
+ {data.id}
+
+
+
+
+
+
+
+ );
+};
+
+GraphNode.propTypes = {
+ data: React.PropTypes.object.isRequired,
+ size: PropTypes.Size,
+};
+
+module.exports = GraphNode;
diff --git a/ui/src/components/topology/icon-data-centers.svg b/ui/src/components/topology/icon-data-centers.svg
new file mode 100644
index 00000000..4df77b7a
--- /dev/null
+++ b/ui/src/components/topology/icon-data-centers.svg
@@ -0,0 +1,20 @@
+
+
\ No newline at end of file
diff --git a/ui/src/components/topology/icon-heart.svg b/ui/src/components/topology/icon-heart.svg
new file mode 100644
index 00000000..3ff9b2a9
--- /dev/null
+++ b/ui/src/components/topology/icon-heart.svg
@@ -0,0 +1,18 @@
+
+
diff --git a/ui/src/components/topology/icon-instances.svg b/ui/src/components/topology/icon-instances.svg
new file mode 100644
index 00000000..ec89bb7e
--- /dev/null
+++ b/ui/src/components/topology/icon-instances.svg
@@ -0,0 +1,20 @@
+
+
\ No newline at end of file
diff --git a/ui/src/components/topology/index.js b/ui/src/components/topology/index.js
index c4f9d7d4..9cad64d2 100644
--- a/ui/src/components/topology/index.js
+++ b/ui/src/components/topology/index.js
@@ -1,425 +1,7 @@
-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 (
-
- );
- }
-
-}
-
-TopologyGraph.propTypes = {
- graph: React.PropTypes.object,
- height: React.PropTypes.number,
- width: React.PropTypes.number,
+module.exports = {
+ TopologyGraph: require('./topology-graph'),
+ TopologyGraphNode: require('./graph-node'),
+ TopologyGraphLink: require('./graph-link'),
+ TopologyGraphNodeButton: require('./graph-node-button'),
+ TopologyGraphNodeMetrics: require('./graph-node-metrics'),
};
-
-module.exports = TopologyGraph;
diff --git a/ui/src/components/topology/prop-types.js b/ui/src/components/topology/prop-types.js
new file mode 100644
index 00000000..ce854981
--- /dev/null
+++ b/ui/src/components/topology/prop-types.js
@@ -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
+};
diff --git a/ui/src/components/topology/story-helper.js b/ui/src/components/topology/story-helper.js
new file mode 100644
index 00000000..3775de36
--- /dev/null
+++ b/ui/src/components/topology/story-helper.js
@@ -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) => (
+
+ ));
+ };
+
+ 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 (
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+module.exports = StoryHelper;
diff --git a/ui/src/components/topology/story.js b/ui/src/components/topology/story.js
index f6710b78..0632f09f 100644
--- a/ui/src/components/topology/story.js
+++ b/ui/src/components/topology/story.js
@@ -1,154 +1,11 @@
const React = require('react');
+const StoryHelper = require('./story-helper');
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', () => (
-
-
-
-
-
+
));
diff --git a/ui/src/components/topology/topology-graph.js b/ui/src/components/topology/topology-graph.js
new file mode 100644
index 00000000..ac51e96e
--- /dev/null
+++ b/ui/src/components/topology/topology-graph.js
@@ -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) => (
+
+ ));
+ }
+
+ renderLinks(nodeSize) {
+ return this.state.dataLinks.map((l, index) => (
+
+ ));
+ }
+
+ render() {
+ const {
+ nodeSize
+ } = this.props;
+
+ return (
+
+
+ {this.renderNodes(nodeSize)}
+
+
+ {this.renderLinks(nodeSize)}
+
+
+ );
+ }
+}
+
+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;