From 77c3fbfb0210d8492ed9f3640c5457848cce0117 Mon Sep 17 00:00:00 2001 From: Ivar Date: Wed, 28 Sep 2016 23:02:23 +0200 Subject: [PATCH] Added support for fetching data. - use "json-server" for serving mocked API responses. - setup redux to fetch responses when featureToggle list is mounted. --- packages/unleash-frontend-next/.eslintignore | 2 + packages/unleash-frontend-next/README.md | 14 +++++ packages/unleash-frontend-next/mock-api.json | 43 ++++++++++++++ packages/unleash-frontend-next/package.json | 10 +++- packages/unleash-frontend-next/src/.eslintrc | 16 +----- .../unleash-frontend-next/src/action/index.js | 11 ---- .../component/feature/AddFeatureToggle.jsx | 9 ++- .../src/component/feature/Feature.jsx | 8 ++- .../src/component/feature/FeatureList.jsx | 44 +++++++++------ .../feature/FeatureListContainer.jsx | 12 ++-- packages/unleash-frontend-next/src/index.jsx | 10 +++- .../src/store/actions.js | 56 +++++++++++++++++++ .../src/store/features.js | 24 +++++--- .../unleash-frontend-next/webpack.config.js | 8 +++ 14 files changed, 203 insertions(+), 64 deletions(-) create mode 100644 packages/unleash-frontend-next/.eslintignore create mode 100644 packages/unleash-frontend-next/README.md create mode 100644 packages/unleash-frontend-next/mock-api.json delete mode 100644 packages/unleash-frontend-next/src/action/index.js create mode 100644 packages/unleash-frontend-next/src/store/actions.js diff --git a/packages/unleash-frontend-next/.eslintignore b/packages/unleash-frontend-next/.eslintignore new file mode 100644 index 0000000000..d1c1dbe227 --- /dev/null +++ b/packages/unleash-frontend-next/.eslintignore @@ -0,0 +1,2 @@ +node_modules +bundle.js diff --git a/packages/unleash-frontend-next/README.md b/packages/unleash-frontend-next/README.md new file mode 100644 index 0000000000..9e0a26fa5b --- /dev/null +++ b/packages/unleash-frontend-next/README.md @@ -0,0 +1,14 @@ +## Start developing: + +1. start mock-api: + +```bash +npm run start:api +``` + +2. start webpack-dev-server with hot-reload: +```bash +npm run start +``` + +Happy coding! diff --git a/packages/unleash-frontend-next/mock-api.json b/packages/unleash-frontend-next/mock-api.json new file mode 100644 index 0000000000..18ac7346df --- /dev/null +++ b/packages/unleash-frontend-next/mock-api.json @@ -0,0 +1,43 @@ +{ + "features": { + "version": 1, + "features": [ + { + "name": "Feature.A", + "description": "lorem ipsum", + "enabled": false, + "strategies": [ + { + "name": "default", + "parameters": {} + } + ], + "strategy": "default", + "parameters": {} + }, + { + "name": "Feature.B", + "description": "lorem ipsum", + "enabled": true, + "strategies": [ + { + "name": "ActiveForUserWithId", + "parameters": { + "userIdList": "123,221,998" + } + }, + { + "name": "GradualRolloutRandom", + "parameters": { + "percentage": "10" + } + } + ], + "strategy": "ActiveForUserWithId", + "parameters": { + "userIdList": "123,221,998" + } + } + ] + } +} diff --git a/packages/unleash-frontend-next/package.json b/packages/unleash-frontend-next/package.json index 7d8c874830..5af42c2565 100644 --- a/packages/unleash-frontend-next/package.json +++ b/packages/unleash-frontend-next/package.json @@ -24,6 +24,8 @@ "scripts": { "build": "webpack -p", "start": "webpack-dev-server --config webpack.config.js --hot --progress --colors --port 3000", + "start:api": "json-server --watch mock-api.json -p 3001", + "lint": "eslint . --ext=js,jsx", "test": "echo 'no test'", "test:ci": "npm run test", "prepublish": "npm run build" @@ -38,7 +40,8 @@ "react-redux": "^4.4.5", "react-router": "^2.8.0", "react-toolbox": "^1.2.1", - "redux": "^3.6.0" + "redux": "^3.6.0", + "redux-thunk": "^2.1.0" }, "devDependencies": { "babel-core": "^6.14.0", @@ -49,7 +52,12 @@ "babel-preset-stage-0": "^6.5.0", "babel-preset-stage-2": "^6.13.0", "css-loader": "^0.25.0", + "eslint": "^3.4.0", + "eslint-config-finn": "1.0.0-alpha.11", + "eslint-config-finn-react": "^1.0.0-alpha.2", + "eslint-plugin-react": "^6.2.0", "extract-text-webpack-plugin": "^1.0.1", + "json-server": "^0.8.21", "node-sass": "~3.7.0", "postcss-loader": "^0.13.0", "redux-devtools": "^3.3.1", diff --git a/packages/unleash-frontend-next/src/.eslintrc b/packages/unleash-frontend-next/src/.eslintrc index b2d0257498..23031855c4 100644 --- a/packages/unleash-frontend-next/src/.eslintrc +++ b/packages/unleash-frontend-next/src/.eslintrc @@ -1,24 +1,12 @@ { "extends": [ "finn", - "finn-react" + "finn-react", + "finn/es-modules" ], "env": { "browser": true, "commonjs": true, "es6": true - }, - "rules": { - "react/sort-comp": "off" - }, - "ecmaFeatures": { - "sourceType": "module" - }, - "parserOptions": { - "sourceType": "module", - "ecmaFeatures": { - "jsx": true, - "experimentalObjectRestSpread": true - } } } diff --git a/packages/unleash-frontend-next/src/action/index.js b/packages/unleash-frontend-next/src/action/index.js deleted file mode 100644 index 69e53f0e88..0000000000 --- a/packages/unleash-frontend-next/src/action/index.js +++ /dev/null @@ -1,11 +0,0 @@ -let nextFeatureId = 0; -export const addFeatureToggle = (featureName) => ({ - type: 'ADD_FEATURE_TOGGLE', - id: nextFeatureId++, - featureName, -}); - -export const toggleFeature = (id) => ({ - type: 'TOGGLE_FEATURE_TOGGLE', - id, -}); diff --git a/packages/unleash-frontend-next/src/component/feature/AddFeatureToggle.jsx b/packages/unleash-frontend-next/src/component/feature/AddFeatureToggle.jsx index 1f569edf2c..2fb060e463 100644 --- a/packages/unleash-frontend-next/src/component/feature/AddFeatureToggle.jsx +++ b/packages/unleash-frontend-next/src/component/feature/AddFeatureToggle.jsx @@ -1,6 +1,6 @@ -import React from 'react'; +import { React, PropTypes } from 'react'; import { connect } from 'react-redux'; -import { addFeatureToggle } from '../../action'; +import { addFeatureToggle } from '../../store/actions'; let AddFeatureToggle = ({ dispatch }) => { @@ -28,4 +28,9 @@ let AddFeatureToggle = ({ dispatch }) => { }; AddFeatureToggle = connect()(AddFeatureToggle); +AddFeatureToggle.propTypes = { + dispatch: PropTypes.func.isRequired, +}; + + export default AddFeatureToggle; diff --git a/packages/unleash-frontend-next/src/component/feature/Feature.jsx b/packages/unleash-frontend-next/src/component/feature/Feature.jsx index 0b41e9c9f0..cb12dfd6c9 100644 --- a/packages/unleash-frontend-next/src/component/feature/Feature.jsx +++ b/packages/unleash-frontend-next/src/component/feature/Feature.jsx @@ -1,8 +1,10 @@ +/* eslint no-shadow: ["error", { "allow": ["name"] }]*/ + import React, { PropTypes } from 'react'; -const Feature = ({ onClick, featureName, enabled }) => ( +const Feature = ({ onClick, name, enabled }) => (
  • - {featureName} is {enabled ? 'enabled ' : 'disabled '} + {name} is {enabled ? 'enabled ' : 'disabled '} toggle
  • ); @@ -10,7 +12,7 @@ const Feature = ({ onClick, featureName, enabled }) => ( Feature.propTypes = { onClick: PropTypes.func.isRequired, enabled: PropTypes.bool.isRequired, - featureName: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, }; export default Feature; diff --git a/packages/unleash-frontend-next/src/component/feature/FeatureList.jsx b/packages/unleash-frontend-next/src/component/feature/FeatureList.jsx index 55347eaab3..324f44ae96 100644 --- a/packages/unleash-frontend-next/src/component/feature/FeatureList.jsx +++ b/packages/unleash-frontend-next/src/component/feature/FeatureList.jsx @@ -1,21 +1,33 @@ import React, { PropTypes } from 'react'; import Feature from './Feature'; -const FeatureList = ({ features, onFeatureClick }) => ( - -); +export default class FeatureList extends React.Component { + constructor () { + super(); + } -FeatureList.propTypes = { - onFeatureClick: PropTypes.func.isRequired, - features: PropTypes.array.isRequired, -}; + static propTypes () { + return { + onFeatureClick: PropTypes.func.isRequired, + features: PropTypes.array.isRequired, + fetchFeatureToggles: PropTypes.array.isRequired, + }; + } -export default FeatureList; + componentDidMount () { + this.props.fetchFeatureToggles(); + } + + render () { + const onFeatureClick = this.props.onFeatureClick; + const features = this.props.features.map(featureToggle => + onFeatureClick(featureToggle.name)} + />); + + return ( + + ); + } +} diff --git a/packages/unleash-frontend-next/src/component/feature/FeatureListContainer.jsx b/packages/unleash-frontend-next/src/component/feature/FeatureListContainer.jsx index 416fa3a5fa..951a9514e0 100644 --- a/packages/unleash-frontend-next/src/component/feature/FeatureListContainer.jsx +++ b/packages/unleash-frontend-next/src/component/feature/FeatureListContainer.jsx @@ -1,17 +1,15 @@ import { connect } from 'react-redux'; -import { toggleFeature } from '../../action'; +import { toggleFeature, fetchFeatureToggles } from '../../store/actions'; import FeatureList from './FeatureList'; const mapStateToProps = (state) => ({ features: state.features, }); -const mapDispatchToProps = (dispatch) => ({ - onFeatureClick: (id) => { - dispatch(toggleFeature(id)); - }, -}); - +const mapDispatchToProps = { + onFeatureClick: toggleFeature, + fetchFeatureToggles, +}; const FeatureListContainer = connect( mapStateToProps, diff --git a/packages/unleash-frontend-next/src/index.jsx b/packages/unleash-frontend-next/src/index.jsx index badafc5a8c..7b13bbe94d 100644 --- a/packages/unleash-frontend-next/src/index.jsx +++ b/packages/unleash-frontend-next/src/index.jsx @@ -2,7 +2,8 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Router, Route, IndexRoute, hashHistory } from 'react-router'; import { Provider } from 'react-redux'; -import { createStore } from 'redux'; +import thunkMiddleware from 'redux-thunk'; +import { createStore, applyMiddleware } from 'redux'; import store from './store'; import App from './App'; @@ -12,7 +13,12 @@ import Strategies from './page/strategies'; import HistoryPage from './page/history'; import Archive from './page/archive'; -const unleashStore = createStore(store); +const unleashStore = createStore( + store, + applyMiddleware( + thunkMiddleware + ) +); ReactDOM.render( diff --git a/packages/unleash-frontend-next/src/store/actions.js b/packages/unleash-frontend-next/src/store/actions.js new file mode 100644 index 0000000000..e55e6538b9 --- /dev/null +++ b/packages/unleash-frontend-next/src/store/actions.js @@ -0,0 +1,56 @@ +export const ADD_FEATURE_TOGGLE = 'ADD_FEATURE_TOGGLE'; +export const TOGGLE_FEATURE_TOGGLE = 'TOGGLE_FEATURE_TOGGLE'; +export const REQUEST_FEATURE_TOGGLES = 'REQUEST_FEATURE_TOGGLES'; +export const RECEIVE_FEATURE_TOGGLES = 'RECEIVE_FEATURE_TOGGLES'; +export const ERROR_RECEIVE_FEATURE_TOGGLES = 'ERROR_RECEIVE_FEATURE_TOGGLES'; + +export const addFeatureToggle = (featureName) => ({ + type: 'ADD_FEATURE_TOGGLE', + name: featureName, +}); + +export const toggleFeature = (featureName) => ({ + type: 'TOGGLE_FEATURE_TOGGLE', + name: featureName, +}); + + +function requestFeatureToggles () { + return { + type: REQUEST_FEATURE_TOGGLES, + }; +} + +function receiveFeatureToggles (json) { + return { + type: RECEIVE_FEATURE_TOGGLES, + featureToggles: json.features.map(features => features), + receivedAt: Date.now(), + }; +} + +function errorReceiveFeatureToggles (statusCode) { + return { + type: ERROR_RECEIVE_FEATURE_TOGGLES, + statusCode, + receivedAt: Date.now(), + }; +} + +export function fetchFeatureToggles () { + return dispatch => { + dispatch(requestFeatureToggles()); + return fetch('/features') + .then(response => { + if (response.ok) { + return response.json(); + } else { + let error = new Error('failed fetching'); + error.status = response.status; + throw error; + } + }) + .then(json => dispatch(receiveFeatureToggles(json))) + .catch(error => dispatch(errorReceiveFeatureToggles(error))); + }; +} diff --git a/packages/unleash-frontend-next/src/store/features.js b/packages/unleash-frontend-next/src/store/features.js index e4a838ea43..04570fab96 100644 --- a/packages/unleash-frontend-next/src/store/features.js +++ b/packages/unleash-frontend-next/src/store/features.js @@ -1,13 +1,18 @@ +import { + ADD_FEATURE_TOGGLE, + TOGGLE_FEATURE_TOGGLE, + RECEIVE_FEATURE_TOGGLES, +} from './actions'; + const feature = (state = {}, action) => { switch (action.type) { - case 'ADD_FEATURE_TOGGLE': + case ADD_FEATURE_TOGGLE: return { - id: action.id, - featureName: action.featureName, + name: action.name, enabled: false, }; - case 'TOGGLE_FEATURE_TOGGLE': - if (state.id !== action.id) { + case TOGGLE_FEATURE_TOGGLE: + if (state.name !== action.name) { return state; } @@ -20,17 +25,20 @@ const feature = (state = {}, action) => { } }; -const features = (state = [{ id: 1, featureName: 'test', enabled: true }], action) => { +const features = (state = [], action) => { switch (action.type) { - case 'ADD_FEATURE_TOGGLE': + case ADD_FEATURE_TOGGLE: return [ ...state, feature(undefined, action), ]; - case 'TOGGLE_FEATURE_TOGGLE': + case TOGGLE_FEATURE_TOGGLE: return state.map(t => feature(t, action) ); + case RECEIVE_FEATURE_TOGGLES: { + return action.featureToggles; + } default: return state; } diff --git a/packages/unleash-frontend-next/webpack.config.js b/packages/unleash-frontend-next/webpack.config.js index 47c07c188e..a159e39a86 100644 --- a/packages/unleash-frontend-next/webpack.config.js +++ b/packages/unleash-frontend-next/webpack.config.js @@ -56,4 +56,12 @@ module.exports = { // stuff not in node_modules can be resolved here. }, + devServer: { + proxy: { + '/features': { + target: 'http://localhost:3001/', + secure: false, + }, + }, + }, };