diff --git a/frontend/src/containers/services/index.js b/frontend/src/containers/services/index.js index d480f0a6..ad70ee73 100644 --- a/frontend/src/containers/services/index.js +++ b/frontend/src/containers/services/index.js @@ -18,7 +18,8 @@ const { const { orgByIdSelector, projectByIdSelector, - servicesByProjectIdSelector + servicesByProjectIdSelector, + servicesForTopologySelector } = selectors; const { @@ -28,7 +29,8 @@ const { const Services = ({ org = {}, project = {}, - services = [] + services = [], + servicesForTopology = [] }) => { const empty = services.length ? null : ( @@ -60,7 +62,8 @@ const Services = ({ Services.propTypes = { org: PropTypes.org, project: PropTypes.project, - services: React.PropTypes.arrayOf(PropTypes.service) + services: React.PropTypes.arrayOf(PropTypes.service), + servicesForTopology: React.PropTypes.arrayOf(React.PropTypes.object) }; const mapStateToProps = (state, { @@ -70,7 +73,9 @@ const mapStateToProps = (state, { }) => ({ org: orgByIdSelector(match.params.org)(state), project: projectByIdSelector(match.params.projectId)(state), - services: servicesByProjectIdSelector(match.params.projectId)(state) + services: servicesByProjectIdSelector(match.params.projectId)(state), + servicesForTopology: + servicesForTopologySelector(match.params.projectId)(state) }); module.exports = connect( diff --git a/frontend/src/mock-state.json b/frontend/src/mock-state.json index 0678afd7..98acb3b3 100644 --- a/frontend/src/mock-state.json +++ b/frontend/src/mock-state.json @@ -642,7 +642,10 @@ }, { "type": "dca08514-72e5-46ce-ad92-e68b3b0914d4", "dataset": "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" - }] + }], + "connections": [ + "be227788-74f1-4e5b-a85f-b5c71cbae8d8" + ] }, { "uuid": "be227788-74f1-4e5b-a85f-b5c71cbae8d8", "id": "wordpress", @@ -658,7 +661,12 @@ }, { "type": "dca08514-72e5-46ce-ad92-e68b3b0914d4", "dataset": "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" - }] + }], + "connections": [ + "6a0eee76-c019-413b-9d5f-44712b55b993", + "6d31aff4-de1e-4042-a983-fbd23d5c530c", + "9572d367-c4ae-4fb1-8ad5-f5e3830e7034" + ] }, { "uuid": "6a0eee76-c019-413b-9d5f-44712b55b993", "id": "nfs", diff --git a/frontend/src/state/selectors.js b/frontend/src/state/selectors.js index d4c88c49..58772efb 100644 --- a/frontend/src/state/selectors.js +++ b/frontend/src/state/selectors.js @@ -110,6 +110,37 @@ const instancesByServiceId = (serviceId) => createSelector( })) ); +const servicesForTopology = (projectId) => createSelector( + [services, projectById(projectId)], + (services, project) => + services.filter((s) => s.project === project.uuid) + .map((service) => ({ + ...service, + uuid: service.uuid, + id: service.id, + name: service.name, + instances: instancesByServiceId(service.uuid).length, + connections: service.connections, + // tmp below + datacentres: 2, + metrics: [ + { + name: 'CPU', + value: '50%' + }, + { + name: 'Memory', + value: '20%' + }, + { + name: 'Network', + value: '2.9Kb/sec' + } + ], + healthy: true + })) +); + const metricsByServiceId = (serviceId) => createSelector( [serviceById(serviceId), metricsData, metricsUI], (service, metrics, metricsUI) => datasets(metrics, service.metrics, metricsUI) @@ -185,6 +216,7 @@ module.exports = { projectsByOrgIdSelector: projectsByOrgId, projectByIdSelector: projectById, servicesByProjectIdSelector: servicesByProjectId, + servicesForTopologySelector: servicesForTopology, instancesByServiceIdSelector: instancesByServiceId, metricsByServiceIdSelector: metricsByServiceId, metricTypesSelector: metricTypes, diff --git a/ui/src/components/topology/data.js b/ui/src/components/topology/data.js index df535bfd..2ce7671c 100644 --- a/ui/src/components/topology/data.js +++ b/ui/src/components/topology/data.js @@ -1,6 +1,155 @@ /*eslint-disable */ -module.exports = { +module.exports = [ + { + "uuid":"081a792c-47e0-4439-924b-2efa9788ae9e", + "id":"nginx", + "name":"Nginx", + "project":"e0ea0c02-55cc-45fe-8064-3e5176a59401", + "instances":1, + "metrics":[ + { + "name":"CPU", + "value":"50%" + }, + { + "name":"Memory", + "value":"20%" + }, + { + "name":"Network", + "value":"2.9Kb/sec" + } + ], + "connections":[ + "be227788-74f1-4e5b-a85f-b5c71cbae8d8" + ], + "healthy":true, + "datacentres":1 + }, + { + "uuid":"be227788-74f1-4e5b-a85f-b5c71cbae8d8", + "id":"wordpress", + "name":"Wordpress", + "project":"e0ea0c02-55cc-45fe-8064-3e5176a59401", + "instances":2, + "metrics":[ + { + "name":"CPU", + "value":"50%" + }, + { + "name":"Memory", + "value":"20%" + }, + { + "name":"Network", + "value":"2.9Kb/sec" + } + ], + "connections":[ + "6a0eee76-c019-413b-9d5f-44712b55b993", + "6d31aff4-de1e-4042-a983-fbd23d5c530c", + "4ee4103e-1a52-4099-a48e-01588f597c70" + ], + "healthy":true, + "datacentres":2 + }, + { + "uuid":"6a0eee76-c019-413b-9d5f-44712b55b993", + "id":"nfs", + "name":"NFS", + "project":"e0ea0c02-55cc-45fe-8064-3e5176a59401", + "instances":2, + "metrics":[ + { + "name":"CPU", + "value":"50%" + }, + { + "name":"Memory", + "value":"20%" + }, + { + "name":"Network", + "value":"2.9Kb/sec" + } + ], + "healthy":true, + "datacentres":2 + }, + { + "uuid":"6d31aff4-de1e-4042-a983-fbd23d5c530c", + "id":"memcached", + "name":"Memcached", + "project":"e0ea0c02-55cc-45fe-8064-3e5176a59401", + "instances":2, + "metrics":[ + { + "name":"CPU", + "value":"50%" + }, + { + "name":"Memory", + "value":"20%" + }, + { + "name":"Network", + "value":"2.9Kb/sec" + } + ], + "healthy":true, + "datacentres":2 + }, + { + "uuid":"4ee4103e-1a52-4099-a48e-01588f597c70", + "id":"percona", + "name":"Percona", + "project":"e0ea0c02-55cc-45fe-8064-3e5176a59401", + "instances":2, + "metrics":[ + { + "name":"CPU", + "value":"50%" + }, + { + "name":"Memory", + "value":"20%" + }, + { + "name":"Network", + "value":"2.9Kb/sec" + } + ], + "healthy":true, + "datacentres":1 + }, + { + "uuid":"97c68055-db88-45c9-ad49-f26da4264777", + "id":"consul", + "name":"Consul", + "project":"e0ea0c02-55cc-45fe-8064-3e5176a59401", + "instances":2, + "metrics":[ + { + "name":"CPU", + "value":"50%" + }, + { + "name":"Memory", + "value":"20%" + }, + { + "name":"Network", + "value":"2.9Kb/sec" + } + ], + "healthy":true, + "datacentres":2 + } +]; + +/*module.exports = { nodes: [ { id: 'Nginx', @@ -131,4 +280,4 @@ module.exports = { target: 'Percona', } ] -}; +};*/ diff --git a/ui/src/components/topology/graph-node-info.js b/ui/src/components/topology/graph-node-info.js index c27a33de..8c81283a 100644 --- a/ui/src/components/topology/graph-node-info.js +++ b/ui/src/components/topology/graph-node-info.js @@ -35,15 +35,12 @@ const StyledDataCentresIcon = styled(DataCentresIcon)` const GraphNodeInfo = ({ connected, - attrs, + datacentres, + instances, + healthy, infoPosition }) => { - const { - dcs, - instances - } = attrs; - return ( @@ -54,7 +51,7 @@ const GraphNodeInfo = ({ y={12} connected={connected} > - {`${dcs} inst.`} + {`${datacentres} inst.`} @@ -71,16 +68,14 @@ const GraphNodeInfo = ({ }; GraphNodeInfo.propTypes = { - attrs: React.PropTypes.shape({ - dcs: React.PropTypes.number, - instances: React.PropTypes.number, - healthy: React.PropTypes.bool - }), connected: React.PropTypes.bool, + datacentres: React.PropTypes.number, + healthy: React.PropTypes.bool, infoPosition: React.PropTypes.shape({ x: React.PropTypes.number, y: React.PropTypes.number - }) + }), + instances: React.PropTypes.number }; module.exports = Baseline( diff --git a/ui/src/components/topology/graph-node-metrics.js b/ui/src/components/topology/graph-node-metrics.js index e747f44b..a53e08f2 100644 --- a/ui/src/components/topology/graph-node-metrics.js +++ b/ui/src/components/topology/graph-node-metrics.js @@ -30,7 +30,7 @@ const GraphNodeMetrics = ({ y={12 + metricSpacing*index} connected={connected} > - {`${metric.name}: ${metric.stat}`} + {`${metric.name}: ${metric.value}`} )); diff --git a/ui/src/components/topology/graph-node.js b/ui/src/components/topology/graph-node.js index eaeb435c..4151e17b 100644 --- a/ui/src/components/topology/graph-node.js +++ b/ui/src/components/topology/graph-node.js @@ -148,7 +148,7 @@ const GraphNode = ({ y={30} connected={connected} > - {data.id} + {data.name} diff --git a/ui/src/components/topology/graph-simulation.js b/ui/src/components/topology/graph-simulation.js index fbed429c..c1bdba86 100644 --- a/ui/src/components/topology/graph-simulation.js +++ b/ui/src/components/topology/graph-simulation.js @@ -13,9 +13,19 @@ const rectRadius = (size) => { return Math.round(hypotenuse(width, height)/2); }; +const createLinks = (services) => + services.reduce((acc, service, index) => + service.connections ? + acc.concat( + service.connections.map((connection, index) => ({ + source: service.uuid, + target: connection + })) + ) : acc + , []); + const createSimulation = ( - nodes, - links, + services, nodeSize, svgSize, onTick, @@ -23,13 +33,12 @@ const createSimulation = ( ) => { // This is not going to work given that as well as the d3 layout stuff, other things might be at play too // We should pass two objects to the components - one for positioning and one for data - const mappedNodes = nodes.map((node, index) => ({ - id: node.id, + const nodes = services.map((service, index) => ({ + id: service.uuid, index: index })); - const mappedLinks = links.map((link, index) => ({ - ...link - })); + + const links = createLinks(services); const { width, @@ -39,22 +48,21 @@ const createSimulation = ( const nodeRadius = rectRadius(nodeSize); return ({ - simulation: d3.forceSimulation(mappedNodes) - .force('link', d3.forceLink(mappedLinks).id(d => d.id)) + simulation: d3.forceSimulation(nodes) + .force('link', d3.forceLink(links).id(d => d.id)) .force('collide', d3.forceCollide(nodeRadius)) .force('center', d3.forceCenter(width/2, height/2)) .on('tick', onTick) .on('end', onEnd), - nodes: mappedNodes, - links: mappedLinks + nodes: nodes, + links: links }); }; // TODO we need to kill the previous simulation const updateSimulation = ( simulation, - nextNodes, - nextLinks, + services, simNodes, simLinks, nodeSize, @@ -62,9 +70,9 @@ const updateSimulation = ( onTick, onEnd ) => { - const mappedNodes = nextNodes.map((nextNode, index) => { + const nodes = services.map((service, index) => { const simNode = simNodes.reduce((acc, n, i) => { - return nextNode.id === n.id ? n : acc; + return service.uuid === n.id ? n : acc; }, null); return simNode ? { @@ -73,14 +81,12 @@ const updateSimulation = ( // fy: simNode.y, index: index } : { - id: nextNode.id, + id: service.uuid, index: index }; }); - const mappedLinks = nextLinks.map((nextLink, index) => ({ - ...nextLink - })); + const links = createLinks(services); const { width, @@ -90,14 +96,14 @@ const updateSimulation = ( const nodeRadius = rectRadius(nodeSize); return ({ - simulation: d3.forceSimulation(mappedNodes) - .force('link', d3.forceLink(mappedLinks).id(d => d.id)) + simulation: d3.forceSimulation(nodes) + .force('link', d3.forceLink(links).id(d => d.id)) .force('collide', d3.forceCollide(nodeRadius)) .force('center', d3.forceCenter(width/2, height/2)) .on('tick', onTick) .on('end', onEnd), - nodes: mappedNodes, - links: mappedLinks + nodes: nodes, + links: links }); }; diff --git a/ui/src/components/topology/story-helper.js b/ui/src/components/topology/story-helper.js index 54700e65..4d180464 100644 --- a/ui/src/components/topology/story-helper.js +++ b/ui/src/components/topology/story-helper.js @@ -1,6 +1,7 @@ const React = require('react'); const Styled = require('styled-components'); const composers = require('../../shared/composers'); +const fns = require('../../shared/functions'); const Input = require('../form/input'); const Select = require('../form/select'); const Topology = require('./'); @@ -10,6 +11,10 @@ const { default: styled } = Styled; +const { + rndId +} = fns; + const { Baseline } = composers; @@ -51,56 +56,60 @@ class StoryHelper extends React.Component { const target = evt.target.target.value; const source = evt.target.source.value; - const links = []; + const node = { + ...data[0], + id: rndId(), + uuid: rndId(), + name: service + }; + + delete node.connections; + if(target) { - links.push({ - target: target, - source: service - }); + node.connections = [ + data.reduce((acc, s, i) => s.id === target ? s.uuid : acc, '') + ]; } - if(source) { - links.push({ - target: service, - source: source - }); - } + const d = this.state.data.map((data, index) => { - if(links.length) { - const data = this.state.data; - this.setState({ - data: { - nodes: [ - ...data.nodes, - { - ...data.nodes[0], - id: service - } - ], - links: [ - ...data.links, - ...links - ] - } + if(data.id === source) { + const connections = data.connections ? + data.connections.concat(node.uuid) : [node.uuid]; + + return ({ + ...data, + connections: connections + }); + } + + return ({ + ...data }); - } + }); + + d.push(node); + + this.setState({ + data: d + }); }; return (
- + { - - + } +
); } diff --git a/ui/src/components/topology/topology-graph.js b/ui/src/components/topology/topology-graph.js index dacfe9ca..61f5d50d 100644 --- a/ui/src/components/topology/topology-graph.js +++ b/ui/src/components/topology/topology-graph.js @@ -42,14 +42,10 @@ let dragInfo = { class TopologyGraph extends React.Component { componentWillMount() { - const { - nodes, - links - } = this.props.data; + const services = this.props.services; const simulationData = createSimulation( - nodes, - links, + services, nodeSize, svgSize//, //() => this.forceUpdate(), @@ -78,25 +74,21 @@ class TopologyGraph extends React.Component { // try freezing exisiting ones... then adding another const { - nodes: simNodes, - links: simLinks + nodes, + links } = this.state; - const { - nodes: nextNodes, - links: nextLinks - } = nextProps.data; + const services = nextProps.services; + // TODO this here means we'll need to evaluate whether to we have more links! // this is tmp for the compare above - if(nextNodes.length !== simNodes.length || - nextLinks.length !== simLinks.length) { + if(services !== nodes.length) { const simulation = this.state.simulation; const nextSimulationData = updateSimulation( simulation, - nextNodes, - nextLinks, - simNodes, - simLinks, + services, + nodes, + links, nodeSize, svgSize, () => this.forceUpdate(), @@ -114,10 +106,10 @@ class TopologyGraph extends React.Component { nextSimulation.tick(); } - /*this.state.simulation.nodes().forEach((node, index) => { - delete node.fx; - delete node.fy; - });*/ + //this.state.simulation.nodes().forEach((node, index) => { + // delete node.fx; + // delete node.fy; + //}); this.setState(nextSimulationData); } @@ -125,26 +117,26 @@ class TopologyGraph extends React.Component { render() { + const services = this.props.services; + const { nodes, links - } = this.props.data; + } = this.state; - const simulationNodes = this.state.nodes; - - const simulationNode = (nodeId) => - simulationNodes.reduce((acc, simNode, index) => { + const node = (nodeId) => + nodes.reduce((acc, simNode, index) => { return simNode.id === nodeId ? simNode : acc; }, {}); - const nodesData = nodes.map((node, index) => ({ - ...node, - ...simulationNode(node.id) + const nodesData = services.map((service, index) => ({ + ...service, + ...node(service.uuid) })); const linksData = links.map((link, index) => ({ - source: simulationNode(link.source), - target: simulationNode(link.target) + source: node(link.source.id), + target: node(link.target.id) })); const onDragStart = (evt, nodeId) => { @@ -183,7 +175,7 @@ class TopologyGraph extends React.Component { }; } - const dragNodes = simulationNodes.map((simNode, index) => { + const dragNodes = nodes.map((simNode, index) => { if(simNode.id === dragInfo.nodeId) { return ({ ...simNode, @@ -263,10 +255,7 @@ class TopologyGraph extends React.Component { } TopologyGraph.propTypes = { - data: React.PropTypes.shape({ - nodes: React.PropTypes.array, - links: React.PropTypes.array - }) + services: React.PropTypes.array }; module.exports = Baseline(