1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-09 01:17:06 +02:00

add fields to strategies

This commit is contained in:
sveisvei 2016-12-13 19:56:52 +01:00
parent 66527bf085
commit 821bf0e19b
13 changed files with 228 additions and 137 deletions

View File

@ -29,9 +29,7 @@ export const HeaderTitle = ({ title, actions, subtitle }) => (
{subtitle && <small>{subtitle}</small>} {subtitle && <small>{subtitle}</small>}
</div> </div>
<div style={{ flex: '1', textAlign: 'right' }}> {actions && <div style={{ flex: '1', textAlign: 'right' }}>{actions}</div>}
{actions}
</div>
</div> </div>
); );

View File

@ -14,9 +14,8 @@ class AddStrategy extends React.Component {
addStrategy = (strategyName) => { addStrategy = (strategyName) => {
const selectedStrategy = this.props.strategies.find(s => s.name === strategyName); const selectedStrategy = this.props.strategies.find(s => s.name === strategyName);
const parameters = {}; const parameters = {};
const keys = Object.keys(selectedStrategy.parametersTemplate || {});
keys.forEach(prop => { parameters[prop] = ''; });
selectedStrategy.parameters.forEach(({ name }) => { parameters[name] = ''; });
this.props.addStrategy({ this.props.addStrategy({
name: selectedStrategy.name, name: selectedStrategy.name,
@ -40,7 +39,9 @@ class AddStrategy extends React.Component {
<IconButton name="add" id="strategies-add" raised accent title="Add Strategy" onClick={this.stopPropagation}/> <IconButton name="add" id="strategies-add" raised accent title="Add Strategy" onClick={this.stopPropagation}/>
<Menu target="strategies-add" valign="bottom" align="right" ripple style={menuStyle}> <Menu target="strategies-add" valign="bottom" align="right" ripple style={menuStyle}>
<MenuItem disabled>Add Strategy:</MenuItem> <MenuItem disabled>Add Strategy:</MenuItem>
{this.props.strategies.map((s) => <MenuItem key={s.name} onClick={() => this.addStrategy(s.name)}>{s.name}</MenuItem>)} {this.props.strategies.map((s) =>
<MenuItem key={s.name} title={s.description} onClick={() => this.addStrategy(s.name)}>{s.name}</MenuItem>)
}
</Menu> </Menu>
</div> </div>
); );

View File

@ -22,13 +22,13 @@ class StrategiesList extends React.Component {
return <i style={{ color: 'red' }}>No strategies added</i>; return <i style={{ color: 'red' }}>No strategies added</i>;
} }
const blocks = configuredStrategies.map((strat, i) => ( const blocks = configuredStrategies.map((strategy, i) => (
<ConfigureStrategy <ConfigureStrategy
key={`${strat.name}-${i}`} key={`${strategy.name}-${i}`}
strategy={strat} strategy={strategy}
removeStrategy={this.props.removeStrategy.bind(null, i)} removeStrategy={this.props.removeStrategy.bind(null, i)}
updateStrategy={this.props.updateStrategy.bind(null, i)} updateStrategy={this.props.updateStrategy.bind(null, i)}
strategyDefinition={strategies.find(s => s.name === strat.name)} /> strategyDefinition={strategies.find(s => s.name === strategy.name)} />
)); ));
return ( return (
<div style={{ display: 'flex', flexWrap: 'wrap' }}> <div style={{ display: 'flex', flexWrap: 'wrap' }}>

View File

@ -47,24 +47,23 @@ class StrategyConfigure extends React.Component {
this.props.removeStrategy(); this.props.removeStrategy();
} }
renderInputFields (strategyDefinition) { renderInputFields ({ parameters }) {
if (strategyDefinition.parametersTemplate) { if (parameters && parameters.length > 0) {
const keys = Object.keys(strategyDefinition.parametersTemplate); return parameters.map(({ name, type, description, required }) => {
if (keys.length === 0) { let value = this.props.strategy.parameters[name];
return null;
}
return keys.map(field => {
const type = strategyDefinition.parametersTemplate[field];
let value = this.props.strategy.parameters[field];
if (type === 'percentage') { if (type === 'percentage') {
if (value == null || (typeof value === 'string' && value === '')) { if (value == null || (typeof value === 'string' && value === '')) {
value = 50; // default value value = 50; // default value
} }
return (<StrategyInputPersentage return (
key={field} <div key={name}>
field={field} <StrategyInputPersentage
onChange={this.handleConfigChange.bind(this, field)} name={name}
value={1 * value} />); onChange={this.handleConfigChange.bind(this, name)}
value={1 * value} />
{description && <p>{description}</p>}
</div>
);
} else if (type === 'list') { } else if (type === 'list') {
let list = []; let list = [];
if (typeof value === 'string') { if (typeof value === 'string') {
@ -73,33 +72,44 @@ class StrategyConfigure extends React.Component {
.split(',') .split(',')
.filter(Boolean); .filter(Boolean);
} }
return (<StrategyInputList key={field} field={field} list={list} setConfig={this.setConfig} />); return (
<div key={name}>
<StrategyInputList name={name} list={list} setConfig={this.setConfig} />
{description && <p>{description}</p>}
</div>
);
} else if (type === 'number') { } else if (type === 'number') {
return ( return (
<Textfield <div key={name}>
pattern="-?[0-9]*(\.[0-9]+)?" <Textfield
error={`${field} is not a number!`} pattern="-?[0-9]*(\.[0-9]+)?"
floatingLabel error={`${name} is not a number!`}
style={{ width: '100%' }} floatingLabel
key={field} required={required}
name={field} style={{ width: '100%' }}
label={field} name={name}
onChange={this.handleConfigChange.bind(this, field)} label={name}
value={value} onChange={this.handleConfigChange.bind(this, name)}
/> value={value}
/>
{description && <p>{description}</p>}
</div>
); );
} else { } else {
return ( return (
<Textfield <div key={name}>
floatingLabel <Textfield
rows={2} floatingLabel
style={{ width: '100%' }} rows={2}
key={field} style={{ width: '100%' }}
name={field} required={required}
label={field} name={name}
onChange={this.handleConfigChange.bind(this, field)} label={name}
value={value} onChange={this.handleConfigChange.bind(this, name)}
/> value={value}
/>
{description && <p>{description}</p>}
</div>
); );
} }
}); });

View File

@ -1,36 +1,80 @@
import React from 'react'; import React, { Component, PropTypes } from 'react';
import { import {
Textfield, Textfield,
IconButton, IconButton,
Chip, Chip,
} from 'react-mdl'; } from 'react-mdl';
export default ({ field, list, setConfig }) => ( export default class InputList extends Component {
<div style={{ margin: '16px 20px' }}>
<h6>{field}</h6>
{list.map((entryValue, index) => (
<Chip style={{ marginRight: '3px' }} onClose={() => {
list[index] = null;
setConfig(field, list.filter(Boolean).join(','));
}}>{entryValue}</Chip>
))}
<form style={{ display: 'block', padding: 0, margin: 0 }} onSubmit={(e) => { static propTypes = {
field: PropTypes.string.isRequired,
list: PropTypes.array.isRequired,
setConfig: PropTypes.func.isRequired,
}
onBlur = (e) => {
this.setValue(e);
window.removeEventListener('keydown', this.onKeyHandler, false);
}
onFocus = (e) => {
e.preventDefault();
e.stopPropagation();
window.addEventListener('keydown', this.onKeyHandler, false);
}
onKeyHandler = (e) => {
if (e.key === 'Enter') {
this.setValue();
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
}
}
setValue = (e) => {
if (e) {
e.preventDefault();
e.stopPropagation();
}
const { field, list, setConfig } = this.props;
const inputValue = document.querySelector(`[name="${field}_input"]`);
if (inputValue && inputValue.value) {
list.push(inputValue.value);
inputValue.value = '';
setConfig(field, list.join(','));
}
}
onClose (index) {
const { field, list, setConfig } = this.props;
list[index] = null;
setConfig(field, list.length === 1 ? '' : list.filter(Boolean).join(','));
}
render () {
const { name, list } = this.props;
return (<div>
<p>{name}</p>
{list.map((entryValue, index) => (
<Chip
key={index + entryValue}
style={{ marginRight: '3px' }}
onClose={() => this.onClose(index)}>{entryValue}</Chip>
))}
const inputValue = document.querySelector(`[name="${field}_input"]`);
if (inputValue && inputValue.value) {
list.push(inputValue.value);
inputValue.value = '';
setConfig(field, list.join(','));
}
}}>
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
<Textfield name={`${field}_input`} style={{ width: '100%', flex: 1 }} floatingLabel label="Add list entry" /> <Textfield
<IconButton name="add" raised style={{ flex: 1, flexGrow: 0, margin: '20px 0 0 10px' }}/> name={`${name}_input`}
style={{ width: '100%', flex: 1 }}
floatingLabel
label="Add list entry"
onFocus={this.onFocus}
onBlur={this.onBlur} />
<IconButton name="add" raised style={{ flex: 1, flexGrow: 0, margin: '20px 0 0 10px' }} onClick={this.setValue} />
</div> </div>
</form>
</div> </div>);
); }
}

View File

@ -8,9 +8,9 @@ const labelStyle = {
fontSize: '12px', fontSize: '12px',
}; };
export default ({ field, value, onChange }) => ( export default ({ name, value, onChange }) => (
<div> <div>
<div style={labelStyle}>{field}: {value}%</div> <div style={labelStyle}>{name}: {value}%</div>
<Slider min={0} max={100} defaultValue={value} value={value} onChange={onChange} label={field} /> <Slider min={0} max={100} defaultValue={value} value={value} onChange={onChange} label={name} />
</div> </div>
); );

View File

@ -57,8 +57,8 @@ export function createActions ({ id, prepare = (v) => v }) {
dispatch(createPop({ id: getId(id, ownProps), key, index })); dispatch(createPop({ id: getId(id, ownProps), key, index }));
}, },
updateInList (key, index, newValue) { updateInList (key, index, newValue, merge = false) {
dispatch(createUp({ id: getId(id, ownProps), key, index, newValue })); dispatch(createUp({ id: getId(id, ownProps), key, index, newValue, merge }));
}, },
incValue (key) { incValue (key) {

View File

@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import { createMapper, createActions } from '../input-helpers'; import { createMapper, createActions } from '../input-helpers';
import { createStrategy } from '../../store/strategy/actions'; import { createStrategy } from '../../store/strategy/actions';
import AddStrategy, { PARAM_PREFIX, TYPE_PREFIX } from './add-strategy'; import AddStrategy from './add-strategy';
const ID = 'add-strategy'; const ID = 'add-strategy';
@ -12,15 +12,28 @@ const prepare = (methods, dispatch) => {
(e) => { (e) => {
e.preventDefault(); e.preventDefault();
const parametersTemplate = {};
Object.keys(input).forEach(key => {
if (key.startsWith(PARAM_PREFIX)) {
parametersTemplate[input[key]] = input[key.replace(PARAM_PREFIX, TYPE_PREFIX)] || 'string';
}
});
input.parametersTemplate = parametersTemplate;
createStrategy(input)(dispatch)
// clean
const parameters = input.parameters
.filter((name) => !!name)
.map(({
name,
type = 'string',
description = '',
required = false,
}) => ({
name,
type,
description,
required,
}));
createStrategy({
name: input.name,
description: input.description,
parameters,
})(dispatch)
.then(() => methods.clear()) .then(() => methods.clear())
// somewhat quickfix / hacky to go back.. // somewhat quickfix / hacky to go back..
.then(() => window.history.back()); .then(() => window.history.back());

View File

@ -1,6 +1,6 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { Textfield, IconButton, Menu, MenuItem } from 'react-mdl'; import { Textfield, IconButton, Menu, MenuItem, Checkbox } from 'react-mdl';
import { HeaderTitle, FormButtons } from '../common'; import { HeaderTitle, FormButtons } from '../common';
@ -15,40 +15,60 @@ const trim = (value) => {
function gerArrayWithEntries (num) { function gerArrayWithEntries (num) {
return Array.from(Array(num)); return Array.from(Array(num));
} }
export const PARAM_PREFIX = 'param_';
export const TYPE_PREFIX = 'type_';
const genParams = (input, num = 0, setValue) => (<div>{gerArrayWithEntries(num).map((v, i) => { const Parameter = ({ set, input = {}, index }) => (
const key = `${PARAM_PREFIX}${i + 1}`; <div style={{ background: '#f1f1f1', margin: '20px 0', padding: '10px' }}>
const typeKey = `${TYPE_PREFIX}${i + 1}`; <Textfield
return ( style={{ width: '50%' }}
<div key={key}> floatingLabel
<Textfield label={`Parameter name ${index + 1}`}
style={{ width: '50%' }} onChange={({ target }) => set({ name: target.value }, true)}
floatingLabel value={input.name} />
label={`Parameter name ${i + 1}`} <div style={{ position: 'relative', display: 'inline-block' }}>
name={key} <span id={`${index}-type-menu`}>
onChange={({ target }) => setValue(key, target.value)} {input.type || 'string'}
value={input[key]} /> <IconButton name="arrow_drop_down" onClick={(evt) => evt.preventDefault()} />
<div style={{ position: 'relative', display: 'inline-block' }}> </span>
<span id={`${key}-type-menu`}> <Menu target={`${index}-type-menu`} align="right">
{input[typeKey] || 'string'} <MenuItem onClick={() => set({ type: 'string' })}>String</MenuItem>
<IconButton name="arrow_drop_down" onClick={(evt) => evt.preventDefault()} /> <MenuItem onClick={() => set({ type: 'percentage' })}>Percentage</MenuItem>
</span> <MenuItem onClick={() => set({ type: 'list' })}>List of values</MenuItem>
<Menu target={`${key}-type-menu`} align="right"> <MenuItem onClick={() => set({ type: 'number' })}>Number</MenuItem>
<MenuItem onClick={() => setValue(typeKey, 'string')}>String</MenuItem> </Menu>
<MenuItem onClick={() => setValue(typeKey, 'percentage')}>Percentage</MenuItem>
<MenuItem onClick={() => setValue(typeKey, 'list')}>List of values</MenuItem>
<MenuItem onClick={() => setValue(typeKey, 'number')}>Number</MenuItem>
</Menu>
</div>
</div> </div>
); <Textfield
})}</div>); floatingLabel
style={{ width: '100%' }}
rows={2}
label={`Parameter name ${index + 1} description`}
onChange={({ target }) => set({ description: target.value })}
value={input.description}
/>
<Checkbox
label="Required"
checked={!!input.required}
onChange={() => set({ required: !input.required })}
ripple
defaultChecked
/>
</div>
);
const Parameters = ({ input = [], count = 0, updateInList }) => (
<div>{
gerArrayWithEntries(count)
.map((v, i) => <Parameter
key={i}
set={(v) => updateInList('parameters', i, v, true)}
index={i}
input={input[i]}
/>)
}</div>);
const AddStrategy = ({ const AddStrategy = ({
input, input,
setValue, setValue,
updateInList,
incValue, incValue,
// clear, // clear,
onCancel, onCancel,
@ -78,7 +98,7 @@ const AddStrategy = ({
</section> </section>
<section style={{ margin: '0 20px' }}> <section style={{ margin: '0 20px' }}>
{genParams(input, input._params, setValue)} <Parameters input={input.parameters} count={input._params} updateInList={updateInList} />
<IconButton raised name="add" title="Add parameter" onClick={(e) => { <IconButton raised name="add" title="Add parameter" onClick={(e) => {
e.preventDefault(); e.preventDefault();
incValue('_params'); incValue('_params');
@ -97,6 +117,7 @@ const AddStrategy = ({
AddStrategy.propTypes = { AddStrategy.propTypes = {
input: PropTypes.object, input: PropTypes.object,
setValue: PropTypes.func, setValue: PropTypes.func,
updateInList: PropTypes.func,
incValue: PropTypes.func, incValue: PropTypes.func,
clear: PropTypes.func, clear: PropTypes.func,
onCancel: PropTypes.func, onCancel: PropTypes.func,

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { List, ListItem, ListItemContent, Chip, Icon, IconButton } from 'react-mdl'; import { List, ListItem, ListItemContent, IconButton } from 'react-mdl';
import { HeaderTitle } from '../common'; import { HeaderTitle } from '../common';
class StrategiesListComponent extends Component { class StrategiesListComponent extends Component {
@ -14,12 +14,6 @@ class StrategiesListComponent extends Component {
this.props.fetchStrategies(); this.props.fetchStrategies();
} }
getParameterMap ({ parametersTemplate }) {
return Object.keys(parametersTemplate || {}).map(k => (
<Chip key={k}><small>{k}</small></Chip>
));
}
render () { render () {
const { strategies, removeStrategy } = this.props; const { strategies, removeStrategy } = this.props;

View File

@ -1,6 +1,5 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Grid, Cell } from 'react-mdl'; import { Grid, Cell, List, ListItem, ListItemContent } from 'react-mdl';
import { AppsLinkList, TogglesLinkList, HeaderTitle } from '../common'; import { AppsLinkList, TogglesLinkList, HeaderTitle } from '../common';
class ShowStrategyComponent extends Component { class ShowStrategyComponent extends Component {
@ -16,13 +15,17 @@ class ShowStrategyComponent extends Component {
} }
} }
renderParameters (parametersTemplate) { renderParameters (params) {
if (parametersTemplate) { if (params) {
return Object.keys(parametersTemplate).map((name, i) => ( return params.map(({ name, type, description, required }, i) => (
<li key={`${name}-${i}`}><strong>{name}</strong> ({parametersTemplate[name]})</li> <ListItem twoLine key={`${name}-${i}`} title={required ? 'Required' : ''}>
<ListItemContent avatar={required ? 'add' : ' '} subtitle={description}>
{name} <small>({type})</small>
</ListItemContent>
</ListItem>
)); ));
} else { } else {
return <li>(no params)</li>; return <ListItem>(no params)</ListItem>;
} }
} }
@ -41,28 +44,28 @@ class ShowStrategyComponent extends Component {
const { const {
name, name,
description, description,
parametersTemplate = {}, parameters = [],
} = strategy; } = strategy;
return ( return (
<div> <div>
<HeaderTitle title={name} subtitle={description} /> <HeaderTitle title={name} subtitle={description} />
<Grid> <Grid>
<Cell col={4}> <Cell col={12}>
<h6>Parameters</h6> <h6>Parameters</h6>
<hr /> <hr />
<ol className="demo-list-item mdl-list"> <List>
{this.renderParameters(parametersTemplate)} {this.renderParameters(parameters)}
</ol> </List>
</Cell> </Cell>
<Cell col={4}> <Cell col={6}>
<h6>Applications using this strategy</h6> <h6>Applications using this strategy</h6>
<hr /> <hr />
<AppsLinkList apps={applications} /> <AppsLinkList apps={applications} />
</Cell> </Cell>
<Cell col={4}> <Cell col={6}>
<h6>Toggles using this strategy</h6> <h6>Toggles using this strategy</h6>
<hr /> <hr />
<TogglesLinkList toggles={toggles} /> <TogglesLinkList toggles={toggles} />

View File

@ -13,7 +13,7 @@ export const createInc = ({ id, key }) => ({ type: actions.INCREMENT_VALUE, id,
export const createSet = ({ id, key, value }) => ({ type: actions.SET_VALUE, id, key, value }); export const createSet = ({ id, key, value }) => ({ type: actions.SET_VALUE, id, key, value });
export const createPush = ({ id, key, value }) => ({ type: actions.LIST_PUSH, id, key, value }); export const createPush = ({ id, key, value }) => ({ type: actions.LIST_PUSH, id, key, value });
export const createPop = ({ id, key, index }) => ({ type: actions.LIST_POP, id, key, index }); export const createPop = ({ id, key, index }) => ({ type: actions.LIST_POP, id, key, index });
export const createUp = ({ id, key, index, newValue }) => ({ type: actions.LIST_UP, id, key, index, newValue }); export const createUp = ({ id, key, index, newValue, merge }) => ({ type: actions.LIST_UP, id, key, index, newValue, merge });
export const createClear = ({ id }) => ({ type: actions.CLEAR, id }); export const createClear = ({ id }) => ({ type: actions.CLEAR, id });
export default actions; export default actions;

View File

@ -48,11 +48,18 @@ function addToList (state, { id, key, value }) {
return state.updateIn(id.concat([key]), (list) => list.push(value)); return state.updateIn(id.concat([key]), (list) => list.push(value));
} }
function updateInList (state, { id, key, index, newValue }) { function updateInList (state, { id, key, index, newValue, merge }) {
state = assertId(state, id); state = assertId(state, id);
state = assertList(state, id, key); state = assertList(state, id, key);
return state.updateIn(id.concat([key]), (list) => list.set(index, newValue)); return state.updateIn(id.concat([key]), (list) => {
if (merge && list.has(index)) {
newValue = list.get(index).merge(new $Map(newValue));
} else if (typeof newValue !== 'string' ) {
newValue = fromJS(newValue);
}
return list.set(index, newValue);
});
} }
function removeFromList (state, { id, key, index }) { function removeFromList (state, { id, key, index }) {