Add topology spike
This commit is contained in:
parent
0fc136c6ae
commit
32f784d461
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"presets": [
|
||||
"react",
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
["transform-object-rest-spread", {
|
||||
"useBuiltIns": true
|
||||
}],
|
||||
"add-module-exports",
|
||||
"transform-es2015-modules-commonjs",
|
||||
"react-hot-loader/babel"
|
||||
],
|
||||
"sourceMaps": "both"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
/node_modules
|
||||
coverage
|
||||
.nyc_output
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"extends": "semistandard",
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 7,
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"babel",
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"generator-star-spacing": 0,
|
||||
"babel/generator-star-spacing": 1,
|
||||
"space-before-function-paren": [2, "never"],
|
||||
"react/jsx-uses-react": 2,
|
||||
"react/jsx-uses-vars": 2,
|
||||
"react/react-in-jsx-scope": 2,
|
||||
"object-curly-newline": ["error", {
|
||||
"minProperties": 1
|
||||
}],
|
||||
"sort-vars": ["error", {
|
||||
"ignoreCase": true
|
||||
}]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/node_modules
|
||||
coverage
|
||||
.nyc_output
|
||||
npm-debug.log
|
|
@ -0,0 +1,29 @@
|
|||
const React = require('react');
|
||||
const Styled = require('styled-components');
|
||||
const ReactRouter = require('react-router');
|
||||
|
||||
const {
|
||||
default: styled
|
||||
} = Styled;
|
||||
|
||||
const {
|
||||
Link
|
||||
} = ReactRouter;
|
||||
|
||||
const App = React.createClass({
|
||||
render: function() {
|
||||
const {
|
||||
children
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{ children }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = App;
|
|
@ -0,0 +1,132 @@
|
|||
module.exports = {
|
||||
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',
|
||||
}
|
||||
]
|
||||
};
|
|
@ -0,0 +1,127 @@
|
|||
const React = require('React');
|
||||
const Styled = require('styled-components');
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
class GraphLink extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
data,
|
||||
nodeSize,
|
||||
index
|
||||
} = this.props;
|
||||
|
||||
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 (
|
||||
<g>
|
||||
<StyledLine x1={sourcePosition.x} x2={targetPosition.x} y1={sourcePosition.y} y2={targetPosition.y} />
|
||||
<g transform={`translate(${targetPosition.x}, ${targetPosition.y}) rotate(${arrowAngle})`}>
|
||||
<StyledCircle cx={0} cy={0} r={9} />
|
||||
<StyledArrow x1={-1} x2={2} y1={-3} y2={0} />
|
||||
<StyledArrow x1={-1} x2={2} y1={3} y2={0} />
|
||||
</g>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GraphLink;
|
||||
|
||||
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
|
||||
}]);
|
|
@ -0,0 +1,56 @@
|
|||
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;
|
||||
`;
|
||||
|
||||
class GraphNodeButton extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
buttonRect,
|
||||
onButtonClick
|
||||
} = this.props;
|
||||
|
||||
const buttonCircleRadius = 2;
|
||||
const buttonCircleSpacing = 2;
|
||||
const buttonCircleY = (buttonRect.height - buttonCircleRadius*4 - buttonCircleSpacing*2)/2;
|
||||
const buttonCircles = [1,2,3].map((item, index) => (
|
||||
<StyledButtonCircle
|
||||
key={index}
|
||||
cx={buttonRect.width/2}
|
||||
cy={buttonCircleY + (buttonCircleRadius*2 + buttonCircleSpacing)*index}
|
||||
r={2}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<g transform={`translate(${buttonRect.x}, ${buttonRect.y})`}>
|
||||
<StyledButton onClick={onButtonClick} width={buttonRect.width} height={buttonRect.height}/>
|
||||
{buttonCircles}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
|
@ -0,0 +1,47 @@
|
|||
const React = require('React');
|
||||
const Styled = require('styled-components');
|
||||
const GraphNodeButton = require('./graph-node-button');
|
||||
|
||||
const {
|
||||
default: styled
|
||||
} = Styled;
|
||||
|
||||
const StyledText = styled.text`
|
||||
fill: white;
|
||||
font-family: LibreFranklin;
|
||||
font-size: 12px;
|
||||
`;
|
||||
|
||||
class GraphNodeMetrics extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
metrics,
|
||||
metricsPosition
|
||||
} = this.props;
|
||||
|
||||
const metricSpacing = 18;
|
||||
const metricsText = metrics.map((metric, index) => (
|
||||
<StyledText x={0} y={12 + metricSpacing*index}>{`${metric.name}: ${metric.stat}`}</StyledText>
|
||||
));
|
||||
|
||||
return (
|
||||
<g transform={`translate(${metricsPosition.x}, ${metricsPosition.y})`}>
|
||||
{metricsText}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
|
@ -0,0 +1,101 @@
|
|||
const React = require('React');
|
||||
const Styled = require('styled-components');
|
||||
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-family: LibreFranklin;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
const StyledInfoText = styled.text`
|
||||
fill: white;
|
||||
font-family: LibreFranklin;
|
||||
font-size: 12px;
|
||||
`;
|
||||
|
||||
class GraphNode extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
data,
|
||||
index,
|
||||
size
|
||||
} = this.props;
|
||||
|
||||
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!!!');
|
||||
}
|
||||
|
||||
console.log('data = ', data);
|
||||
const paddingLeft = 18-halfWidth;
|
||||
const metricsPosition = {
|
||||
x: paddingLeft,
|
||||
y: 89 - halfHeight
|
||||
}
|
||||
|
||||
return (
|
||||
<g transform={`translate(${data.x}, ${data.y})`}>
|
||||
<StyledShadowRect x={-halfWidth} y={3-halfHeight} width={width} height={height} />
|
||||
<StyledRect x={-halfWidth} y={-halfHeight} width={width} height={height} />
|
||||
<StyledLine x1={-halfWidth} y1={lineY} x2={halfWidth} y2={lineY} />
|
||||
<StyledLine x1={lineX} y1={-halfHeight} x2={lineX} y2={lineY} />
|
||||
<StyledText x={paddingLeft} y={30 - halfHeight}>{data.id}</StyledText>
|
||||
<HeartIcon />
|
||||
<g>
|
||||
<path d='M0,10 C-5,-10 18,-10, 20,0 M20,0 C22,-10 45,-10, 40,10 L20,30 L0,10' />
|
||||
</g>
|
||||
<GraphNodeButton buttonRect={buttonRect} onButtonClick={onButtonClick} />
|
||||
<GraphNodeMetrics metrics={data.metrics} metricsPosition={metricsPosition} />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GraphNode;
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="9px" height="13px" viewBox="0 0 9 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>icon: data center</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Project:-topology-of-services" transform="translate(-575.000000, -487.000000)" fill="#FFFFFF">
|
||||
<g id="services-copy" transform="translate(213.000000, 426.000000)">
|
||||
<g id="service:-nginx" transform="translate(263.000000, 0.000000)">
|
||||
<g id="metric">
|
||||
<g id="data-centers-&-instanecs" transform="translate(18.000000, 59.000000)">
|
||||
<path d="M81,15 L90,15 L90,2 L81,2 L81,15 Z M83,13 L88,13 L88,4 L83,4 L83,13 Z M84,6 L87.001,6 L87.001,5 L84,5 L84,6 Z M84,8 L87.001,8 L87.001,7 L84,7 L84,8 Z M84,10 L87.001,10 L87.001,9 L84,9 L84,10 Z" id="icon:--data-center"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="80px" height="70px" viewBox="0 0 80 70" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>icon: health</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Project:-topology-of-services" transform="translate(0, 0)" fill="#FFFFFF">
|
||||
<g id="services-copy" transform="translate(0, 0)">
|
||||
<g id="service:-nginx" transform="translate(0, 0)">
|
||||
<g id="icon:-state" transform="translate(0, 0)">
|
||||
<path d="M9.47745233,6.60270759 L8.95496861,7.04565311 L8.51133742,6.60270759 C7.70841297,5.79909747 6.40563205,5.79909747 5.60270759,6.60270759 C4.79909747,7.40631772 4.79909747,8.70841297 5.60270759,9.5120231 L8.95496861,12.8642841 L12.3668833,9.5120231 C13.1698077,8.70841297 13.2301471,7.40631772 12.4265369,6.60270759 C11.6229268,5.79909747 10.2810625,5.79909747 9.47745233,6.60270759 Z" id="icon:-health"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="18px" height="9px" viewBox="0 0 18 9" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>icon: instances</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Project:-topology-of-services" transform="translate(-494.000000, -489.000000)" fill="#FFFFFF">
|
||||
<g id="services-copy" transform="translate(213.000000, 426.000000)">
|
||||
<g id="service:-nginx" transform="translate(263.000000, 0.000000)">
|
||||
<g id="metric">
|
||||
<g id="data-centers-&-instanecs" transform="translate(18.000000, 59.000000)">
|
||||
<path d="M4.5,4 C2.015,4 0,6.015 0,8.5 C0,10.985 2.015,13 4.5,13 C6.985,13 9,10.985 9,8.5 C9,6.015 6.985,4 4.5,4 M13.0909091,4 C12.7145455,4 12.3512727,4.047 12,4.12 C14.184,4.576 15.8181818,6.359 15.8181818,8.5 C15.8181818,10.641 14.184,12.424 12,12.88 C12.3512727,12.953 12.7145455,13 13.0909091,13 C15.8018182,13 18,10.985 18,8.5 C18,6.015 15.8018182,4 13.0909091,4 M14,8.5 C14,10.985 11.8018182,13 9.09090909,13 C8.71454545,13 8.35127273,12.953 8,12.88 C10.184,12.424 11.8181818,10.641 11.8181818,8.5 C11.8181818,6.359 10.184,4.576 8,4.12 C8.35127273,4.047 8.71454545,4 9.09090909,4 C11.8018182,4 14,6.015 14,8.5" id="icon:-instances"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,137 @@
|
|||
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) => (
|
||||
<GraphNode
|
||||
key={index}
|
||||
data={n}
|
||||
index={index}
|
||||
size={nodeSize}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
renderLinks(nodeSize) {
|
||||
return this.state.dataLinks.map((l, index) => (
|
||||
<GraphLink
|
||||
key={index}
|
||||
data={l}
|
||||
index={index}
|
||||
nodeSize={nodeSize}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
nodeSize
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<StyledSvg>
|
||||
<g>
|
||||
{this.renderNodes(nodeSize)}
|
||||
</g>
|
||||
<g>
|
||||
{this.renderLinks(nodeSize)}
|
||||
</g>
|
||||
</StyledSvg>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
|
@ -0,0 +1,46 @@
|
|||
const ReactDOM = require('react-dom');
|
||||
const React = require('react');
|
||||
const store = require('./store')();
|
||||
const nes = require('nes/dist/client');
|
||||
|
||||
const {
|
||||
Client
|
||||
} = nes;
|
||||
|
||||
const client = new Client(`ws://${document.location.host}`);
|
||||
|
||||
client.connect((err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
console.log('connected');
|
||||
|
||||
client.subscribe('/stats/5', (update, flag) => {
|
||||
store.dispatch({
|
||||
type: 'UPDATE_STATS',
|
||||
payload: update
|
||||
})
|
||||
}, (err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
console.log('subscribed');
|
||||
});
|
||||
});
|
||||
|
||||
const render = () => {
|
||||
const Root = require('./root');
|
||||
|
||||
ReactDOM.render(
|
||||
<Root store={store} />,
|
||||
document.getElementById('root')
|
||||
);
|
||||
};
|
||||
|
||||
render();
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept('./root', render);
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
const React = require('react');
|
||||
const ReactHotLoader = require('react-hot-loader');
|
||||
const ReactRedux = require('react-redux');
|
||||
const ReactRouter = require('react-router');
|
||||
const App = require('./app');
|
||||
const Topology = require('./topology');
|
||||
|
||||
const {
|
||||
AppContainer
|
||||
} = ReactHotLoader;
|
||||
|
||||
const {
|
||||
Provider
|
||||
} = ReactRedux;
|
||||
|
||||
const {
|
||||
Router,
|
||||
Route,
|
||||
IndexRoute,
|
||||
browserHistory
|
||||
} = ReactRouter;
|
||||
|
||||
module.exports = ({
|
||||
store
|
||||
}) => {
|
||||
return (
|
||||
<AppContainer>
|
||||
<Provider store={store}>
|
||||
<Router history={browserHistory}>
|
||||
<Route path="/" component={App}>
|
||||
<IndexRoute component={Topology} />
|
||||
</Route>
|
||||
</Router>
|
||||
</Provider>
|
||||
</AppContainer>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
const takeRight = require('lodash.takeright');
|
||||
const redux = require('redux');
|
||||
|
||||
const {
|
||||
createStore,
|
||||
compose,
|
||||
combineReducers,
|
||||
applyMiddleware
|
||||
} = redux;
|
||||
|
||||
const reducer = (state, action) => {
|
||||
if (action.type !== 'UPDATE_STATS') {
|
||||
return state;
|
||||
}
|
||||
|
||||
const data = (state.data || []).concat([action.payload]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
data: takeRight(data, 50)
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = (state = Object.freeze({})) => {
|
||||
return createStore(reducer, state, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
|
||||
};
|
|
@ -0,0 +1,93 @@
|
|||
const React = require('react');
|
||||
const Styled = require('styled-components');
|
||||
const TopologyGraph = require('./graph/topology-graph');
|
||||
const data = require('./data');
|
||||
|
||||
const {
|
||||
default: styled
|
||||
} = Styled;
|
||||
|
||||
const StyledSvg = styled.svg`
|
||||
width: 1024px;
|
||||
height: 860px;
|
||||
`;
|
||||
|
||||
const StyledForm = styled.form`
|
||||
margin: 20px;
|
||||
`;
|
||||
|
||||
class Topology extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
//nasty
|
||||
this.state = {
|
||||
data: {
|
||||
nodes: [
|
||||
...data.nodes
|
||||
],
|
||||
links: [
|
||||
...data.links
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
} = this.props;
|
||||
|
||||
const nodeSize = {
|
||||
width: 180,
|
||||
height: 156
|
||||
};
|
||||
|
||||
const onSubmit = (evt) => {
|
||||
evt.preventDefault();
|
||||
console.log('submit ', evt.target.service.value);
|
||||
console.log('submit ', evt.target.link.value);
|
||||
const service = evt.target.service.value;
|
||||
const target = evt.target.link.value;
|
||||
const data = this.state.data;
|
||||
|
||||
this.setState({
|
||||
data: {
|
||||
nodes: [
|
||||
...data.nodes,
|
||||
{
|
||||
id: evt.target.service.value
|
||||
}
|
||||
],
|
||||
links: [
|
||||
...data.links,
|
||||
{
|
||||
source: service,
|
||||
target: target
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const options = data.nodes.map((n, index) => (
|
||||
<option key={index} value={n.id}>{n.id}</option>
|
||||
));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StyledForm onSubmit={onSubmit}>
|
||||
<label>New service name</label>
|
||||
<input name='service' type='text' placeholder='Service name' />
|
||||
<label>Service to link to</label>
|
||||
<select name='link'>
|
||||
{ options }
|
||||
</select>
|
||||
<input type='submit' value='submit' />
|
||||
</StyledForm>
|
||||
<TopologyGraph data={this.state.data} nodeSize={nodeSize} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Topology;
|
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"name": "chartjs-graphing-spike",
|
||||
"private": true,
|
||||
"license": "private",
|
||||
"main": "server/index.js",
|
||||
"dependencies": {
|
||||
"autoprefixer": "^6.5.1",
|
||||
"babel-eslint": "^7.0.0",
|
||||
"babel-loader": "^6.2.5",
|
||||
"babel-plugin-add-module-exports": "^0.2.1",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.16.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.16.0",
|
||||
"babel-plugin-transform-runtime": "^6.15.0",
|
||||
"babel-preset-es2015": "^6.16.0",
|
||||
"babel-preset-react": "^6.16.0",
|
||||
"babel-preset-react-hmre": "^1.1.1",
|
||||
"babel-runtime": "^6.11.6",
|
||||
"build-array": "^1.0.0",
|
||||
"component-emitter": "^1.2.1",
|
||||
"css-loader": "^0.25.0",
|
||||
"d3": "^4.5.0",
|
||||
"hapi": "^15.2.0",
|
||||
"hapi-webpack-dev-plugin": "^1.1.4",
|
||||
"inert": "^4.0.2",
|
||||
"lodash.takeright": "^4.1.1",
|
||||
"nes": "^6.3.1",
|
||||
"react": "^15.3.2",
|
||||
"react-dom": "^15.3.2",
|
||||
"react-hot-loader": "^3.0.0-beta.6",
|
||||
"react-redux": "^4.4.5",
|
||||
"react-router": "^3.0.0",
|
||||
"redux": "^3.6.0",
|
||||
"require-dir": "^0.3.1",
|
||||
"style-loader": "^0.13.1",
|
||||
"styled-components": "^1.2.1",
|
||||
"svg-react-loader": "^0.3.7",
|
||||
"transform-props-with": "^2.1.0",
|
||||
"validator": "^6.2.0",
|
||||
"webpack": "^1.13.2",
|
||||
"webpack-dev-server": "^1.16.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-register": "^6.16.3",
|
||||
"eslint": "^3.8.1",
|
||||
"eslint-config-semistandard": "^7.0.0",
|
||||
"eslint-config-standard": "^6.2.0",
|
||||
"eslint-plugin-babel": "^3.3.0",
|
||||
"eslint-plugin-promise": "^3.3.0",
|
||||
"eslint-plugin-react": "^6.4.1",
|
||||
"eslint-plugin-standard": "^2.0.1",
|
||||
"json-loader": "^0.5.4"
|
||||
},
|
||||
"ava": {
|
||||
"require": [
|
||||
"babel-register"
|
||||
],
|
||||
"babel": "inherit"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
# redux-form
|
||||
|
||||
## summary
|
||||
|
||||
- [x] form values in redux store
|
||||
- [x] clear / retain values in store
|
||||
- [x] pre-populate form
|
||||
- [x] validation on field / form level
|
||||
- [x] multi page form
|
||||
- [x] custom form components
|
||||
- [ ] requires updates to existing ui components as props to custom components are passed in the following format:
|
||||
|
||||
`"props": { "input": "value": "", "name": "", "onChange": "", "onFocus": "", ... }, "meta": { "valid": "", "error": "", ... }, "anyOtherPropsOnField": "", ... }`
|
||||
|
||||
- [ ] explore proxying props from Field to custom components from above shape to a flat form as expected by custom components
|
||||
- [ ] consider creating component that handles logic and display of label and error which would be reused by form components to avoid code duplication for this functionality
|
|
@ -0,0 +1,29 @@
|
|||
const requireDir = require('require-dir');
|
||||
const plugins = require('./plugins');
|
||||
const routes = requireDir('./routes');
|
||||
const Hapi = require('hapi');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const server = new Hapi.Server();
|
||||
|
||||
server.connection({
|
||||
host: 'localhost',
|
||||
port: 8000
|
||||
});
|
||||
|
||||
server.register(plugins, (err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
Object.keys(routes).forEach((name) => {
|
||||
routes[name](server);
|
||||
});
|
||||
|
||||
server.start((err) => {
|
||||
server.connections.forEach((conn) => {
|
||||
console.log(`started at: ${conn.info.uri}`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
const Emitter = require('component-emitter');
|
||||
|
||||
const cdm = {};
|
||||
|
||||
module.exports = (server) => ({
|
||||
on: (id) => {
|
||||
console.log('on', cdm[id]);
|
||||
if (cdm[id] && (cdm[id].sockets > 0)) {
|
||||
cdm[id].sockets +=1;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let messageId = 0;
|
||||
const interval = setInterval(() => {
|
||||
console.log(`publishing /stats/${id}`);
|
||||
|
||||
server.publish(`/stats/${id}`, {
|
||||
when: new Date().getTime(),
|
||||
cpu: Math.random() * 100
|
||||
});
|
||||
}, 45);
|
||||
|
||||
cdm[id] = {
|
||||
interval,
|
||||
sockets: 1
|
||||
};
|
||||
},
|
||||
off: (id) => {
|
||||
if (!(cdm[id].sockets -= 1)) {
|
||||
clearInterval(cdm[id].interval);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
const webpack = require('webpack');
|
||||
const path = require('path');
|
||||
|
||||
const cfg = require('../webpack.config.js');
|
||||
|
||||
module.exports = [
|
||||
require('inert'),
|
||||
require('nes'), {
|
||||
register: require('hapi-webpack-dev-plugin'),
|
||||
options: {
|
||||
compiler: webpack(cfg),
|
||||
devIndex: path.join(__dirname, '../static')
|
||||
}
|
||||
}
|
||||
];
|
|
@ -0,0 +1,11 @@
|
|||
const path = require('path');
|
||||
|
||||
module.exports = (server) => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/',
|
||||
handler: (request, reply) => {
|
||||
reply.file(path.join(__dirname, '../../static/index.html'));
|
||||
}
|
||||
});
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
const Metric = require('../metric');
|
||||
|
||||
module.exports = (server) => {
|
||||
const metric = Metric(server);
|
||||
|
||||
server.subscription('/stats/{id}', {
|
||||
onSubscribe: (socket, path, params, next) => {
|
||||
console.log('onSubscribe');
|
||||
metric.on(params.id);
|
||||
next();
|
||||
},
|
||||
onUnsubscribe: (socket, path, params, next) => {
|
||||
console.log('onUnsubscribe');
|
||||
metric.off(params.id);
|
||||
next();
|
||||
}
|
||||
});
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
const path = require('path');
|
||||
|
||||
module.exports = (server) => {
|
||||
// server.route({
|
||||
// method: 'GET',
|
||||
// path: '/{param*}',
|
||||
// handler: {
|
||||
// directory: {
|
||||
// path: path.join(__dirname, '../../static'),
|
||||
// redirectToSlash: true,
|
||||
// index: true
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
const Pkg = require('../../package.json');
|
||||
|
||||
const internals = {
|
||||
response: {
|
||||
version: Pkg.version
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = (server) => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/ops/version',
|
||||
config: {
|
||||
description: 'Returns the version of the server',
|
||||
handler: (request, reply) => reply(internals.response)
|
||||
}
|
||||
});
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang='en-US'>
|
||||
<head>
|
||||
<title>React Boilerplate</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://necolas.github.io/normalize.css/latest/normalize.css" />
|
||||
<link rel="stylesheet" type="text/css" href="https://rawgit.com/epochjs/epoch/master/dist/css/epoch.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id='root'></div>
|
||||
<script src='/static/bundle.js'></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,55 @@
|
|||
const webpack = require('webpack');
|
||||
const path = require('path');
|
||||
|
||||
const config = {
|
||||
debug: true,
|
||||
devtool: 'source-map',
|
||||
context: path.join(__dirname, './client'),
|
||||
app: path.join(__dirname, './client/index.js'),
|
||||
entry: [
|
||||
'webpack-dev-server/client?http://localhost:8888',
|
||||
'webpack/hot/only-dev-server',
|
||||
'react-hot-loader/patch',
|
||||
'./index.js'
|
||||
],
|
||||
output: {
|
||||
path: path.join(__dirname, './static'),
|
||||
publicPath: '/static/',
|
||||
filename: 'bundle.js'
|
||||
},
|
||||
plugins: [
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NoErrorsPlugin()
|
||||
],
|
||||
module: {
|
||||
loaders: [{
|
||||
test: /js?$/,
|
||||
exclude: /node_modules/,
|
||||
include: [
|
||||
path.join(__dirname, './client')
|
||||
],
|
||||
loaders: ['babel']
|
||||
}, {
|
||||
test: /\.json?$/,
|
||||
exclude: /node_modules/,
|
||||
include: [
|
||||
path.join(__dirname, './client')
|
||||
],
|
||||
loaders: ['json']
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const devServer = {
|
||||
hot: true,
|
||||
compress: true,
|
||||
lazy: false,
|
||||
publicPath: config.output.publicPath,
|
||||
historyApiFallback: {
|
||||
index: './static/index.html'
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Object.assign({
|
||||
devServer
|
||||
}, config);
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue