diff --git a/cloudapi-graphql/src/endpoint.js b/cloudapi-graphql/src/endpoint.js new file mode 100644 index 00000000..1df73b88 --- /dev/null +++ b/cloudapi-graphql/src/endpoint.js @@ -0,0 +1,8 @@ +const graphqlHTTP = require('express-graphql'); +const schema = require('./schema'); + +module.exports = graphqlHTTP(() => ({ + schema: schema, + graphiql: true, + pretty: true +})); \ No newline at end of file diff --git a/cloudapi-graphql/src/index.js b/cloudapi-graphql/src/index.js index f20231be..46470017 100644 --- a/cloudapi-graphql/src/index.js +++ b/cloudapi-graphql/src/index.js @@ -1,14 +1,8 @@ const express = require('express'); -const graphqlHTTP = require('express-graphql'); -const schema = require('./schema'); const app = express(); -app.use('/graphql', graphqlHTTP(() => ({ - schema: schema, - graphiql: true, - pretty: true -}))); +app.use('/graphql', require('./endpoint')); app.listen(3000, (err) => { if (err) { diff --git a/frontend/.babelrc b/frontend/.babelrc new file mode 100644 index 00000000..983f315f --- /dev/null +++ b/frontend/.babelrc @@ -0,0 +1,36 @@ +{ + "sourceMaps": "both", + "presets": [ + "react" + ], + "plugins": [ + "react-hot-loader/babel", + "transform-es2015-modules-commonjs", + "add-module-exports", + "transform-exponentiation-operator", + "syntax-async-functions", + ["transform-object-rest-spread", { + "useBuiltIns": true + }], + ["fast-async", { + "runtimePatten": "directive", + "compiler": { + "promises": false, + "es7": true, + "lazyThenables": true + } + }] + ], + "env": { + "test": { + "plugins": [ + "transform-async-to-generator", [ + "transform-runtime", { + "polyfill": false, + "regenerator": false + } + ] + ] + } + } +} diff --git a/frontend/.eslintignore b/frontend/.eslintignore new file mode 100644 index 00000000..b1d4c644 --- /dev/null +++ b/frontend/.eslintignore @@ -0,0 +1,3 @@ +/node_modules +coverage +.nyc_output \ No newline at end of file diff --git a/frontend/.eslintrc b/frontend/.eslintrc new file mode 100644 index 00000000..55e4244d --- /dev/null +++ b/frontend/.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 + }] + } +} diff --git a/frontend/.tern-project b/frontend/.tern-project new file mode 100644 index 00000000..22bab84c --- /dev/null +++ b/frontend/.tern-project @@ -0,0 +1,15 @@ +{ + "libs": [ + "ecmascript", + "browser" + ], + "plugins": { + "doc_comment": true, + "local-scope": true, + "jsx": true, + "node": true, + "webpack": { + "configPath": "./webpack/index.js" + } + } +} \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..f1cf7b5d --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,45 @@ +# Joyent Dashboard Frontend + +## start + +``` +npm run start +``` + +## test + +``` +npm run test +``` + +## structure + +``` +. +├── src +│   ├── containers +│   ├── index.js +│   ├── root.js +│   └── state +│   ├── actions.js +│   ├── reducers +│   ├── store.js +│   └── thunks +├── static +├── test +├── webpack +├── .babelrc +└── .eslintrc +``` + + - **src/index.js**: Renders `src/root.js` and bootstraps hot module reloading. + - **src/root.js**: The main component that wraps `react-redux`, `react-router` and `react-hot-loader`. + - **src/state/store.js**: Exports a function that creates a `redux` store instance with all the middlewares and reducers configured. + - **src/state/actions.js**: Not only exports all the actions available (declared in the file), but also goes through all the thunks and exports them. + - **src/state/thunks**: Directory to place thunks so that actions or reducers don't get too confusing. + - **src/state/reducers**: Each file here represents a reducer scope. So, `state.app` will be controlled in `reducers/app.js`. + - **test**: Self explanatory. + - **webpack**: Webpack configuration for multiple enviroments. Development configuration includes a dev-server and hot module replacement support. + - **.babelrc**: This babel configuration outputs ES2015 code, so it will produce code only for modern browsers. +Also, async/await is supported. + - **.eslintrc**:ESLint configuration. It's basically [semistandard](https://github.com/Flet/semistandard) with `space-before-function-paren` probited; diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..32903e47 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,70 @@ +{ + "name": "joyent-dashboard", + "version": "1.0.0", + "private": true, + "license": "private", + "scripts": { + "start": "webpack-dev-server --open --config webpack/index.js", + "lint": "eslint .", + "test": "NODE_ENV=test nyc ava test/*.js --verbose", + "open": "nyc report --reporter=html & open coverage/index.html", + "coverage": "nyc check-coverage --statements 100 --functions 100 --lines 100 --branches 100" + }, + "dependencies": { + "babel-core": "^6.17.0", + "babel-loader": "^6.2.5", + "babel-plugin-add-module-exports": "^0.2.1", + "babel-plugin-syntax-async-functions": "^6.13.0", + "babel-plugin-transform-es2015-modules-commonjs": "^6.16.0", + "babel-plugin-transform-object-rest-spread": "^6.16.0", + "babel-preset-react": "^6.16.0", + "constant-case": "^2.0.0", + "fast-async": "^6.1.1", + "json-loader": "^0.5.4", + "react": "^15.3.2", + "react-dom": "^15.3.2", + "react-hot-loader": "^3.0.0-beta.6", + "react-redux": "^4.4.5", + "react-router": "^4.0.0-alpha.4", + "reduce-reducers": "^0.1.2", + "redux": "^3.6.0", + "redux-actions": "^0.12.0", + "redux-batched-actions": "^0.1.3", + "redux-logger": "^2.7.0", + "redux-promise-middleware": "^4.1.0", + "redux-thunk": "^2.1.0", + "webpack": "^2.1.0-beta.25" + }, + "devDependencies": { + "ava": "^0.16.0", + "babel-eslint": "^7.0.0", + "babel-plugin-transform-async-to-generator": "^6.16.0", + "babel-plugin-transform-runtime": "^6.15.0", + "babel-register": "^6.16.3", + "enzyme": "^2.5.1", + "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", + "nyc": "^8.3.1", + "pre-commit": "^1.1.3", + "react-addons-test-utils": "^15.3.2", + "webpack-dev-server": "^1.16.2" + }, + "ava": { + "failFast": true, + "cache": false, + "require": [ + "babel-register" + ], + "babel": "inherit" + }, + "pre-commit": [ + "lint", + "test", + "coverage" + ] +} diff --git a/frontend/src/containers/app.js b/frontend/src/containers/app.js new file mode 100644 index 00000000..b7600a42 --- /dev/null +++ b/frontend/src/containers/app.js @@ -0,0 +1,57 @@ +const React = require('react'); +const ReactRedux = require('react-redux'); +const ReactRouter = require('react-router'); + +const actions = require('../state/actions'); +const Home = require('./home'); +const NotFound = require('./not-found'); + +const { + updateRouter +} = actions; + +const { + connect +} = ReactRedux; + +const { + Miss, + Match +} = ReactRouter; + +const App = connect()(React.createClass({ + componentWillMount: function() { + const { + router, + dispatch + } = this.props; + + // ugly hack needed because of a limitation of react-router api + // that doens't pass it's instance to matched routes + dispatch(updateRouter(router)); + }, + render: function() { + const { + children + } = this.props; + + if (!Array.isArray(children)) { + return children; + } + + return ( +
+ {children} +
+ ); + } +})); + +module.exports = (props) => { + return ( + + + + + ); +}; diff --git a/frontend/src/containers/home.js b/frontend/src/containers/home.js new file mode 100644 index 00000000..db54116e --- /dev/null +++ b/frontend/src/containers/home.js @@ -0,0 +1,9 @@ +const React = require('react'); + +module.exports = () => { + return ( +
+

Home

+
+ ); +}; diff --git a/frontend/src/containers/not-found.js b/frontend/src/containers/not-found.js new file mode 100644 index 00000000..b5ae7b4d --- /dev/null +++ b/frontend/src/containers/not-found.js @@ -0,0 +1,10 @@ +const React = require('react'); + +module.exports = () => { + return ( +
+

404

+

Not Found

+
+ ); +}; diff --git a/frontend/src/index.js b/frontend/src/index.js new file mode 100644 index 00000000..af547af9 --- /dev/null +++ b/frontend/src/index.js @@ -0,0 +1,17 @@ +const ReactDOM = require('react-dom'); +const React = require('react'); + +const render = () => { + const Root = require('./root'); + + ReactDOM.render( + , + document.getElementById('root') + ); +}; + +render(); + +if (module.hot) { + module.hot.accept('./root', render); +} diff --git a/frontend/src/root.js b/frontend/src/root.js new file mode 100644 index 00000000..78fc1998 --- /dev/null +++ b/frontend/src/root.js @@ -0,0 +1,31 @@ +const React = require('react'); +const ReactHotLoader = require('react-hot-loader'); +const ReactRedux = require('react-redux'); +const ReactRouter = require('react-router'); + +const App = require('./containers/app'); +const store = require('./state/store'); + +const { + AppContainer +} = ReactHotLoader; + +const { + Provider +} = ReactRedux; + +const { + BrowserRouter +} = ReactRouter; + +module.exports = () => { + return ( + + + + {App} + + + + ); +}; diff --git a/frontend/src/state/actions.js b/frontend/src/state/actions.js new file mode 100644 index 00000000..01993adb --- /dev/null +++ b/frontend/src/state/actions.js @@ -0,0 +1,13 @@ +const constantCase = require('constant-case'); +const ReduxActions = require('redux-actions'); + +const { + createAction +} = ReduxActions; + +const APP = constantCase(process.env['APP_NAME']); + +module.exports = { + ...require('./thunks'), + updateRouter: createAction(`${APP}/APP/UPDATE_ROUTER`) +}; diff --git a/frontend/src/state/reducers/app.js b/frontend/src/state/reducers/app.js new file mode 100644 index 00000000..3d301ee3 --- /dev/null +++ b/frontend/src/state/reducers/app.js @@ -0,0 +1,19 @@ +const ReduxActions = require('redux-actions'); +const actions = require('../actions'); + +const { + handleActions +} = ReduxActions; + +const { + updateRouter +} = actions; + +module.exports = handleActions({ + [updateRouter.toString()]: (state, action) => { + return { + ...state, + router: action.payload + }; + } +}, {}); diff --git a/frontend/src/state/reducers/index.js b/frontend/src/state/reducers/index.js new file mode 100644 index 00000000..935f2b24 --- /dev/null +++ b/frontend/src/state/reducers/index.js @@ -0,0 +1,11 @@ +const Redux = require('redux'); + +const { + combineReducers +} = Redux; + +module.exports = () => { + return combineReducers({ + app: require('./app.js') + }); +}; diff --git a/frontend/src/state/store.js b/frontend/src/state/store.js new file mode 100644 index 00000000..16dd22d8 --- /dev/null +++ b/frontend/src/state/store.js @@ -0,0 +1,26 @@ +const createLogger = require('redux-logger'); +const createReducer = require('./reducers'); +const enableBatching = require('redux-batched-actions').enableBatching; +const promiseMiddleware = require('redux-promise-middleware').default; +const redux = require('redux'); +const thunk = require('redux-thunk').default; + +const { + createStore, + compose, + applyMiddleware +} = redux; + +module.exports = (state = Object.freeze({})) => { + return createStore( + enableBatching(createReducer()), + state, + compose( + applyMiddleware( + createLogger(), + promiseMiddleware(), + thunk + ) + ) + ); +}; diff --git a/frontend/src/state/thunks/app.js b/frontend/src/state/thunks/app.js new file mode 100644 index 00000000..831133ae --- /dev/null +++ b/frontend/src/state/thunks/app.js @@ -0,0 +1,15 @@ +const transitionTo = (pathname) => (dispatch, getState) => { + const { + app: { + router: { + transitionTo + } + } + } = getState(); + + return transitionTo(pathname); +}; + +module.exports = { + transitionTo +}; diff --git a/frontend/src/state/thunks/index.js b/frontend/src/state/thunks/index.js new file mode 100644 index 00000000..7d61673d --- /dev/null +++ b/frontend/src/state/thunks/index.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('./app') +}; diff --git a/frontend/static/.gitkeep b/frontend/static/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/frontend/static/index.html b/frontend/static/index.html new file mode 100644 index 00000000..d36d2c24 --- /dev/null +++ b/frontend/static/index.html @@ -0,0 +1,10 @@ + + + + React Boilerplate + + +
+ + + diff --git a/frontend/test/index.js b/frontend/test/index.js new file mode 100644 index 00000000..adde0c48 --- /dev/null +++ b/frontend/test/index.js @@ -0,0 +1,25 @@ +const test = require('ava'); +const enzyme = require('enzyme'); +const React = require('react'); + +const { + shallow +} = enzyme; + +test('renders without exploding', (t) => { + const App = require('../src/containers/app'); + const wrapper = shallow(); + t.deepEqual(wrapper.length, 1); +}); + +test('renders without exploding', (t) => { + const Home = require('../src/containers/home'); + const wrapper = shallow(); + t.deepEqual(wrapper.length, 1); +}); + +test('renders without exploding', (t) => { + const NotFound = require('../src/containers/not-found'); + const wrapper = shallow(); + t.deepEqual(wrapper.length, 1); +}); diff --git a/frontend/webpack/config.js b/frontend/webpack/config.js new file mode 100644 index 00000000..d07044da --- /dev/null +++ b/frontend/webpack/config.js @@ -0,0 +1,39 @@ +const pkg = require('../package.json'); +const webpack = require('webpack'); +const path = require('path'); + +module.exports = { + context: path.join(__dirname, '../src'), + output: { + path: path.join(__dirname, '../static'), + publicPath: '/static/', + filename: 'bundle.js' + }, + plugins: [ + new webpack.NoErrorsPlugin(), + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: JSON.stringify(process.env['NODE_ENV'] || 'development'), + APP_NAME: JSON.stringify(pkg.name), + APP_VERSION: JSON.stringify(pkg.version) + } + }) + ], + module: { + loaders: [{ + test: /js?$/, + exclude: /node_modules/, + include: [ + path.join(__dirname, '../src') + ], + loaders: ['babel'] + }, { + test: /\.json?$/, + exclude: /node_modules/, + include: [ + path.join(__dirname, '../src') + ], + loaders: ['json'] + }] + } +}; diff --git a/frontend/webpack/development.js b/frontend/webpack/development.js new file mode 100644 index 00000000..b657d827 --- /dev/null +++ b/frontend/webpack/development.js @@ -0,0 +1,30 @@ +const graphql = require('../../cloudapi-graphql/src/endpoint'); +const config = require('./config.js'); +const webpack = require('webpack'); + +const devServer = { + hot: true, + compress: true, + lazy: false, + publicPath: config.output.publicPath, + setup: (app) => { + app.use('/graphql', graphql); + }, + historyApiFallback: { + index: './static/index.html' + } +}; + +module.exports = Object.assign(config, { + entry: [ + 'react-hot-loader/patch', + 'webpack-dev-server/client?http://localhost:8080', + 'webpack/hot/only-dev-server', + './index.js' + ], + plugins: config.plugins.concat([ + new webpack.HotModuleReplacementPlugin() + ]), + devtool: 'source-map', + devServer +}); diff --git a/frontend/webpack/index.js b/frontend/webpack/index.js new file mode 100644 index 00000000..f1cf1a92 --- /dev/null +++ b/frontend/webpack/index.js @@ -0,0 +1,2 @@ +const NODE_ENV = process.env['NODE_ENV'] || 'development'; +module.exports = require(`./${NODE_ENV}`); diff --git a/frontend/webpack/production.js b/frontend/webpack/production.js new file mode 100644 index 00000000..7d1a0c48 --- /dev/null +++ b/frontend/webpack/production.js @@ -0,0 +1,22 @@ +const config = require('./config.js'); +const webpack = require('webpack'); + +module.exports = Object.assign(config, { + entry: [ + './index.js' + ], + plugins: config.plugins.concat([ + new webpack.optimize.DedupePlugin(), + new webpack.optimize.OccurrenceOrderPlugin(true), + new webpack.optimize.UglifyJsPlugin() + ]), + devtool: 'eval' +}); + +/* + * Maybe add in the future: + * - https://github.com/lettertwo/appcache-webpack-plugin + * - https://github.com/NekR/offline-plugin + * - https://github.com/goldhand/sw-precache-webpack-plugin + * - https://github.com/Klathmon/imagemin-webpack-plugin + */