1
0
mirror of https://github.com/yldio/copilot.git synced 2024-12-29 05:10:05 +02:00

Add topology spike

This commit is contained in:
JUDIT GRESKOVITS 2017-02-06 16:11:01 +00:00
parent 0fc136c6ae
commit 32f784d461
30 changed files with 6009 additions and 0 deletions

View File

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

View File

@ -0,0 +1,3 @@
/node_modules
coverage
.nyc_output

View File

@ -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
}]
}
}

4
spikes/graphs-topology/d3/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/node_modules
coverage
.nyc_output
npm-debug.log

29
spikes/graphs-topology/d3/client/app.js vendored Normal file
View File

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

132
spikes/graphs-topology/d3/client/data.js vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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-&amp;-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

View File

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

View File

@ -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-&amp;-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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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