Topology data and component with new data
This commit is contained in:
parent
561b00b73a
commit
78312f7a68
@ -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(
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
}
|
||||
]
|
||||
};
|
||||
};*/
|
||||
|
@ -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(
|
||||
|
@ -30,7 +30,7 @@ const GraphNodeMetrics = ({
|
||||
y={12 + metricSpacing*index}
|
||||
connected={connected}
|
||||
>
|
||||
{`${metric.name}: ${metric.stat}`}
|
||||
{`${metric.name}: ${metric.value}`}
|
||||
</StyledText>
|
||||
));
|
||||
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user