1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01: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>}
</div>
<div style={{ flex: '1', textAlign: 'right' }}>
{actions}
</div>
{actions && <div style={{ flex: '1', textAlign: 'right' }}>{actions}</div>}
</div>
);

View File

@ -14,9 +14,8 @@ class AddStrategy extends React.Component {
addStrategy = (strategyName) => {
const selectedStrategy = this.props.strategies.find(s => s.name === strategyName);
const parameters = {};
const keys = Object.keys(selectedStrategy.parametersTemplate || {});
keys.forEach(prop => { parameters[prop] = ''; });
selectedStrategy.parameters.forEach(({ name }) => { parameters[name] = ''; });
this.props.addStrategy({
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}/>
<Menu target="strategies-add" valign="bottom" align="right" ripple style={menuStyle}>
<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>
</div>
);

View File

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

View File

@ -47,24 +47,23 @@ class StrategyConfigure extends React.Component {
this.props.removeStrategy();
}
renderInputFields (strategyDefinition) {
if (strategyDefinition.parametersTemplate) {
const keys = Object.keys(strategyDefinition.parametersTemplate);
if (keys.length === 0) {
return null;
}
return keys.map(field => {
const type = strategyDefinition.parametersTemplate[field];
let value = this.props.strategy.parameters[field];
renderInputFields ({ parameters }) {
if (parameters && parameters.length > 0) {
return parameters.map(({ name, type, description, required }) => {
let value = this.props.strategy.parameters[name];
if (type === 'percentage') {
if (value == null || (typeof value === 'string' && value === '')) {
value = 50; // default value
}
return (<StrategyInputPersentage
key={field}
field={field}
onChange={this.handleConfigChange.bind(this, field)}
value={1 * value} />);
return (
<div key={name}>
<StrategyInputPersentage
name={name}
onChange={this.handleConfigChange.bind(this, name)}
value={1 * value} />
{description && <p>{description}</p>}
</div>
);
} else if (type === 'list') {
let list = [];
if (typeof value === 'string') {
@ -73,33 +72,44 @@ class StrategyConfigure extends React.Component {
.split(',')
.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') {
return (
<Textfield
pattern="-?[0-9]*(\.[0-9]+)?"
error={`${field} is not a number!`}
floatingLabel
style={{ width: '100%' }}
key={field}
name={field}
label={field}
onChange={this.handleConfigChange.bind(this, field)}
value={value}
/>
<div key={name}>
<Textfield
pattern="-?[0-9]*(\.[0-9]+)?"
error={`${name} is not a number!`}
floatingLabel
required={required}
style={{ width: '100%' }}
name={name}
label={name}
onChange={this.handleConfigChange.bind(this, name)}
value={value}
/>
{description && <p>{description}</p>}
</div>
);
} else {
return (
<Textfield
floatingLabel
rows={2}
style={{ width: '100%' }}
key={field}
name={field}
label={field}
onChange={this.handleConfigChange.bind(this, field)}
value={value}
/>
<div key={name}>
<Textfield
floatingLabel
rows={2}
style={{ width: '100%' }}
required={required}
name={name}
label={name}
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 {
Textfield,
IconButton,
Chip,
} from 'react-mdl';
export default ({ field, list, setConfig }) => (
<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>
))}
export default class InputList extends Component {
<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.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' }}>
<Textfield name={`${field}_input`} style={{ width: '100%', flex: 1 }} floatingLabel label="Add list entry" />
<IconButton name="add" raised style={{ flex: 1, flexGrow: 0, margin: '20px 0 0 10px' }}/>
<Textfield
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>
</form>
</div>
);
</div>);
}
}

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import { createMapper, createActions } from '../input-helpers';
import { createStrategy } from '../../store/strategy/actions';
import AddStrategy, { PARAM_PREFIX, TYPE_PREFIX } from './add-strategy';
import AddStrategy from './add-strategy';
const ID = 'add-strategy';
@ -12,15 +12,28 @@ const prepare = (methods, dispatch) => {
(e) => {
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())
// somewhat quickfix / hacky to go back..
.then(() => window.history.back());

View File

@ -1,6 +1,6 @@
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';
@ -15,40 +15,60 @@ const trim = (value) => {
function gerArrayWithEntries (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 key = `${PARAM_PREFIX}${i + 1}`;
const typeKey = `${TYPE_PREFIX}${i + 1}`;
return (
<div key={key}>
<Textfield
style={{ width: '50%' }}
floatingLabel
label={`Parameter name ${i + 1}`}
name={key}
onChange={({ target }) => setValue(key, target.value)}
value={input[key]} />
<div style={{ position: 'relative', display: 'inline-block' }}>
<span id={`${key}-type-menu`}>
{input[typeKey] || 'string'}
<IconButton name="arrow_drop_down" onClick={(evt) => evt.preventDefault()} />
</span>
<Menu target={`${key}-type-menu`} align="right">
<MenuItem onClick={() => setValue(typeKey, 'string')}>String</MenuItem>
<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>
const Parameter = ({ set, input = {}, index }) => (
<div style={{ background: '#f1f1f1', margin: '20px 0', padding: '10px' }}>
<Textfield
style={{ width: '50%' }}
floatingLabel
label={`Parameter name ${index + 1}`}
onChange={({ target }) => set({ name: target.value }, true)}
value={input.name} />
<div style={{ position: 'relative', display: 'inline-block' }}>
<span id={`${index}-type-menu`}>
{input.type || 'string'}
<IconButton name="arrow_drop_down" onClick={(evt) => evt.preventDefault()} />
</span>
<Menu target={`${index}-type-menu`} align="right">
<MenuItem onClick={() => set({ type: 'string' })}>String</MenuItem>
<MenuItem onClick={() => set({ type: 'percentage' })}>Percentage</MenuItem>
<MenuItem onClick={() => set({ type: 'list' })}>List of values</MenuItem>
<MenuItem onClick={() => set({ type: 'number' })}>Number</MenuItem>
</Menu>
</div>
);
})}</div>);
<Textfield
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 = ({
input,
setValue,
updateInList,
incValue,
// clear,
onCancel,
@ -78,7 +98,7 @@ const AddStrategy = ({
</section>
<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) => {
e.preventDefault();
incValue('_params');
@ -97,6 +117,7 @@ const AddStrategy = ({
AddStrategy.propTypes = {
input: PropTypes.object,
setValue: PropTypes.func,
updateInList: PropTypes.func,
incValue: PropTypes.func,
clear: PropTypes.func,
onCancel: PropTypes.func,

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react';
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';
class StrategiesListComponent extends Component {
@ -14,12 +14,6 @@ class StrategiesListComponent extends Component {
this.props.fetchStrategies();
}
getParameterMap ({ parametersTemplate }) {
return Object.keys(parametersTemplate || {}).map(k => (
<Chip key={k}><small>{k}</small></Chip>
));
}
render () {
const { strategies, removeStrategy } = this.props;

View File

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

View File

@ -48,11 +48,18 @@ function addToList (state, { id, key, 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 = 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 }) {