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
+ */