mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
fix: UX should not eagerly store strategy updates! (#240)
Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai>
This commit is contained in:
parent
d0a54d6859
commit
00f411d9d2
@ -67,7 +67,7 @@
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"immutable": "^3.8.1",
|
||||
"jest": "^26.6.3",
|
||||
"lodash": "^4.17.15",
|
||||
"lodash": "^4.17.20",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-sass": "^4.5.3",
|
||||
|
@ -8,6 +8,10 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sectionPadding {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.horisontalScroll {
|
||||
overflow-x: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
@ -34,9 +34,9 @@ export const HeaderTitle = ({ title, actions, subtitle }) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
borderBottom: '1px solid #f1f1f1',
|
||||
borderBottom: '1px solid #f9f9f9',
|
||||
marginBottom: '10px',
|
||||
padding: '16px 20px ',
|
||||
padding: '16px',
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: '2' }}>
|
||||
|
@ -104,3 +104,5 @@ export const modalStyles = {
|
||||
transform: 'translate(-50%, -50%)',
|
||||
},
|
||||
};
|
||||
|
||||
export const updateIndexInArray = (array, index, newValue) => array.map((v, i) => (i === index ? newValue : v));
|
||||
|
@ -28,75 +28,72 @@ exports[`render the create feature page 1`] = `
|
||||
col={4}
|
||||
>
|
||||
<react-mdl-Textfield
|
||||
className="fullwidth"
|
||||
floatingLabel={true}
|
||||
label="Name"
|
||||
name="name"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
placeholder="Unique-name"
|
||||
style={
|
||||
Object {
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
value="feature"
|
||||
/>
|
||||
</react-mdl-Cell>
|
||||
<react-mdl-Cell
|
||||
col={2}
|
||||
col={3}
|
||||
>
|
||||
<Connect(FeatureTypeSelectComponent)
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</react-mdl-Cell>
|
||||
<react-mdl-Cell
|
||||
col={2}
|
||||
style={
|
||||
Object {
|
||||
"paddingTop": "14px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<react-mdl-Switch
|
||||
checked={false}
|
||||
onChange={[Function]}
|
||||
>
|
||||
Disabled
|
||||
</react-mdl-Switch>
|
||||
</react-mdl-Cell>
|
||||
</react-mdl-Grid>
|
||||
<section
|
||||
style={
|
||||
Object {
|
||||
"padding": "0 16px",
|
||||
}
|
||||
}
|
||||
className="sectionPadding"
|
||||
>
|
||||
<Connect(ProjectSelectComponent)
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</section>
|
||||
<section
|
||||
style={
|
||||
Object {
|
||||
"padding": "0 16px",
|
||||
}
|
||||
}
|
||||
className="sectionPadding"
|
||||
>
|
||||
<react-mdl-Textfield
|
||||
className="fullwidth"
|
||||
floatingLabel={true}
|
||||
label="Description"
|
||||
onChange={[Function]}
|
||||
placeholder="A short description of the feature toggle"
|
||||
rows={1}
|
||||
style={
|
||||
Object {
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
value="Description"
|
||||
/>
|
||||
</section>
|
||||
<section
|
||||
style={
|
||||
Object {
|
||||
"padding": "10px 16px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<react-mdl-Switch
|
||||
checked={false}
|
||||
onChange={[Function]}
|
||||
>
|
||||
Disabled
|
||||
feature toggle
|
||||
</react-mdl-Switch>
|
||||
</section>
|
||||
<section
|
||||
style={
|
||||
Object {
|
||||
"margin": "40px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(StrategiesList)
|
||||
editable={true}
|
||||
featureToggleName="feature"
|
||||
saveStrategies={[Function]}
|
||||
/>
|
||||
</section>
|
||||
<react-mdl-CardActions>
|
||||
<FormButtons
|
||||
onCancel={[MockFunction]}
|
||||
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { Textfield, Switch, Card, CardTitle, CardActions, Grid, Cell } from 'react-mdl';
|
||||
import FeatureTypeSelect from '../feature-type-select-container';
|
||||
import ProjectSelect from '../project-select-container';
|
||||
import StrategiesList from '../strategy/strategies-list-add-container';
|
||||
|
||||
import { FormButtons, styles as commonStyles } from '../../common';
|
||||
import { trim } from '../../common/util';
|
||||
@ -28,7 +29,7 @@ class AddFeatureComponent extends Component {
|
||||
<Cell col={4}>
|
||||
<Textfield
|
||||
floatingLabel
|
||||
style={{ width: '100%' }}
|
||||
className={commonStyles.fullwidth}
|
||||
label="Name"
|
||||
placeholder="Unique-name"
|
||||
name="name"
|
||||
@ -38,27 +39,17 @@ class AddFeatureComponent extends Component {
|
||||
onChange={v => setValue('name', trim(v.target.value))}
|
||||
/>
|
||||
</Cell>
|
||||
<Cell col={2}>
|
||||
<Cell col={3}>
|
||||
<FeatureTypeSelect value={input.type} onChange={v => setValue('type', v.target.value)} />
|
||||
</Cell>
|
||||
<Cell col={2} style={{ paddingTop: '14px' }}>
|
||||
<Switch
|
||||
checked={input.enabled}
|
||||
onChange={() => {
|
||||
setValue('enabled', !input.enabled);
|
||||
}}
|
||||
>
|
||||
{input.enabled ? 'Enabled' : 'Disabled'}
|
||||
</Switch>
|
||||
</Cell>
|
||||
</Grid>
|
||||
<section style={{ padding: '0 16px' }}>
|
||||
<section className={commonStyles.sectionPadding}>
|
||||
<ProjectSelect value={input.project} onChange={v => setValue('project', v.target.value)} />
|
||||
</section>
|
||||
<section style={{ padding: '0 16px' }}>
|
||||
<section className={commonStyles.sectionPadding}>
|
||||
<Textfield
|
||||
floatingLabel
|
||||
style={{ width: '100%' }}
|
||||
className={commonStyles.fullwidth}
|
||||
rows={1}
|
||||
label="Description"
|
||||
placeholder="A short description of the feature toggle"
|
||||
@ -67,6 +58,24 @@ class AddFeatureComponent extends Component {
|
||||
onChange={v => setValue('description', v.target.value)}
|
||||
/>
|
||||
</section>
|
||||
<section style={{ padding: '10px 16px' }}>
|
||||
<Switch
|
||||
checked={input.enabled}
|
||||
onChange={() => {
|
||||
setValue('enabled', !input.enabled);
|
||||
}}
|
||||
>
|
||||
{input.enabled ? 'Enabled' : 'Disabled'} feature toggle
|
||||
</Switch>
|
||||
</section>
|
||||
<section style={{ margin: '40px 0' }}>
|
||||
<StrategiesList
|
||||
configuredStrategies={input.strategies}
|
||||
featureToggleName={input.name}
|
||||
saveStrategies={s => setValue('strategies', s)}
|
||||
editable
|
||||
/>
|
||||
</section>
|
||||
<CardActions>
|
||||
<FormButtons submitText={'Create'} onCancel={onCancel} />
|
||||
</CardActions>
|
||||
|
@ -61,7 +61,10 @@ class WrapperComponent extends Component {
|
||||
evt.preventDefault();
|
||||
const { createFeatureToggles, history } = this.props;
|
||||
const { featureToggle } = this.state;
|
||||
featureToggle.strategies = [defaultStrategy];
|
||||
|
||||
if (featureToggle.strategies < 1) {
|
||||
featureToggle.strategies = [defaultStrategy];
|
||||
}
|
||||
|
||||
createFeatureToggles(featureToggle).then(() => history.push(`/features/strategies/${featureToggle.name}`));
|
||||
};
|
||||
@ -78,6 +81,7 @@ class WrapperComponent extends Component {
|
||||
onCancel={this.onCancel}
|
||||
validateName={this.validateName}
|
||||
setValue={this.setValue}
|
||||
setStrategies={this.setStrategies}
|
||||
input={this.state.featureToggle}
|
||||
errors={this.state.errors}
|
||||
/>
|
||||
|
@ -31,7 +31,7 @@ export default class InputList extends Component {
|
||||
const newValues = value.split(/,\s*/).filter(a => !list.includes(a));
|
||||
if (newValues.length > 0) {
|
||||
const newList = list.concat(newValues).filter(a => a);
|
||||
setConfig(name, newList.join(','), true);
|
||||
setConfig(name, newList.join(','));
|
||||
}
|
||||
this.textInput.inputRef.value = '';
|
||||
}
|
||||
@ -40,7 +40,7 @@ export default class InputList extends Component {
|
||||
onClose(index) {
|
||||
const { name, list, setConfig } = this.props;
|
||||
list[index] = null;
|
||||
setConfig(name, list.length === 1 ? '' : list.filter(Boolean).join(','), true);
|
||||
setConfig(name, list.length === 1 ? '' : list.filter(Boolean).join(','));
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function LoadingStrategy() {
|
||||
return (
|
||||
<div>
|
||||
<p>Loading definition...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -21,6 +21,7 @@ class AddStrategy extends React.Component {
|
||||
strategies: PropTypes.array.isRequired,
|
||||
addStrategy: PropTypes.func,
|
||||
featureToggleName: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
addStrategy(strategyName) {
|
||||
@ -45,10 +46,11 @@ class AddStrategy extends React.Component {
|
||||
|
||||
render() {
|
||||
const menuStyle = {
|
||||
maxHeight: '300px',
|
||||
maxHeight: '400px',
|
||||
overflowY: 'auto',
|
||||
backgroundColor: 'rgb(247, 248, 255)',
|
||||
};
|
||||
const { disabled = false } = this.props;
|
||||
return (
|
||||
<div style={{ position: 'relative', width: '25px', height: '25px', display: 'inline-block' }}>
|
||||
<IconButton
|
||||
@ -56,6 +58,7 @@ class AddStrategy extends React.Component {
|
||||
id="strategies-add"
|
||||
raised
|
||||
accent
|
||||
disabled={disabled}
|
||||
title="Add Strategy"
|
||||
onClick={this.stopPropagation}
|
||||
/>
|
||||
|
@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import arrayMove from 'array-move';
|
||||
|
||||
import ConfigureStrategy from './strategy-configure-container';
|
||||
import AddStrategy from './strategies-add';
|
||||
import { HeaderTitle } from '../../common';
|
||||
import { updateIndexInArray } from '../../common/util';
|
||||
import styles from './strategy.module.scss';
|
||||
|
||||
const StrategiesList = props => {
|
||||
const updateStrategy = index => strategy => {
|
||||
const newStrategy = { ...strategy };
|
||||
const newStrategies = updateIndexInArray(props.configuredStrategies, index, newStrategy);
|
||||
props.saveStrategies(newStrategies);
|
||||
};
|
||||
|
||||
const saveStrategy = () => () => {
|
||||
// not needed for create flow
|
||||
};
|
||||
|
||||
const addStrategy = strategy => {
|
||||
const strategies = [...props.configuredStrategies];
|
||||
strategies.push({ ...strategy });
|
||||
props.saveStrategies(strategies);
|
||||
};
|
||||
|
||||
const moveStrategy = async (index, toIndex) => {
|
||||
const strategies = arrayMove(props.configuredStrategies, index, toIndex);
|
||||
await props.saveStrategies(strategies);
|
||||
};
|
||||
|
||||
const removeStrategy = index => async () => {
|
||||
props.saveStrategies(props.configuredStrategies.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const { strategies, configuredStrategies, featureToggleName } = props;
|
||||
|
||||
const hasName = featureToggleName && featureToggleName.length > 1;
|
||||
|
||||
const blocks = configuredStrategies.map((strategy, i) => (
|
||||
<ConfigureStrategy
|
||||
index={i}
|
||||
key={i}
|
||||
featureToggleName={featureToggleName}
|
||||
strategy={strategy}
|
||||
moveStrategy={moveStrategy}
|
||||
removeStrategy={removeStrategy(i)}
|
||||
updateStrategy={updateStrategy(i)}
|
||||
saveStrategy={saveStrategy(i)}
|
||||
strategyDefinition={strategies.find(s => s.name === strategy.name)}
|
||||
editable
|
||||
movable
|
||||
/>
|
||||
));
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<div className={styles.strategyListAdd}>
|
||||
<HeaderTitle
|
||||
title="Activation strategies"
|
||||
actions={
|
||||
<AddStrategy
|
||||
strategies={strategies}
|
||||
addStrategy={addStrategy}
|
||||
disabled={!hasName}
|
||||
featureToggleName={featureToggleName}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<div className={styles.strategyList}>
|
||||
{blocks.length > 0 ? (
|
||||
blocks
|
||||
) : (
|
||||
<p style={{ maxWidth: '800px' }}>
|
||||
An activation strategy allows you to control how a feature toggle is enabled in your
|
||||
applications. If you do not specify any activation strategies you will get the "default"
|
||||
strategy.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DndProvider>
|
||||
);
|
||||
};
|
||||
|
||||
StrategiesList.propTypes = {
|
||||
strategies: PropTypes.array.isRequired,
|
||||
configuredStrategies: PropTypes.array.isRequired,
|
||||
featureToggleName: PropTypes.string.isRequired,
|
||||
saveStrategies: PropTypes.func,
|
||||
};
|
||||
|
||||
export default StrategiesList;
|
@ -0,0 +1,8 @@
|
||||
import { connect } from 'react-redux';
|
||||
import StrategiesList from './strategies-list-add-component';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
strategies: state.strategies.get('list').toArray(),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, undefined)(StrategiesList);
|
@ -0,0 +1,162 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import arrayMove from 'array-move';
|
||||
import { Button, Icon } from 'react-mdl';
|
||||
|
||||
import ConfigureStrategy from './strategy-configure-container';
|
||||
import AddStrategy from './strategies-add';
|
||||
import { HeaderTitle } from '../../common';
|
||||
import { updateIndexInArray } from '../../common/util';
|
||||
import styles from './strategy.module.scss';
|
||||
|
||||
const cleanStrategy = strategy => ({
|
||||
name: strategy.name,
|
||||
parameters: cloneDeep(strategy.parameters),
|
||||
constraints: cloneDeep(strategy.constraints || []),
|
||||
});
|
||||
|
||||
const StrategiesList = props => {
|
||||
const [editableStrategies, updateEditableStrategies] = useState(cloneDeep(props.configuredStrategies));
|
||||
const dirty = editableStrategies.some(p => p.dirty);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dirty) {
|
||||
updateEditableStrategies(cloneDeep(props.configuredStrategies));
|
||||
}
|
||||
}, [props.configuredStrategies]);
|
||||
|
||||
const updateStrategy = index => (strategy, dirty = true) => {
|
||||
const newStrategy = { ...strategy, dirty };
|
||||
const newStrategies = updateIndexInArray(editableStrategies, index, newStrategy);
|
||||
updateEditableStrategies(newStrategies);
|
||||
};
|
||||
|
||||
const saveStrategy = index => async () => {
|
||||
const strategies = [...props.configuredStrategies];
|
||||
const strategy = editableStrategies[index];
|
||||
const cleanedStrategy = cleanStrategy(strategy);
|
||||
|
||||
if (strategy.new) {
|
||||
strategies.push(cleanedStrategy);
|
||||
} else {
|
||||
strategies[index] = cleanedStrategy;
|
||||
}
|
||||
|
||||
// store in server
|
||||
await props.saveStrategies(strategies);
|
||||
|
||||
// update local state
|
||||
updateStrategy(index)(cleanedStrategy, false);
|
||||
};
|
||||
|
||||
const addStrategy = strategy => {
|
||||
const strategies = [...editableStrategies];
|
||||
strategies.push({ ...strategy, dirty: true, new: true });
|
||||
updateEditableStrategies(strategies);
|
||||
};
|
||||
|
||||
const moveStrategy = async (index, toIndex) => {
|
||||
if (!dirty) {
|
||||
// console.log(`move strategy from ${index} to ${toIndex}`);
|
||||
const strategies = arrayMove(editableStrategies, index, toIndex);
|
||||
await props.saveStrategies(strategies);
|
||||
updateEditableStrategies(strategies);
|
||||
}
|
||||
};
|
||||
|
||||
const removeStrategy = index => async () => {
|
||||
// eslint-disable-next-line no-alert
|
||||
if (window.confirm('Are you sure you want to remove this activation strategy?')) {
|
||||
const strategy = editableStrategies[index];
|
||||
if (!strategy.new) {
|
||||
await props.saveStrategies(props.configuredStrategies.filter((_, i) => i !== index));
|
||||
}
|
||||
|
||||
updateEditableStrategies(editableStrategies.filter((_, i) => i !== index));
|
||||
}
|
||||
};
|
||||
|
||||
const clearAll = () => {
|
||||
updateEditableStrategies(cloneDeep(props.configuredStrategies));
|
||||
};
|
||||
|
||||
const saveAll = async () => {
|
||||
const cleanedStrategies = editableStrategies.map(cleanStrategy);
|
||||
await props.saveStrategies(cleanedStrategies);
|
||||
updateEditableStrategies(cleanedStrategies);
|
||||
};
|
||||
|
||||
const { strategies, configuredStrategies, featureToggleName, editable } = props;
|
||||
|
||||
if (!configuredStrategies || configuredStrategies.length === 0) {
|
||||
return (
|
||||
<p style={{ padding: '0 16px' }}>
|
||||
<i>No activation strategies selected.</i>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
const resolveStrategyDefinition = strategyName => {
|
||||
if (!strategies || strategies.length === 0) {
|
||||
return { name: 'Loading' };
|
||||
}
|
||||
return strategies.find(s => s.name === strategyName);
|
||||
};
|
||||
|
||||
const blocks = editableStrategies.map((strategy, i) => (
|
||||
<ConfigureStrategy
|
||||
index={i}
|
||||
key={i}
|
||||
featureToggleName={featureToggleName}
|
||||
strategy={strategy}
|
||||
moveStrategy={moveStrategy}
|
||||
removeStrategy={removeStrategy(i)}
|
||||
updateStrategy={updateStrategy(i)}
|
||||
saveStrategy={saveStrategy(i)}
|
||||
strategyDefinition={resolveStrategyDefinition(strategy.name)}
|
||||
editable={editable}
|
||||
movable={!dirty}
|
||||
/>
|
||||
));
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
{editable && (
|
||||
<HeaderTitle
|
||||
title="Activation strategies"
|
||||
actions={
|
||||
<AddStrategy
|
||||
strategies={strategies}
|
||||
addStrategy={addStrategy}
|
||||
featureToggleName={featureToggleName}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.strategyList}>{blocks}</div>
|
||||
<div style={{ visibility: dirty ? 'visible' : 'hidden', padding: '10px' }}>
|
||||
<Button type="submit" ripple raised primary icon="add" onClick={saveAll}>
|
||||
<Icon name="save" />
|
||||
Save all
|
||||
</Button>
|
||||
|
||||
<Button accent type="cancel" onClick={clearAll}>
|
||||
Clear all
|
||||
</Button>
|
||||
</div>
|
||||
</DndProvider>
|
||||
);
|
||||
};
|
||||
|
||||
StrategiesList.propTypes = {
|
||||
strategies: PropTypes.array.isRequired,
|
||||
configuredStrategies: PropTypes.array.isRequired,
|
||||
featureToggleName: PropTypes.string.isRequired,
|
||||
saveStrategies: PropTypes.func,
|
||||
editable: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default StrategiesList;
|
@ -0,0 +1,8 @@
|
||||
import { connect } from 'react-redux';
|
||||
import StrategiesList from './strategies-list-component';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
strategies: state.strategies.get('list').toArray(),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, undefined)(StrategiesList);
|
@ -1,76 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ConfigureStrategy from './strategy-configure-container';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
|
||||
const randomKeys = length => Array.from({ length }, () => Math.random());
|
||||
|
||||
class StrategiesList extends React.Component {
|
||||
static propTypes = {
|
||||
strategies: PropTypes.array.isRequired,
|
||||
configuredStrategies: PropTypes.array.isRequired,
|
||||
featureToggleName: PropTypes.string.isRequired,
|
||||
updateStrategy: PropTypes.func,
|
||||
removeStrategy: PropTypes.func,
|
||||
moveStrategy: PropTypes.func,
|
||||
editable: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super();
|
||||
// temporal hack, until strategies get UIDs
|
||||
this.state = { keys: randomKeys(props.configuredStrategies.length) };
|
||||
}
|
||||
|
||||
moveStrategy = async (index, toIndex) => {
|
||||
await this.props.moveStrategy(index, toIndex);
|
||||
this.setState({ keys: randomKeys(this.props.configuredStrategies.length) });
|
||||
};
|
||||
removeStrategy = async index => {
|
||||
await this.props.removeStrategy(index);
|
||||
this.setState({ keys: randomKeys(this.props.configuredStrategies.length) });
|
||||
};
|
||||
|
||||
componentDidUpdate(props) {
|
||||
const { keys } = this.state;
|
||||
if (keys.length < props.configuredStrategies.length) {
|
||||
// eslint-disable-next-line react/no-did-update-set-state
|
||||
this.setState({ keys: randomKeys(props.configuredStrategies.length) });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { strategies, configuredStrategies, updateStrategy, featureToggleName, editable } = this.props;
|
||||
|
||||
const { keys } = this.state;
|
||||
if (!configuredStrategies || configuredStrategies.length === 0) {
|
||||
return (
|
||||
<p style={{ padding: '0 16px' }}>
|
||||
<i>No activation strategies selected.</i>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
const blocks = configuredStrategies.map((strategy, i) => (
|
||||
<ConfigureStrategy
|
||||
index={i}
|
||||
key={`${keys[i]}}`}
|
||||
featureToggleName={featureToggleName}
|
||||
strategy={strategy}
|
||||
moveStrategy={this.moveStrategy}
|
||||
removeStrategy={this.removeStrategy.bind(this, i)}
|
||||
updateStrategy={updateStrategy ? updateStrategy.bind(null, i) : null}
|
||||
strategyDefinition={strategies.find(s => s.name === strategy.name)}
|
||||
editable={editable}
|
||||
/>
|
||||
));
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap' }}>{blocks}</div>
|
||||
</DndProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default StrategiesList;
|
@ -8,6 +8,7 @@ import DefaultStrategy from './default-strategy';
|
||||
import GeneralStrategy from './general-strategy';
|
||||
import UserWithIdStrategy from './user-with-id-strategy';
|
||||
import UnknownStrategy from './unknown-strategy';
|
||||
import LoadingStrategy from './loading-strategy';
|
||||
|
||||
import styles from './strategy.module.scss';
|
||||
|
||||
@ -18,61 +19,35 @@ export default class StrategyConfigureComponent extends React.Component {
|
||||
index: PropTypes.number.isRequired,
|
||||
strategyDefinition: PropTypes.object,
|
||||
updateStrategy: PropTypes.func,
|
||||
saveStrategy: PropTypes.func,
|
||||
removeStrategy: PropTypes.func,
|
||||
moveStrategy: PropTypes.func,
|
||||
isDragging: PropTypes.bool.isRequired,
|
||||
hovered: PropTypes.bool,
|
||||
movable: PropTypes.bool,
|
||||
connectDragPreview: PropTypes.func.isRequired,
|
||||
connectDragSource: PropTypes.func.isRequired,
|
||||
connectDropTarget: PropTypes.func.isRequired,
|
||||
editable: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super();
|
||||
this.state = {
|
||||
constraints: props.strategy.constraints ? [...props.strategy.constraints] : [],
|
||||
parameters: { ...props.strategy.parameters },
|
||||
edit: false,
|
||||
dirty: false,
|
||||
index: props.index,
|
||||
};
|
||||
}
|
||||
|
||||
updateParameters = parameters => {
|
||||
const { constraints } = this.state;
|
||||
const updatedStrategy = Object.assign({}, this.props.strategy, {
|
||||
parameters,
|
||||
constraints,
|
||||
});
|
||||
const { strategy } = this.props;
|
||||
const updatedStrategy = { ...strategy, parameters };
|
||||
this.props.updateStrategy(updatedStrategy);
|
||||
};
|
||||
|
||||
updateConstraints = constraints => {
|
||||
this.setState({ constraints, dirty: true });
|
||||
const { strategy } = this.props;
|
||||
const updatedStrategy = { ...strategy, constraints };
|
||||
this.props.updateStrategy(updatedStrategy);
|
||||
};
|
||||
|
||||
updateParameter = async (field, value, forceUp = false) => {
|
||||
const { parameters } = this.state;
|
||||
updateParameter = async (field, value) => {
|
||||
const { strategy } = this.props;
|
||||
const parameters = { ...strategy.parameters };
|
||||
parameters[field] = value;
|
||||
if (forceUp) {
|
||||
await this.updateParameters(parameters);
|
||||
this.setState({ parameters, dirty: false });
|
||||
} else {
|
||||
this.setState({ parameters, dirty: true });
|
||||
}
|
||||
};
|
||||
|
||||
onSave = evt => {
|
||||
evt.preventDefault();
|
||||
const { parameters } = this.state;
|
||||
this.updateParameters(parameters);
|
||||
this.setState({ edit: false, dirty: false });
|
||||
};
|
||||
|
||||
handleRemove = evt => {
|
||||
evt.preventDefault();
|
||||
this.props.removeStrategy();
|
||||
};
|
||||
|
||||
resolveInputType() {
|
||||
@ -81,6 +56,8 @@ export default class StrategyConfigureComponent extends React.Component {
|
||||
return UnknownStrategy;
|
||||
}
|
||||
switch (strategyDefinition.name) {
|
||||
case 'Loading':
|
||||
return LoadingStrategy;
|
||||
case 'default':
|
||||
return DefaultStrategy;
|
||||
case 'flexibleRollout':
|
||||
@ -93,7 +70,6 @@ export default class StrategyConfigureComponent extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dirty, parameters } = this.state;
|
||||
const {
|
||||
isDragging,
|
||||
hovered,
|
||||
@ -104,11 +80,14 @@ export default class StrategyConfigureComponent extends React.Component {
|
||||
strategyDefinition,
|
||||
strategy,
|
||||
index,
|
||||
removeStrategy,
|
||||
saveStrategy,
|
||||
movable,
|
||||
} = this.props;
|
||||
|
||||
const { name } = strategy;
|
||||
const { name, dirty, parameters } = strategy;
|
||||
|
||||
const description = strategyDefinition ? strategyDefinition.description : 'Uknown';
|
||||
const description = strategyDefinition ? strategyDefinition.description : 'Unknown';
|
||||
const InputType = this.resolveInputType(name);
|
||||
|
||||
const cardClasses = [styles.card];
|
||||
@ -143,7 +122,7 @@ export default class StrategyConfigureComponent extends React.Component {
|
||||
editable={editable}
|
||||
/>
|
||||
<Button
|
||||
onClick={this.onSave}
|
||||
onClick={saveStrategy}
|
||||
accent
|
||||
raised
|
||||
ripple
|
||||
@ -164,17 +143,23 @@ export default class StrategyConfigureComponent extends React.Component {
|
||||
</Link>
|
||||
{editable && (
|
||||
<IconButton
|
||||
title="Remove strategy from toggle"
|
||||
title="Remove this activation strategy"
|
||||
name="delete"
|
||||
onClick={this.handleRemove}
|
||||
onClick={removeStrategy}
|
||||
/>
|
||||
)}
|
||||
{editable &&
|
||||
movable &&
|
||||
connectDragSource(
|
||||
<span className={styles.reorderIcon}>
|
||||
<Icon name="reorder" />
|
||||
</span>
|
||||
)}
|
||||
{editable && !movable && (
|
||||
<span className={[styles.reorderIcon, styles.disabled].join(' ')}>
|
||||
<Icon name="reorder" title="You can not reorder while editing." />
|
||||
</span>
|
||||
)}
|
||||
</CardMenu>
|
||||
</Card>
|
||||
</div>
|
||||
|
@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
export default {
|
||||
strategyDefinition: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
parameters: PropTypes.array,
|
||||
}).isRequired,
|
||||
parameters: PropTypes.object.isRequired,
|
||||
|
@ -1,7 +1,7 @@
|
||||
.item {
|
||||
flex: 1;
|
||||
min-width: 400px;
|
||||
max-width: 100%;
|
||||
max-width: 537px;
|
||||
margin: 5px 0 5px 5px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@ -85,15 +85,25 @@
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
color: silver;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.paddingDesktop {
|
||||
.strategyList {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.strategyListAdd {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.paddingDesktop {
|
||||
.strategyList {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import strategyInputProps from './strategy-input-props';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function UknownStrategy({ strategy }) {
|
||||
export default function UnknownStrategy({ strategy }) {
|
||||
const { name } = strategy;
|
||||
return (
|
||||
<div>
|
||||
@ -13,4 +13,8 @@ export default function UknownStrategy({ strategy }) {
|
||||
);
|
||||
}
|
||||
|
||||
UknownStrategy.propTypes = strategyInputProps;
|
||||
UnknownStrategy.propTypes = {
|
||||
strategy: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
@ -1,3 +1,27 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`render the create feature page 1`] = `""`;
|
||||
exports[`render the create feature page 1`] = `
|
||||
<section>
|
||||
<Connect(StrategiesList)
|
||||
addStrategy={[MockFunction]}
|
||||
configuredStrategies={
|
||||
Array [
|
||||
Object {
|
||||
"name": "default",
|
||||
},
|
||||
]
|
||||
}
|
||||
editable={true}
|
||||
featureToggleName="some-toggle"
|
||||
initCallRequired={false}
|
||||
moveStrategy={[MockFunction]}
|
||||
onCancel={[MockFunction]}
|
||||
onSubmit={[MockFunction]}
|
||||
removeStrategy={[MockFunction]}
|
||||
setValue={[MockFunction]}
|
||||
title="title"
|
||||
updateStrategy={[MockFunction]}
|
||||
validateName={[MockFunction]}
|
||||
/>
|
||||
</section>
|
||||
`;
|
||||
|
@ -1,19 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import StrategiesList from '../strategy/strategies-list';
|
||||
import AddStrategy from '../strategy/strategies-add';
|
||||
import { HeaderTitle } from '../../common';
|
||||
|
||||
import styles from '../strategy/strategy.module.scss';
|
||||
import StrategiesList from '../strategy/strategies-list-container';
|
||||
|
||||
// TODO: do we still need this wrapper?
|
||||
function UpdateStrategiesComponent(props) {
|
||||
const { editable, configuredStrategies, strategies } = props;
|
||||
const { configuredStrategies } = props;
|
||||
if (!configuredStrategies || configuredStrategies.length === 0) return null;
|
||||
if (!strategies || strategies.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className={styles.paddingDesktop}>
|
||||
{editable && <HeaderTitle title="Activation strategies" actions={<AddStrategy {...props} />} />}
|
||||
<section>
|
||||
<StrategiesList {...props} />
|
||||
</section>
|
||||
);
|
||||
@ -23,10 +18,6 @@ UpdateStrategiesComponent.propTypes = {
|
||||
featureToggleName: PropTypes.string.isRequired,
|
||||
strategies: PropTypes.array,
|
||||
configuredStrategies: PropTypes.array.isRequired,
|
||||
addStrategy: PropTypes.func.isRequired,
|
||||
removeStrategy: PropTypes.func.isRequired,
|
||||
moveStrategy: PropTypes.func.isRequired,
|
||||
updateStrategy: PropTypes.func.isRequired,
|
||||
editable: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
/* eslint-disable no-console */
|
||||
import { connect } from 'react-redux';
|
||||
import arrayMove from 'array-move';
|
||||
|
||||
import { requestUpdateFeatureToggleStrategies } from '../../../store/feature-toggle/actions';
|
||||
import UpdateStrategiesComponent from './update-strategies-component';
|
||||
@ -8,39 +7,11 @@ import UpdateStrategiesComponent from './update-strategies-component';
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
featureToggleName: ownProps.featureToggle.name,
|
||||
configuredStrategies: ownProps.featureToggle.strategies,
|
||||
strategies: state.strategies.get('list').toArray(),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, ownProps) => ({
|
||||
addStrategy: s => {
|
||||
console.log(`add ${s}`);
|
||||
saveStrategies: strategies => {
|
||||
const featureToggle = ownProps.featureToggle;
|
||||
const strategies = featureToggle.strategies.concat(s);
|
||||
return requestUpdateFeatureToggleStrategies(featureToggle, strategies)(dispatch);
|
||||
},
|
||||
|
||||
removeStrategy: index => {
|
||||
console.log(`remove ${index}`);
|
||||
const featureToggle = ownProps.featureToggle;
|
||||
const strategies = featureToggle.strategies.filter((_, i) => i !== index);
|
||||
return requestUpdateFeatureToggleStrategies(featureToggle, strategies)(dispatch);
|
||||
},
|
||||
|
||||
moveStrategy: (index, toIndex) => {
|
||||
// methods.moveItem('strategies', index, toIndex);
|
||||
console.log(`move strategy from ${index} to ${toIndex}`);
|
||||
console.log(ownProps.featureToggle);
|
||||
const featureToggle = ownProps.featureToggle;
|
||||
const strategies = arrayMove(featureToggle.strategies, index, toIndex);
|
||||
return requestUpdateFeatureToggleStrategies(featureToggle, strategies)(dispatch);
|
||||
},
|
||||
|
||||
updateStrategy: (index, s) => {
|
||||
// methods.updateInList('strategies', index, n);
|
||||
console.log(`update strtegy at index ${index} with ${JSON.stringify(s)}`);
|
||||
const featureToggle = ownProps.featureToggle;
|
||||
const strategies = featureToggle.strategies.concat();
|
||||
strategies[index] = s;
|
||||
return requestUpdateFeatureToggleStrategies(featureToggle, strategies)(dispatch);
|
||||
},
|
||||
});
|
||||
|
@ -13,10 +13,10 @@ exports[`renders correctly with one strategy 1`] = `
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"borderBottom": "1px solid #f1f1f1",
|
||||
"borderBottom": "1px solid #f9f9f9",
|
||||
"display": "flex",
|
||||
"marginBottom": "10px",
|
||||
"padding": "16px 20px ",
|
||||
"padding": "16px",
|
||||
}
|
||||
}
|
||||
>
|
||||
@ -104,10 +104,10 @@ exports[`renders correctly with one strategy without permissions 1`] = `
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"borderBottom": "1px solid #f1f1f1",
|
||||
"borderBottom": "1px solid #f9f9f9",
|
||||
"display": "flex",
|
||||
"marginBottom": "10px",
|
||||
"padding": "16px 20px ",
|
||||
"padding": "16px",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
@ -10,10 +10,10 @@ exports[`renders correctly with one strategy 1`] = `
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"borderBottom": "1px solid #f1f1f1",
|
||||
"borderBottom": "1px solid #f9f9f9",
|
||||
"display": "flex",
|
||||
"marginBottom": "10px",
|
||||
"padding": "16px 20px ",
|
||||
"padding": "16px",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
@ -13,10 +13,10 @@ exports[`renders a list with elements correctly 1`] = `
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"borderBottom": "1px solid #f1f1f1",
|
||||
"borderBottom": "1px solid #f9f9f9",
|
||||
"display": "flex",
|
||||
"marginBottom": "10px",
|
||||
"padding": "16px 20px ",
|
||||
"padding": "16px",
|
||||
}
|
||||
}
|
||||
>
|
||||
@ -92,10 +92,10 @@ exports[`renders an empty list correctly 1`] = `
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"borderBottom": "1px solid #f1f1f1",
|
||||
"borderBottom": "1px solid #f9f9f9",
|
||||
"display": "flex",
|
||||
"marginBottom": "10px",
|
||||
"padding": "16px 20px ",
|
||||
"padding": "16px",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
@ -6533,7 +6533,7 @@ lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
|
||||
integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
|
||||
|
||||
lodash@^4.17.19:
|
||||
lodash@^4.17.19, lodash@^4.17.20:
|
||||
version "4.17.20"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
||||
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
||||
|
Loading…
Reference in New Issue
Block a user