mirror of
https://github.com/Unleash/unleash.git
synced 2025-10-18 11:14:57 +02:00
fix: Should update activation strategies immediately (#229)
This commit is contained in:
parent
af8c9c1683
commit
8a083ce748
@ -1,8 +1,8 @@
|
|||||||
import { connect } from 'react-redux';
|
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 { fetchArchive, revive } from './../../store/archive-actions';
|
||||||
import { updateSettingForGroup } from './../../store/settings/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 mapStateToProps = mapStateToPropsConfigurable(false);
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { fetchArchive, revive } from './../../store/archive-actions';
|
import { fetchArchive, revive } from './../../store/archive-actions';
|
||||||
import ViewToggleComponent from './../feature/view-component';
|
import ViewToggleComponent from './../feature/view/view-component';
|
||||||
import { hasPermission } from '../../permissions';
|
import { hasPermission } from '../../permissions';
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
@ -72,3 +72,13 @@ export function updateWeight(variants, totalWeight) {
|
|||||||
return variant;
|
return variant;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function loadNameFromHash() {
|
||||||
|
let field = '';
|
||||||
|
try {
|
||||||
|
[, field] = document.location.hash.match(/name=([a-z0-9-_.]+)/i);
|
||||||
|
} catch (e) {
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Progress from './../progress';
|
import Progress from '../progress-component';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
|
|
||||||
jest.mock('react-mdl');
|
jest.mock('react-mdl');
|
||||||
|
@ -96,15 +96,6 @@ exports[`render the create feature page 1`] = `
|
|||||||
}
|
}
|
||||||
value="Description"
|
value="Description"
|
||||||
/>
|
/>
|
||||||
<StrategiesSection
|
|
||||||
addStrategy={[MockFunction]}
|
|
||||||
configuredStrategies={Array []}
|
|
||||||
featureToggleName="feature"
|
|
||||||
moveStrategy={[MockFunction]}
|
|
||||||
removeStrategy={[MockFunction]}
|
|
||||||
updateStrategy={[MockFunction]}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
</section>
|
</section>
|
||||||
<react-mdl-CardActions>
|
<react-mdl-CardActions>
|
||||||
<FormButtons
|
<FormButtons
|
@ -1,9 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AddFeatureComponent from './../form-add-feature-component';
|
import AddFeatureComponent from '../add-feature-component';
|
||||||
import { shallow } from 'enzyme/build/index';
|
import { shallow } from 'enzyme/build';
|
||||||
|
|
||||||
jest.mock('react-mdl');
|
jest.mock('react-mdl');
|
||||||
jest.mock('../strategies-section-container', () => 'StrategiesSection');
|
|
||||||
|
|
||||||
it('render the create feature page', () => {
|
it('render the create feature page', () => {
|
||||||
let input = {
|
let input = {
|
@ -1,13 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Textfield, Switch, Card, CardTitle, CardActions, Grid, Cell } from 'react-mdl';
|
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 FeatureTypeSelect from './feature-type-select-container';
|
import ProjectSelect from '../project-select-container';
|
||||||
import ProjectSelect from './project-select-container';
|
|
||||||
|
|
||||||
import { FormButtons } from './../../common';
|
import { FormButtons, styles as commonStyles } from '../../common';
|
||||||
import { styles as commonStyles } from '../../common';
|
import { trim } from '../../common/util';
|
||||||
import { trim } from './util';
|
|
||||||
|
|
||||||
class AddFeatureComponent extends Component {
|
class AddFeatureComponent extends Component {
|
||||||
// static displayName = `AddFeatureComponent-${getDisplayName(Component)}`;
|
// static displayName = `AddFeatureComponent-${getDisplayName(Component)}`;
|
||||||
@ -20,20 +18,7 @@ class AddFeatureComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { input, errors, setValue, validateName, onSubmit, onCancel } = this.props;
|
||||||
input,
|
|
||||||
errors,
|
|
||||||
setValue,
|
|
||||||
validateName,
|
|
||||||
addStrategy,
|
|
||||||
removeStrategy,
|
|
||||||
updateStrategy,
|
|
||||||
moveStrategy,
|
|
||||||
onSubmit,
|
|
||||||
onCancel,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const configuredStrategies = input.strategies || [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||||
@ -81,19 +66,6 @@ class AddFeatureComponent extends Component {
|
|||||||
value={input.description}
|
value={input.description}
|
||||||
onChange={v => setValue('description', v.target.value)}
|
onChange={v => setValue('description', v.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{input.name ? (
|
|
||||||
<StrategiesSection
|
|
||||||
configuredStrategies={configuredStrategies}
|
|
||||||
featureToggleName={input.name}
|
|
||||||
addStrategy={addStrategy}
|
|
||||||
updateStrategy={updateStrategy}
|
|
||||||
moveStrategy={moveStrategy}
|
|
||||||
removeStrategy={removeStrategy}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<br />
|
|
||||||
</section>
|
</section>
|
||||||
<CardActions>
|
<CardActions>
|
||||||
<FormButtons submitText={'Create'} onCancel={onCancel} />
|
<FormButtons submitText={'Create'} onCancel={onCancel} />
|
||||||
@ -108,10 +80,6 @@ AddFeatureComponent.propTypes = {
|
|||||||
input: PropTypes.object,
|
input: PropTypes.object,
|
||||||
errors: PropTypes.object,
|
errors: PropTypes.object,
|
||||||
setValue: PropTypes.func.isRequired,
|
setValue: PropTypes.func.isRequired,
|
||||||
addStrategy: PropTypes.func.isRequired,
|
|
||||||
removeStrategy: PropTypes.func.isRequired,
|
|
||||||
moveStrategy: PropTypes.func.isRequired,
|
|
||||||
updateStrategy: PropTypes.func.isRequired,
|
|
||||||
onSubmit: PropTypes.func.isRequired,
|
onSubmit: PropTypes.func.isRequired,
|
||||||
onCancel: PropTypes.func.isRequired,
|
onCancel: PropTypes.func.isRequired,
|
||||||
validateName: PropTypes.func.isRequired,
|
validateName: PropTypes.func.isRequired,
|
@ -1,12 +1,18 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import arrayMove from 'array-move';
|
import { createFeatureToggles, validateName } from '../../../store/feature-actions';
|
||||||
import { createFeatureToggles, validateName } from './../../../store/feature-actions';
|
import AddFeatureComponent from './add-feature-component';
|
||||||
import AddFeatureComponent from './form-add-feature-component';
|
import { loadNameFromHash } from '../../common/util';
|
||||||
import { loadNameFromHash } from './util';
|
|
||||||
|
|
||||||
const defaultStrategy = { name: 'default' };
|
const defaultStrategy = {
|
||||||
|
name: 'flexibleRollout',
|
||||||
|
parameters: {
|
||||||
|
rollout: '100',
|
||||||
|
stickiness: 'default',
|
||||||
|
groupId: 'a-new.toggle',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
function resolveCurrentProjectId(settings) {
|
function resolveCurrentProjectId(settings) {
|
||||||
if (!settings.currentProjectId) {
|
if (!settings.currentProjectId) {
|
||||||
@ -20,7 +26,7 @@ function resolveCurrentProjectId(settings) {
|
|||||||
|
|
||||||
class WrapperComponent extends Component {
|
class WrapperComponent extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super();
|
||||||
const name = loadNameFromHash();
|
const name = loadNameFromHash();
|
||||||
this.state = {
|
this.state = {
|
||||||
featureToggle: {
|
featureToggle: {
|
||||||
@ -28,6 +34,7 @@ class WrapperComponent extends Component {
|
|||||||
description: '',
|
description: '',
|
||||||
type: 'release',
|
type: 'release',
|
||||||
strategies: [],
|
strategies: [],
|
||||||
|
variants: [],
|
||||||
enabled: true,
|
enabled: true,
|
||||||
project: props.currentProjectId,
|
project: props.currentProjectId,
|
||||||
},
|
},
|
||||||
@ -54,49 +61,11 @@ class WrapperComponent extends Component {
|
|||||||
this.setState({ errors });
|
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 => {
|
onSubmit = evt => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const { createFeatureToggles, history } = this.props;
|
const { createFeatureToggles, history } = this.props;
|
||||||
const { featureToggle } = this.state;
|
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}`));
|
createFeatureToggles(featureToggle).then(() => history.push(`/features/strategies/${featureToggle.name}`));
|
||||||
};
|
};
|
||||||
@ -111,10 +80,6 @@ class WrapperComponent extends Component {
|
|||||||
<AddFeatureComponent
|
<AddFeatureComponent
|
||||||
onSubmit={this.onSubmit}
|
onSubmit={this.onSubmit}
|
||||||
onCancel={this.onCancel}
|
onCancel={this.onCancel}
|
||||||
addStrategy={this.addStrategy}
|
|
||||||
updateStrategy={this.updateStrategy}
|
|
||||||
removeStrategy={this.removeStrategy}
|
|
||||||
moveStrategy={this.moveStrategy}
|
|
||||||
validateName={this.validateName}
|
validateName={this.validateName}
|
||||||
setValue={this.setValue}
|
setValue={this.setValue}
|
||||||
input={this.state.featureToggle}
|
input={this.state.featureToggle}
|
@ -7,7 +7,7 @@ import { Button, Icon, Textfield, Checkbox, Card, CardTitle, CardActions } from
|
|||||||
|
|
||||||
import { styles as commonStyles } from '../../common';
|
import { styles as commonStyles } from '../../common';
|
||||||
|
|
||||||
import { trim } from './util';
|
import { trim } from '../../common/util';
|
||||||
|
|
||||||
class CopyFeatureComponent extends Component {
|
class CopyFeatureComponent extends Component {
|
||||||
// static displayName = `AddFeatureComponent-${getDisplayName(Component)}`;
|
// static displayName = `AddFeatureComponent-${getDisplayName(Component)}`;
|
@ -1,6 +1,6 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import CopyFeatureComponent from './form-copy-feature-component';
|
import CopyFeatureComponent from './copy-feature-component';
|
||||||
import { createFeatureToggles, validateName, fetchFeatureToggles } from './../../../store/feature-actions';
|
import { createFeatureToggles, validateName, fetchFeatureToggles } from '../../../store/feature-actions';
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
history: props.history,
|
history: props.history,
|
@ -1,6 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import MySelect from '../../common/select';
|
import MySelect from '../common/select';
|
||||||
|
|
||||||
class FeatureTypeSelectComponent extends Component {
|
class FeatureTypeSelectComponent extends Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
@ -1,6 +1,6 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import FeatureTypeSelectComponent from './feature-type-select-component';
|
import FeatureTypeSelectComponent from './feature-type-select-component';
|
||||||
import { fetchFeatureTypes } from './../../../store/feature-type/actions';
|
import { fetchFeatureTypes } from './../../store/feature-type/actions';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
types: state.featureTypes.toJS(),
|
types: state.featureTypes.toJS(),
|
@ -1,23 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`render the create feature page 1`] = `
|
|
||||||
<form>
|
|
||||||
<section
|
|
||||||
className="paddingDesktop"
|
|
||||||
>
|
|
||||||
<StrategiesSection
|
|
||||||
addStrategy={[MockFunction]}
|
|
||||||
configuredStrategies={Array []}
|
|
||||||
featureToggleName="feature"
|
|
||||||
moveStrategy={[MockFunction]}
|
|
||||||
removeStrategy={[MockFunction]}
|
|
||||||
updateStrategy={[MockFunction]}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<FormButtons
|
|
||||||
onCancel={[MockFunction]}
|
|
||||||
submitText="Update"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</form>
|
|
||||||
`;
|
|
@ -1,49 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`renders add strategy form with a list of available strategies 1`] = `
|
|
||||||
<div
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"display": "inline-block",
|
|
||||||
"height": "25px",
|
|
||||||
"position": "relative",
|
|
||||||
"width": "25px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<react-mdl-IconButton
|
|
||||||
accent={true}
|
|
||||||
id="strategies-add"
|
|
||||||
name="add"
|
|
||||||
onClick={[Function]}
|
|
||||||
raised={true}
|
|
||||||
title="Add Strategy"
|
|
||||||
/>
|
|
||||||
<react-mdl-Menu
|
|
||||||
align="right"
|
|
||||||
ripple={true}
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"backgroundColor": "rgb(247, 248, 255)",
|
|
||||||
"maxHeight": "300px",
|
|
||||||
"overflowY": "auto",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
target="strategies-add"
|
|
||||||
valign="bottom"
|
|
||||||
>
|
|
||||||
<react-mdl-MenuItem
|
|
||||||
disabled={true}
|
|
||||||
>
|
|
||||||
Add Strategy:
|
|
||||||
</react-mdl-MenuItem>
|
|
||||||
<react-mdl-MenuItem
|
|
||||||
key="default"
|
|
||||||
onClick={[Function]}
|
|
||||||
title="Default on/off strategy."
|
|
||||||
>
|
|
||||||
default
|
|
||||||
</react-mdl-MenuItem>
|
|
||||||
</react-mdl-Menu>
|
|
||||||
</div>
|
|
||||||
`;
|
|
@ -1,174 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`renders correctly when disabled 1`] = `
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
<i>
|
|
||||||
Please specify the list of
|
|
||||||
<code>
|
|
||||||
featureName
|
|
||||||
</code>
|
|
||||||
:
|
|
||||||
</i>
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"display": "flex",
|
|
||||||
"flexWrap": "wrap",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<react-mdl-Chip
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"marginRight": "3px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
item1
|
|
||||||
</react-mdl-Chip>
|
|
||||||
<react-mdl-Chip
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"marginRight": "3px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
item2
|
|
||||||
</react-mdl-Chip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`renders strategy with empty list as param 1`] = `
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
<i>
|
|
||||||
Please specify the list of
|
|
||||||
<code>
|
|
||||||
featureName
|
|
||||||
</code>
|
|
||||||
:
|
|
||||||
</i>
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"display": "flex",
|
|
||||||
"flexWrap": "wrap",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"display": "flex",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<react-mdl-Textfield
|
|
||||||
floatingLabel={true}
|
|
||||||
label="Enter value (val1, val2)"
|
|
||||||
name="featureName_input"
|
|
||||||
onBlur={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"flex": 1,
|
|
||||||
"width": "100%",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<react-mdl-IconButton
|
|
||||||
name="add"
|
|
||||||
onClick={[Function]}
|
|
||||||
raised={true}
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"flex": 1,
|
|
||||||
"flexGrow": 0,
|
|
||||||
"margin": "20px 0 0 10px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`renders strategy with list as param 1`] = `
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
<i>
|
|
||||||
Please specify the list of
|
|
||||||
<code>
|
|
||||||
featureName
|
|
||||||
</code>
|
|
||||||
:
|
|
||||||
</i>
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"display": "flex",
|
|
||||||
"flexWrap": "wrap",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<react-mdl-Chip
|
|
||||||
onClose={[Function]}
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"marginRight": "3px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
item1
|
|
||||||
</react-mdl-Chip>
|
|
||||||
<react-mdl-Chip
|
|
||||||
onClose={[Function]}
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"marginRight": "3px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
item2
|
|
||||||
</react-mdl-Chip>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"display": "flex",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<react-mdl-Textfield
|
|
||||||
floatingLabel={true}
|
|
||||||
label="Enter value (val1, val2)"
|
|
||||||
name="featureName_input"
|
|
||||||
onBlur={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"flex": 1,
|
|
||||||
"width": "100%",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<react-mdl-IconButton
|
|
||||||
name="add"
|
|
||||||
onClick={[Function]}
|
|
||||||
raised={true}
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"flex": 1,
|
|
||||||
"flexGrow": 0,
|
|
||||||
"margin": "20px 0 0 10px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
@ -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 (
|
|
||||||
<div>
|
|
||||||
<br />
|
|
||||||
<h5>Rollout</h5>
|
|
||||||
<StrategyInputPercentage
|
|
||||||
name="percentage"
|
|
||||||
value={1 * rollout}
|
|
||||||
minLabel="off"
|
|
||||||
maxLabel="on"
|
|
||||||
onChange={evt => handleConfigChange('rollout', evt)}
|
|
||||||
/>
|
|
||||||
<div style={{ margin: '0 17px' }}>
|
|
||||||
<Select
|
|
||||||
name="stickiness"
|
|
||||||
label="Stickiness"
|
|
||||||
options={stickinessOptions}
|
|
||||||
value={stickiness}
|
|
||||||
onChange={evt => handleConfigChange('stickiness', evt)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Textfield
|
|
||||||
floatingLabel
|
|
||||||
label="groupId"
|
|
||||||
value={groupId}
|
|
||||||
onChange={evt => handleConfigChange('groupId', evt)}
|
|
||||||
/>{' '}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 (
|
|
||||||
<form onSubmit={onSubmit(input, features)}>
|
|
||||||
<section className={styles.paddingDesktop}>
|
|
||||||
<StrategiesSection
|
|
||||||
configuredStrategies={configuredStrategies}
|
|
||||||
featureToggleName={input.name}
|
|
||||||
addStrategy={addStrategy}
|
|
||||||
updateStrategy={updateStrategy}
|
|
||||||
moveStrategy={moveStrategy}
|
|
||||||
removeStrategy={removeStrategy}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<br />
|
|
||||||
<FormButtons submitText={'Update'} onCancel={onCancel} />
|
|
||||||
</section>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
@ -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);
|
|
@ -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 (
|
|
||||||
<section style={{ padding: '16px' }}>
|
|
||||||
<StrategiesSection configuredStrategies={configuredStrategies} />
|
|
||||||
<br />
|
|
||||||
<Button type="cancel" ripple raised onClick={onCancel} style={{ float: 'right' }}>
|
|
||||||
<Icon name="cancel" />
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewFeatureComponent.propTypes = {
|
|
||||||
input: PropTypes.object,
|
|
||||||
onCancel: PropTypes.func.isRequired,
|
|
||||||
initCallRequired: PropTypes.bool,
|
|
||||||
init: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ViewFeatureComponent;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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 <ProgressBar indeterminate />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ padding: '10px 0' }}>
|
|
||||||
{this.props.addStrategy ? (
|
|
||||||
<HeaderTitle title="Activation strategies" actions={<AddStrategy {...this.props} />} />
|
|
||||||
) : (
|
|
||||||
<span />
|
|
||||||
)}
|
|
||||||
<StrategiesList {...this.props} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StrategiesSectionComponent;
|
|
@ -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 <h6>{strategyDefinition.description}</h6>;
|
|
||||||
}
|
|
||||||
if (strategyDefinition.name === 'flexibleRollout') {
|
|
||||||
return (
|
|
||||||
<FlexibleRolloutStrategyInput
|
|
||||||
strategy={this.props.strategy}
|
|
||||||
featureToggleName={this.props.featureToggleName}
|
|
||||||
updateStrategy={this.props.updateStrategy}
|
|
||||||
handleConfigChange={this.handleConfigChange.bind(this)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return <div>{this.renderInputFields(strategyDefinition)}</div>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div key={name}>
|
|
||||||
<br />
|
|
||||||
<StrategyInputPercentage
|
|
||||||
name={name}
|
|
||||||
onChange={this.handleConfigChange.bind(this, name)}
|
|
||||||
value={1 * value}
|
|
||||||
minLabel="off"
|
|
||||||
maxLabel="on"
|
|
||||||
/>
|
|
||||||
{description && <p className={styles.helpText}>{description}</p>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (type === 'list') {
|
|
||||||
let list = [];
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
list = value
|
|
||||||
.trim()
|
|
||||||
.split(',')
|
|
||||||
.filter(Boolean);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div key={name}>
|
|
||||||
<StrategyInputList
|
|
||||||
name={name}
|
|
||||||
list={list}
|
|
||||||
disabled={!this.props.updateStrategy}
|
|
||||||
setConfig={this.setConfig}
|
|
||||||
/>
|
|
||||||
{description && <p className={styles.helpText}>{description}</p>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (type === 'number') {
|
|
||||||
return (
|
|
||||||
<div key={name}>
|
|
||||||
<Textfield
|
|
||||||
pattern="-?[0-9]*(\.[0-9]+)?"
|
|
||||||
error={`${name} is not a number!`}
|
|
||||||
floatingLabel
|
|
||||||
required={required}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
name={name}
|
|
||||||
label={name}
|
|
||||||
onChange={this.handleConfigChange.bind(this, name)}
|
|
||||||
value={value}
|
|
||||||
/>
|
|
||||||
{description && <p className={styles.helpText}>{description}</p>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (type === 'boolean') {
|
|
||||||
if (!value) {
|
|
||||||
this.handleSwitchChange(name, value);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div key={name}>
|
|
||||||
<Switch
|
|
||||||
onChange={this.handleSwitchChange.bind(this, name, value)}
|
|
||||||
checked={value === 'true'}
|
|
||||||
>
|
|
||||||
{name}{' '}
|
|
||||||
{description && (
|
|
||||||
<Tooltip label={description}>
|
|
||||||
<Icon name="info" style={{ color: 'rgba(0, 0, 0, 0.7)', fontSize: '1em' }} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (name === 'groupId' && !value) {
|
|
||||||
this.setConfig('groupId', this.props.featureToggleName);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div key={name}>
|
|
||||||
<Textfield
|
|
||||||
floatingLabel
|
|
||||||
rows={1}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
required={required}
|
|
||||||
name={name}
|
|
||||||
label={name}
|
|
||||||
onChange={this.handleConfigChange.bind(this, name)}
|
|
||||||
value={value}
|
|
||||||
/>
|
|
||||||
{description && <p className={styles.helpText}>{description}</p>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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 = (
|
|
||||||
<Card
|
|
||||||
shadow={0}
|
|
||||||
className={styles.card}
|
|
||||||
style={{ opacity: isDragging ? '0.1' : '1', overflow: 'visible' }}
|
|
||||||
>
|
|
||||||
<CardTitle className={styles.cardTitle} title={description}>
|
|
||||||
<Icon name="extension" />
|
|
||||||
|
|
||||||
{name}
|
|
||||||
</CardTitle>
|
|
||||||
|
|
||||||
{strategyContent && <CardActions border>{strategyContent}</CardActions>}
|
|
||||||
|
|
||||||
<CardMenu className="mdl-color-text--white">
|
|
||||||
<Link
|
|
||||||
title="View strategy"
|
|
||||||
to={`/strategies/view/${name}`}
|
|
||||||
className={styles.editLink}
|
|
||||||
title={description}
|
|
||||||
>
|
|
||||||
<Icon name="info" />
|
|
||||||
</Link>
|
|
||||||
{this.props.removeStrategy ? (
|
|
||||||
<IconButton title="Remove strategy from toggle" name="delete" onClick={this.handleRemove} />
|
|
||||||
) : (
|
|
||||||
<span />
|
|
||||||
)}
|
|
||||||
{connectDragSource(
|
|
||||||
<span className={styles.reorderIcon}>
|
|
||||||
<Icon name="reorder" />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</CardMenu>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const { name } = this.props.strategy;
|
|
||||||
item = (
|
|
||||||
<Card shadow={0} className={styles.card}>
|
|
||||||
<CardTitle>"{name}" deleted?</CardTitle>
|
|
||||||
<CardText>
|
|
||||||
The strategy "{name}" does not exist on this server.
|
|
||||||
<Link to={`/strategies/create?name=${name}`}>Want to create it now?</Link>
|
|
||||||
</CardText>
|
|
||||||
<CardActions>
|
|
||||||
<Button onClick={this.handleRemove} label="remove strategy" accent raised>
|
|
||||||
Remove
|
|
||||||
</Button>
|
|
||||||
</CardActions>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return connectDragPreview(connectDropTarget(<div className={styles.item}>{item}</div>));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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);
|
|
@ -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 }) => (
|
|
||||||
<div style={{ marginBottom: '20px' }}>
|
|
||||||
<Grid noSpacing style={{ margin: '0 15px' }}>
|
|
||||||
<Cell col={1}>
|
|
||||||
<span style={infoLabelStyle}>{minLabel}</span>
|
|
||||||
</Cell>
|
|
||||||
<Cell col={10} style={labelStyle}>
|
|
||||||
{name}: {value}%
|
|
||||||
</Cell>
|
|
||||||
<Cell col={1} style={{ textAlign: 'right' }}>
|
|
||||||
<span style={infoLabelStyle}>{maxLabel}</span>
|
|
||||||
</Cell>
|
|
||||||
</Grid>
|
|
||||||
<Slider min={0} max={100} value={value} onChange={onChange} label={name} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
InputPercentage.propTypes = {
|
|
||||||
name: PropTypes.string,
|
|
||||||
minLabel: PropTypes.string,
|
|
||||||
maxLabel: PropTypes.string,
|
|
||||||
value: PropTypes.number,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InputPercentage;
|
|
@ -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;
|
|
||||||
}
|
|
@ -188,7 +188,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
</react-mdl-CardActions>
|
</react-mdl-CardActions>
|
||||||
<hr />
|
<hr />
|
||||||
<react-mdl-List>
|
<react-mdl-List>
|
||||||
<Feature
|
<ListItem
|
||||||
feature={
|
feature={
|
||||||
Object {
|
Object {
|
||||||
"name": "Another",
|
"name": "Another",
|
||||||
@ -384,7 +384,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
</react-mdl-CardActions>
|
</react-mdl-CardActions>
|
||||||
<hr />
|
<hr />
|
||||||
<react-mdl-List>
|
<react-mdl-List>
|
||||||
<Feature
|
<ListItem
|
||||||
feature={
|
feature={
|
||||||
Object {
|
Object {
|
||||||
"name": "Another",
|
"name": "Another",
|
@ -1,9 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import Feature from './../feature-list-item-component';
|
import Feature from '../list-item-component';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import { UPDATE_FEATURE } from '../../../permissions';
|
import { UPDATE_FEATURE } from '../../../../permissions';
|
||||||
|
|
||||||
jest.mock('react-mdl');
|
jest.mock('react-mdl');
|
||||||
jest.mock('../feature-type-container');
|
jest.mock('../feature-type-container');
|
@ -3,12 +3,12 @@ import { MemoryRouter } from 'react-router-dom';
|
|||||||
|
|
||||||
import FeatureListComponent from './../list-component';
|
import FeatureListComponent from './../list-component';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import { CREATE_FEATURE } from '../../../permissions';
|
import { CREATE_FEATURE } from '../../../../permissions';
|
||||||
|
|
||||||
jest.mock('react-mdl');
|
jest.mock('react-mdl');
|
||||||
jest.mock('../feature-list-item-component', () => ({
|
jest.mock('../list-item-component', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: 'Feature',
|
default: 'ListItem',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('../project-container', () => 'Project');
|
jest.mock('../project-container', () => 'Project');
|
@ -1,7 +1,7 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
import { Chip } from 'react-mdl';
|
import { Chip } from 'react-mdl';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import styles from './feature.scss';
|
import styles from './list.module.scss';
|
||||||
|
|
||||||
function StatusComponent({ type, types, onClick }) {
|
function StatusComponent({ type, types, onClick }) {
|
||||||
const typeObject = types.find(o => o.id === type) || { id: type, name: type };
|
const typeObject = types.find(o => o.id === type) || { id: type, name: type };
|
@ -3,10 +3,10 @@ import PropTypes from 'prop-types';
|
|||||||
import { debounce } from 'debounce';
|
import { debounce } from 'debounce';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Icon, FABButton, Menu, MenuItem, Card, CardActions, List } from 'react-mdl';
|
import { Icon, FABButton, Menu, MenuItem, Card, CardActions, List } from 'react-mdl';
|
||||||
import Feature from './feature-list-item-component';
|
import Feature from './list-item-component';
|
||||||
import { MenuItemWithIcon, DropdownButton, styles as commonStyles } from '../common';
|
import { MenuItemWithIcon, DropdownButton, styles as commonStyles } from '../../common';
|
||||||
import SearchField from '../common/search-field';
|
import SearchField from '../../common/search-field';
|
||||||
import { CREATE_FEATURE } from '../../permissions';
|
import { CREATE_FEATURE } from '../../../permissions';
|
||||||
import ProjectMenu from './project-container';
|
import ProjectMenu from './project-container';
|
||||||
|
|
||||||
export default class FeatureListComponent extends React.Component {
|
export default class FeatureListComponent extends React.Component {
|
@ -1,9 +1,9 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { toggleFeature, fetchFeatureToggles } from '../../store/feature-actions';
|
import { toggleFeature, fetchFeatureToggles } from '../../../store/feature-actions';
|
||||||
import { updateSettingForGroup } from '../../store/settings/actions';
|
import { updateSettingForGroup } from '../../../store/settings/actions';
|
||||||
|
|
||||||
import FeatureListComponent from './list-component';
|
import FeatureListComponent from './list-component';
|
||||||
import { hasPermission } from '../../permissions';
|
import { hasPermission } from '../../../permissions';
|
||||||
|
|
||||||
export const mapStateToPropsConfigurable = isFeature => state => {
|
export const mapStateToPropsConfigurable = isFeature => state => {
|
||||||
const featureMetrics = state.featureMetrics.toJS();
|
const featureMetrics = state.featureMetrics.toJS();
|
@ -3,13 +3,13 @@ import PropTypes from 'prop-types';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Switch, ListItem, ListItemAction, Icon } from 'react-mdl';
|
import { Switch, ListItem, ListItemAction, Icon } from 'react-mdl';
|
||||||
import TimeAgo from 'react-timeago';
|
import TimeAgo from 'react-timeago';
|
||||||
import Progress from './progress';
|
import Progress from '../progress-component';
|
||||||
import { UPDATE_FEATURE } from '../../permissions';
|
import { UPDATE_FEATURE } from '../../../permissions';
|
||||||
import { calc, styles as commonStyles } from '../common';
|
import { calc, styles as commonStyles } from '../../common';
|
||||||
import Status from './status-component';
|
import Status from '../status-component';
|
||||||
import FeatureType from './feature-type-container';
|
import FeatureType from './feature-type-container';
|
||||||
|
|
||||||
import styles from './feature.scss';
|
import styles from './list.module.scss';
|
||||||
|
|
||||||
const Feature = ({
|
const Feature = ({
|
||||||
feature,
|
feature,
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Menu, MenuItem } from 'react-mdl';
|
import { Menu, MenuItem } from 'react-mdl';
|
||||||
import { DropdownButton } from '../common';
|
import { DropdownButton } from '../../common';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const ALL_PROJECTS = { id: '*', name: '> All projects' };
|
const ALL_PROJECTS = { id: '*', name: '> All projects' };
|
@ -1,6 +1,6 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import Component from './project-component';
|
import Component from './project-component';
|
||||||
import { fetchProjects } from './../../store/project/actions';
|
import { fetchProjects } from './../../../store/project/actions';
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => ({
|
const mapStateToProps = (state, ownProps) => ({
|
||||||
projects: state.projects.toJS(),
|
projects: state.projects.toJS(),
|
@ -1,10 +1,10 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import styles from './progress-styles.scss';
|
import styles from './progress.module.scss';
|
||||||
|
|
||||||
class Progress extends Component {
|
class Progress extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super();
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
percentage: props.initialAnimation ? 0 : props.percentage,
|
percentage: props.initialAnimation ? 0 : props.percentage,
|
@ -1,6 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import MySelect from '../../common/select';
|
import MySelect from '../common/select';
|
||||||
|
|
||||||
class ProjectSelectComponent extends Component {
|
class ProjectSelectComponent extends Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
@ -1,6 +1,6 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ProjectSelectComponent from './project-select-component';
|
import ProjectSelectComponent from './project-select-component';
|
||||||
import { fetchProjects } from './../../../store/project/actions';
|
import { fetchProjects } from './../../store/project/actions';
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const projects = state.projects.toJS();
|
const projects = state.projects.toJS();
|
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"jest": true
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AddStrategy from './../strategies-add';
|
import AddStrategy from '../strategies-add';
|
||||||
import { shallow } from 'enzyme/build/index';
|
import { shallow } from 'enzyme/build';
|
||||||
|
|
||||||
jest.mock('react-mdl');
|
jest.mock('react-mdl');
|
||||||
|
|
||||||
@ -22,7 +22,12 @@ let eventMock = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
const buildComponent = (addStrategy, fetchStrategies, strategies) => (
|
const buildComponent = (addStrategy, fetchStrategies, strategies) => (
|
||||||
<AddStrategy addStrategy={addStrategy} fetchStrategies={fetchStrategies} strategies={strategies} />
|
<AddStrategy
|
||||||
|
addStrategy={addStrategy}
|
||||||
|
fetchStrategies={fetchStrategies}
|
||||||
|
strategies={strategies}
|
||||||
|
featureToggleName="nameOfToggle"
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
it('renders add strategy form with a list of available strategies', () => {
|
it('renders add strategy form with a list of available strategies', () => {
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import InputList from './../strategy-input-list';
|
import InputList from '../../strategy/input-list';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
@ -39,34 +39,6 @@ it('go inside onFocus', () => {
|
|||||||
wrapper.find('react-mdl-Textfield').simulate('focus', focusMock);
|
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(<InputList list={list} name={name} setConfig={jest.fn()} />);
|
|
||||||
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(<InputList list={list} name={name} setConfig={jest.fn()} />);
|
|
||||||
wrapper.find('react-mdl-Textfield').simulate('blur', focusMock);
|
|
||||||
expect(onFocus).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('spy onClose', () => {
|
it('spy onClose', () => {
|
||||||
let list = ['item1'];
|
let list = ['item1'];
|
||||||
const name = 'featureName';
|
const name = 'featureName';
|
10
frontend/src/component/feature/strategy/default-strategy.jsx
Normal file
10
frontend/src/component/feature/strategy/default-strategy.jsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export default function DefaultStrategy({ strategyDefinition }) {
|
||||||
|
return <h6>{strategyDefinition.description}</h6>;
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultStrategy.propTypes = {
|
||||||
|
strategyDefinition: PropTypes.object.isRequired,
|
||||||
|
};
|
@ -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 (
|
||||||
|
<div>
|
||||||
|
<br />
|
||||||
|
<strong>Rollout</strong>
|
||||||
|
<StrategyInputPercentage
|
||||||
|
name="Percentage"
|
||||||
|
value={1 * rollout}
|
||||||
|
minLabel="off"
|
||||||
|
maxLabel="on"
|
||||||
|
disabled={!editable}
|
||||||
|
onChange={evt => this.onConfiguUpdate('rollout', evt)}
|
||||||
|
id={`${index}-groupId`}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
name="stickiness"
|
||||||
|
label="Stickiness"
|
||||||
|
options={stickinessOptions}
|
||||||
|
value={stickiness}
|
||||||
|
disabled={!editable}
|
||||||
|
onChange={evt => this.onConfiguUpdate('stickiness', evt)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textfield
|
||||||
|
floatingLabel
|
||||||
|
label="groupId"
|
||||||
|
value={groupId}
|
||||||
|
disabled={!editable}
|
||||||
|
onChange={evt => this.onConfiguUpdate('groupId', evt)}
|
||||||
|
id={`${index}-groupId`}
|
||||||
|
/>{' '}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
107
frontend/src/component/feature/strategy/general-strategy.jsx
Normal file
107
frontend/src/component/feature/strategy/general-strategy.jsx
Normal file
@ -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 (
|
||||||
|
<div key={name}>
|
||||||
|
<br />
|
||||||
|
<StrategyInputPercentage
|
||||||
|
name={name}
|
||||||
|
onChange={onChange.bind(this, name)}
|
||||||
|
value={1 * value}
|
||||||
|
minLabel="off"
|
||||||
|
maxLabel="on"
|
||||||
|
/>
|
||||||
|
{description && <p className={styles.helpText}>{description}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (type === 'list') {
|
||||||
|
let list = [];
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
list = value
|
||||||
|
.trim()
|
||||||
|
.split(',')
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div key={name}>
|
||||||
|
<StrategyInputList name={name} list={list} disabled={!editable} setConfig={updateParameter} />
|
||||||
|
{description && <p className={styles.helpText}>{description}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (type === 'number') {
|
||||||
|
return (
|
||||||
|
<div key={name}>
|
||||||
|
<Textfield
|
||||||
|
pattern="-?[0-9]*(\.[0-9]+)?"
|
||||||
|
error={`${name} is not a number!`}
|
||||||
|
floatingLabel
|
||||||
|
required={required}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
name={name}
|
||||||
|
label={name}
|
||||||
|
onChange={onChange.bind(this, name)}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
{description && <p className={styles.helpText}>{description}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (type === 'boolean') {
|
||||||
|
return (
|
||||||
|
<div key={name} style={{ padding: '20px 0' }}>
|
||||||
|
<Switch onChange={handleSwitchChange.bind(this, name, value)} checked={value === 'true'}>
|
||||||
|
{name}{' '}
|
||||||
|
{description && (
|
||||||
|
<Tooltip label={description}>
|
||||||
|
<Icon name="info" style={{ color: 'rgba(0, 0, 0, 0.7)', fontSize: '1em' }} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div key={name}>
|
||||||
|
<Textfield
|
||||||
|
floatingLabel
|
||||||
|
rows={1}
|
||||||
|
placeholder=""
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
required={required}
|
||||||
|
name={name}
|
||||||
|
label={name}
|
||||||
|
onChange={onChange.bind(this, name)}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
{description && <p className={styles.helpText}>{description}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
GeneralStrategyInput.propTypes = strategyInputProps;
|
@ -10,79 +10,69 @@ export default class InputList extends Component {
|
|||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
onBlur(e) {
|
onBlur = e => {
|
||||||
this.setValue(e);
|
this.setValue(e);
|
||||||
window.removeEventListener('keydown', this.onKeyHandler, false);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
onFocus(e) {
|
onKeyDown = e => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
window.addEventListener('keydown', this.onKeyHandler, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
onKeyHandler = e => {
|
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
this.setValue();
|
this.setValue(e);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setValue = e => {
|
setValue = evt => {
|
||||||
if (e) {
|
evt.preventDefault();
|
||||||
e.preventDefault();
|
const value = evt.target.value;
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { name, list, setConfig } = this.props;
|
const { name, list, setConfig } = this.props;
|
||||||
if (this.textInput && this.textInput.inputRef && this.textInput.inputRef.value) {
|
if (value) {
|
||||||
const newValues = this.textInput.inputRef.value.split(/,\s*/);
|
const newValues = value.split(/,\s*/).filter(a => !list.includes(a));
|
||||||
|
if (newValues.length > 0) {
|
||||||
const newList = list.concat(newValues).filter(a => a);
|
const newList = list.concat(newValues).filter(a => a);
|
||||||
|
setConfig(name, newList.join(','), true);
|
||||||
|
}
|
||||||
this.textInput.inputRef.value = '';
|
this.textInput.inputRef.value = '';
|
||||||
setConfig(name, newList.join(','));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onClose(index) {
|
onClose(index) {
|
||||||
const { name, list, setConfig } = this.props;
|
const { name, list, setConfig } = this.props;
|
||||||
list[index] = null;
|
list[index] = null;
|
||||||
setConfig(name, list.length === 1 ? '' : list.filter(Boolean).join(','));
|
setConfig(name, list.length === 1 ? '' : list.filter(Boolean).join(','), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { name, list, disabled } = this.props;
|
const { name, list, disabled } = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<strong>List of {name}</strong>
|
||||||
<i>
|
<div style={{ display: 'flex', flexWrap: 'wrap', margin: '10px 0' }}>
|
||||||
Please specify the list of <code>{name}</code>:
|
|
||||||
</i>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
|
|
||||||
{list.map((entryValue, index) => (
|
{list.map((entryValue, index) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={index + entryValue}
|
key={index + entryValue}
|
||||||
style={{ marginRight: '3px' }}
|
className="mdl-color--blue-grey-100"
|
||||||
|
style={{ marginRight: '3px', border: '1px solid' }}
|
||||||
onClose={disabled ? undefined : () => this.onClose(index)}
|
onClose={disabled ? undefined : () => this.onClose(index)}
|
||||||
|
title="Remove value"
|
||||||
>
|
>
|
||||||
{entryValue}
|
{entryValue}
|
||||||
</Chip>
|
</Chip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{disabled ? (
|
{disabled ? (
|
||||||
''
|
''
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
<Textfield
|
<Textfield
|
||||||
name={`${name}_input`}
|
name={`input_field`}
|
||||||
style={{ width: '100%', flex: 1 }}
|
style={{ width: '100%', maxWidth: '200px', flex: 1 }}
|
||||||
floatingLabel
|
floatingLabel
|
||||||
label="Enter value (val1, val2)"
|
label="Add items:"
|
||||||
onFocus={this.onFocus.bind(this)}
|
placeholder=""
|
||||||
onBlur={this.onBlur.bind(this)}
|
onBlur={this.onBlur}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
ref={input => {
|
ref={input => {
|
||||||
this.textInput = input;
|
this.textInput = input;
|
||||||
}}
|
}}
|
53
frontend/src/component/feature/strategy/input-percentage.jsx
Normal file
53
frontend/src/component/feature/strategy/input-percentage.jsx
Normal file
@ -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 }) => (
|
||||||
|
<div style={{ margin: '20px 0' }}>
|
||||||
|
<table style={{ width: '100%' }} colSpan="0" cellSpacing="0" cellPadding="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style={{ textAlign: 'left', paddingLeft: '20px', width: '20px' }}>
|
||||||
|
<span style={infoLabelStyle}>{minLabel}</span>
|
||||||
|
</td>
|
||||||
|
<td style={{ textAlign: 'center' }}>
|
||||||
|
<strong title={name} style={labelStyle}>
|
||||||
|
{value}%
|
||||||
|
</strong>
|
||||||
|
</td>
|
||||||
|
<td style={{ textAlign: 'right', paddingRight: '20px', width: '20px' }}>
|
||||||
|
<span style={infoLabelStyle}>{maxLabel}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colSpan="3">
|
||||||
|
<Slider min={0} max={100} value={value} onChange={onChange} label={name} disabled={disabled} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
InputPercentage.propTypes = {
|
||||||
|
name: PropTypes.string,
|
||||||
|
minLabel: PropTypes.string,
|
||||||
|
maxLabel: PropTypes.string,
|
||||||
|
value: PropTypes.number,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InputPercentage;
|
@ -2,19 +2,34 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Menu, MenuItem, IconButton } from 'react-mdl';
|
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 {
|
class AddStrategy extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
strategies: PropTypes.array.isRequired,
|
strategies: PropTypes.array.isRequired,
|
||||||
addStrategy: PropTypes.func,
|
addStrategy: PropTypes.func,
|
||||||
fetchStrategies: PropTypes.func.isRequired,
|
featureToggleName: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
addStrategy(strategyName) {
|
addStrategy(strategyName) {
|
||||||
|
const featureToggleName = this.props.featureToggleName;
|
||||||
const selectedStrategy = this.props.strategies.find(s => s.name === strategyName);
|
const selectedStrategy = this.props.strategies.find(s => s.name === strategyName);
|
||||||
const parameters = {};
|
const parameters = {};
|
||||||
|
|
||||||
selectedStrategy.parameters.forEach(({ name }) => {
|
selectedStrategy.parameters.forEach(({ name }) => {
|
||||||
parameters[name] = '';
|
parameters[name] = resolveDefaultParamVale(name, featureToggleName);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.props.addStrategy({
|
this.props.addStrategy({
|
@ -1,9 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ConfigureStrategy from './strategy-configure';
|
import ConfigureStrategy from './strategy-configure-container';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
|
|
||||||
|
const randomKeys = length => Array.from({ length }, () => Math.random());
|
||||||
|
|
||||||
class StrategiesList extends React.Component {
|
class StrategiesList extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
strategies: PropTypes.array.isRequired,
|
strategies: PropTypes.array.isRequired,
|
||||||
@ -12,18 +14,36 @@ class StrategiesList extends React.Component {
|
|||||||
updateStrategy: PropTypes.func,
|
updateStrategy: PropTypes.func,
|
||||||
removeStrategy: PropTypes.func,
|
removeStrategy: PropTypes.func,
|
||||||
moveStrategy: PropTypes.func,
|
moveStrategy: PropTypes.func,
|
||||||
|
editable: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
constructor(props) {
|
||||||
const {
|
super();
|
||||||
strategies,
|
// temporal hack, until strategies get UIDs
|
||||||
configuredStrategies,
|
this.state = { keys: randomKeys(props.configuredStrategies.length) };
|
||||||
moveStrategy,
|
}
|
||||||
removeStrategy,
|
|
||||||
updateStrategy,
|
|
||||||
featureToggleName,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
|
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) {
|
if (!configuredStrategies || configuredStrategies.length === 0) {
|
||||||
return (
|
return (
|
||||||
<p style={{ padding: '0 16px' }}>
|
<p style={{ padding: '0 16px' }}>
|
||||||
@ -35,13 +55,14 @@ class StrategiesList extends React.Component {
|
|||||||
const blocks = configuredStrategies.map((strategy, i) => (
|
const blocks = configuredStrategies.map((strategy, i) => (
|
||||||
<ConfigureStrategy
|
<ConfigureStrategy
|
||||||
index={i}
|
index={i}
|
||||||
key={`${strategy.id}-${i}`}
|
key={`${keys[i]}}`}
|
||||||
featureToggleName={featureToggleName}
|
featureToggleName={featureToggleName}
|
||||||
strategy={strategy}
|
strategy={strategy}
|
||||||
moveStrategy={moveStrategy}
|
moveStrategy={this.moveStrategy}
|
||||||
removeStrategy={removeStrategy ? removeStrategy.bind(null, i) : null}
|
removeStrategy={this.removeStrategy.bind(this, i)}
|
||||||
updateStrategy={updateStrategy ? updateStrategy.bind(null, i) : null}
|
updateStrategy={updateStrategy ? updateStrategy.bind(null, i) : null}
|
||||||
strategyDefinition={strategies.find(s => s.name === strategy.name)}
|
strategyDefinition={strategies.find(s => s.name === strategy.name)}
|
||||||
|
editable={editable}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
return (
|
return (
|
@ -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(
|
||||||
|
<div className={styles.item}>
|
||||||
|
<Card shadow={0} className={cardClasses.join(' ')}>
|
||||||
|
<CardTitle className={styles.cardTitle} title={description}>
|
||||||
|
<Icon name="extension" />
|
||||||
|
|
||||||
|
{name}
|
||||||
|
</CardTitle>
|
||||||
|
|
||||||
|
<CardText>
|
||||||
|
<InputType
|
||||||
|
parameters={parameters}
|
||||||
|
strategy={strategy}
|
||||||
|
strategyDefinition={strategyDefinition}
|
||||||
|
updateParameter={this.updateParameter}
|
||||||
|
index={index}
|
||||||
|
editable={editable}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={this.onSave}
|
||||||
|
accent
|
||||||
|
raised
|
||||||
|
ripple
|
||||||
|
style={{ visibility: dirty ? 'visible' : 'hidden' }}
|
||||||
|
>
|
||||||
|
Save changes
|
||||||
|
</Button>
|
||||||
|
</CardText>
|
||||||
|
|
||||||
|
<CardMenu className="mdl-color-text--white">
|
||||||
|
<Link
|
||||||
|
title="View strategy"
|
||||||
|
to={`/strategies/view/${name}`}
|
||||||
|
className={styles.editLink}
|
||||||
|
title={description}
|
||||||
|
>
|
||||||
|
<Icon name="info" />
|
||||||
|
</Link>
|
||||||
|
{editable && (
|
||||||
|
<IconButton
|
||||||
|
title="Remove strategy from toggle"
|
||||||
|
name="delete"
|
||||||
|
onClick={this.handleRemove}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{editable &&
|
||||||
|
connectDragSource(
|
||||||
|
<span className={styles.reorderIcon}>
|
||||||
|
<Icon name="reorder" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</CardMenu>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
@ -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,
|
||||||
|
};
|
@ -11,9 +11,19 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
background-color: #f2f9fc;
|
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;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +52,7 @@
|
|||||||
.cardTitle {
|
.cardTitle {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
height: 65px;
|
height: 65px;
|
||||||
background-color: #607d8b !important;
|
background-color: rgba(96, 125, 139, .85) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.helpText {
|
.helpText {
|
16
frontend/src/component/feature/strategy/unknown-strategy.jsx
Normal file
16
frontend/src/component/feature/strategy/unknown-strategy.jsx
Normal file
@ -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 (
|
||||||
|
<div>
|
||||||
|
<p>The strategy "{name}" does not exist on this server.</p>
|
||||||
|
<Link to={`/strategies/create?name=${name}`}>Want to create it now?</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
UknownStrategy.propTypes = strategyInputProps;
|
@ -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 (
|
||||||
|
<div>
|
||||||
|
<InputList name="userIds" list={list} disabled={!editable} setConfig={updateParameter} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
UserWithIdStrategy.propTypes = strategyInputProps;
|
@ -4,7 +4,7 @@ import Modal from 'react-modal';
|
|||||||
import { Button, Textfield, DialogActions, Grid, Cell, Icon, Switch } from 'react-mdl';
|
import { Button, Textfield, DialogActions, Grid, Cell, Icon, Switch } from 'react-mdl';
|
||||||
import styles from './variant.scss';
|
import styles from './variant.scss';
|
||||||
import MySelect from '../../common/select';
|
import MySelect from '../../common/select';
|
||||||
import { trim } from '../form/util';
|
import { trim } from '../../common/util';
|
||||||
import { weightTypes } from './enums';
|
import { weightTypes } from './enums';
|
||||||
import OverrideConfig from './override-config';
|
import OverrideConfig from './override-config';
|
||||||
|
|
||||||
|
5
frontend/src/component/feature/view/__tests__/.eslintrc
Normal file
5
frontend/src/component/feature/view/__tests__/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"jest": true
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`render the create feature page 1`] = `""`;
|
@ -203,7 +203,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</react-mdl-Tab>
|
</react-mdl-Tab>
|
||||||
</react-mdl-Tabs>
|
</react-mdl-Tabs>
|
||||||
<UpdateFeatureToggleComponent
|
<UpdateStrategiesComponent
|
||||||
featureToggle={
|
featureToggle={
|
||||||
Object {
|
Object {
|
||||||
"createdAt": "2018-02-04T20:27:52.127Z",
|
"createdAt": "2018-02-04T20:27:52.127Z",
|
@ -1,20 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import UpdateFeatureComponent from './../form-update-feature-component';
|
import UpdateStrategiesComponent from '../update-strategies-component';
|
||||||
import { shallow } from 'enzyme/build/index';
|
import { shallow } from 'enzyme/build';
|
||||||
|
|
||||||
jest.mock('react-mdl');
|
jest.mock('react-mdl');
|
||||||
jest.mock('../strategies-section-container', () => 'StrategiesSection');
|
// jest.mock('../strategies-section-container', () => 'StrategiesSection');
|
||||||
|
|
||||||
it('render the create feature page', () => {
|
it('render the create feature page', () => {
|
||||||
let input = {
|
let strategies = [{ name: 'default' }];
|
||||||
name: 'feature',
|
|
||||||
errors: {},
|
|
||||||
description: 'Description',
|
|
||||||
enabled: false,
|
|
||||||
};
|
|
||||||
const tree = shallow(
|
const tree = shallow(
|
||||||
<UpdateFeatureComponent
|
<UpdateStrategiesComponent
|
||||||
input={input}
|
featureToggleName="some-toggle"
|
||||||
|
configuredStrategies={strategies}
|
||||||
onSubmit={jest.fn()}
|
onSubmit={jest.fn()}
|
||||||
setValue={jest.fn()}
|
setValue={jest.fn()}
|
||||||
addStrategy={jest.fn()}
|
addStrategy={jest.fn()}
|
@ -3,15 +3,15 @@ import { MemoryRouter } from 'react-router-dom';
|
|||||||
|
|
||||||
import ViewFeatureToggleComponent from './../view-component';
|
import ViewFeatureToggleComponent from './../view-component';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import { DELETE_FEATURE, UPDATE_FEATURE } from '../../../permissions';
|
import { DELETE_FEATURE, UPDATE_FEATURE } from '../../../../permissions';
|
||||||
|
|
||||||
jest.mock('react-mdl');
|
jest.mock('react-mdl');
|
||||||
jest.mock('../form/form-update-feature-container', () => ({
|
jest.mock('../update-strategies-container', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: 'UpdateFeatureToggleComponent',
|
default: 'UpdateStrategiesComponent',
|
||||||
}));
|
}));
|
||||||
jest.mock('../form/feature-type-select-container', () => 'FeatureTypeSelect');
|
jest.mock('../../feature-type-select-container', () => 'FeatureTypeSelect');
|
||||||
jest.mock('../form/project-select-container', () => 'ProjectSelect');
|
jest.mock('../../project-select-container', () => 'ProjectSelect');
|
||||||
|
|
||||||
test('renders correctly with one feature', () => {
|
test('renders correctly with one feature', () => {
|
||||||
const feature = {
|
const feature = {
|
@ -1,11 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Grid, Cell, Icon, Chip, ChipContact } from 'react-mdl';
|
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 { Link } from 'react-router-dom';
|
||||||
import { AppsLinkList, calc } from '../common';
|
import { AppsLinkList, calc } from '../../common';
|
||||||
import { formatFullDateTimeWithLocale } from '../common/util';
|
import { formatFullDateTimeWithLocale } from '../../common/util';
|
||||||
import styles from './metrics.scss';
|
import styles from './metric.module.scss';
|
||||||
|
|
||||||
const StrategyChipItem = ({ strategy }) => (
|
const StrategyChipItem = ({ strategy }) => (
|
||||||
<Chip className={styles.chip}>
|
<Chip className={styles.chip}>
|
@ -1,6 +1,6 @@
|
|||||||
import { connect } from 'react-redux';
|
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';
|
import MatricComponent from './metric-component';
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Menu, MenuItem } from 'react-mdl';
|
import { Menu, MenuItem } from 'react-mdl';
|
||||||
import { DropdownButton } from '../common';
|
import { DropdownButton } from '../../common';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
export default function StatusUpdateComponent({ stale, updateStale }) {
|
export default function StatusUpdateComponent({ stale, updateStale }) {
|
@ -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 (
|
||||||
|
<section className={styles.paddingDesktop}>
|
||||||
|
{editable && <HeaderTitle title="Activation strategies" actions={<AddStrategy {...props} />} />}
|
||||||
|
<StrategiesList {...props} />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
@ -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);
|
@ -3,17 +3,16 @@ import PropTypes from 'prop-types';
|
|||||||
import { Tabs, Tab, ProgressBar, Button, Card, CardTitle, CardActions, Switch, CardText } from 'react-mdl';
|
import { Tabs, Tab, ProgressBar, Button, Card, CardTitle, CardActions, Switch, CardText } from 'react-mdl';
|
||||||
import { Link } from 'react-router-dom';
|
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 MetricComponent from './metric-container';
|
||||||
import EditFeatureToggle from './form/form-update-feature-container';
|
import UpdateStrategies from './update-strategies-container';
|
||||||
import EditVariants from './variant/update-variant-container';
|
import EditVariants from '../variant/update-variant-container';
|
||||||
import ViewFeatureToggle from './form/form-view-feature-container';
|
import FeatureTypeSelect from '../feature-type-select-container';
|
||||||
import FeatureTypeSelect from './form/feature-type-select-container';
|
import ProjectSelect from '../project-select-container';
|
||||||
import ProjectSelect from './form/project-select-container';
|
import UpdateDescriptionComponent from './update-description-component';
|
||||||
import UpdateDescriptionComponent from './form/update-description-component';
|
import { styles as commonStyles } from '../../common';
|
||||||
import { styles as commonStyles } from '../common';
|
import { CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE } from '../../../permissions';
|
||||||
import { CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE } from '../../permissions';
|
import StatusComponent from '../status-component';
|
||||||
import StatusComponent from './status-component';
|
|
||||||
import StatusUpdateComponent from './status-update-component';
|
import StatusUpdateComponent from './status-update-component';
|
||||||
|
|
||||||
const TABS = {
|
const TABS = {
|
||||||
@ -40,6 +39,7 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
|||||||
revive: PropTypes.func,
|
revive: PropTypes.func,
|
||||||
fetchArchive: PropTypes.func,
|
fetchArchive: PropTypes.func,
|
||||||
fetchFeatureToggles: PropTypes.func,
|
fetchFeatureToggles: PropTypes.func,
|
||||||
|
fetchFeatureToggle: PropTypes.func,
|
||||||
editFeatureToggle: PropTypes.func,
|
editFeatureToggle: PropTypes.func,
|
||||||
featureToggle: PropTypes.object,
|
featureToggle: PropTypes.object,
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
@ -65,10 +65,17 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
|||||||
} else if (TABS[activeTab] === TABS.strategies) {
|
} else if (TABS[activeTab] === TABS.strategies) {
|
||||||
if (this.isFeatureView && hasPermission(UPDATE_FEATURE)) {
|
if (this.isFeatureView && hasPermission(UPDATE_FEATURE)) {
|
||||||
return (
|
return (
|
||||||
<EditFeatureToggle featureToggle={featureToggle} features={features} history={this.props.history} />
|
<UpdateStrategies featureToggle={featureToggle} features={features} history={this.props.history} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <ViewFeatureToggle featureToggle={featureToggle} />;
|
return (
|
||||||
|
<UpdateStrategies
|
||||||
|
featureToggle={featureToggle}
|
||||||
|
features={features}
|
||||||
|
history={this.props.history}
|
||||||
|
editable={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else if (TABS[activeTab] === TABS.variants) {
|
} else if (TABS[activeTab] === TABS.variants) {
|
||||||
return (
|
return (
|
||||||
<EditVariants
|
<EditVariants
|
||||||
@ -85,6 +92,10 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
|||||||
|
|
||||||
goToTab(tabName, featureToggleName) {
|
goToTab(tabName, featureToggleName) {
|
||||||
let view = this.props.fetchFeatureToggles ? 'features' : 'archive';
|
let view = this.props.fetchFeatureToggles ? 'features' : 'archive';
|
||||||
|
if (view === 'features' && tabName === 'strategies') {
|
||||||
|
const { featureToggleName, fetchFeatureToggle } = this.props;
|
||||||
|
fetchFeatureToggle(featureToggleName);
|
||||||
|
}
|
||||||
this.props.history.push(`/${view}/${tabName}/${featureToggleName}`);
|
this.props.history.push(`/${view}/${tabName}/${featureToggleName}`);
|
||||||
}
|
}
|
||||||
|
|
@ -2,14 +2,15 @@ import { connect } from 'react-redux';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
fetchFeatureToggles,
|
fetchFeatureToggles,
|
||||||
|
fetchFeatureToggle,
|
||||||
toggleFeature,
|
toggleFeature,
|
||||||
setStale,
|
setStale,
|
||||||
removeFeatureToggle,
|
removeFeatureToggle,
|
||||||
editFeatureToggle,
|
editFeatureToggle,
|
||||||
} from './../../store/feature-actions';
|
} from './../../../store/feature-actions';
|
||||||
|
|
||||||
import ViewToggleComponent from './view-component';
|
import ViewToggleComponent from './view-component';
|
||||||
import { hasPermission } from '../../permissions';
|
import { hasPermission } from '../../../permissions';
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
(state, props) => ({
|
(state, props) => ({
|
||||||
@ -20,6 +21,7 @@ export default connect(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
fetchFeatureToggles,
|
fetchFeatureToggles,
|
||||||
|
fetchFeatureToggle,
|
||||||
toggleFeature,
|
toggleFeature,
|
||||||
setStale,
|
setStale,
|
||||||
removeFeatureToggle,
|
removeFeatureToggle,
|
@ -18,6 +18,12 @@ function fetchAll() {
|
|||||||
.then(response => response.json());
|
.then(response => response.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fetchFeatureToggle(name) {
|
||||||
|
return fetch(`${URI}/${name}`, { credentials: 'include' })
|
||||||
|
.then(throwIfNotSuccess)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
function create(featureToggle) {
|
function create(featureToggle) {
|
||||||
return validateToggle(featureToggle)
|
return validateToggle(featureToggle)
|
||||||
.then(() =>
|
.then(() =>
|
||||||
@ -82,6 +88,7 @@ function remove(featureToggleName) {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
fetchAll,
|
fetchAll,
|
||||||
|
fetchFeatureToggle,
|
||||||
create,
|
create,
|
||||||
validate,
|
validate,
|
||||||
update,
|
update,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
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';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const render = ({ history, match: { params } }) => (
|
const render = ({ history, match: { params } }) => (
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
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';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const render = ({ history }) => <AddFeatureToggleForm title="Create feature toggle" history={history} />;
|
const render = ({ history }) => <AddFeatureToggleForm title="Create feature toggle" history={history} />;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import FeatureListContainer from './../../component/feature/list-container';
|
import FeatureListContainer from './../../component/feature/list/list-container';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const render = ({ history }) => <FeatureListContainer history={history} />;
|
const render = ({ history }) => <FeatureListContainer history={history} />;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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 {
|
export default class Features extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -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 ERROR_REMOVE_FEATURE_TOGGLE = 'ERROR_REMOVE_FEATURE_TOGGLE';
|
||||||
export const UPDATE_FEATURE_TOGGLE_STRATEGIES = 'UPDATE_FEATURE_TOGGLE_STRATEGIES';
|
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) {
|
export function toggleFeature(enable, name) {
|
||||||
debug('Toggle feature toggle ', name);
|
debug('Toggle feature toggle ', name);
|
||||||
return dispatch => {
|
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() {
|
export function fetchFeatureToggles() {
|
||||||
debug('Start fetching feature toggles');
|
debug('Start fetching feature toggles');
|
||||||
return dispatch => {
|
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) {
|
export function createFeatureToggles(featureToggle) {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
dispatch({ type: START_CREATE_FEATURE_TOGGLE });
|
dispatch({ type: START_CREATE_FEATURE_TOGGLE });
|
||||||
|
@ -4,6 +4,7 @@ const debug = require('debug')('unleash:feature-store');
|
|||||||
import {
|
import {
|
||||||
ADD_FEATURE_TOGGLE,
|
ADD_FEATURE_TOGGLE,
|
||||||
RECEIVE_FEATURE_TOGGLES,
|
RECEIVE_FEATURE_TOGGLES,
|
||||||
|
RECEIVE_FEATURE_TOGGLE,
|
||||||
UPDATE_FEATURE_TOGGLE,
|
UPDATE_FEATURE_TOGGLE,
|
||||||
UPDATE_FEATURE_TOGGLE_STRATEGIES,
|
UPDATE_FEATURE_TOGGLE_STRATEGIES,
|
||||||
REMOVE_FEATURE_TOGGLE,
|
REMOVE_FEATURE_TOGGLE,
|
||||||
@ -47,6 +48,15 @@ const features = (state = new List([]), action) => {
|
|||||||
return toggle;
|
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:
|
case RECEIVE_FEATURE_TOGGLES:
|
||||||
debug(RECEIVE_FEATURE_TOGGLES, action);
|
debug(RECEIVE_FEATURE_TOGGLES, action);
|
||||||
return new List(action.featureToggles.map($Map));
|
return new List(action.featureToggles.map($Map));
|
||||||
|
@ -2,6 +2,7 @@ import { fetchUIConfig } from './ui-config/actions';
|
|||||||
import { fetchContext } from './context/actions';
|
import { fetchContext } from './context/actions';
|
||||||
import { fetchFeatureTypes } from './feature-type/actions';
|
import { fetchFeatureTypes } from './feature-type/actions';
|
||||||
import { fetchProjects } from './project/actions';
|
import { fetchProjects } from './project/actions';
|
||||||
|
import { fetchStrategies } from './strategy/actions';
|
||||||
|
|
||||||
export function loadInitalData() {
|
export function loadInitalData() {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
@ -9,5 +10,6 @@ export function loadInitalData() {
|
|||||||
fetchContext()(dispatch);
|
fetchContext()(dispatch);
|
||||||
fetchFeatureTypes()(dispatch);
|
fetchFeatureTypes()(dispatch);
|
||||||
fetchProjects()(dispatch);
|
fetchProjects()(dispatch);
|
||||||
|
fetchStrategies()(dispatch);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user