diff --git a/packages/unleash-frontend-next/src/component/feature/AddFeatureToggle.jsx b/packages/unleash-frontend-next/src/component/feature/AddFeatureToggle.jsx index f9371286f7..cc798e02ec 100644 --- a/packages/unleash-frontend-next/src/component/feature/AddFeatureToggle.jsx +++ b/packages/unleash-frontend-next/src/component/feature/AddFeatureToggle.jsx @@ -2,10 +2,7 @@ import React, { PropTypes } from 'react'; import { connect } from 'react-redux'; import { createFeatureToggles } from '../../store/feature-actions'; import AddFeatureToggleUI from './AddFeatureToggleUI'; - -const mapStateToProps = (state) => ({ - strategies: state.strategies.toJS(), -}); +import { fetchStrategies } from '../../store/strategy-actions'; class AddFeatureToggle extends React.Component { constructor () { @@ -57,21 +54,27 @@ class AddFeatureToggle extends React.Component { this.setState({ strategies }); } + componentDidMount () { + this.props.fetchStrategies(); + } + render () { return ( -
- -
+ ); } } -export default connect(mapStateToProps)(AddFeatureToggle); +const mapStateToProps = (state) => ({ + strategies: state.strategies.get('list').toArray(), +}); + +export default connect(mapStateToProps, { fetchStrategies })(AddFeatureToggle); diff --git a/packages/unleash-frontend-next/src/component/feature/AddFeatureToggleStrategy.jsx b/packages/unleash-frontend-next/src/component/feature/AddFeatureToggleStrategy.jsx index ed6fd46a9e..b914b90d73 100644 --- a/packages/unleash-frontend-next/src/component/feature/AddFeatureToggleStrategy.jsx +++ b/packages/unleash-frontend-next/src/component/feature/AddFeatureToggleStrategy.jsx @@ -33,7 +33,7 @@ class AddFeatureToggleStrategy extends React.Component { renderAddLink () { return (
- Add strategy + Add strategy
); } diff --git a/packages/unleash-frontend-next/src/component/input-helpers.js b/packages/unleash-frontend-next/src/component/input-helpers.js new file mode 100644 index 0000000000..2200669156 --- /dev/null +++ b/packages/unleash-frontend-next/src/component/input-helpers.js @@ -0,0 +1,32 @@ +import { createInc, createClear, createSet } from '../store/input-actions'; + +export function createMapper (id, prepare = (v) => v) { + return (state) => { + let input; + if (state.input.has(id)) { + input = state.input.get(id).toJS(); + } else { + input = {}; + } + + return prepare({ + input, + }, state); + }; +} + +export function createActions (id, prepare = (v) => v) { + return (dispatch) => (prepare({ + clear () { + dispatch(createClear({ id })); + }, + + setValue (key, value) { + dispatch(createSet({ id, key, value })); + }, + + incValue (key) { + dispatch(createInc({ id, key })); + }, + }, dispatch)); +} diff --git a/packages/unleash-frontend-next/src/component/strategies/add-strategy.jsx b/packages/unleash-frontend-next/src/component/strategies/add-strategy.jsx new file mode 100644 index 0000000000..2ffeb4b667 --- /dev/null +++ b/packages/unleash-frontend-next/src/component/strategies/add-strategy.jsx @@ -0,0 +1,111 @@ +import React, { PropTypes } from 'react'; +import { connect } from 'react-redux'; + +import Input from 'react-toolbox/lib/input'; +import Button from 'react-toolbox/lib/button'; + +import { createMapper, createActions } from '../input-helpers'; +import { createStrategy } from '../../store/strategy-actions'; + +function gerArrayWithEntries (num) { + return Array.from(Array(num)); +} +const PARAM_PREFIX = 'param_'; +const genParams = (input, num = 0, setValue) => { + return (
{gerArrayWithEntries(num).map((v, i) => { + const key = `${PARAM_PREFIX}${i + 1}`; + return ( + setValue(key, value)} + value={input[key]} /> + ); + })}
); +}; + +const AddStrategy = ({ + input, + setValue, + incValue, + // clear, + onCancel, + onSubmit, +}) => ( +
+
+ setValue('name', value)} + value={input.name} + /> + setValue('description', value)} + value={input.description} + /> +
+ +
+ {genParams(input, input._params, setValue)} +
+ +
+
+ +
+
+
+); + +AddStrategy.propTypes = { + input: PropTypes.object, + setValue: PropTypes.func, + incValue: PropTypes.func, + clear: PropTypes.func, + onCancel: PropTypes.func, + onSubmit: PropTypes.func, +}; + +const ID = 'add-strategy'; + +const actions = createActions(ID, (methods, dispatch) => { + methods.onSubmit = (input) => ( + (e) => { + e.preventDefault(); + + const parametersTemplate = {}; + Object.keys(input).forEach(key => { + if (key.startsWith(PARAM_PREFIX)) { + parametersTemplate[input[key]] = 'string'; + } + }); + input.parametersTemplate = parametersTemplate; + + createStrategy(input)(dispatch) + .then(() => methods.clear()) + // somewhat quickfix / hacky to go back.. + .then(() => window.history.back()); + } + ); + + methods.onCancel = (e) => { + e.preventDefault(); + // somewhat quickfix / hacky to go back.. + window.history.back(); + }; + + + return methods; +}); + +export default connect(createMapper(ID), actions)(AddStrategy); diff --git a/packages/unleash-frontend-next/src/component/strategies/index.jsx b/packages/unleash-frontend-next/src/component/strategies/index.jsx new file mode 100644 index 0000000000..aea177acf2 --- /dev/null +++ b/packages/unleash-frontend-next/src/component/strategies/index.jsx @@ -0,0 +1,24 @@ +import { connect } from 'react-redux'; +import StrategiesListComponent from './list.jsx'; +import { fetchStrategies, removeStrategy } from '../../store/strategy-actions'; + +const mapStateToProps = (state) => { + const list = state.strategies.get('list').toArray(); + + return { + strategies: list, + }; +}; + +const mapDispatchToProps = (dispatch) => ({ + removeStrategy: (strategy) => { + if (window.confirm('Are you sure you want to remove this strategy?')) { // eslint-disable-line no-alert + removeStrategy(strategy)(dispatch); + } + }, + fetchStrategies: () => fetchStrategies()(dispatch), +}); + +const StrategiesListContainer = connect(mapStateToProps, mapDispatchToProps)(StrategiesListComponent); + +export default StrategiesListContainer; diff --git a/packages/unleash-frontend-next/src/component/strategies/list.jsx b/packages/unleash-frontend-next/src/component/strategies/list.jsx new file mode 100644 index 0000000000..f0145cd0fe --- /dev/null +++ b/packages/unleash-frontend-next/src/component/strategies/list.jsx @@ -0,0 +1,52 @@ +import React, { Component } from 'react'; +import { List, ListItem, ListSubHeader, ListDivider } from 'react-toolbox/lib/list'; +import FontIcon from 'react-toolbox/lib/font_icon'; +import style from './strategies.scss'; + +class StrategiesListComponent extends Component { + + static contextTypes = { + router: React.PropTypes.object, + } + + componentDidMount () { + this.props.fetchStrategies(); + } + + getParameterMap ({ parametersTemplate }) { + return Object.keys(parametersTemplate || {}).map(k => ( + {k} + )); + } + + render () { + const { strategies, removeStrategy } = this.props; + + return ( + + + {strategies.length > 0 ? strategies.map((strategy, i) => { + const actions = this.getParameterMap(strategy).concat([ + , + ]); + + + return ( + + ); + }) : } + + this.context.router.push('/strategies/create')} + caption="Add" legend="new strategy" leftIcon="add" /> + + ); + } +} + + +export default StrategiesListComponent; diff --git a/packages/unleash-frontend-next/src/component/strategies/strategies.scss b/packages/unleash-frontend-next/src/component/strategies/strategies.scss new file mode 100644 index 0000000000..4f726475a9 --- /dev/null +++ b/packages/unleash-frontend-next/src/component/strategies/strategies.scss @@ -0,0 +1,18 @@ +.label { + font-size: 75%; + color: #aaa; + padding: 4px 5px 3px 5px; + background-color: #ddd; + border-radius: 5px; + margin-right: 5px; +} + + +.non-style-button { + cursor: pointer; + color: #757575; + background: none; + border: 0; + padding: 0; + margin: 0; +} diff --git a/packages/unleash-frontend-next/src/component/strategy/AddStrategy.jsx b/packages/unleash-frontend-next/src/component/strategy/AddStrategy.jsx deleted file mode 100644 index c6d009d7d8..0000000000 --- a/packages/unleash-frontend-next/src/component/strategy/AddStrategy.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, { PropTypes } from 'react'; -import { connect } from 'react-redux'; -import { Button } from 'react-toolbox'; - - -class AddStrategy extends React.Component { - constructor () { - super(); - this.state = { - name: '', - parameters: {}, - }; - } - - static propTypes () { - return { - StrategyDefinitions: PropTypes.array.isRequired, - }; - } - - static contextTypes = { - router: React.PropTypes.object, - } - - onSubmit = (evt) => { - evt.preventDefault(); - }; - - addStrategy = (evt) => { - evt.preventDefault(); - } - - handleChange = (key, value) => { - const change = {}; - change[key] = value; - - const newState = Object.assign({}, this.state, change); - this.setState(newState); - }; - - render () { - return ( -
-
- New Strategy: -
- ); - } -} - -export default connect()(AddStrategy); diff --git a/packages/unleash-frontend-next/src/data/strategy-api.js b/packages/unleash-frontend-next/src/data/strategy-api.js new file mode 100644 index 0000000000..f88dde5ec8 --- /dev/null +++ b/packages/unleash-frontend-next/src/data/strategy-api.js @@ -0,0 +1,42 @@ +const URI = '/strategies'; + +const headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', +}; + +function throwIfNotSuccess (response) { + if (!response.ok) { + let error = new Error('API call failed'); + error.status = response.status; + throw error; + } + return response; +} + +function fetchAll () { + return fetch(URI) + .then(throwIfNotSuccess) + .then(response => response.json()); +} + +function create (strategy) { + return fetch(URI, { + method: 'POST', + headers, + body: JSON.stringify(strategy), + }).then(throwIfNotSuccess); +} + +function remove (strategy) { + return fetch(`${URI}/${strategy.name}`, { + method: 'DELETE', + headers, + }).then(throwIfNotSuccess); +} + +module.exports = { + fetchAll, + create, + remove, +}; diff --git a/packages/unleash-frontend-next/src/index.jsx b/packages/unleash-frontend-next/src/index.jsx index a16fa4e1e2..cc64462701 100644 --- a/packages/unleash-frontend-next/src/index.jsx +++ b/packages/unleash-frontend-next/src/index.jsx @@ -12,6 +12,7 @@ import Features from './page/features'; import CreateFeatureToggle from './page/features/create'; import EditFeatureToggle from './page/features/edit'; import Strategies from './page/strategies'; +import CreateStrategies from './page/strategies/create'; import HistoryPage from './page/history'; import Archive from './page/archive'; @@ -31,6 +32,7 @@ ReactDOM.render( + diff --git a/packages/unleash-frontend-next/src/page/strategies/create.js b/packages/unleash-frontend-next/src/page/strategies/create.js new file mode 100644 index 0000000000..0a8dd7bfe3 --- /dev/null +++ b/packages/unleash-frontend-next/src/page/strategies/create.js @@ -0,0 +1,4 @@ +import React from 'react'; +import AddStrategies from '../../component/strategies/add-strategy'; + +export default () => (); diff --git a/packages/unleash-frontend-next/src/page/strategies/index.js b/packages/unleash-frontend-next/src/page/strategies/index.js index 7c493453fa..0d224abb06 100644 --- a/packages/unleash-frontend-next/src/page/strategies/index.js +++ b/packages/unleash-frontend-next/src/page/strategies/index.js @@ -1,11 +1,4 @@ -import React, { Component } from 'react'; +import React from 'react'; +import Strategies from '../../component/strategies'; -export default class Strategies extends Component { - render () { - return ( -
-

Strategies

-
- ); - } -}; +export default () => (); diff --git a/packages/unleash-frontend-next/src/store/index.js b/packages/unleash-frontend-next/src/store/index.js index f4cd0f8de7..bfd7a780fd 100644 --- a/packages/unleash-frontend-next/src/store/index.js +++ b/packages/unleash-frontend-next/src/store/index.js @@ -1,10 +1,12 @@ import { combineReducers } from 'redux'; import features from './feature-store'; import strategies from './strategy-store'; +import input from './input-store'; const unleashStore = combineReducers({ features, strategies, + input, }); export default unleashStore; diff --git a/packages/unleash-frontend-next/src/store/input-actions.js b/packages/unleash-frontend-next/src/store/input-actions.js new file mode 100644 index 0000000000..d8c2f96a99 --- /dev/null +++ b/packages/unleash-frontend-next/src/store/input-actions.js @@ -0,0 +1,11 @@ +export const actions = { + SET_VALUE: 'SET_VALUE', + INCREMENT_VALUE: 'INCREMENT_VALUE', + CLEAR: 'CLEAR', +}; + +export const createInc = ({ id, key }) => ({ type: actions.INCREMENT_VALUE, id, key }); +export const createSet = ({ id, key, value }) => ({ type: actions.SET_VALUE, id, key, value }); +export const createClear = ({ id }) => ({ type: actions.CLEAR, id }); + +export default actions; diff --git a/packages/unleash-frontend-next/src/store/input-store.js b/packages/unleash-frontend-next/src/store/input-store.js new file mode 100644 index 0000000000..109417a08d --- /dev/null +++ b/packages/unleash-frontend-next/src/store/input-store.js @@ -0,0 +1,54 @@ +import { Map as $Map } from 'immutable'; +import actions from './input-actions'; + +function getInitState () { + return new $Map(); +} + +function assertId (state, id) { + if (!state.has(id)) { + return state.set(id, new $Map({ inputId: id })); + } + return state; +} + +function setKeyValue (state, { id, key, value }) { + state = assertId(state, id); + return state.setIn([id, key], value); +} + +function increment (state, { id, key }) { + state = assertId(state, id); + return state.updateIn([id, key], (value = 0) => value + 1); +} + +function clear (state, { id }) { + if (state.has(id)) { + return state.remove(id); + } + return state; +} + +const inputState = (state = getInitState(), action) => { + + if (!action.id) { + return state; + } + + switch (action.type) { + case actions.SET_VALUE: + if (actions.key != null && actions.value != null) { + throw new Error('Missing required key / value'); + } + return setKeyValue(state, action); + case actions.INCREMENT_VALUE: + return increment(state, action); + case actions.CLEAR: + return clear(state, action); + default: + // console.log('TYPE', action.type, action); + return state; + } +}; + +export default inputState; diff --git a/packages/unleash-frontend-next/src/store/strategy-actions.js b/packages/unleash-frontend-next/src/store/strategy-actions.js new file mode 100644 index 0000000000..cc006c7084 --- /dev/null +++ b/packages/unleash-frontend-next/src/store/strategy-actions.js @@ -0,0 +1,63 @@ +import api from '../data/strategy-api'; + +export const ADD_STRATEGY = 'ADD_STRATEGY'; +export const REMOVE_STRATEGY = 'REMOVE_STRATEGY'; +export const REQUEST_STRATEGIES = 'REQUEST_STRATEGIES'; +export const START_CREATE_STRATEGY = 'START_CREATE_STRATEGY'; +export const RECEIVE_STRATEGIES = 'RECEIVE_STRATEGIES'; +export const ERROR_RECEIVE_STRATEGIES = 'ERROR_RECEIVE_STRATEGIES'; +export const ERROR_CREATING_STRATEGY = 'ERROR_CREATING_STRATEGY'; + +const addStrategy = (strategy) => ({ type: ADD_STRATEGY, strategy }); +const createRemoveStrategy = (strategy) => ({ type: REMOVE_STRATEGY, strategy }); + +const errorCreatingStrategy = (statusCode) => ({ + type: ERROR_CREATING_STRATEGY, + statusCode, +}); + +const startRequest = () => ({ type: REQUEST_STRATEGIES }); + + +const receiveStrategies = (json) => ({ + type: RECEIVE_STRATEGIES, + value: json.strategies, +}); + +const startCreate = () => ({ type: START_CREATE_STRATEGY }); + +const errorReceiveStrategies = (statusCode) => ({ + type: ERROR_RECEIVE_STRATEGIES, + statusCode, +}); + +export function fetchStrategies () { + return dispatch => { + dispatch(startRequest()); + + return api.fetchAll() + .then(json => dispatch(receiveStrategies(json))) + .catch(error => dispatch(errorReceiveStrategies(error))); + }; +} + +export function createStrategy (strategy) { + return dispatch => { + dispatch(startCreate()); + + return api.create(strategy) + .then(() => dispatch(addStrategy(strategy))) + .catch(error => dispatch(errorCreatingStrategy(error))); + }; +} + + +export function removeStrategy (strategy) { + return dispatch => { + return api.remove(strategy) + .then(() => dispatch(createRemoveStrategy(strategy))) + .catch(error => dispatch(errorCreatingStrategy(error))); + }; +} + + diff --git a/packages/unleash-frontend-next/src/store/strategy-store.js b/packages/unleash-frontend-next/src/store/strategy-store.js index 2ee40754d8..72900eaf22 100644 --- a/packages/unleash-frontend-next/src/store/strategy-store.js +++ b/packages/unleash-frontend-next/src/store/strategy-store.js @@ -1,18 +1,26 @@ import { List, Map as $Map } from 'immutable'; +import { RECEIVE_STRATEGIES, REMOVE_STRATEGY, ADD_STRATEGY } from './strategy-actions'; -const init = new List([ - new $Map({ name: 'default', description: 'Default on/off strategy' }), - new $Map( - { - name: 'ActiveForUserWithEmail', - description: 'Active for user with specified email', - parametersTemplate: { emails: 'string', ids: 'string' }, - }), -]); +function getInitState () { + return new $Map({ list: new List() }); +} +function removeStrategy (state, action) { + const indexToRemove = state.get('list').indexOf(action.strategy); + if (indexToRemove !== -1) { + return state.update('list', (list) => list.remove(indexToRemove)); + } + return state; +} -const strategies = (state = init, action) => { +const strategies = (state = getInitState(), action) => { switch (action.type) { + case RECEIVE_STRATEGIES: + return state.set('list', new List(action.value)); + case REMOVE_STRATEGY: + return removeStrategy(state, action); + case ADD_STRATEGY: + return state.update('list', (list) => list.push(action.strategy)); default: return state; }