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 { const {
orgByIdSelector, orgByIdSelector,
projectByIdSelector, projectByIdSelector,
servicesByProjectIdSelector servicesByProjectIdSelector,
servicesForTopologySelector
} = selectors; } = selectors;
const { const {
@ -28,7 +29,8 @@ const {
const Services = ({ const Services = ({
org = {}, org = {},
project = {}, project = {},
services = [] services = [],
servicesForTopology = []
}) => { }) => {
const empty = services.length ? null : ( const empty = services.length ? null : (
<EmptyServices /> <EmptyServices />
@ -60,7 +62,8 @@ const Services = ({
Services.propTypes = { Services.propTypes = {
org: PropTypes.org, org: PropTypes.org,
project: PropTypes.project, 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, { const mapStateToProps = (state, {
@ -70,7 +73,9 @@ const mapStateToProps = (state, {
}) => ({ }) => ({
org: orgByIdSelector(match.params.org)(state), org: orgByIdSelector(match.params.org)(state),
project: projectByIdSelector(match.params.projectId)(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( module.exports = connect(

View File

@ -642,7 +642,10 @@
}, { }, {
"type": "dca08514-72e5-46ce-ad92-e68b3b0914d4", "type": "dca08514-72e5-46ce-ad92-e68b3b0914d4",
"dataset": "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" "dataset": "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec"
}] }],
"connections": [
"be227788-74f1-4e5b-a85f-b5c71cbae8d8"
]
}, { }, {
"uuid": "be227788-74f1-4e5b-a85f-b5c71cbae8d8", "uuid": "be227788-74f1-4e5b-a85f-b5c71cbae8d8",
"id": "wordpress", "id": "wordpress",
@ -658,7 +661,12 @@
}, { }, {
"type": "dca08514-72e5-46ce-ad92-e68b3b0914d4", "type": "dca08514-72e5-46ce-ad92-e68b3b0914d4",
"dataset": "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" "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", "uuid": "6a0eee76-c019-413b-9d5f-44712b55b993",
"id": "nfs", "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( const metricsByServiceId = (serviceId) => createSelector(
[serviceById(serviceId), metricsData, metricsUI], [serviceById(serviceId), metricsData, metricsUI],
(service, metrics, metricsUI) => datasets(metrics, service.metrics, metricsUI) (service, metrics, metricsUI) => datasets(metrics, service.metrics, metricsUI)
@ -185,6 +216,7 @@ module.exports = {
projectsByOrgIdSelector: projectsByOrgId, projectsByOrgIdSelector: projectsByOrgId,
projectByIdSelector: projectById, projectByIdSelector: projectById,
servicesByProjectIdSelector: servicesByProjectId, servicesByProjectIdSelector: servicesByProjectId,
servicesForTopologySelector: servicesForTopology,
instancesByServiceIdSelector: instancesByServiceId, instancesByServiceIdSelector: instancesByServiceId,
metricsByServiceIdSelector: metricsByServiceId, metricsByServiceIdSelector: metricsByServiceId,
metricTypesSelector: metricTypes, metricTypesSelector: metricTypes,

View File

@ -1,6 +1,155 @@
/*eslint-disable */ /*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: [ nodes: [
{ {
id: 'Nginx', id: 'Nginx',
@ -131,4 +280,4 @@ module.exports = {
target: 'Percona', target: 'Percona',
} }
] ]
}; };*/

View File

@ -35,15 +35,12 @@ const StyledDataCentresIcon = styled(DataCentresIcon)`
const GraphNodeInfo = ({ const GraphNodeInfo = ({
connected, connected,
attrs, datacentres,
instances,
healthy,
infoPosition infoPosition
}) => { }) => {
const {
dcs,
instances
} = attrs;
return ( return (
<g transform={`translate(${infoPosition.x}, ${infoPosition.y})`}> <g transform={`translate(${infoPosition.x}, ${infoPosition.y})`}>
<g transform={'translate(0, 2)'}> <g transform={'translate(0, 2)'}>
@ -54,7 +51,7 @@ const GraphNodeInfo = ({
y={12} y={12}
connected={connected} connected={connected}
> >
{`${dcs} inst.`} {`${datacentres} inst.`}
</StyledText> </StyledText>
<g transform={'translate(82, 0)'}> <g transform={'translate(82, 0)'}>
<StyledDataCentresIcon connected={connected} /> <StyledDataCentresIcon connected={connected} />
@ -71,16 +68,14 @@ const GraphNodeInfo = ({
}; };
GraphNodeInfo.propTypes = { GraphNodeInfo.propTypes = {
attrs: React.PropTypes.shape({
dcs: React.PropTypes.number,
instances: React.PropTypes.number,
healthy: React.PropTypes.bool
}),
connected: React.PropTypes.bool, connected: React.PropTypes.bool,
datacentres: React.PropTypes.number,
healthy: React.PropTypes.bool,
infoPosition: React.PropTypes.shape({ infoPosition: React.PropTypes.shape({
x: React.PropTypes.number, x: React.PropTypes.number,
y: React.PropTypes.number y: React.PropTypes.number
}) }),
instances: React.PropTypes.number
}; };
module.exports = Baseline( module.exports = Baseline(

View File

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

View File

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

View File

@ -13,9 +13,19 @@ const rectRadius = (size) => {
return Math.round(hypotenuse(width, height)/2); 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 = ( const createSimulation = (
nodes, services,
links,
nodeSize, nodeSize,
svgSize, svgSize,
onTick, 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 // 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 // We should pass two objects to the components - one for positioning and one for data
const mappedNodes = nodes.map((node, index) => ({ const nodes = services.map((service, index) => ({
id: node.id, id: service.uuid,
index: index index: index
})); }));
const mappedLinks = links.map((link, index) => ({
...link const links = createLinks(services);
}));
const { const {
width, width,
@ -39,22 +48,21 @@ const createSimulation = (
const nodeRadius = rectRadius(nodeSize); const nodeRadius = rectRadius(nodeSize);
return ({ return ({
simulation: d3.forceSimulation(mappedNodes) simulation: d3.forceSimulation(nodes)
.force('link', d3.forceLink(mappedLinks).id(d => d.id)) .force('link', d3.forceLink(links).id(d => d.id))
.force('collide', d3.forceCollide(nodeRadius)) .force('collide', d3.forceCollide(nodeRadius))
.force('center', d3.forceCenter(width/2, height/2)) .force('center', d3.forceCenter(width/2, height/2))
.on('tick', onTick) .on('tick', onTick)
.on('end', onEnd), .on('end', onEnd),
nodes: mappedNodes, nodes: nodes,
links: mappedLinks links: links
}); });
}; };
// TODO we need to kill the previous simulation // TODO we need to kill the previous simulation
const updateSimulation = ( const updateSimulation = (
simulation, simulation,
nextNodes, services,
nextLinks,
simNodes, simNodes,
simLinks, simLinks,
nodeSize, nodeSize,
@ -62,9 +70,9 @@ const updateSimulation = (
onTick, onTick,
onEnd onEnd
) => { ) => {
const mappedNodes = nextNodes.map((nextNode, index) => { const nodes = services.map((service, index) => {
const simNode = simNodes.reduce((acc, n, i) => { const simNode = simNodes.reduce((acc, n, i) => {
return nextNode.id === n.id ? n : acc; return service.uuid === n.id ? n : acc;
}, null); }, null);
return simNode ? { return simNode ? {
@ -73,14 +81,12 @@ const updateSimulation = (
// fy: simNode.y, // fy: simNode.y,
index: index index: index
} : { } : {
id: nextNode.id, id: service.uuid,
index: index index: index
}; };
}); });
const mappedLinks = nextLinks.map((nextLink, index) => ({ const links = createLinks(services);
...nextLink
}));
const { const {
width, width,
@ -90,14 +96,14 @@ const updateSimulation = (
const nodeRadius = rectRadius(nodeSize); const nodeRadius = rectRadius(nodeSize);
return ({ return ({
simulation: d3.forceSimulation(mappedNodes) simulation: d3.forceSimulation(nodes)
.force('link', d3.forceLink(mappedLinks).id(d => d.id)) .force('link', d3.forceLink(links).id(d => d.id))
.force('collide', d3.forceCollide(nodeRadius)) .force('collide', d3.forceCollide(nodeRadius))
.force('center', d3.forceCenter(width/2, height/2)) .force('center', d3.forceCenter(width/2, height/2))
.on('tick', onTick) .on('tick', onTick)
.on('end', onEnd), .on('end', onEnd),
nodes: mappedNodes, nodes: nodes,
links: mappedLinks links: links
}); });
}; };

View File

@ -1,6 +1,7 @@
const React = require('react'); const React = require('react');
const Styled = require('styled-components'); const Styled = require('styled-components');
const composers = require('../../shared/composers'); const composers = require('../../shared/composers');
const fns = require('../../shared/functions');
const Input = require('../form/input'); const Input = require('../form/input');
const Select = require('../form/select'); const Select = require('../form/select');
const Topology = require('./'); const Topology = require('./');
@ -10,6 +11,10 @@ const {
default: styled default: styled
} = Styled; } = Styled;
const {
rndId
} = fns;
const { const {
Baseline Baseline
} = composers; } = composers;
@ -51,56 +56,60 @@ class StoryHelper extends React.Component {
const target = evt.target.target.value; const target = evt.target.target.value;
const source = evt.target.source.value; const source = evt.target.source.value;
const links = []; const node = {
...data[0],
id: rndId(),
uuid: rndId(),
name: service
};
delete node.connections;
if(target) { if(target) {
links.push({ node.connections = [
target: target, data.reduce((acc, s, i) => s.id === target ? s.uuid : acc, '')
source: service ];
});
} }
if(source) { const d = this.state.data.map((data, index) => {
links.push({
target: service,
source: source
});
}
if(links.length) { if(data.id === source) {
const data = this.state.data; const connections = data.connections ?
this.setState({ data.connections.concat(node.uuid) : [node.uuid];
data: {
nodes: [ return ({
...data.nodes, ...data,
{ connections: connections
...data.nodes[0], });
id: service }
}
], return ({
links: [ ...data
...data.links,
...links
]
}
}); });
} });
d.push(node);
this.setState({
data: d
});
}; };
return ( return (
<div> <div>
<StyledForm onSubmit={onAddService}> {<StyledForm onSubmit={onAddService}>
<Input name='service' placeholder='Service name' /> <Input name='service' placeholder='Service name' />
<Select name='target'> <Select name='target'>
<option value=''>Select a service to link to (optional)</option> <option value=''>Select a service to link to (optional)</option>
{linkOptions(data.nodes)} {linkOptions(data)}
</Select> </Select>
<Select name='source'> <Select name='source'>
<option value=''>Select a service to link from (optional)</option> <option value=''>Select a service to link from (optional)</option>
{linkOptions(data.nodes)} {linkOptions(data)}
</Select> </Select>
<Input name='Add service' type='submit' /> <Input name='Add service' type='submit' />
</StyledForm> </StyledForm>}
<TopologyGraph data={data} /> <TopologyGraph services={data} />
</div> </div>
); );
} }

View File

@ -42,14 +42,10 @@ let dragInfo = {
class TopologyGraph extends React.Component { class TopologyGraph extends React.Component {
componentWillMount() { componentWillMount() {
const { const services = this.props.services;
nodes,
links
} = this.props.data;
const simulationData = createSimulation( const simulationData = createSimulation(
nodes, services,
links,
nodeSize, nodeSize,
svgSize//, svgSize//,
//() => this.forceUpdate(), //() => this.forceUpdate(),
@ -78,25 +74,21 @@ class TopologyGraph extends React.Component {
// try freezing exisiting ones... then adding another // try freezing exisiting ones... then adding another
const { const {
nodes: simNodes, nodes,
links: simLinks links
} = this.state; } = this.state;
const { const services = nextProps.services;
nodes: nextNodes, // TODO this here means we'll need to evaluate whether to we have more links!
links: nextLinks
} = nextProps.data;
// this is tmp for the compare above // this is tmp for the compare above
if(nextNodes.length !== simNodes.length || if(services !== nodes.length) {
nextLinks.length !== simLinks.length) {
const simulation = this.state.simulation; const simulation = this.state.simulation;
const nextSimulationData = updateSimulation( const nextSimulationData = updateSimulation(
simulation, simulation,
nextNodes, services,
nextLinks, nodes,
simNodes, links,
simLinks,
nodeSize, nodeSize,
svgSize, svgSize,
() => this.forceUpdate(), () => this.forceUpdate(),
@ -114,10 +106,10 @@ class TopologyGraph extends React.Component {
nextSimulation.tick(); nextSimulation.tick();
} }
/*this.state.simulation.nodes().forEach((node, index) => { //this.state.simulation.nodes().forEach((node, index) => {
delete node.fx; // delete node.fx;
delete node.fy; // delete node.fy;
});*/ //});
this.setState(nextSimulationData); this.setState(nextSimulationData);
} }
@ -125,26 +117,26 @@ class TopologyGraph extends React.Component {
render() { render() {
const services = this.props.services;
const { const {
nodes, nodes,
links links
} = this.props.data; } = this.state;
const simulationNodes = this.state.nodes; const node = (nodeId) =>
nodes.reduce((acc, simNode, index) => {
const simulationNode = (nodeId) =>
simulationNodes.reduce((acc, simNode, index) => {
return simNode.id === nodeId ? simNode : acc; return simNode.id === nodeId ? simNode : acc;
}, {}); }, {});
const nodesData = nodes.map((node, index) => ({ const nodesData = services.map((service, index) => ({
...node, ...service,
...simulationNode(node.id) ...node(service.uuid)
})); }));
const linksData = links.map((link, index) => ({ const linksData = links.map((link, index) => ({
source: simulationNode(link.source), source: node(link.source.id),
target: simulationNode(link.target) target: node(link.target.id)
})); }));
const onDragStart = (evt, nodeId) => { 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) { if(simNode.id === dragInfo.nodeId) {
return ({ return ({
...simNode, ...simNode,
@ -263,10 +255,7 @@ class TopologyGraph extends React.Component {
} }
TopologyGraph.propTypes = { TopologyGraph.propTypes = {
data: React.PropTypes.shape({ services: React.PropTypes.array
nodes: React.PropTypes.array,
links: React.PropTypes.array
})
}; };
module.exports = Baseline( module.exports = Baseline(