Topology data and component with new data

This commit is contained in:
Alex Windett 2017-02-22 15:18:18 +00:00 committed by Judit Greskovits
parent 561b00b73a
commit 78312f7a68
10 changed files with 312 additions and 117 deletions

View File

@ -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 : (
<EmptyServices />
@ -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(

View File

@ -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",

View File

@ -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,

View File

@ -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',
}
]
};
};*/

View File

@ -35,15 +35,12 @@ const StyledDataCentresIcon = styled(DataCentresIcon)`
const GraphNodeInfo = ({
connected,
attrs,
datacentres,
instances,
healthy,
infoPosition
}) => {
const {
dcs,
instances
} = attrs;
return (
<g transform={`translate(${infoPosition.x}, ${infoPosition.y})`}>
<g transform={'translate(0, 2)'}>
@ -54,7 +51,7 @@ const GraphNodeInfo = ({
y={12}
connected={connected}
>
{`${dcs} inst.`}
{`${datacentres} inst.`}
</StyledText>
<g transform={'translate(82, 0)'}>
<StyledDataCentresIcon connected={connected} />
@ -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(

View File

@ -30,7 +30,7 @@ const GraphNodeMetrics = ({
y={12 + metricSpacing*index}
connected={connected}
>
{`${metric.name}: ${metric.stat}`}
{`${metric.name}: ${metric.value}`}
</StyledText>
));

View File

@ -148,7 +148,7 @@ const GraphNode = ({
y={30}
connected={connected}
>
{data.id}
{data.name}
</StyledText>
<g transform={`translate(${115}, ${15})`}>
<HeartCircle
@ -164,7 +164,9 @@ const GraphNode = ({
connected={connected}
/>
<GraphNodeInfo
attrs={data.attrs}
datacentres={data.datacentres}
instances={data.instances}
healthy
infoPosition={infoPosition}
connected={connected}
/>

View File

@ -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
});
};

View File

@ -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 (
<div>
<StyledForm onSubmit={onAddService}>
{<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)}
{linkOptions(data)}
</Select>
<Select name='source'>
<option value=''>Select a service to link from (optional)</option>
{linkOptions(data.nodes)}
{linkOptions(data)}
</Select>
<Input name='Add service' type='submit' />
</StyledForm>
<TopologyGraph data={data} />
</StyledForm>}
<TopologyGraph services={data} />
</div>
);
}

View File

@ -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(