From 8a083ce748452020a65a629cea26077d4f46e0a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Fri, 27 Nov 2020 22:23:44 +0100 Subject: [PATCH] fix: Should update activation strategies immediately (#229) --- .../archive/archive-list-container.js | 4 +- .../src/component/archive/view-container.js | 2 +- frontend/src/component/common/util.js | 10 + .../feature/__tests__/progress-test.jsx | 2 +- .../{form => create}/__tests__/.eslintrc | 0 .../add-feature-component-test.jsx.snap} | 9 - .../__tests__/add-feature-component-test.jsx} | 5 +- .../add-feature-component.jsx} | 42 +-- .../add-feature-container.jsx} | 63 +--- .../copy-feature-component.jsx} | 2 +- .../copy-feature-container.jsx} | 4 +- .../feature-type-select-component.jsx | 2 +- .../feature-type-select-container.jsx | 2 +- ...orm-update-feature-component-test.jsx.snap | 23 -- .../__snapshots__/strategy-add-test.jsx.snap | 49 --- .../strategy-input-list-test.jsx.snap | 174 ---------- .../form/flexible-rollout-strategy-input.jsx | 87 ----- .../form/form-update-feature-component.jsx | 70 ---- .../form/form-update-feature-container.jsx | 74 ----- .../form/form-view-feature-component.jsx | 31 -- .../form/form-view-feature-container.jsx | 39 --- .../form/strategies-section-container.jsx | 12 - .../feature/form/strategies-section.jsx | 41 --- .../feature/form/strategy-configure.jsx | 299 ------------------ .../form/strategy-input-percentage.jsx | 41 --- frontend/src/component/feature/form/util.js | 17 - .../feature-list-item-component-test.jsx.snap | 0 .../list-component-test.jsx.snap | 4 +- .../feature-list-item-component-test.jsx | 4 +- .../__tests__/list-component-test.jsx | 6 +- .../{ => list}/feature-type-component.jsx | 2 +- .../{ => list}/feature-type-container.jsx | 0 .../feature/{ => list}/list-component.jsx | 8 +- .../feature/{ => list}/list-container.jsx | 6 +- .../list-item-component.jsx} | 10 +- .../{feature.scss => list/list.module.scss} | 0 .../feature/{ => list}/project-component.jsx | 2 +- .../feature/{ => list}/project-container.jsx | 2 +- .../{progress.jsx => progress-component.jsx} | 4 +- ...gress-styles.scss => progress.module.scss} | 0 .../{form => }/project-select-component.jsx | 2 +- .../{form => }/project-select-container.jsx | 2 +- .../feature/strategy/__test__/.eslintrc | 5 + .../__test__}/strategy-add-test.jsx | 11 +- .../__test__}/strategy-input-list-test.jsx | 30 +- .../feature/strategy/default-strategy.jsx | 10 + .../strategy/flexible-rollout-strategy.jsx | 66 ++++ .../feature/strategy/general-strategy.jsx | 107 +++++++ .../input-list.jsx} | 60 ++-- .../feature/strategy/input-percentage.jsx | 53 ++++ .../{form => strategy}/strategies-add.jsx | 19 +- .../{form => strategy}/strategies-list.jsx | 47 ++- .../strategy/strategy-configure-component.jsx | 176 +++++++++++ .../strategy/strategy-configure-container.jsx | 57 ++++ .../feature/strategy/strategy-input-props.js | 11 + .../strategy.module.scss} | 14 +- .../feature/strategy/unknown-strategy.jsx | 16 + .../strategy/user-with-id-strategy.jsx | 23 ++ .../component/feature/variant/add-variant.jsx | 2 +- .../feature/view/__tests__/.eslintrc | 5 + .../update-strategies-component-test.jsx.snap | 3 + .../view-component-test.jsx.snap | 2 +- .../update-strategies-component-test.jsx} | 18 +- .../__tests__/view-component-test.jsx | 10 +- .../feature/{ => view}/metric-component.jsx | 8 +- .../feature/{ => view}/metric-container.jsx | 2 +- .../{metrics.scss => view/metric.module.scss} | 0 .../{ => view}/status-update-component.jsx | 2 +- .../update-description-component.jsx | 0 .../view/update-strategies-component.jsx | 37 +++ .../view/update-strategies-container.jsx | 48 +++ .../feature/{ => view}/view-component.jsx | 35 +- .../feature/{ => view}/view-container.jsx | 6 +- frontend/src/data/feature-api.js | 7 + frontend/src/page/features/copy.js | 2 +- frontend/src/page/features/create.js | 2 +- frontend/src/page/features/index.js | 2 +- frontend/src/page/features/show.js | 2 +- frontend/src/store/feature-actions.js | 25 ++ frontend/src/store/feature-store.js | 10 + frontend/src/store/loader.js | 2 + 81 files changed, 875 insertions(+), 1216 deletions(-) rename frontend/src/component/feature/{form => create}/__tests__/.eslintrc (100%) rename frontend/src/component/feature/{form/__tests__/__snapshots__/form-add-feature-component-test.jsx.snap => create/__tests__/__snapshots__/add-feature-component-test.jsx.snap} (88%) rename frontend/src/component/feature/{form/__tests__/form-add-feature-component-test.jsx => create/__tests__/add-feature-component-test.jsx} (94%) rename frontend/src/component/feature/{form/form-add-feature-component.jsx => create/add-feature-component.jsx} (71%) rename frontend/src/component/feature/{form/form-add-feature-container.jsx => create/add-feature-container.jsx} (58%) rename frontend/src/component/feature/{form/form-copy-feature-component.jsx => create/copy-feature-component.jsx} (99%) rename frontend/src/component/feature/{form/form-copy-feature-container.jsx => create/copy-feature-container.jsx} (84%) rename frontend/src/component/feature/{form => }/feature-type-select-component.jsx (95%) rename frontend/src/component/feature/{form => }/feature-type-select-container.jsx (81%) delete mode 100644 frontend/src/component/feature/form/__tests__/__snapshots__/form-update-feature-component-test.jsx.snap delete mode 100644 frontend/src/component/feature/form/__tests__/__snapshots__/strategy-add-test.jsx.snap delete mode 100644 frontend/src/component/feature/form/__tests__/__snapshots__/strategy-input-list-test.jsx.snap delete mode 100644 frontend/src/component/feature/form/flexible-rollout-strategy-input.jsx delete mode 100644 frontend/src/component/feature/form/form-update-feature-component.jsx delete mode 100644 frontend/src/component/feature/form/form-update-feature-container.jsx delete mode 100644 frontend/src/component/feature/form/form-view-feature-component.jsx delete mode 100644 frontend/src/component/feature/form/form-view-feature-container.jsx delete mode 100644 frontend/src/component/feature/form/strategies-section-container.jsx delete mode 100644 frontend/src/component/feature/form/strategies-section.jsx delete mode 100644 frontend/src/component/feature/form/strategy-configure.jsx delete mode 100644 frontend/src/component/feature/form/strategy-input-percentage.jsx delete mode 100644 frontend/src/component/feature/form/util.js rename frontend/src/component/feature/{ => list}/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap (100%) rename frontend/src/component/feature/{ => list}/__tests__/__snapshots__/list-component-test.jsx.snap (99%) rename frontend/src/component/feature/{ => list}/__tests__/feature-list-item-component-test.jsx (95%) rename frontend/src/component/feature/{ => list}/__tests__/list-component-test.jsx (93%) rename frontend/src/component/feature/{ => list}/feature-type-component.jsx (93%) rename frontend/src/component/feature/{ => list}/feature-type-container.jsx (100%) rename frontend/src/component/feature/{ => list}/list-component.jsx (97%) rename frontend/src/component/feature/{ => list}/list-container.jsx (94%) rename frontend/src/component/feature/{feature-list-item-component.jsx => list/list-item-component.jsx} (93%) rename frontend/src/component/feature/{feature.scss => list/list.module.scss} (100%) rename frontend/src/component/feature/{ => list}/project-component.jsx (97%) rename frontend/src/component/feature/{ => list}/project-container.jsx (85%) rename frontend/src/component/feature/{progress.jsx => progress-component.jsx} (98%) rename frontend/src/component/feature/{progress-styles.scss => progress.module.scss} (100%) rename frontend/src/component/feature/{form => }/project-select-component.jsx (96%) rename frontend/src/component/feature/{form => }/project-select-container.jsx (84%) create mode 100644 frontend/src/component/feature/strategy/__test__/.eslintrc rename frontend/src/component/feature/{form/__tests__ => strategy/__test__}/strategy-add-test.jsx (81%) rename frontend/src/component/feature/{form/__tests__ => strategy/__test__}/strategy-input-list-test.jsx (66%) create mode 100644 frontend/src/component/feature/strategy/default-strategy.jsx create mode 100644 frontend/src/component/feature/strategy/flexible-rollout-strategy.jsx create mode 100644 frontend/src/component/feature/strategy/general-strategy.jsx rename frontend/src/component/feature/{form/strategy-input-list.jsx => strategy/input-list.jsx} (60%) create mode 100644 frontend/src/component/feature/strategy/input-percentage.jsx rename frontend/src/component/feature/{form => strategy}/strategies-add.jsx (77%) rename frontend/src/component/feature/{form => strategy}/strategies-list.jsx (51%) create mode 100644 frontend/src/component/feature/strategy/strategy-configure-component.jsx create mode 100644 frontend/src/component/feature/strategy/strategy-configure-container.jsx create mode 100644 frontend/src/component/feature/strategy/strategy-input-props.js rename frontend/src/component/feature/{form/strategy.scss => strategy/strategy.module.scss} (80%) create mode 100644 frontend/src/component/feature/strategy/unknown-strategy.jsx create mode 100644 frontend/src/component/feature/strategy/user-with-id-strategy.jsx create mode 100644 frontend/src/component/feature/view/__tests__/.eslintrc create mode 100644 frontend/src/component/feature/view/__tests__/__snapshots__/update-strategies-component-test.jsx.snap rename frontend/src/component/feature/{ => view}/__tests__/__snapshots__/view-component-test.jsx.snap (99%) rename frontend/src/component/feature/{form/__tests__/form-update-feature-component-test.jsx => view/__tests__/update-strategies-component-test.jsx} (58%) rename frontend/src/component/feature/{ => view}/__tests__/view-component-test.jsx (79%) rename frontend/src/component/feature/{ => view}/metric-component.jsx (96%) rename frontend/src/component/feature/{ => view}/metric-container.jsx (90%) rename frontend/src/component/feature/{metrics.scss => view/metric.module.scss} (100%) rename frontend/src/component/feature/{ => view}/status-update-component.jsx (95%) rename frontend/src/component/feature/{form => view}/update-description-component.jsx (100%) create mode 100644 frontend/src/component/feature/view/update-strategies-component.jsx create mode 100644 frontend/src/component/feature/view/update-strategies-container.jsx rename frontend/src/component/feature/{ => view}/view-component.jsx (89%) rename frontend/src/component/feature/{ => view}/view-container.jsx (82%) diff --git a/frontend/src/component/archive/archive-list-container.js b/frontend/src/component/archive/archive-list-container.js index 5c12f58e06..7d9a2e0953 100644 --- a/frontend/src/component/archive/archive-list-container.js +++ b/frontend/src/component/archive/archive-list-container.js @@ -1,8 +1,8 @@ import { connect } from 'react-redux'; -import FeatureListComponent from './../feature/list-component'; +import FeatureListComponent from './../feature/list/list-component'; import { fetchArchive, revive } from './../../store/archive-actions'; import { updateSettingForGroup } from './../../store/settings/actions'; -import { mapStateToPropsConfigurable } from '../feature/list-container'; +import { mapStateToPropsConfigurable } from '../feature/list/list-container'; const mapStateToProps = mapStateToPropsConfigurable(false); const mapDispatchToProps = { diff --git a/frontend/src/component/archive/view-container.js b/frontend/src/component/archive/view-container.js index f4bd906017..4e7dcd4254 100644 --- a/frontend/src/component/archive/view-container.js +++ b/frontend/src/component/archive/view-container.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import { fetchArchive, revive } from './../../store/archive-actions'; -import ViewToggleComponent from './../feature/view-component'; +import ViewToggleComponent from './../feature/view/view-component'; import { hasPermission } from '../../permissions'; export default connect( diff --git a/frontend/src/component/common/util.js b/frontend/src/component/common/util.js index d68733576f..da12c8de39 100644 --- a/frontend/src/component/common/util.js +++ b/frontend/src/component/common/util.js @@ -72,3 +72,13 @@ export function updateWeight(variants, totalWeight) { return variant; }); } + +export function loadNameFromHash() { + let field = ''; + try { + [, field] = document.location.hash.match(/name=([a-z0-9-_.]+)/i); + } catch (e) { + // nothing + } + return field; +} diff --git a/frontend/src/component/feature/__tests__/progress-test.jsx b/frontend/src/component/feature/__tests__/progress-test.jsx index b7bf7b1324..8c8f67f8ff 100644 --- a/frontend/src/component/feature/__tests__/progress-test.jsx +++ b/frontend/src/component/feature/__tests__/progress-test.jsx @@ -1,6 +1,6 @@ import React from 'react'; -import Progress from './../progress'; +import Progress from '../progress-component'; import renderer from 'react-test-renderer'; jest.mock('react-mdl'); diff --git a/frontend/src/component/feature/form/__tests__/.eslintrc b/frontend/src/component/feature/create/__tests__/.eslintrc similarity index 100% rename from frontend/src/component/feature/form/__tests__/.eslintrc rename to frontend/src/component/feature/create/__tests__/.eslintrc diff --git a/frontend/src/component/feature/form/__tests__/__snapshots__/form-add-feature-component-test.jsx.snap b/frontend/src/component/feature/create/__tests__/__snapshots__/add-feature-component-test.jsx.snap similarity index 88% rename from frontend/src/component/feature/form/__tests__/__snapshots__/form-add-feature-component-test.jsx.snap rename to frontend/src/component/feature/create/__tests__/__snapshots__/add-feature-component-test.jsx.snap index 81dcd7d24d..94d8a69403 100644 --- a/frontend/src/component/feature/form/__tests__/__snapshots__/form-add-feature-component-test.jsx.snap +++ b/frontend/src/component/feature/create/__tests__/__snapshots__/add-feature-component-test.jsx.snap @@ -96,15 +96,6 @@ exports[`render the create feature page 1`] = ` } value="Description" /> - -
'StrategiesSection'); it('render the create feature page', () => { let input = { diff --git a/frontend/src/component/feature/form/form-add-feature-component.jsx b/frontend/src/component/feature/create/add-feature-component.jsx similarity index 71% rename from frontend/src/component/feature/form/form-add-feature-component.jsx rename to frontend/src/component/feature/create/add-feature-component.jsx index 978959dbfa..88ebc99754 100644 --- a/frontend/src/component/feature/form/form-add-feature-component.jsx +++ b/frontend/src/component/feature/create/add-feature-component.jsx @@ -1,13 +1,11 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { Textfield, Switch, Card, CardTitle, CardActions, Grid, Cell } from 'react-mdl'; -import StrategiesSection from './strategies-section-container'; -import FeatureTypeSelect from './feature-type-select-container'; -import ProjectSelect from './project-select-container'; +import FeatureTypeSelect from '../feature-type-select-container'; +import ProjectSelect from '../project-select-container'; -import { FormButtons } from './../../common'; -import { styles as commonStyles } from '../../common'; -import { trim } from './util'; +import { FormButtons, styles as commonStyles } from '../../common'; +import { trim } from '../../common/util'; class AddFeatureComponent extends Component { // static displayName = `AddFeatureComponent-${getDisplayName(Component)}`; @@ -20,20 +18,7 @@ class AddFeatureComponent extends Component { } render() { - const { - input, - errors, - setValue, - validateName, - addStrategy, - removeStrategy, - updateStrategy, - moveStrategy, - onSubmit, - onCancel, - } = this.props; - - const configuredStrategies = input.strategies || []; + const { input, errors, setValue, validateName, onSubmit, onCancel } = this.props; return ( @@ -81,19 +66,6 @@ class AddFeatureComponent extends Component { value={input.description} onChange={v => setValue('description', v.target.value)} /> - - {input.name ? ( - - ) : null} - -
@@ -108,10 +80,6 @@ AddFeatureComponent.propTypes = { input: PropTypes.object, errors: PropTypes.object, setValue: PropTypes.func.isRequired, - addStrategy: PropTypes.func.isRequired, - removeStrategy: PropTypes.func.isRequired, - moveStrategy: PropTypes.func.isRequired, - updateStrategy: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired, validateName: PropTypes.func.isRequired, diff --git a/frontend/src/component/feature/form/form-add-feature-container.jsx b/frontend/src/component/feature/create/add-feature-container.jsx similarity index 58% rename from frontend/src/component/feature/form/form-add-feature-container.jsx rename to frontend/src/component/feature/create/add-feature-container.jsx index 9049d25115..423bec3369 100644 --- a/frontend/src/component/feature/form/form-add-feature-container.jsx +++ b/frontend/src/component/feature/create/add-feature-container.jsx @@ -1,12 +1,18 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import arrayMove from 'array-move'; -import { createFeatureToggles, validateName } from './../../../store/feature-actions'; -import AddFeatureComponent from './form-add-feature-component'; -import { loadNameFromHash } from './util'; +import { createFeatureToggles, validateName } from '../../../store/feature-actions'; +import AddFeatureComponent from './add-feature-component'; +import { loadNameFromHash } from '../../common/util'; -const defaultStrategy = { name: 'default' }; +const defaultStrategy = { + name: 'flexibleRollout', + parameters: { + rollout: '100', + stickiness: 'default', + groupId: 'a-new.toggle', + }, +}; function resolveCurrentProjectId(settings) { if (!settings.currentProjectId) { @@ -20,7 +26,7 @@ function resolveCurrentProjectId(settings) { class WrapperComponent extends Component { constructor(props) { - super(props); + super(); const name = loadNameFromHash(); this.state = { featureToggle: { @@ -28,6 +34,7 @@ class WrapperComponent extends Component { description: '', type: 'release', strategies: [], + variants: [], enabled: true, project: props.currentProjectId, }, @@ -54,49 +61,11 @@ class WrapperComponent extends Component { this.setState({ errors }); }; - addStrategy = strat => { - strat.id = Math.round(Math.random() * 10000000); - const { featureToggle } = this.state; - const strategies = featureToggle.strategies.concat(strat); - featureToggle.strategies = strategies; - this.setState({ featureToggle, dirty: true }); - }; - - moveStrategy = (index, toIndex) => { - const { featureToggle } = this.state; - const strategies = arrayMove(featureToggle.strategies, index, toIndex); - featureToggle.strategies = strategies; - this.setState({ featureToggle, dirty: true }); - }; - - removeStrategy = index => { - const { featureToggle } = this.state; - const strategies = featureToggle.strategies.filter((_, i) => i !== index); - featureToggle.strategies = strategies; - this.setState({ featureToggle, dirty: true }); - }; - - updateStrategy = (index, strat) => { - const { featureToggle } = this.state; - const strategies = featureToggle.strategies.concat(); - strategies[index] = strat; - featureToggle.strategies = strategies; - this.setState({ featureToggle, dirty: true }); - }; - onSubmit = evt => { evt.preventDefault(); const { createFeatureToggles, history } = this.props; const { featureToggle } = this.state; - featureToggle.createdAt = new Date(); - - if (Array.isArray(featureToggle.strategies) && featureToggle.strategies.length > 0) { - featureToggle.strategies.forEach(s => { - delete s.id; - }); - } else { - featureToggle.strategies = [defaultStrategy]; - } + featureToggle.strategies = [defaultStrategy]; createFeatureToggles(featureToggle).then(() => history.push(`/features/strategies/${featureToggle.name}`)); }; @@ -111,10 +80,6 @@ class WrapperComponent extends Component { ({ history: props.history, diff --git a/frontend/src/component/feature/form/feature-type-select-component.jsx b/frontend/src/component/feature/feature-type-select-component.jsx similarity index 95% rename from frontend/src/component/feature/form/feature-type-select-component.jsx rename to frontend/src/component/feature/feature-type-select-component.jsx index 804eff4b9a..c286cd4968 100644 --- a/frontend/src/component/feature/form/feature-type-select-component.jsx +++ b/frontend/src/component/feature/feature-type-select-component.jsx @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import MySelect from '../../common/select'; +import MySelect from '../common/select'; class FeatureTypeSelectComponent extends Component { componentDidMount() { diff --git a/frontend/src/component/feature/form/feature-type-select-container.jsx b/frontend/src/component/feature/feature-type-select-container.jsx similarity index 81% rename from frontend/src/component/feature/form/feature-type-select-container.jsx rename to frontend/src/component/feature/feature-type-select-container.jsx index a58462ec82..2d822c98ae 100644 --- a/frontend/src/component/feature/form/feature-type-select-container.jsx +++ b/frontend/src/component/feature/feature-type-select-container.jsx @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import FeatureTypeSelectComponent from './feature-type-select-component'; -import { fetchFeatureTypes } from './../../../store/feature-type/actions'; +import { fetchFeatureTypes } from './../../store/feature-type/actions'; const mapStateToProps = state => ({ types: state.featureTypes.toJS(), diff --git a/frontend/src/component/feature/form/__tests__/__snapshots__/form-update-feature-component-test.jsx.snap b/frontend/src/component/feature/form/__tests__/__snapshots__/form-update-feature-component-test.jsx.snap deleted file mode 100644 index ce0af96403..0000000000 --- a/frontend/src/component/feature/form/__tests__/__snapshots__/form-update-feature-component-test.jsx.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`render the create feature page 1`] = ` -
-
- -
- -
-
-`; diff --git a/frontend/src/component/feature/form/__tests__/__snapshots__/strategy-add-test.jsx.snap b/frontend/src/component/feature/form/__tests__/__snapshots__/strategy-add-test.jsx.snap deleted file mode 100644 index 983315d52d..0000000000 --- a/frontend/src/component/feature/form/__tests__/__snapshots__/strategy-add-test.jsx.snap +++ /dev/null @@ -1,49 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders add strategy form with a list of available strategies 1`] = ` -
- - - - Add Strategy: - - - default - - -
-`; diff --git a/frontend/src/component/feature/form/__tests__/__snapshots__/strategy-input-list-test.jsx.snap b/frontend/src/component/feature/form/__tests__/__snapshots__/strategy-input-list-test.jsx.snap deleted file mode 100644 index 8bea8dadf4..0000000000 --- a/frontend/src/component/feature/form/__tests__/__snapshots__/strategy-input-list-test.jsx.snap +++ /dev/null @@ -1,174 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders correctly when disabled 1`] = ` -
-

- - Please specify the list of - - featureName - - : - -

-
- - item1 - - - item2 - -
- -
-`; - -exports[`renders strategy with empty list as param 1`] = ` -
-

- - Please specify the list of - - featureName - - : - -

-
-
- - -
-
-`; - -exports[`renders strategy with list as param 1`] = ` -
-

- - Please specify the list of - - featureName - - : - -

-
- - item1 - - - item2 - -
-
- - -
-
-`; diff --git a/frontend/src/component/feature/form/flexible-rollout-strategy-input.jsx b/frontend/src/component/feature/form/flexible-rollout-strategy-input.jsx deleted file mode 100644 index 87f6da5f47..0000000000 --- a/frontend/src/component/feature/form/flexible-rollout-strategy-input.jsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { Textfield } from 'react-mdl'; -import Select from '../../common/select'; - -import StrategyInputPercentage from './strategy-input-percentage'; - -const stickinessOptions = [ - { key: 'default', label: 'default' }, - { key: 'userId', label: 'userId' }, - { key: 'sessionId', label: 'sessionId' }, - { key: 'random', label: 'random' }, -]; - -export default class FlexibleRolloutStrategy extends Component { - static propTypes = { - strategy: PropTypes.object.isRequired, - featureToggleName: PropTypes.string.isRequired, - updateStrategy: PropTypes.func.isRequired, - handleConfigChange: PropTypes.func.isRequired, - }; - - // eslint-disable-next-line camelcase - UNSAFE_componentWillMount() { - const { strategy, featureToggleName } = this.props; - if (!strategy.parameters.rollout) { - this.setConfig('rollout', 100); - } - - if (!strategy.parameters.stickiness) { - this.setConfig('stickiness', 'default'); - } - - if (!strategy.parameters.groupId) { - this.setConfig('groupId', featureToggleName); - } - } - - setConfig = (key, value) => { - const parameters = this.props.strategy.parameters || {}; - parameters[key] = value; - - const updatedStrategy = Object.assign({}, this.props.strategy, { - parameters, - }); - - this.props.updateStrategy(updatedStrategy); - }; - - render() { - const { strategy, handleConfigChange } = this.props; - - const rollout = strategy.parameters.rollout; - const stickiness = strategy.parameters.stickiness; - const groupId = strategy.parameters.groupId; - - return ( -
-
-
Rollout
- handleConfigChange('rollout', evt)} - /> -
- this.onConfiguUpdate('stickiness', evt)} + /> +   + this.onConfiguUpdate('groupId', evt)} + id={`${index}-groupId`} + />{' '} +
+
+ ); + } +} diff --git a/frontend/src/component/feature/strategy/general-strategy.jsx b/frontend/src/component/feature/strategy/general-strategy.jsx new file mode 100644 index 0000000000..373dcf03c6 --- /dev/null +++ b/frontend/src/component/feature/strategy/general-strategy.jsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { Textfield, Icon, Switch, Tooltip } from 'react-mdl'; +import strategyInputProps from './strategy-input-props'; +import StrategyInputPercentage from './input-percentage'; +import StrategyInputList from './input-list'; +import styles from './strategy.module.scss'; + +export default function GeneralStrategyInput({ parameters, strategyDefinition, updateParameter, editable }) { + const onChange = (field, evt) => { + evt.preventDefault(); + const value = evt.target.value; + updateParameter(field, value); + }; + + const handleSwitchChange = (key, currentValue) => { + const value = currentValue === 'true' ? 'false' : 'true'; + updateParameter(key, value); + }; + + if (strategyDefinition.parameters && strategyDefinition.parameters.length > 0) { + return strategyDefinition.parameters.map(({ name, type, description, required }) => { + let value = parameters[name]; + if (type === 'percentage') { + if (value == null || (typeof value === 'string' && value === '')) { + value = 0; + } + return ( +
+
+ + {description &&

{description}

} +
+ ); + } else if (type === 'list') { + let list = []; + if (typeof value === 'string') { + list = value + .trim() + .split(',') + .filter(Boolean); + } + return ( +
+ + {description &&

{description}

} +
+ ); + } else if (type === 'number') { + return ( +
+ + {description &&

{description}

} +
+ ); + } else if (type === 'boolean') { + return ( +
+ + {name}{' '} + {description && ( + + + + )} + +
+ ); + } else { + return ( +
+ + {description &&

{description}

} +
+ ); + } + }); + } + return null; +} + +GeneralStrategyInput.propTypes = strategyInputProps; diff --git a/frontend/src/component/feature/form/strategy-input-list.jsx b/frontend/src/component/feature/strategy/input-list.jsx similarity index 60% rename from frontend/src/component/feature/form/strategy-input-list.jsx rename to frontend/src/component/feature/strategy/input-list.jsx index bf66283237..6bb6b3176a 100644 --- a/frontend/src/component/feature/form/strategy-input-list.jsx +++ b/frontend/src/component/feature/strategy/input-list.jsx @@ -10,79 +10,69 @@ export default class InputList extends Component { disabled: PropTypes.bool, }; - onBlur(e) { + 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 => { + onKeyDown = e => { if (e.key === 'Enter') { - this.setValue(); + this.setValue(e); e.preventDefault(); e.stopPropagation(); } }; - setValue = e => { - if (e) { - e.preventDefault(); - e.stopPropagation(); - } + setValue = evt => { + evt.preventDefault(); + const value = evt.target.value; const { name, list, setConfig } = this.props; - if (this.textInput && this.textInput.inputRef && this.textInput.inputRef.value) { - const newValues = this.textInput.inputRef.value.split(/,\s*/); - const newList = list.concat(newValues).filter(a => a); + if (value) { + 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); + } this.textInput.inputRef.value = ''; - setConfig(name, newList.join(',')); } }; onClose(index) { const { name, list, setConfig } = this.props; list[index] = null; - setConfig(name, list.length === 1 ? '' : list.filter(Boolean).join(',')); + setConfig(name, list.length === 1 ? '' : list.filter(Boolean).join(','), true); } render() { const { name, list, disabled } = this.props; return (
-

- - Please specify the list of {name}: - -

- -
+ List of {name} +
{list.map((entryValue, index) => ( this.onClose(index)} + title="Remove value" > {entryValue} ))}
- {disabled ? ( '' ) : (
{ this.textInput = input; }} diff --git a/frontend/src/component/feature/strategy/input-percentage.jsx b/frontend/src/component/feature/strategy/input-percentage.jsx new file mode 100644 index 0000000000..e6c0c85ea2 --- /dev/null +++ b/frontend/src/component/feature/strategy/input-percentage.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Slider } from 'react-mdl'; + +const labelStyle = { + textAlign: 'center', + color: 'rgb(96,125,139)', + fontSize: '2em', +}; + +const infoLabelStyle = { + fontSize: '0.8em', + color: 'gray', + paddingBottom: '-3px', +}; + +const InputPercentage = ({ name, minLabel, maxLabel, value, onChange, disabled = false }) => ( +
+ + + + + + + + + + + +
+ {minLabel} + + + {value}% + + + {maxLabel}  +
+ +
+
+); + +InputPercentage.propTypes = { + name: PropTypes.string, + minLabel: PropTypes.string, + maxLabel: PropTypes.string, + value: PropTypes.number, + onChange: PropTypes.func.isRequired, + disabled: PropTypes.bool, +}; + +export default InputPercentage; diff --git a/frontend/src/component/feature/form/strategies-add.jsx b/frontend/src/component/feature/strategy/strategies-add.jsx similarity index 77% rename from frontend/src/component/feature/form/strategies-add.jsx rename to frontend/src/component/feature/strategy/strategies-add.jsx index 429022e2cf..fa3a028eff 100644 --- a/frontend/src/component/feature/form/strategies-add.jsx +++ b/frontend/src/component/feature/strategy/strategies-add.jsx @@ -2,19 +2,34 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Menu, MenuItem, IconButton } from 'react-mdl'; +function resolveDefaultParamVale(name, featureToggleName) { + switch (name) { + case 'percentage': + case 'rollout': + return '100'; + case 'stickiness': + return 'default'; + case 'groupId': + return featureToggleName; + default: + return ''; + } +} + class AddStrategy extends React.Component { static propTypes = { strategies: PropTypes.array.isRequired, addStrategy: PropTypes.func, - fetchStrategies: PropTypes.func.isRequired, + featureToggleName: PropTypes.string.isRequired, }; addStrategy(strategyName) { + const featureToggleName = this.props.featureToggleName; const selectedStrategy = this.props.strategies.find(s => s.name === strategyName); const parameters = {}; selectedStrategy.parameters.forEach(({ name }) => { - parameters[name] = ''; + parameters[name] = resolveDefaultParamVale(name, featureToggleName); }); this.props.addStrategy({ diff --git a/frontend/src/component/feature/form/strategies-list.jsx b/frontend/src/component/feature/strategy/strategies-list.jsx similarity index 51% rename from frontend/src/component/feature/form/strategies-list.jsx rename to frontend/src/component/feature/strategy/strategies-list.jsx index d156b37b9f..64ecf2b0b8 100644 --- a/frontend/src/component/feature/form/strategies-list.jsx +++ b/frontend/src/component/feature/strategy/strategies-list.jsx @@ -1,9 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import ConfigureStrategy from './strategy-configure'; +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, @@ -12,18 +14,36 @@ class StrategiesList extends React.Component { updateStrategy: PropTypes.func, removeStrategy: PropTypes.func, moveStrategy: PropTypes.func, + editable: PropTypes.bool, }; - render() { - const { - strategies, - configuredStrategies, - moveStrategy, - removeStrategy, - updateStrategy, - featureToggleName, - } = this.props; + 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 (

@@ -35,13 +55,14 @@ class StrategiesList extends React.Component { const blocks = configuredStrategies.map((strategy, i) => ( s.name === strategy.name)} + editable={editable} /> )); return ( diff --git a/frontend/src/component/feature/strategy/strategy-configure-component.jsx b/frontend/src/component/feature/strategy/strategy-configure-component.jsx new file mode 100644 index 0000000000..07672e4b3f --- /dev/null +++ b/frontend/src/component/feature/strategy/strategy-configure-component.jsx @@ -0,0 +1,176 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Card, CardTitle, CardText, CardMenu, IconButton, Icon } from 'react-mdl'; +import { Link } from 'react-router-dom'; + +import FlexibleRolloutStrategy from './flexible-rollout-strategy'; +import DefaultStrategy from './default-strategy'; +import GeneralStrategy from './general-strategy'; +import UserWithIdStrategy from './user-with-id-strategy'; +import UnknownStrategy from './unknown-strategy'; + +import styles from './strategy.module.scss'; + +export default class StrategyConfigureComponent extends React.Component { + /* eslint-enable */ + static propTypes = { + strategy: PropTypes.object.isRequired, + index: PropTypes.number.isRequired, + strategyDefinition: PropTypes.object, + updateStrategy: PropTypes.func, + removeStrategy: PropTypes.func, + moveStrategy: PropTypes.func, + isDragging: PropTypes.bool.isRequired, + hovered: PropTypes.bool, + connectDragPreview: PropTypes.func.isRequired, + connectDragSource: PropTypes.func.isRequired, + connectDropTarget: PropTypes.func.isRequired, + editable: PropTypes.bool, + }; + + constructor(props) { + super(); + this.state = { + parameters: { ...props.strategy.parameters }, + edit: false, + dirty: false, + index: props.index, + }; + } + + updateParameters = parameters => { + const updatedStrategy = Object.assign({}, this.props.strategy, { + parameters, + }); + this.props.updateStrategy(updatedStrategy); + }; + + updateParameter = async (field, value, forceUp = false) => { + const { parameters } = this.state; + 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() { + const { strategyDefinition } = this.props; + if (!strategyDefinition) { + return UnknownStrategy; + } + switch (strategyDefinition.name) { + case 'default': + return DefaultStrategy; + case 'flexibleRollout': + return FlexibleRolloutStrategy; + case 'userWithId': + return UserWithIdStrategy; + default: + return GeneralStrategy; + } + } + + render() { + const { dirty, parameters } = this.state; + const { + isDragging, + hovered, + editable, + connectDragSource, + connectDragPreview, + connectDropTarget, + strategyDefinition, + strategy, + index, + } = this.props; + + const { name } = strategy; + + const description = strategyDefinition ? strategyDefinition.description : 'Uknown'; + const InputType = this.resolveInputType(name); + + const cardClasses = [styles.card]; + if (dirty) { + cardClasses.push('mdl-color--pink-50'); + } + if (isDragging) { + cardClasses.push(styles.isDragging); + } + if (hovered) { + cardClasses.push(styles.isDroptarget); + } + + return connectDragPreview( + connectDropTarget( +

+ + + +   + {name} + + + + + + + + + + + + {editable && ( + + )} + {editable && + connectDragSource( + + + + )} + + +
+ ) + ); + } +} diff --git a/frontend/src/component/feature/strategy/strategy-configure-container.jsx b/frontend/src/component/feature/strategy/strategy-configure-container.jsx new file mode 100644 index 0000000000..553a504a5f --- /dev/null +++ b/frontend/src/component/feature/strategy/strategy-configure-container.jsx @@ -0,0 +1,57 @@ +import { DragSource, DropTarget } from 'react-dnd'; +import flow from 'lodash/flow'; + +import StrategyConfigure from './strategy-configure-component'; + +const dragSource = { + beginDrag(props) { + return { + id: props.id, + index: props.index, + }; + }, + endDrag(props, monitor) { + if (!monitor.didDrop()) { + return; + } + const result = monitor.getDropResult(); + if (typeof result.index === 'number' && props.index !== result.index) { + props.moveStrategy(props.index, result.index); + } + }, +}; + +const dragTarget = { + drop(props) { + return { + index: props.index, + }; + }, +}; + +/** + * Specifies which props to inject into your component. + */ +function collect(connect, monitor) { + return { + connectDragSource: connect.dragSource(), + connectDragPreview: connect.dragPreview(), + isDragging: monitor.isDragging(), + }; +} + +function collectTarget(connect, monitor) { + return { + highlighted: monitor.canDrop(), + hovered: monitor.isOver(), + connectDropTarget: connect.dropTarget(), + }; +} + +const type = 'strategy'; +export default flow( + // eslint-disable-next-line new-cap + DragSource(type, dragSource, collect), + // eslint-disable-next-line new-cap + DropTarget(type, dragTarget, collectTarget) +)(StrategyConfigure); diff --git a/frontend/src/component/feature/strategy/strategy-input-props.js b/frontend/src/component/feature/strategy/strategy-input-props.js new file mode 100644 index 0000000000..ad80363906 --- /dev/null +++ b/frontend/src/component/feature/strategy/strategy-input-props.js @@ -0,0 +1,11 @@ +import PropTypes from 'prop-types'; + +export default { + strategyDefinition: PropTypes.shape({ + parameters: PropTypes.array, + }).isRequired, + parameters: PropTypes.object.isRequired, + updateParameter: PropTypes.func.isRequired, + editable: PropTypes.bool.isRequired, + index: PropTypes.number.isRequired, +}; diff --git a/frontend/src/component/feature/form/strategy.scss b/frontend/src/component/feature/strategy/strategy.module.scss similarity index 80% rename from frontend/src/component/feature/form/strategy.scss rename to frontend/src/component/feature/strategy/strategy.module.scss index d6e54e2e84..7a80650ff7 100644 --- a/frontend/src/component/feature/form/strategy.scss +++ b/frontend/src/component/feature/strategy/strategy.module.scss @@ -11,9 +11,19 @@ width: 100%; display: block; background-color: #f2f9fc; + overflow: 'visible'; } -.item:first-child { +.isDragging { + opacity: 0.4; + border: 2px dotted black; +} + +.isDroptarget { + border: 2px solid rgba(96, 125, 139, 1) !important; +} + +.item:nth-child(odd) { margin-left: 0; } @@ -42,7 +52,7 @@ .cardTitle { color: #fff; height: 65px; - background-color: #607d8b !important; + background-color: rgba(96, 125, 139, .85) !important; } .helpText { diff --git a/frontend/src/component/feature/strategy/unknown-strategy.jsx b/frontend/src/component/feature/strategy/unknown-strategy.jsx new file mode 100644 index 0000000000..4373b642bd --- /dev/null +++ b/frontend/src/component/feature/strategy/unknown-strategy.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import strategyInputProps from './strategy-input-props'; + +import { Link } from 'react-router-dom'; + +export default function UknownStrategy({ strategy }) { + const { name } = strategy; + return ( +
+

The strategy "{name}" does not exist on this server.

+ Want to create it now? +
+ ); +} + +UknownStrategy.propTypes = strategyInputProps; diff --git a/frontend/src/component/feature/strategy/user-with-id-strategy.jsx b/frontend/src/component/feature/strategy/user-with-id-strategy.jsx new file mode 100644 index 0000000000..953ba71bf0 --- /dev/null +++ b/frontend/src/component/feature/strategy/user-with-id-strategy.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import strategyInputProps from './strategy-input-props'; +import InputList from './input-list'; + +export default function UserWithIdStrategy({ editable, parameters, updateParameter }) { + const value = parameters.userIds; + + let list = []; + if (typeof value === 'string') { + list = value + .trim() + .split(',') + .filter(Boolean); + } + + return ( +
+ +
+ ); +} + +UserWithIdStrategy.propTypes = strategyInputProps; diff --git a/frontend/src/component/feature/variant/add-variant.jsx b/frontend/src/component/feature/variant/add-variant.jsx index 20d75c3d38..793a569ac1 100644 --- a/frontend/src/component/feature/variant/add-variant.jsx +++ b/frontend/src/component/feature/variant/add-variant.jsx @@ -4,7 +4,7 @@ import Modal from 'react-modal'; import { Button, Textfield, DialogActions, Grid, Cell, Icon, Switch } from 'react-mdl'; import styles from './variant.scss'; import MySelect from '../../common/select'; -import { trim } from '../form/util'; +import { trim } from '../../common/util'; import { weightTypes } from './enums'; import OverrideConfig from './override-config'; diff --git a/frontend/src/component/feature/view/__tests__/.eslintrc b/frontend/src/component/feature/view/__tests__/.eslintrc new file mode 100644 index 0000000000..eba2077219 --- /dev/null +++ b/frontend/src/component/feature/view/__tests__/.eslintrc @@ -0,0 +1,5 @@ +{ + "env": { + "jest": true + } +} diff --git a/frontend/src/component/feature/view/__tests__/__snapshots__/update-strategies-component-test.jsx.snap b/frontend/src/component/feature/view/__tests__/__snapshots__/update-strategies-component-test.jsx.snap new file mode 100644 index 0000000000..9b0de07dff --- /dev/null +++ b/frontend/src/component/feature/view/__tests__/__snapshots__/update-strategies-component-test.jsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`render the create feature page 1`] = `""`; diff --git a/frontend/src/component/feature/__tests__/__snapshots__/view-component-test.jsx.snap b/frontend/src/component/feature/view/__tests__/__snapshots__/view-component-test.jsx.snap similarity index 99% rename from frontend/src/component/feature/__tests__/__snapshots__/view-component-test.jsx.snap rename to frontend/src/component/feature/view/__tests__/__snapshots__/view-component-test.jsx.snap index 3a038dcd4b..917b6edf7d 100644 --- a/frontend/src/component/feature/__tests__/__snapshots__/view-component-test.jsx.snap +++ b/frontend/src/component/feature/view/__tests__/__snapshots__/view-component-test.jsx.snap @@ -203,7 +203,7 @@ exports[`renders correctly with one feature 1`] = ` - 'StrategiesSection'); +// jest.mock('../strategies-section-container', () => 'StrategiesSection'); it('render the create feature page', () => { - let input = { - name: 'feature', - errors: {}, - description: 'Description', - enabled: false, - }; + let strategies = [{ name: 'default' }]; const tree = shallow( - ({ +jest.mock('../update-strategies-container', () => ({ __esModule: true, - default: 'UpdateFeatureToggleComponent', + default: 'UpdateStrategiesComponent', })); -jest.mock('../form/feature-type-select-container', () => 'FeatureTypeSelect'); -jest.mock('../form/project-select-container', () => 'ProjectSelect'); +jest.mock('../../feature-type-select-container', () => 'FeatureTypeSelect'); +jest.mock('../../project-select-container', () => 'ProjectSelect'); test('renders correctly with one feature', () => { const feature = { diff --git a/frontend/src/component/feature/metric-component.jsx b/frontend/src/component/feature/view/metric-component.jsx similarity index 96% rename from frontend/src/component/feature/metric-component.jsx rename to frontend/src/component/feature/view/metric-component.jsx index 3cdc4e3227..0bde8de5f9 100644 --- a/frontend/src/component/feature/metric-component.jsx +++ b/frontend/src/component/feature/view/metric-component.jsx @@ -1,11 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Grid, Cell, Icon, Chip, ChipContact } from 'react-mdl'; -import Progress from './progress'; +import Progress from '../progress-component'; import { Link } from 'react-router-dom'; -import { AppsLinkList, calc } from '../common'; -import { formatFullDateTimeWithLocale } from '../common/util'; -import styles from './metrics.scss'; +import { AppsLinkList, calc } from '../../common'; +import { formatFullDateTimeWithLocale } from '../../common/util'; +import styles from './metric.module.scss'; const StrategyChipItem = ({ strategy }) => ( diff --git a/frontend/src/component/feature/metric-container.jsx b/frontend/src/component/feature/view/metric-container.jsx similarity index 90% rename from frontend/src/component/feature/metric-container.jsx rename to frontend/src/component/feature/view/metric-container.jsx index 544b038893..3fcc984f0a 100644 --- a/frontend/src/component/feature/metric-container.jsx +++ b/frontend/src/component/feature/view/metric-container.jsx @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; -import { fetchFeatureMetrics, fetchSeenApps } from '../../store/feature-metrics-actions'; +import { fetchFeatureMetrics, fetchSeenApps } from '../../../store/feature-metrics-actions'; import MatricComponent from './metric-component'; diff --git a/frontend/src/component/feature/metrics.scss b/frontend/src/component/feature/view/metric.module.scss similarity index 100% rename from frontend/src/component/feature/metrics.scss rename to frontend/src/component/feature/view/metric.module.scss diff --git a/frontend/src/component/feature/status-update-component.jsx b/frontend/src/component/feature/view/status-update-component.jsx similarity index 95% rename from frontend/src/component/feature/status-update-component.jsx rename to frontend/src/component/feature/view/status-update-component.jsx index f5dfbbea8e..5af929b5ac 100644 --- a/frontend/src/component/feature/status-update-component.jsx +++ b/frontend/src/component/feature/view/status-update-component.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { Menu, MenuItem } from 'react-mdl'; -import { DropdownButton } from '../common'; +import { DropdownButton } from '../../common'; import PropTypes from 'prop-types'; export default function StatusUpdateComponent({ stale, updateStale }) { diff --git a/frontend/src/component/feature/form/update-description-component.jsx b/frontend/src/component/feature/view/update-description-component.jsx similarity index 100% rename from frontend/src/component/feature/form/update-description-component.jsx rename to frontend/src/component/feature/view/update-description-component.jsx diff --git a/frontend/src/component/feature/view/update-strategies-component.jsx b/frontend/src/component/feature/view/update-strategies-component.jsx new file mode 100644 index 0000000000..d364af3985 --- /dev/null +++ b/frontend/src/component/feature/view/update-strategies-component.jsx @@ -0,0 +1,37 @@ +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'; + +function UpdateStrategiesComponent(props) { + const { editable, configuredStrategies, strategies } = props; + if (!configuredStrategies || configuredStrategies.length === 0) return null; + if (!strategies || strategies.length === 0) return null; + + return ( +
+ {editable && } />} + +
+ ); +} + +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, +}; + +UpdateStrategiesComponent.defaultProps = { + editable: true, +}; + +export default UpdateStrategiesComponent; diff --git a/frontend/src/component/feature/view/update-strategies-container.jsx b/frontend/src/component/feature/view/update-strategies-container.jsx new file mode 100644 index 0000000000..f48665b8ed --- /dev/null +++ b/frontend/src/component/feature/view/update-strategies-container.jsx @@ -0,0 +1,48 @@ +/* eslint-disable no-console */ +import { connect } from 'react-redux'; +import arrayMove from 'array-move'; + +import { requestUpdateFeatureToggleStrategies } from '../../../store/feature-actions'; +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}`); + 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); + }, +}); + +export default connect(mapStateToProps, mapDispatchToProps)(UpdateStrategiesComponent); diff --git a/frontend/src/component/feature/view-component.jsx b/frontend/src/component/feature/view/view-component.jsx similarity index 89% rename from frontend/src/component/feature/view-component.jsx rename to frontend/src/component/feature/view/view-component.jsx index bcf8e05cd2..5c8f809c02 100644 --- a/frontend/src/component/feature/view-component.jsx +++ b/frontend/src/component/feature/view/view-component.jsx @@ -3,17 +3,16 @@ import PropTypes from 'prop-types'; import { Tabs, Tab, ProgressBar, Button, Card, CardTitle, CardActions, Switch, CardText } from 'react-mdl'; import { Link } from 'react-router-dom'; -import HistoryComponent from '../history/history-list-toggle-container'; +import HistoryComponent from '../../history/history-list-toggle-container'; import MetricComponent from './metric-container'; -import EditFeatureToggle from './form/form-update-feature-container'; -import EditVariants from './variant/update-variant-container'; -import ViewFeatureToggle from './form/form-view-feature-container'; -import FeatureTypeSelect from './form/feature-type-select-container'; -import ProjectSelect from './form/project-select-container'; -import UpdateDescriptionComponent from './form/update-description-component'; -import { styles as commonStyles } from '../common'; -import { CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE } from '../../permissions'; -import StatusComponent from './status-component'; +import UpdateStrategies from './update-strategies-container'; +import EditVariants from '../variant/update-variant-container'; +import FeatureTypeSelect from '../feature-type-select-container'; +import ProjectSelect from '../project-select-container'; +import UpdateDescriptionComponent from './update-description-component'; +import { styles as commonStyles } from '../../common'; +import { CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE } from '../../../permissions'; +import StatusComponent from '../status-component'; import StatusUpdateComponent from './status-update-component'; const TABS = { @@ -40,6 +39,7 @@ export default class ViewFeatureToggleComponent extends React.Component { revive: PropTypes.func, fetchArchive: PropTypes.func, fetchFeatureToggles: PropTypes.func, + fetchFeatureToggle: PropTypes.func, editFeatureToggle: PropTypes.func, featureToggle: PropTypes.object, history: PropTypes.object.isRequired, @@ -65,10 +65,17 @@ export default class ViewFeatureToggleComponent extends React.Component { } else if (TABS[activeTab] === TABS.strategies) { if (this.isFeatureView && hasPermission(UPDATE_FEATURE)) { return ( - + ); } - return ; + return ( + + ); } else if (TABS[activeTab] === TABS.variants) { return ( ({ @@ -20,6 +21,7 @@ export default connect( }), { fetchFeatureToggles, + fetchFeatureToggle, toggleFeature, setStale, removeFeatureToggle, diff --git a/frontend/src/data/feature-api.js b/frontend/src/data/feature-api.js index 2bc082eaa4..185fc02cf3 100644 --- a/frontend/src/data/feature-api.js +++ b/frontend/src/data/feature-api.js @@ -18,6 +18,12 @@ function fetchAll() { .then(response => response.json()); } +function fetchFeatureToggle(name) { + return fetch(`${URI}/${name}`, { credentials: 'include' }) + .then(throwIfNotSuccess) + .then(response => response.json()); +} + function create(featureToggle) { return validateToggle(featureToggle) .then(() => @@ -82,6 +88,7 @@ function remove(featureToggleName) { export default { fetchAll, + fetchFeatureToggle, create, validate, update, diff --git a/frontend/src/page/features/copy.js b/frontend/src/page/features/copy.js index 8fbc608aca..bce9a49d68 100644 --- a/frontend/src/page/features/copy.js +++ b/frontend/src/page/features/copy.js @@ -1,5 +1,5 @@ import React from 'react'; -import CopyFeatureToggleForm from '../../component/feature/form/form-copy-feature-container'; +import CopyFeatureToggleForm from '../../component/feature/create/copy-feature-container'; import PropTypes from 'prop-types'; const render = ({ history, match: { params } }) => ( diff --git a/frontend/src/page/features/create.js b/frontend/src/page/features/create.js index 8531c74052..2ebec98857 100644 --- a/frontend/src/page/features/create.js +++ b/frontend/src/page/features/create.js @@ -1,5 +1,5 @@ import React from 'react'; -import AddFeatureToggleForm from '../../component/feature/form/form-add-feature-container'; +import AddFeatureToggleForm from '../../component/feature/create/add-feature-container'; import PropTypes from 'prop-types'; const render = ({ history }) => ; diff --git a/frontend/src/page/features/index.js b/frontend/src/page/features/index.js index bf18de3066..0f2c163ed9 100644 --- a/frontend/src/page/features/index.js +++ b/frontend/src/page/features/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import FeatureListContainer from './../../component/feature/list-container'; +import FeatureListContainer from './../../component/feature/list/list-container'; import PropTypes from 'prop-types'; const render = ({ history }) => ; diff --git a/frontend/src/page/features/show.js b/frontend/src/page/features/show.js index af7c14203f..853664b111 100644 --- a/frontend/src/page/features/show.js +++ b/frontend/src/page/features/show.js @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import ViewFeatureToggle from './../../component/feature/view-container'; +import ViewFeatureToggle from './../../component/feature/view/view-container'; export default class Features extends PureComponent { static propTypes = { diff --git a/frontend/src/store/feature-actions.js b/frontend/src/store/feature-actions.js index 39d978e273..73ec90e3c3 100644 --- a/frontend/src/store/feature-actions.js +++ b/frontend/src/store/feature-actions.js @@ -19,6 +19,10 @@ export const ERROR_UPDATE_FEATURE_TOGGLE = 'ERROR_UPDATE_FEATURE_TOGGLE'; export const ERROR_REMOVE_FEATURE_TOGGLE = 'ERROR_REMOVE_FEATURE_TOGGLE'; export const UPDATE_FEATURE_TOGGLE_STRATEGIES = 'UPDATE_FEATURE_TOGGLE_STRATEGIES'; +export const RECEIVE_FEATURE_TOGGLE = 'RECEIVE_FEATURE_TOGGLE'; +export const START_FETCH_FEATURE_TOGGLE = 'START_FETCH_FEATURE_TOGGLE'; +export const ERROR_FETCH_FEATURE_TOGGLE = 'START_FETCH_FEATURE_TOGGLE'; + export function toggleFeature(enable, name) { debug('Toggle feature toggle ', name); return dispatch => { @@ -49,6 +53,15 @@ function receiveFeatureToggles(json) { }; } +function receiveFeatureToggle(featureToggle) { + debug('reviced feature toggle', featureToggle); + return { + type: RECEIVE_FEATURE_TOGGLE, + featureToggle, + receivedAt: Date.now(), + }; +} + export function fetchFeatureToggles() { debug('Start fetching feature toggles'); return dispatch => { @@ -61,6 +74,18 @@ export function fetchFeatureToggles() { }; } +export function fetchFeatureToggle(name) { + debug('Start fetching feature toggles'); + return dispatch => { + dispatch({ type: START_FETCH_FEATURE_TOGGLE }); + + return api + .fetchFeatureToggle(name) + .then(json => dispatch(receiveFeatureToggle(json))) + .catch(dispatchAndThrow(dispatch, ERROR_FETCH_FEATURE_TOGGLE)); + }; +} + export function createFeatureToggles(featureToggle) { return dispatch => { dispatch({ type: START_CREATE_FEATURE_TOGGLE }); diff --git a/frontend/src/store/feature-store.js b/frontend/src/store/feature-store.js index eb004d855f..950cd26c04 100644 --- a/frontend/src/store/feature-store.js +++ b/frontend/src/store/feature-store.js @@ -4,6 +4,7 @@ const debug = require('debug')('unleash:feature-store'); import { ADD_FEATURE_TOGGLE, RECEIVE_FEATURE_TOGGLES, + RECEIVE_FEATURE_TOGGLE, UPDATE_FEATURE_TOGGLE, UPDATE_FEATURE_TOGGLE_STRATEGIES, REMOVE_FEATURE_TOGGLE, @@ -47,6 +48,15 @@ const features = (state = new List([]), action) => { return toggle; } }); + case RECEIVE_FEATURE_TOGGLE: + debug(RECEIVE_FEATURE_TOGGLE, action); + return state.map(toggle => { + if (toggle.get('name') === action.featureToggle.name) { + return new $Map(action.featureToggle); + } else { + return toggle; + } + }); case RECEIVE_FEATURE_TOGGLES: debug(RECEIVE_FEATURE_TOGGLES, action); return new List(action.featureToggles.map($Map)); diff --git a/frontend/src/store/loader.js b/frontend/src/store/loader.js index ff7e743daa..c302d37469 100644 --- a/frontend/src/store/loader.js +++ b/frontend/src/store/loader.js @@ -2,6 +2,7 @@ import { fetchUIConfig } from './ui-config/actions'; import { fetchContext } from './context/actions'; import { fetchFeatureTypes } from './feature-type/actions'; import { fetchProjects } from './project/actions'; +import { fetchStrategies } from './strategy/actions'; export function loadInitalData() { return dispatch => { @@ -9,5 +10,6 @@ export function loadInitalData() { fetchContext()(dispatch); fetchFeatureTypes()(dispatch); fetchProjects()(dispatch); + fetchStrategies()(dispatch); }; }