mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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