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 (
);
}
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,
+}) => (
+
+);
+
+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 (
-
-
-
- );
- }
-}
-
-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;
}