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)}
- />
-
-
-
- );
- }
-}
diff --git a/frontend/src/component/feature/form/form-update-feature-component.jsx b/frontend/src/component/feature/form/form-update-feature-component.jsx
deleted file mode 100644
index 1b8942407c..0000000000
--- a/frontend/src/component/feature/form/form-update-feature-component.jsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import StrategiesSection from './strategies-section-container';
-
-import styles from './strategy.scss';
-
-import { FormButtons } from './../../common';
-
-class UpdateFeatureComponent extends Component {
- // static displayName = `UpdateFeatureComponent-{getDisplayName(Component)}`;
- // eslint-disable-next-line camelcase
- UNSAFE_componentWillMount() {
- // TODO unwind this stuff
- if (this.props.initCallRequired === true) {
- this.props.init(this.props.input);
- }
- }
-
- render() {
- const {
- input,
- features,
- addStrategy,
- removeStrategy,
- updateStrategy,
- moveStrategy,
- onSubmit,
- onCancel,
- } = this.props;
- const {
- name, // eslint-disable-line
- } = input;
- const configuredStrategies = input.strategies || [];
-
- return (
-
- );
- }
-}
-
-UpdateFeatureComponent.propTypes = {
- input: PropTypes.object,
- features: PropTypes.array,
- 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,
- initCallRequired: PropTypes.bool,
- init: PropTypes.func,
-};
-
-export default UpdateFeatureComponent;
diff --git a/frontend/src/component/feature/form/form-update-feature-container.jsx b/frontend/src/component/feature/form/form-update-feature-container.jsx
deleted file mode 100644
index b643bde3ae..0000000000
--- a/frontend/src/component/feature/form/form-update-feature-container.jsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import { connect } from 'react-redux';
-
-import { requestUpdateFeatureToggleStrategies } from '../../../store/feature-actions';
-import { createMapper, createActions } from '../../input-helpers';
-import UpdateFeatureToggleComponent from './form-update-feature-component';
-
-const ID = 'edit-feature-toggle';
-function getId(props) {
- return [ID, props.featureToggle.name];
-}
-// TODO: need to scope to the active featureToggle
-// best is to emulate the "input-storage"?
-const mapStateToProps = createMapper({
- id: getId,
- getDefault: (state, ownProps) => {
- ownProps.featureToggle.strategies.forEach((strategy, index) => {
- strategy.id = Math.round(Math.random() * 1000000 * (1 + index));
- });
- return ownProps.featureToggle;
- },
- prepare: props => {
- props.editmode = true;
- return props;
- },
-});
-
-const prepare = (methods, dispatch, ownProps) => {
- methods.onSubmit = (input, features) => e => {
- e.preventDefault();
-
- // This view will only update strategies!
- const featureToggle = features.find(f => f.name === input.name);
-
- const updatedStrategies = JSON.parse(
- JSON.stringify(input.strategies, (key, value) => (key === 'id' ? undefined : value))
- );
-
- requestUpdateFeatureToggleStrategies(featureToggle, updatedStrategies)(dispatch);
- };
-
- methods.onCancel = evt => {
- evt.preventDefault();
- methods.clear();
- ownProps.history.push(`/features`);
- };
-
- methods.addStrategy = v => {
- v.id = Math.round(Math.random() * 10000000);
- methods.pushToList('strategies', v);
- };
-
- methods.removeStrategy = index => {
- methods.removeFromList('strategies', index);
- };
-
- methods.moveStrategy = (index, toIndex) => {
- methods.moveItem('strategies', index, toIndex);
- };
-
- methods.updateStrategy = (index, n) => {
- methods.updateInList('strategies', index, n);
- };
-
- methods.validateName = () => {};
-
- return methods;
-};
-
-const actions = createActions({
- id: getId,
- prepare,
-});
-
-export default connect(mapStateToProps, actions)(UpdateFeatureToggleComponent);
diff --git a/frontend/src/component/feature/form/form-view-feature-component.jsx b/frontend/src/component/feature/form/form-view-feature-component.jsx
deleted file mode 100644
index 7cc3ebdf64..0000000000
--- a/frontend/src/component/feature/form/form-view-feature-component.jsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import StrategiesSection from './strategies-section-container';
-import { Button, Icon } from 'react-mdl';
-
-class ViewFeatureComponent extends Component {
- render() {
- const { input, onCancel } = this.props;
- const configuredStrategies = input.strategies || [];
-
- return (
-
- );
- }
-}
-
-ViewFeatureComponent.propTypes = {
- input: PropTypes.object,
- onCancel: PropTypes.func.isRequired,
- initCallRequired: PropTypes.bool,
- init: PropTypes.func,
-};
-
-export default ViewFeatureComponent;
diff --git a/frontend/src/component/feature/form/form-view-feature-container.jsx b/frontend/src/component/feature/form/form-view-feature-container.jsx
deleted file mode 100644
index 1f7dc4c463..0000000000
--- a/frontend/src/component/feature/form/form-view-feature-container.jsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { connect } from 'react-redux';
-import { createMapper, createActions } from '../../input-helpers';
-import ViewFeatureToggleComponent from './form-view-feature-component';
-
-const ID = 'view-feature-toggle';
-function getId(props) {
- return [ID, props.featureToggle.name];
-}
-// TODO: need to scope to the active featureToggle
-// best is to emulate the "input-storage"?
-const mapStateToProps = createMapper({
- id: getId,
- getDefault: (state, ownProps) => {
- ownProps.featureToggle.strategies.forEach((strategy, index) => {
- strategy.id = Math.round(Math.random() * 1000000 * (1 + index));
- });
- return ownProps.featureToggle;
- },
- prepare: props => {
- props.editmode = true;
- return props;
- },
-});
-
-const prepare = methods => {
- methods.onCancel = evt => {
- evt.preventDefault();
- methods.clear();
- this.props.history.push(`/archive`);
- };
- return methods;
-};
-
-const actions = createActions({
- id: getId,
- prepare,
-});
-
-export default connect(mapStateToProps, actions)(ViewFeatureToggleComponent);
diff --git a/frontend/src/component/feature/form/strategies-section-container.jsx b/frontend/src/component/feature/form/strategies-section-container.jsx
deleted file mode 100644
index dc6e6a2d89..0000000000
--- a/frontend/src/component/feature/form/strategies-section-container.jsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { connect } from 'react-redux';
-import StrategiesSectionComponent from './strategies-section';
-import { fetchStrategies } from '../../../store/strategy/actions';
-
-const StrategiesSection = connect(
- (state, ownProps) => ({
- strategies: state.strategies.get('list').toArray(),
- configuredStrategies: ownProps.configuredStrategies,
- }),
- { fetchStrategies }
-)(StrategiesSectionComponent);
-export default StrategiesSection;
diff --git a/frontend/src/component/feature/form/strategies-section.jsx b/frontend/src/component/feature/form/strategies-section.jsx
deleted file mode 100644
index cbf9f1ffe2..0000000000
--- a/frontend/src/component/feature/form/strategies-section.jsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { ProgressBar } from 'react-mdl';
-import StrategiesList from './strategies-list';
-import AddStrategy from './strategies-add';
-import { HeaderTitle } from '../../common';
-
-class StrategiesSectionComponent extends React.Component {
- static propTypes = {
- strategies: PropTypes.array.isRequired,
- featureToggleName: PropTypes.string,
- addStrategy: PropTypes.func,
- removeStrategy: PropTypes.func,
- updateStrategy: PropTypes.func,
- fetchStrategies: PropTypes.func,
- };
-
- // eslint-disable-next-line camelcase
- UNSAFE_componentWillMount() {
- this.props.fetchStrategies();
- }
-
- render() {
- if (!this.props.strategies || this.props.strategies.length === 0) {
- return ;
- }
-
- return (
-
- {this.props.addStrategy ? (
- } />
- ) : (
-
- )}
-
-
- );
- }
-}
-
-export default StrategiesSectionComponent;
diff --git a/frontend/src/component/feature/form/strategy-configure.jsx b/frontend/src/component/feature/form/strategy-configure.jsx
deleted file mode 100644
index d12ba10895..0000000000
--- a/frontend/src/component/feature/form/strategy-configure.jsx
+++ /dev/null
@@ -1,299 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import {
- Textfield,
- Button,
- Card,
- CardTitle,
- CardText,
- CardActions,
- CardMenu,
- IconButton,
- Icon,
- Switch,
- Tooltip,
-} from 'react-mdl';
-import { DragSource, DropTarget } from 'react-dnd';
-import { Link } from 'react-router-dom';
-import flow from 'lodash/flow';
-import StrategyInputPercentage from './strategy-input-percentage';
-import FlexibleRolloutStrategyInput from './flexible-rollout-strategy-input';
-import StrategyInputList from './strategy-input-list';
-import styles from './strategy.scss';
-
-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(),
- };
-}
-
-class StrategyConfigure extends React.Component {
- /* eslint-enable */
- static propTypes = {
- strategy: PropTypes.object.isRequired,
- featureToggleName: PropTypes.string.isRequired,
- strategyDefinition: PropTypes.object,
- updateStrategy: PropTypes.func,
- removeStrategy: PropTypes.func,
- moveStrategy: PropTypes.func,
- isDragging: PropTypes.bool.isRequired,
- connectDragPreview: PropTypes.func.isRequired,
- connectDragSource: PropTypes.func.isRequired,
- connectDropTarget: PropTypes.func.isRequired,
- };
-
- handleConfigChange = (key, e) => {
- this.setConfig(key, e.target.value);
- };
-
- handleSwitchChange = (key, currentValue) => {
- const value = currentValue === 'false' ? 'true' : 'false';
- this.setConfig(key, value);
- };
-
- setConfig = (key, value) => {
- const parameters = this.props.strategy.parameters || {};
- parameters[key] = value;
-
- const updatedStrategy = Object.assign({}, this.props.strategy, {
- parameters,
- });
-
- this.props.updateStrategy(updatedStrategy);
- };
-
- handleRemove = evt => {
- evt.preventDefault();
- this.props.removeStrategy();
- };
-
- renderStrategContent(strategyDefinition) {
- if (strategyDefinition.name === 'default') {
- return {strategyDefinition.description}
;
- }
- if (strategyDefinition.name === 'flexibleRollout') {
- return (
-
- );
- } else {
- return {this.renderInputFields(strategyDefinition)}
;
- }
- }
-
- renderInputFields({ parameters }) {
- if (parameters && parameters.length > 0) {
- return parameters.map(({ name, type, description, required }) => {
- let value = this.props.strategy.parameters[name];
- if (type === 'percentage') {
- if (value == null || (typeof value === 'string' && value === '')) {
- this.setConfig(name, 50);
- }
- 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') {
- if (!value) {
- this.handleSwitchChange(name, value);
- }
- return (
-
-
- {name}{' '}
- {description && (
-
-
-
- )}
-
-
- );
- } else {
- if (name === 'groupId' && !value) {
- this.setConfig('groupId', this.props.featureToggleName);
- }
- return (
-
-
- {description &&
{description}
}
-
- );
- }
- });
- }
- return null;
- }
-
- render() {
- const { isDragging, connectDragPreview, connectDragSource, connectDropTarget } = this.props;
-
- let item;
- if (this.props.strategyDefinition) {
- const description = this.props.strategyDefinition.description;
- const strategyContent = this.renderStrategContent(this.props.strategyDefinition);
- const { name } = this.props.strategy;
- item = (
-
-
-
-
- {name}
-
-
- {strategyContent && {strategyContent}}
-
-
-
-
-
- {this.props.removeStrategy ? (
-
- ) : (
-
- )}
- {connectDragSource(
-
-
-
- )}
-
-
- );
- } else {
- const { name } = this.props.strategy;
- item = (
-
- "{name}" deleted?
-
- The strategy "{name}" does not exist on this server.
- Want to create it now?
-
-
-
-
-
- );
- }
-
- return connectDragPreview(connectDropTarget({item}
));
- }
-}
-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/form/strategy-input-percentage.jsx b/frontend/src/component/feature/form/strategy-input-percentage.jsx
deleted file mode 100644
index 4f0e7b6976..0000000000
--- a/frontend/src/component/feature/form/strategy-input-percentage.jsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { Slider, Grid, Cell } from 'react-mdl';
-
-const labelStyle = {
- textAlign: 'center',
- color: '#3f51b5',
-};
-
-const infoLabelStyle = {
- fontSize: '0.8em',
- color: 'gray',
- paddingBottom: '-3px',
-};
-
-const InputPercentage = ({ name, minLabel, maxLabel, value, onChange }) => (
-
-
-
- {minLabel}
- |
-
- {name}: {value}%
- |
-
- {maxLabel}
- |
-
-
-
-);
-
-InputPercentage.propTypes = {
- name: PropTypes.string,
- minLabel: PropTypes.string,
- maxLabel: PropTypes.string,
- value: PropTypes.number,
- onChange: PropTypes.func.isRequired,
-};
-
-export default InputPercentage;
diff --git a/frontend/src/component/feature/form/util.js b/frontend/src/component/feature/form/util.js
deleted file mode 100644
index 0fcca83bda..0000000000
--- a/frontend/src/component/feature/form/util.js
+++ /dev/null
@@ -1,17 +0,0 @@
-export const trim = value => {
- if (value && value.trim) {
- return value.trim();
- } else {
- return value;
- }
-};
-
-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__/__snapshots__/feature-list-item-component-test.jsx.snap b/frontend/src/component/feature/list/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap
similarity index 100%
rename from frontend/src/component/feature/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap
rename to frontend/src/component/feature/list/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap
diff --git a/frontend/src/component/feature/__tests__/__snapshots__/list-component-test.jsx.snap b/frontend/src/component/feature/list/__tests__/__snapshots__/list-component-test.jsx.snap
similarity index 99%
rename from frontend/src/component/feature/__tests__/__snapshots__/list-component-test.jsx.snap
rename to frontend/src/component/feature/list/__tests__/__snapshots__/list-component-test.jsx.snap
index 9867baaab4..c9a866393d 100644
--- a/frontend/src/component/feature/__tests__/__snapshots__/list-component-test.jsx.snap
+++ b/frontend/src/component/feature/list/__tests__/__snapshots__/list-component-test.jsx.snap
@@ -188,7 +188,7 @@ exports[`renders correctly with one feature 1`] = `
-
- ({
+jest.mock('../list-item-component', () => ({
__esModule: true,
- default: 'Feature',
+ default: 'ListItem',
}));
jest.mock('../project-container', () => 'Project');
diff --git a/frontend/src/component/feature/feature-type-component.jsx b/frontend/src/component/feature/list/feature-type-component.jsx
similarity index 93%
rename from frontend/src/component/feature/feature-type-component.jsx
rename to frontend/src/component/feature/list/feature-type-component.jsx
index 19489e6326..4000f96fd1 100644
--- a/frontend/src/component/feature/feature-type-component.jsx
+++ b/frontend/src/component/feature/list/feature-type-component.jsx
@@ -1,7 +1,7 @@
import React, { memo } from 'react';
import { Chip } from 'react-mdl';
import PropTypes from 'prop-types';
-import styles from './feature.scss';
+import styles from './list.module.scss';
function StatusComponent({ type, types, onClick }) {
const typeObject = types.find(o => o.id === type) || { id: type, name: type };
diff --git a/frontend/src/component/feature/feature-type-container.jsx b/frontend/src/component/feature/list/feature-type-container.jsx
similarity index 100%
rename from frontend/src/component/feature/feature-type-container.jsx
rename to frontend/src/component/feature/list/feature-type-container.jsx
diff --git a/frontend/src/component/feature/list-component.jsx b/frontend/src/component/feature/list/list-component.jsx
similarity index 97%
rename from frontend/src/component/feature/list-component.jsx
rename to frontend/src/component/feature/list/list-component.jsx
index 3a83318c1c..eb7681eeb1 100644
--- a/frontend/src/component/feature/list-component.jsx
+++ b/frontend/src/component/feature/list/list-component.jsx
@@ -3,10 +3,10 @@ import PropTypes from 'prop-types';
import { debounce } from 'debounce';
import { Link } from 'react-router-dom';
import { Icon, FABButton, Menu, MenuItem, Card, CardActions, List } from 'react-mdl';
-import Feature from './feature-list-item-component';
-import { MenuItemWithIcon, DropdownButton, styles as commonStyles } from '../common';
-import SearchField from '../common/search-field';
-import { CREATE_FEATURE } from '../../permissions';
+import Feature from './list-item-component';
+import { MenuItemWithIcon, DropdownButton, styles as commonStyles } from '../../common';
+import SearchField from '../../common/search-field';
+import { CREATE_FEATURE } from '../../../permissions';
import ProjectMenu from './project-container';
export default class FeatureListComponent extends React.Component {
diff --git a/frontend/src/component/feature/list-container.jsx b/frontend/src/component/feature/list/list-container.jsx
similarity index 94%
rename from frontend/src/component/feature/list-container.jsx
rename to frontend/src/component/feature/list/list-container.jsx
index f3e4be8f88..1ca51def01 100644
--- a/frontend/src/component/feature/list-container.jsx
+++ b/frontend/src/component/feature/list/list-container.jsx
@@ -1,9 +1,9 @@
import { connect } from 'react-redux';
-import { toggleFeature, fetchFeatureToggles } from '../../store/feature-actions';
-import { updateSettingForGroup } from '../../store/settings/actions';
+import { toggleFeature, fetchFeatureToggles } from '../../../store/feature-actions';
+import { updateSettingForGroup } from '../../../store/settings/actions';
import FeatureListComponent from './list-component';
-import { hasPermission } from '../../permissions';
+import { hasPermission } from '../../../permissions';
export const mapStateToPropsConfigurable = isFeature => state => {
const featureMetrics = state.featureMetrics.toJS();
diff --git a/frontend/src/component/feature/feature-list-item-component.jsx b/frontend/src/component/feature/list/list-item-component.jsx
similarity index 93%
rename from frontend/src/component/feature/feature-list-item-component.jsx
rename to frontend/src/component/feature/list/list-item-component.jsx
index 916d3ec09e..e85db6c43c 100644
--- a/frontend/src/component/feature/feature-list-item-component.jsx
+++ b/frontend/src/component/feature/list/list-item-component.jsx
@@ -3,13 +3,13 @@ import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Switch, ListItem, ListItemAction, Icon } from 'react-mdl';
import TimeAgo from 'react-timeago';
-import Progress from './progress';
-import { UPDATE_FEATURE } from '../../permissions';
-import { calc, styles as commonStyles } from '../common';
-import Status from './status-component';
+import Progress from '../progress-component';
+import { UPDATE_FEATURE } from '../../../permissions';
+import { calc, styles as commonStyles } from '../../common';
+import Status from '../status-component';
import FeatureType from './feature-type-container';
-import styles from './feature.scss';
+import styles from './list.module.scss';
const Feature = ({
feature,
diff --git a/frontend/src/component/feature/feature.scss b/frontend/src/component/feature/list/list.module.scss
similarity index 100%
rename from frontend/src/component/feature/feature.scss
rename to frontend/src/component/feature/list/list.module.scss
diff --git a/frontend/src/component/feature/project-component.jsx b/frontend/src/component/feature/list/project-component.jsx
similarity index 97%
rename from frontend/src/component/feature/project-component.jsx
rename to frontend/src/component/feature/list/project-component.jsx
index 44d76d8f05..bd0b9493de 100644
--- a/frontend/src/component/feature/project-component.jsx
+++ b/frontend/src/component/feature/list/project-component.jsx
@@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { Menu, MenuItem } from 'react-mdl';
-import { DropdownButton } from '../common';
+import { DropdownButton } from '../../common';
import PropTypes from 'prop-types';
const ALL_PROJECTS = { id: '*', name: '> All projects' };
diff --git a/frontend/src/component/feature/project-container.jsx b/frontend/src/component/feature/list/project-container.jsx
similarity index 85%
rename from frontend/src/component/feature/project-container.jsx
rename to frontend/src/component/feature/list/project-container.jsx
index f43350084a..ea40511c94 100644
--- a/frontend/src/component/feature/project-container.jsx
+++ b/frontend/src/component/feature/list/project-container.jsx
@@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import Component from './project-component';
-import { fetchProjects } from './../../store/project/actions';
+import { fetchProjects } from './../../../store/project/actions';
const mapStateToProps = (state, ownProps) => ({
projects: state.projects.toJS(),
diff --git a/frontend/src/component/feature/progress.jsx b/frontend/src/component/feature/progress-component.jsx
similarity index 98%
rename from frontend/src/component/feature/progress.jsx
rename to frontend/src/component/feature/progress-component.jsx
index b65a557ece..36286bb370 100644
--- a/frontend/src/component/feature/progress.jsx
+++ b/frontend/src/component/feature/progress-component.jsx
@@ -1,10 +1,10 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
-import styles from './progress-styles.scss';
+import styles from './progress.module.scss';
class Progress extends Component {
constructor(props) {
- super(props);
+ super();
this.state = {
percentage: props.initialAnimation ? 0 : props.percentage,
diff --git a/frontend/src/component/feature/progress-styles.scss b/frontend/src/component/feature/progress.module.scss
similarity index 100%
rename from frontend/src/component/feature/progress-styles.scss
rename to frontend/src/component/feature/progress.module.scss
diff --git a/frontend/src/component/feature/form/project-select-component.jsx b/frontend/src/component/feature/project-select-component.jsx
similarity index 96%
rename from frontend/src/component/feature/form/project-select-component.jsx
rename to frontend/src/component/feature/project-select-component.jsx
index 0c4c540cd9..cb3b87a790 100644
--- a/frontend/src/component/feature/form/project-select-component.jsx
+++ b/frontend/src/component/feature/project-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 ProjectSelectComponent extends Component {
componentDidMount() {
diff --git a/frontend/src/component/feature/form/project-select-container.jsx b/frontend/src/component/feature/project-select-container.jsx
similarity index 84%
rename from frontend/src/component/feature/form/project-select-container.jsx
rename to frontend/src/component/feature/project-select-container.jsx
index ba148e60f7..7f4e26f31e 100644
--- a/frontend/src/component/feature/form/project-select-container.jsx
+++ b/frontend/src/component/feature/project-select-container.jsx
@@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import ProjectSelectComponent from './project-select-component';
-import { fetchProjects } from './../../../store/project/actions';
+import { fetchProjects } from './../../store/project/actions';
const mapStateToProps = state => {
const projects = state.projects.toJS();
diff --git a/frontend/src/component/feature/strategy/__test__/.eslintrc b/frontend/src/component/feature/strategy/__test__/.eslintrc
new file mode 100644
index 0000000000..eba2077219
--- /dev/null
+++ b/frontend/src/component/feature/strategy/__test__/.eslintrc
@@ -0,0 +1,5 @@
+{
+ "env": {
+ "jest": true
+ }
+}
diff --git a/frontend/src/component/feature/form/__tests__/strategy-add-test.jsx b/frontend/src/component/feature/strategy/__test__/strategy-add-test.jsx
similarity index 81%
rename from frontend/src/component/feature/form/__tests__/strategy-add-test.jsx
rename to frontend/src/component/feature/strategy/__test__/strategy-add-test.jsx
index 46a35c6130..979027f4f1 100644
--- a/frontend/src/component/feature/form/__tests__/strategy-add-test.jsx
+++ b/frontend/src/component/feature/strategy/__test__/strategy-add-test.jsx
@@ -1,6 +1,6 @@
import React from 'react';
-import AddStrategy from './../strategies-add';
-import { shallow } from 'enzyme/build/index';
+import AddStrategy from '../strategies-add';
+import { shallow } from 'enzyme/build';
jest.mock('react-mdl');
@@ -22,7 +22,12 @@ let eventMock = {
},
};
const buildComponent = (addStrategy, fetchStrategies, strategies) => (
-
+
);
it('renders add strategy form with a list of available strategies', () => {
diff --git a/frontend/src/component/feature/form/__tests__/strategy-input-list-test.jsx b/frontend/src/component/feature/strategy/__test__/strategy-input-list-test.jsx
similarity index 66%
rename from frontend/src/component/feature/form/__tests__/strategy-input-list-test.jsx
rename to frontend/src/component/feature/strategy/__test__/strategy-input-list-test.jsx
index 5be4471733..4e18c72d79 100644
--- a/frontend/src/component/feature/form/__tests__/strategy-input-list-test.jsx
+++ b/frontend/src/component/feature/strategy/__test__/strategy-input-list-test.jsx
@@ -1,5 +1,5 @@
import React from 'react';
-import InputList from './../strategy-input-list';
+import InputList from '../../strategy/input-list';
import renderer from 'react-test-renderer';
import { shallow } from 'enzyme';
@@ -39,34 +39,6 @@ it('go inside onFocus', () => {
wrapper.find('react-mdl-Textfield').simulate('focus', focusMock);
});
-// https://github.com/airbnb/enzyme/issues/944
-it('spy onFocus', () => {
- let list = ['item1'];
- const name = 'featureName';
- const onFocus = jest.spyOn(InputList.prototype, 'onFocus');
- let focusMock = {
- preventDefault: () => {},
- stopPropagation: () => {},
- key: 'e',
- };
- const wrapper = shallow();
- wrapper.find('react-mdl-Textfield').simulate('focus', focusMock);
- expect(onFocus).toHaveBeenCalled();
-});
-
-it('spy onBlur', () => {
- let list = ['item1'];
- const name = 'featureName';
- const onFocus = jest.spyOn(InputList.prototype, 'onBlur');
- let focusMock = {
- preventDefault: () => {},
- stopPropagation: () => {},
- };
- const wrapper = shallow();
- wrapper.find('react-mdl-Textfield').simulate('blur', focusMock);
- expect(onFocus).toHaveBeenCalled();
-});
-
it('spy onClose', () => {
let list = ['item1'];
const name = 'featureName';
diff --git a/frontend/src/component/feature/strategy/default-strategy.jsx b/frontend/src/component/feature/strategy/default-strategy.jsx
new file mode 100644
index 0000000000..e58782bfeb
--- /dev/null
+++ b/frontend/src/component/feature/strategy/default-strategy.jsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+export default function DefaultStrategy({ strategyDefinition }) {
+ return {strategyDefinition.description}
;
+}
+
+DefaultStrategy.propTypes = {
+ strategyDefinition: PropTypes.object.isRequired,
+};
diff --git a/frontend/src/component/feature/strategy/flexible-rollout-strategy.jsx b/frontend/src/component/feature/strategy/flexible-rollout-strategy.jsx
new file mode 100644
index 0000000000..58bddb6eb7
--- /dev/null
+++ b/frontend/src/component/feature/strategy/flexible-rollout-strategy.jsx
@@ -0,0 +1,66 @@
+import React, { Component } from 'react';
+import { Textfield } from 'react-mdl';
+import strategyInputProps from './strategy-input-props';
+import Select from '../../common/select';
+
+import StrategyInputPercentage from './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 = strategyInputProps;
+
+ onConfiguUpdate = (field, evt) => {
+ evt.preventDefault();
+ const value = evt.target.value;
+ this.props.updateParameter(field, value);
+ };
+
+ render() {
+ const { editable, parameters, index } = this.props;
+
+ const rollout = parameters.rollout;
+ const stickiness = parameters.stickiness;
+ const groupId = parameters.groupId;
+
+ return (
+
+
+
Rollout
+
this.onConfiguUpdate('rollout', 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 (
+
+ );
+}
+
+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);
};
}