mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
add strategies
This commit is contained in:
parent
c4adff4a2d
commit
3003cc87f0
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
@ -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" />
|
||||
|
||||
<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);
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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);
|
42
packages/unleash-frontend-next/src/data/strategy-api.js
Normal file
42
packages/unleash-frontend-next/src/data/strategy-api.js
Normal 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,
|
||||
};
|
@ -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>
|
||||
|
@ -0,0 +1,4 @@
|
||||
import React from 'react';
|
||||
import AddStrategies from '../../component/strategies/add-strategy';
|
||||
|
||||
export default () => (<AddStrategies />);
|
@ -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 />);
|
||||
|
@ -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;
|
||||
|
11
packages/unleash-frontend-next/src/store/input-actions.js
Normal file
11
packages/unleash-frontend-next/src/store/input-actions.js
Normal 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;
|
54
packages/unleash-frontend-next/src/store/input-store.js
Normal file
54
packages/unleash-frontend-next/src/store/input-store.js
Normal 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;
|
63
packages/unleash-frontend-next/src/store/strategy-actions.js
Normal file
63
packages/unleash-frontend-next/src/store/strategy-actions.js
Normal 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)));
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user