diff --git a/spikes/graphs-matrix/epoch/.babelrc b/spikes/graphs-matrix/epoch/.babelrc new file mode 100644 index 00000000..82cc857a --- /dev/null +++ b/spikes/graphs-matrix/epoch/.babelrc @@ -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" +} diff --git a/spikes/graphs-matrix/epoch/.eslintignore b/spikes/graphs-matrix/epoch/.eslintignore new file mode 100644 index 00000000..683e721c --- /dev/null +++ b/spikes/graphs-matrix/epoch/.eslintignore @@ -0,0 +1,3 @@ +/node_modules +coverage +.nyc_output diff --git a/spikes/graphs-matrix/epoch/.eslintrc b/spikes/graphs-matrix/epoch/.eslintrc new file mode 100644 index 00000000..19bd88dc --- /dev/null +++ b/spikes/graphs-matrix/epoch/.eslintrc @@ -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 + }] + } +} \ No newline at end of file diff --git a/spikes/graphs-matrix/epoch/.gitignore b/spikes/graphs-matrix/epoch/.gitignore new file mode 100644 index 00000000..c21b0ba0 --- /dev/null +++ b/spikes/graphs-matrix/epoch/.gitignore @@ -0,0 +1,4 @@ +/node_modules +coverage +.nyc_output +npm-debug.log diff --git a/spikes/graphs-matrix/epoch/client/actions.js b/spikes/graphs-matrix/epoch/client/actions.js new file mode 100644 index 00000000..4136fe1e --- /dev/null +++ b/spikes/graphs-matrix/epoch/client/actions.js @@ -0,0 +1,64 @@ +const takeRight = require('lodash.takeright'); + +const actions = { + 'UPDATE_STATS': (state, action) => { + const data = (state[action.subscription] || []).concat([action.payload]); + + return { + ...state, + [action.subscription]: takeRight(data, 50) + }; + } +}; + +module.exports = (state, action) => { + return !actions[action.type] ? state : actions[action.type](state, action); +}; + +module.exports.subscribe = (id) => (dispatch, getState) => { + const { + ws + } = getState(); + + const p = new Promise((resolve, reject) => { + ws.subscribe(`/stats/${id}`, (update, flag) => { + dispatch({ + type: 'UPDATE_STATS', + payload: update, + subscription: id + }); + }, (err) => { + if (err) { + return reject(err); + } + + resolve(); + }); + }); + + return dispatch({ + action: 'SUBSCRIBE', + payload: p + }); +}; + +module.exports.unsubscribe = (id) => (dispatch, getState) => { + const { + ws + } = getState(); + + const p = new Promise((resolve, reject) => { + ws.unsubscribe(`/stats/${id}`, null, (err) => { + if (err) { + return reject(err); + } + + resolve(); + }); + }); + + return dispatch({ + action: 'UNSUBSCRIBE', + payload: p + }); +}; \ No newline at end of file diff --git a/spikes/graphs-matrix/epoch/client/epoch.js b/spikes/graphs-matrix/epoch/client/epoch.js new file mode 100644 index 00000000..daac3937 --- /dev/null +++ b/spikes/graphs-matrix/epoch/client/epoch.js @@ -0,0 +1,63 @@ +// injects into `window` (ikr) +require('epoch-charting'); + +const ReactRedux = require('react-redux'); +const React = require('react'); + +const { + Time: { + Bar + } +} = window.Epoch; + +const { + connect +} = ReactRedux; + +const style = { + height: '220px' +}; + +module.exports = React.createClass({ + ref: function(name) { + this._refs = this._refs || {}; + + return (el) => { + this._refs[name] = el; + }; + }, + fromData: function(data) { + return (data || []).map((d) => { + return { + y: d.cpu, + time: d.when + }; + }); + }, + componentDidMount: function() { + this.chart = new Bar({ + el: this._refs.component, + type: 'time.bar', + data: [{ + label: 'A', + values: [] + }] + }); + }, + componentWillReceiveProps: function(nextProps) { + this.fromData(this.props.data).forEach((r) => this.chart.push([r])); + }, + render: function() { + const className = (this.props.median > 50) + ? 'red' + : 'blue'; + + return ( +
+ ); + } +}); \ No newline at end of file diff --git a/spikes/graphs-matrix/epoch/client/index.js b/spikes/graphs-matrix/epoch/client/index.js new file mode 100644 index 00000000..34d1748c --- /dev/null +++ b/spikes/graphs-matrix/epoch/client/index.js @@ -0,0 +1,37 @@ +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'); +}); + +const store = Store({ + ws: client +}); + +const render = () => { + const Root = require('./root'); + + ReactDOM.render( + , + document.getElementById('root') + ); +}; + +render(); + +if (module.hot) { + module.hot.accept('./root', render); +} diff --git a/spikes/graphs-matrix/epoch/client/matrix.js b/spikes/graphs-matrix/epoch/client/matrix.js new file mode 100644 index 00000000..ae2ca2b8 --- /dev/null +++ b/spikes/graphs-matrix/epoch/client/matrix.js @@ -0,0 +1,95 @@ +const React = require('react'); +const buildArray = require('build-array'); +const ReactRedux = require('react-redux'); +const Epoch = require('./epoch'); +const actions = require('./actions'); + +const { + connect +} = ReactRedux; + +const { + subscribe, + unsubscribe +} = actions; + +const mapStateToProps = (state, ownProps) => { + return { + data: state[ownProps.id] + }; +}; + +const mapDispatchToProps = (dispatch, ownProps) => { + return { + subscribe: () => { + return dispatch(subscribe(ownProps.id)); + }, + unsubscribe: () => { + return unsubscribe(ownProps.id); + } + } +}; + +const Graph = connect( + mapStateToProps, + mapDispatchToProps, +)(React.createClass({ + componentWillMount: function() { + this.props.subscribe(); + }, + componentWillUnmount: function() { + this.props.unsubscribe(); + }, + render: function() { + const { + data = [] + } = this.props; + + const median = data.reduce((sum, v) => (sum + v.cpu), 0) / data.length; + + const bg = median > 50 + ? 'rgba(205, 54, 54, 0.3)' + : 'rgba(54, 74, 205, 0.3)'; + + const shadow = median > 50 + ? 'inset 0 1px 0 0 rgba(248, 51, 51, 0.5)' + : 'inset 0 1px 0 0 rgba(54, 73, 205, 0.5)'; + + return ( + + ); + } +})); + +module.exports = ({ + x, + y +}) => { + const m = buildArray(y).map((v, i) => { + const m = buildArray(x).map((v, y) => { + const id = `${i}${y}`; + return ( +
+ +
+ ); + }); + + return ( +
+ {m} +
+ ); + }); + + return ( +
+ {m} +
+ ); +}; diff --git a/spikes/graphs-matrix/epoch/client/root.js b/spikes/graphs-matrix/epoch/client/root.js new file mode 100644 index 00000000..58534619 --- /dev/null +++ b/spikes/graphs-matrix/epoch/client/root.js @@ -0,0 +1,24 @@ +const React = require('react'); +const ReactHotLoader = require('react-hot-loader'); +const ReactRedux = require('react-redux'); +const Matrix = require('./matrix'); + +const { + AppContainer +} = ReactHotLoader; + +const { + Provider +} = ReactRedux; + +module.exports = ({ + store +}) => { + return ( + + + + + + ); +}; diff --git a/spikes/graphs-matrix/epoch/client/store.js b/spikes/graphs-matrix/epoch/client/store.js new file mode 100644 index 00000000..452af806 --- /dev/null +++ b/spikes/graphs-matrix/epoch/client/store.js @@ -0,0 +1,19 @@ +const createLogger = require('redux-logger'); +const promiseMiddleware = require('redux-promise-middleware').default; +const thunk = require('redux-thunk').default; +const redux = require('redux'); +const reducer = require('./actions'); + +const { + createStore, + compose, + applyMiddleware +} = redux; + +module.exports = (state = Object.freeze({})) => { + return createStore(reducer, state, applyMiddleware( + createLogger(), + promiseMiddleware(), + thunk + )); +}; diff --git a/spikes/graphs-matrix/epoch/package.json b/spikes/graphs-matrix/epoch/package.json new file mode 100644 index 00000000..f3c2dfbb --- /dev/null +++ b/spikes/graphs-matrix/epoch/package.json @@ -0,0 +1,62 @@ +{ + "name": "epoch-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", + "classnames": "^2.2.5", + "component-emitter": "^1.2.1", + "css-loader": "^0.25.0", + "d3": "^4.3.0", + "epoch-charting": "^0.8.4", + "hapi": "^15.2.0", + "hapi-webpack-dev-plugin": "^1.1.4", + "inert": "^4.0.2", + "lodash.takeright": "^4.1.1", + "nes": "^6.3.1", + "postcss-loader": "^1.0.0", + "postcss-modules-values": "^1.2.2", + "postcss-nested": "^1.0.0", + "react": "^15.3.2", + "react-dom": "^15.3.2", + "react-hot-loader": "^3.0.0-beta.6", + "react-redux": "^4.4.5", + "redux": "^3.6.0", + "redux-logger": "^2.7.4", + "redux-promise-middleware": "^4.1.0", + "redux-thunk": "^2.1.0", + "require-dir": "^0.3.1", + "style-loader": "^0.13.1", + "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" + } +} diff --git a/spikes/graphs-matrix/epoch/readme.md b/spikes/graphs-matrix/epoch/readme.md new file mode 100644 index 00000000..5172d589 --- /dev/null +++ b/spikes/graphs-matrix/epoch/readme.md @@ -0,0 +1,15 @@ +# Epoch + + - [x] Graphs should maintain aspect ration + - [ ] Graphs should match Antonas' first draft designs + - [x] Should have 3 Graphs on each row + - [x] Should be a 3 x 4 matrix of graphs, showing different data + - [x] Graphs should not jitter, ideally smoothly move across the x axis + - [x] All graphs should be a bar graph + - [ ] Animations when a graph comes into view + +## notes + + - Epoch is not responsive. Even though they maintain aspect ratio, using a responsive grid they get cluttered between each other + - With short update intervals, the graphs start using to much cpu and can't handle it + - Even looking at the [documentation](https://epochjs.github.io/epoch/styles), it's not obvious how styling works and I wasn't able to make it work. diff --git a/spikes/graphs-matrix/epoch/server/index.js b/spikes/graphs-matrix/epoch/server/index.js new file mode 100644 index 00000000..b2a6a529 --- /dev/null +++ b/spikes/graphs-matrix/epoch/server/index.js @@ -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}`); + }); + }); +}); diff --git a/spikes/graphs-matrix/epoch/server/metric.js b/spikes/graphs-matrix/epoch/server/metric.js new file mode 100644 index 00000000..0d64fad2 --- /dev/null +++ b/spikes/graphs-matrix/epoch/server/metric.js @@ -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 + }); + }, 400); + + cdm[id] = { + interval, + sockets: 1 + }; + }, + off: (id) => { + if (!(cdm[id].sockets -= 1)) { + clearInterval(cdm[id].interval); + } + } +}); diff --git a/spikes/graphs-matrix/epoch/server/plugins.js b/spikes/graphs-matrix/epoch/server/plugins.js new file mode 100644 index 00000000..84944329 --- /dev/null +++ b/spikes/graphs-matrix/epoch/server/plugins.js @@ -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') + } + } +]; diff --git a/spikes/graphs-matrix/epoch/server/routes/home.js b/spikes/graphs-matrix/epoch/server/routes/home.js new file mode 100644 index 00000000..48ead969 --- /dev/null +++ b/spikes/graphs-matrix/epoch/server/routes/home.js @@ -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')); + } + }); +}; diff --git a/spikes/graphs-matrix/epoch/server/routes/metrics.js b/spikes/graphs-matrix/epoch/server/routes/metrics.js new file mode 100644 index 00000000..e6e1c261 --- /dev/null +++ b/spikes/graphs-matrix/epoch/server/routes/metrics.js @@ -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(); + } + }); +}; diff --git a/spikes/graphs-matrix/epoch/server/routes/static.js b/spikes/graphs-matrix/epoch/server/routes/static.js new file mode 100644 index 00000000..5a41f602 --- /dev/null +++ b/spikes/graphs-matrix/epoch/server/routes/static.js @@ -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 + // } + // } + // }); +}; diff --git a/spikes/graphs-matrix/epoch/server/routes/version.js b/spikes/graphs-matrix/epoch/server/routes/version.js new file mode 100644 index 00000000..987747cb --- /dev/null +++ b/spikes/graphs-matrix/epoch/server/routes/version.js @@ -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) + } + }); +}; diff --git a/spikes/graphs-matrix/epoch/static/index.html b/spikes/graphs-matrix/epoch/static/index.html new file mode 100644 index 00000000..fd29e69d --- /dev/null +++ b/spikes/graphs-matrix/epoch/static/index.html @@ -0,0 +1,983 @@ + + + + React Boilerplate + + + + + + +
+ + + \ No newline at end of file diff --git a/spikes/graphs-matrix/epoch/webpack.config.js b/spikes/graphs-matrix/epoch/webpack.config.js new file mode 100644 index 00000000..792e8f4d --- /dev/null +++ b/spikes/graphs-matrix/epoch/webpack.config.js @@ -0,0 +1,65 @@ +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:8080', + '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(), + new webpack.ProvidePlugin({ + 'd3': 'd3' + }) + ], + 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'] + }, { + test: /\.css$/, + exclude: /node_modules/, + include: [ + path.join(__dirname, './client') + ], + loader: 'style-loader!css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader' + }] + } +}; + +const devServer = { + hot: true, + compress: true, + lazy: false, + publicPath: config.output.publicPath, + historyApiFallback: { + index: './static/index.html' + } +}; + +module.exports = Object.assign({ + devServer +}, config);