1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

add strategies

This commit is contained in:
sveisvei 2016-10-24 18:32:50 +02:00 committed by Ivar Conradi Østhus
parent c4adff4a2d
commit 3003cc87f0
17 changed files with 456 additions and 90 deletions

View File

@ -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 (
<div>
<AddFeatureToggleUI
strategies={this.props.strategies}
featureToggle={this.state}
updateField={this.updateField}
addStrategy={this.addStrategy}
removeStrategy={this.removeStrategy}
onSubmit={this.onSubmit}
onCancel={this.onCancel}
/>
</div>
<AddFeatureToggleUI
strategies={this.props.strategies}
featureToggle={this.state}
updateField={this.updateField}
addStrategy={this.addStrategy}
removeStrategy={this.removeStrategy}
onSubmit={this.onSubmit}
onCancel={this.onCancel}
/>
);
}
}
export default connect(mapStateToProps)(AddFeatureToggle);
const mapStateToProps = (state) => ({
strategies: state.strategies.get('list').toArray(),
});
export default connect(mapStateToProps, { fetchStrategies })(AddFeatureToggle);

View File

@ -33,7 +33,7 @@ class AddFeatureToggleStrategy extends React.Component {
renderAddLink () {
return (
<div>
<a href="" onClick={this.showConfigure}>Add strategy</a>
<a href="#" onClick={this.showConfigure}>Add strategy</a>
</div>
);
}

View File

@ -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));
}

View File

@ -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 (<div>{gerArrayWithEntries(num).map((v, i) => {
const key = `${PARAM_PREFIX}${i + 1}`;
return (
<Input
type="text"
label={`Parameter name ${i + 1}`}
name={key} key={key}
onChange={(value) => setValue(key, value)}
value={input[key]} />
);
})}</div>);
};
const AddStrategy = ({
input,
setValue,
incValue,
// clear,
onCancel,
onSubmit,
}) => (
<form onSubmit={onSubmit(input)}>
<section>
<Input type="text" label="Strategy name"
name="name" required
pattern="^[0-9a-zA-Z\.\-]+$"
onChange={(value) => setValue('name', value)}
value={input.name}
/>
<Input type="text" multiline label="Description"
name="description"
onChange={(value) => setValue('description', value)}
value={input.description}
/>
</section>
<section>
{genParams(input, input._params, setValue)}
<Button icon="add" accent label="Add parameter" onClick={(e) => {
e.preventDefault();
incValue('_params');
}}/>
</section>
<br />
<hr />
<section>
<Button type="submit" raised primary label="Create" />
&nbsp;
<Button type="cancel" raised label="Cancel" onClick={onCancel} />
</section>
</form>
);
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);

View File

@ -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;

View File

@ -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 => (
<span className={style.label} key={k}>{k}</span>
));
}
render () {
const { strategies, removeStrategy } = this.props;
return (
<List ripple >
<ListSubHeader caption="Strategies" />
{strategies.length > 0 ? strategies.map((strategy, i) => {
const actions = this.getParameterMap(strategy).concat([
<button className={style['non-style-button']} key="1" onClick={() => removeStrategy(strategy)}>
<FontIcon value="delete" />
</button>,
]);
return (
<ListItem key={i} rightActions={actions}
caption={strategy.name}
legend={strategy.description} />
);
}) : <ListItem caption="No entries" />}
<ListDivider />
<ListItem
onClick={() => this.context.router.push('/strategies/create')}
caption="Add" legend="new strategy" leftIcon="add" />
</List>
);
}
}
export default StrategiesListComponent;

View File

@ -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;
}

View File

@ -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 (
<div>
<form onSubmit={this.onSubmit}>
New Strategy:
<Button type="submit" raised primary label="Create" />
</form>
</div>
);
}
}
export default connect()(AddStrategy);

View File

@ -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,
};

View File

@ -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(
<Route path="/features/create" component={CreateFeatureToggle} />
<Route path="/features/edit/:name" component={EditFeatureToggle} />
<Route path="/strategies" component={Strategies} />
<Route path="/strategies/create" component={CreateStrategies} />
<Route path="/history" component={HistoryPage} />
<Route path="/archive" component={Archive} />
</Route>

View File

@ -0,0 +1,4 @@
import React from 'react';
import AddStrategies from '../../component/strategies/add-strategy';
export default () => (<AddStrategies />);

View File

@ -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 (
<div>
<h1>Strategies</h1>
</div>
);
}
};
export default () => (<Strategies />);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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)));
};
}

View File

@ -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;
}