1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-04 00:18:01 +01:00

Merge pull request #167 from Unleash/variants

Add support for variants
This commit is contained in:
Ivar Conradi Østhus 2019-02-08 09:17:04 +01:00 committed by GitHub
commit d0c8cf25a3
14 changed files with 901 additions and 5 deletions

View File

@ -8,7 +8,8 @@ The latest version of this document is always available in
[releases][releases-url]. [releases][releases-url].
## [Unreleased] ## [3.2.0]
- feat: Initial beta support for variants
- feature: Show tooltips and featuretoggle names in event view - feature: Show tooltips and featuretoggle names in event view
## [3.1.4] ## [3.1.4]

View File

@ -1,7 +1,7 @@
{ {
"name": "unleash-frontend", "name": "unleash-frontend",
"description": "unleash your features", "description": "unleash your features",
"version": "3.1.4", "version": "3.2.0-beta.5",
"keywords": [ "keywords": [
"unleash", "unleash",
"feature toggle", "feature toggle",

View File

@ -32,6 +32,7 @@ test('renders correctly with one feature', () => {
activeTab={'strategies'} activeTab={'strategies'}
featureToggleName="another" featureToggleName="another"
features={[feature]} features={[feature]}
betaFlags={[]}
featureToggle={feature} featureToggle={feature}
fetchFeatureToggles={jest.fn()} fetchFeatureToggles={jest.fn()}
history={{}} history={{}}

View File

@ -0,0 +1,244 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly with with variants 1`] = `
<section
style={
Object {
"padding": "16px",
}
}
>
<p>
Variants is a new
<i>
beta feature
</i>
and the implementation is subject to change at any time until it is made in to a permanent feature. In order to use variants you will have use a Client SDK which supports variants. You should read more about variants in the 
<a
href="https://unleash.github.io/docs/beta_features"
target="_blank"
>
user documentation
</a>
.
</p>
<p
style={
Object {
"backgroundColor": "rgba(255, 229, 100, 0.3)",
"padding": "5px",
}
}
>
The sum of variants weights needs to be a constant number to guarantee consistent hashing in the client implementations, this is why we will sometime allocate a few more percentages to the first variant if the sum is not exactly 100. In a final version of this feature it should be possible to the user to manually set the percentages for each variant.
</p>
<p>
<a
href="#add-variant"
onClick={[Function]}
title="Add variant"
>
Add variant
</a>
</p>
<form>
<table
className="mdl-data-table mdl-shadow--2dp variantTable"
>
<thead>
<tr>
<th>
Name
</th>
<th>
Weight
</th>
<th
className="actions"
/>
</tr>
</thead>
<tbody>
<tr>
<td
onClick={[Function]}
>
blue
</td>
<td>
34
</td>
<td
className="actions"
>
<react-mdl-IconButton
name="expand_more"
onClick={[Function]}
/>
<react-mdl-IconButton
name="delete"
onClick={[Function]}
/>
</td>
</tr>
<tr>
<td
onClick={[Function]}
>
yellow
</td>
<td>
33
</td>
<td
className="actions"
>
<react-mdl-IconButton
name="expand_more"
onClick={[Function]}
/>
<react-mdl-IconButton
name="delete"
onClick={[Function]}
/>
</td>
</tr>
<tr>
<td
onClick={[Function]}
>
orange
</td>
<td>
33
</td>
<td
className="actions"
>
<react-mdl-IconButton
name="expand_more"
onClick={[Function]}
/>
<react-mdl-IconButton
name="delete"
onClick={[Function]}
/>
</td>
</tr>
</tbody>
</table>
<br />
<div>
<react-mdl-Button
icon="add"
primary={true}
raised={true}
ripple={true}
type="submit"
>
<react-mdl-Icon
name="add"
/>
   
Save
</react-mdl-Button>
 
<react-mdl-Button
onClick={[MockFunction]}
raised={true}
ripple={true}
style={
Object {
"float": "right",
}
}
type="cancel"
>
<react-mdl-Icon
name="cancel"
/>
    Cancel
</react-mdl-Button>
</div>
</form>
</section>
`;
exports[`renders correctly with without variants 1`] = `
<section
style={
Object {
"padding": "16px",
}
}
>
<p>
Variants is a new
<i>
beta feature
</i>
and the implementation is subject to change at any time until it is made in to a permanent feature. In order to use variants you will have use a Client SDK which supports variants. You should read more about variants in the 
<a
href="https://unleash.github.io/docs/beta_features"
target="_blank"
>
user documentation
</a>
.
</p>
<p
style={
Object {
"backgroundColor": "rgba(255, 229, 100, 0.3)",
"padding": "5px",
}
}
>
The sum of variants weights needs to be a constant number to guarantee consistent hashing in the client implementations, this is why we will sometime allocate a few more percentages to the first variant if the sum is not exactly 100. In a final version of this feature it should be possible to the user to manually set the percentages for each variant.
</p>
<p>
<a
href="#add-variant"
onClick={[Function]}
title="Add variant"
>
Add variant
</a>
</p>
</section>
`;
exports[`renders correctly with without variants and no permissions 1`] = `
<section
style={
Object {
"padding": "16px",
}
}
>
<p>
Variants is a new
<i>
beta feature
</i>
and the implementation is subject to change at any time until it is made in to a permanent feature. In order to use variants you will have use a Client SDK which supports variants. You should read more about variants in the 
<a
href="https://unleash.github.io/docs/beta_features"
target="_blank"
>
user documentation
</a>
.
</p>
<p
style={
Object {
"backgroundColor": "rgba(255, 229, 100, 0.3)",
"padding": "5px",
}
}
>
The sum of variants weights needs to be a constant number to guarantee consistent hashing in the client implementations, this is why we will sometime allocate a few more percentages to the first variant if the sum is not exactly 100. In a final version of this feature it should be possible to the user to manually set the percentages for each variant.
</p>
</section>
`;

View File

@ -0,0 +1,143 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import UpdateVariant from './../update-variant-component';
import renderer from 'react-test-renderer';
import { UPDATE_FEATURE } from '../../../../permissions';
jest.mock('react-mdl');
test('renders correctly with without variants', () => {
const featureToggle = {
name: 'Another',
description: "another's description",
enabled: false,
strategies: [
{
name: 'gradualRolloutRandom',
parameters: {
percentage: 50,
},
},
],
createdAt: '2018-02-04T20:27:52.127Z',
};
const tree = renderer.create(
<MemoryRouter>
<UpdateVariant
key={0}
input={featureToggle}
onCancel={jest.fn()}
features={[]}
setValue={jest.fn()}
addVariant={jest.fn()}
removeVariant={jest.fn()}
updateVariant={jest.fn()}
onSubmit={jest.fn()}
onCancel={jest.fn()}
init={jest.fn()}
hasPermission={permission => permission === UPDATE_FEATURE}
/>
</MemoryRouter>
);
expect(tree).toMatchSnapshot();
});
test('renders correctly with without variants and no permissions', () => {
const featureToggle = {
name: 'Another',
description: "another's description",
enabled: false,
strategies: [
{
name: 'gradualRolloutRandom',
parameters: {
percentage: 50,
},
},
],
createdAt: '2018-02-04T20:27:52.127Z',
};
const tree = renderer.create(
<MemoryRouter>
<UpdateVariant
key={0}
input={featureToggle}
onCancel={jest.fn()}
features={[]}
setValue={jest.fn()}
addVariant={jest.fn()}
removeVariant={jest.fn()}
updateVariant={jest.fn()}
onSubmit={jest.fn()}
onCancel={jest.fn()}
init={jest.fn()}
hasPermission={() => false}
/>
</MemoryRouter>
);
expect(tree).toMatchSnapshot();
});
test('renders correctly with with variants', () => {
const featureToggle = {
name: 'toggle.variants',
description: 'description',
enabled: false,
strategies: [
{
name: 'gradualRolloutRandom',
parameters: {
percentage: 50,
},
},
],
variants: [
{
name: 'blue',
weight: 34,
overrides: [
{
field: 'userId',
values: ['1337', '123'],
},
],
},
{
name: 'yellow',
weight: 33,
},
{
name: 'orange',
weight: 33,
payload: {
type: 'string',
value: '{"color": "blue", "animated": false}',
},
},
],
createdAt: '2018-02-04T20:27:52.127Z',
};
const tree = renderer.create(
<MemoryRouter>
<UpdateVariant
key={0}
input={featureToggle}
onCancel={jest.fn()}
features={[]}
setValue={jest.fn()}
addVariant={jest.fn()}
removeVariant={jest.fn()}
updateVariant={jest.fn()}
onSubmit={jest.fn()}
onCancel={jest.fn()}
init={jest.fn()}
hasPermission={permission => permission === UPDATE_FEATURE}
/>
</MemoryRouter>
);
expect(tree).toMatchSnapshot();
});

View File

@ -0,0 +1,163 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormButtons } from '../../common';
import VariantViewComponent from './variant-view-component';
import VariantEditComponent from './variant-edit-component';
import styles from './variant.scss';
import { UPDATE_FEATURE } from '../../../permissions';
class UpdateVariantComponent extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
// TODO unwind this stuff
if (this.props.initCallRequired === true) {
this.props.init(this.props.input);
}
}
updateWeight(newWeight, newSize) {
const variants = this.props.input.variants || [];
variants.forEach((v, i) => {
v.weight = newWeight;
this.props.updateVariant(i, v);
});
// Make sure the sum of weigths is 100.
const sum = newWeight * newSize;
if (sum !== 100) {
const first = variants[0];
first.weight = 100 - sum + first.weight;
this.props.updateVariant(0, first);
}
}
addVariant = (e, variants) => {
e.preventDefault();
const size = variants.length + 1;
const percentage = parseInt(1 / size * 100);
const variant = {
name: '',
weight: percentage,
edit: true,
};
this.updateWeight(percentage, size);
this.props.addVariant(variant);
};
removeVariant = (e, index) => {
e.preventDefault();
const variants = this.props.input.variants;
const size = variants.length - 1;
const percentage = parseInt(1 / size * 100);
this.updateWeight(percentage, size);
this.props.removeVariant(index);
};
editVariant = (e, index, v) => {
e.preventDefault();
if (this.props.hasPermission(UPDATE_FEATURE)) {
v.edit = true;
this.props.updateVariant(index, v);
}
};
closeVariant = (e, index, v) => {
e.preventDefault();
v.edit = false;
this.props.updateVariant(index, v);
};
updateVariant = (index, newVariant) => {
this.props.updateVariant(index, newVariant);
};
renderVariant = (variant, index) =>
variant.edit ? (
<VariantEditComponent
key={index}
variant={variant}
removeVariant={e => this.removeVariant(e, index)}
closeVariant={e => this.closeVariant(e, index, variant)}
updateVariant={this.updateVariant.bind(this, index)}
/>
) : (
<VariantViewComponent
key={index}
variant={variant}
editVariant={e => this.editVariant(e, index, variant)}
removeVariant={e => this.removeVariant(e, index)}
hasPermission={this.props.hasPermission}
/>
);
render() {
const { onSubmit, onCancel, input, features } = this.props;
const variants = input.variants || [];
return (
<section style={{ padding: '16px' }}>
<p>
Variants is a new <i>beta feature</i> and the implementation is subject to change at any time until
it is made in to a permanent feature. In order to use variants you will have use a Client SDK which
supports variants. You should read more about variants in the&nbsp;
<a target="_blank" href="https://unleash.github.io/docs/beta_features">
user documentation
</a>.
</p>
<p style={{ backgroundColor: 'rgba(255, 229, 100, 0.3)', padding: '5px' }}>
The sum of variants weights needs to be a constant number to guarantee consistent hashing in the
client implementations, this is why we will sometime allocate a few more percentages to the first
variant if the sum is not exactly 100. In a final version of this feature it should be possible to
the user to manually set the percentages for each variant.
</p>
{this.props.hasPermission(UPDATE_FEATURE) ? (
<p>
<a href="#add-variant" title="Add variant" onClick={e => this.addVariant(e, variants)}>
Add variant
</a>
</p>
) : null}
{variants.length > 0 ? (
<form onSubmit={onSubmit(input, features)}>
<table className={['mdl-data-table mdl-shadow--2dp', styles.variantTable].join(' ')}>
<thead>
<tr>
<th>Name</th>
<th>Weight</th>
<th className={styles.actions} />
</tr>
</thead>
<tbody>{variants.map(this.renderVariant)}</tbody>
</table>
<br />
{this.props.hasPermission(UPDATE_FEATURE) ? (
<FormButtons submitText={'Save'} onCancel={onCancel} />
) : null}
</form>
) : null}
</section>
);
}
}
UpdateVariantComponent.propTypes = {
input: PropTypes.object,
features: PropTypes.array,
setValue: PropTypes.func.isRequired,
addVariant: PropTypes.func.isRequired,
removeVariant: PropTypes.func.isRequired,
updateVariant: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
initCallRequired: PropTypes.bool,
init: PropTypes.func,
hasPermission: PropTypes.func.isRequired,
};
export default UpdateVariantComponent;

View File

@ -0,0 +1,67 @@
import { connect } from 'react-redux';
import { requestUpdateFeatureToggleVariants } from '../../../store/feature-actions';
import { createMapper, createActions } from '../../input-helpers';
import UpdateFeatureToggleComponent from './update-variant-component';
const ID = 'edit-toggle-variants';
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,
prepare: props => {
props.editmode = true;
return props;
},
});
const prepare = (methods, dispatch, ownProps) => {
methods.onSubmit = (input, features) => e => {
e.preventDefault();
const featureToggle = features.find(f => f.name === input.name);
// Kind of a hack
featureToggle.strategies.forEach(s => (s.id = undefined));
const variants = input.variants.map(v => {
delete v.edit;
return v;
});
requestUpdateFeatureToggleVariants(featureToggle, variants)(dispatch);
variants.forEach((v, i) => methods.updateInList('variants', i, v));
};
methods.onCancel = evt => {
evt.preventDefault();
ownProps.history.push(`/features/view/${ownProps.featureToggle.name}`);
};
methods.addVariant = v => {
methods.pushToList('variants', v);
};
methods.removeVariant = index => {
methods.removeFromList('variants', index);
};
methods.updateVariant = (index, n) => {
methods.updateInList('variants', index, n);
};
methods.validateName = () => {};
return methods;
};
const actions = createActions({
id: getId,
prepare,
});
export default connect(mapStateToProps, actions)(UpdateFeatureToggleComponent);

View File

@ -0,0 +1,162 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { IconButton, Cell, Grid, Textfield, Tooltip, Icon } from 'react-mdl';
import styles from './variant.scss';
class VariantEditComponent extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.refs.name.inputRef.focus();
}
getUserIdOverrides(variant) {
const overrides = variant.overrides || [];
const userIdOverrides = overrides.find(o => o.contextName === 'userId') || { values: [] };
return userIdOverrides.values.join(', ');
}
toggleEditMode = e => {
e.preventDefault();
this.setState({
editmode: !this.state.editmode,
});
};
updateToggleName = e => {
e.preventDefault();
const variant = this.props.variant;
variant.name = e.target.value;
this.props.updateVariant(variant);
};
updatePayload = e => {
e.preventDefault();
const variant = this.props.variant;
variant.payload = {
type: 'string',
value: e.target.value,
};
this.props.updateVariant(variant);
};
updateOverrides = (contextName, e) => {
e.preventDefault();
const values = e.target.value.split(',').map(v => v.trim());
const variant = this.props.variant;
// Clean empty string. (should be moved to action)
if (values.length === 1 && !values[0]) {
variant.overrides = undefined;
} else {
variant.overrides = [{ contextName, values }];
}
this.props.updateVariant(variant);
};
render() {
const { variant, closeVariant, removeVariant } = this.props;
const payload = variant.payload ? variant.payload.value : '';
const userIdOverrides = this.getUserIdOverrides(variant);
return (
<tr style={{ backgroundColor: '#EFEFEF' }}>
<td>
<Grid noSpacing>
<Cell col={6}>
<Textfield
floatingLabel
ref="name"
label="Name"
name="name"
required
value={variant.name}
onChange={this.updateToggleName}
/>
</Cell>
<Cell col={6} />
</Grid>
<Grid noSpacing>
<Cell col={11}>
<Textfield
floatingLabel
rows={1}
label="Payload"
name="payload"
style={{ width: '100%' }}
value={payload}
onChange={this.updatePayload}
/>
</Cell>
<Cell col={1} style={{ margin: 'auto', padding: '0 5px' }}>
<Tooltip
label={
<span>
Passed to the variant object. <br />
Can be anything (json, value, csv)
</span>
}
>
<Icon name="info" />
</Tooltip>
</Cell>
</Grid>
<Grid noSpacing>
<Cell col={11}>
<Textfield
floatingLabel
label="overrides.userId"
name="overrides.userId"
style={{ width: '100%' }}
value={userIdOverrides}
onChange={this.updateOverrides.bind(this, 'userId')}
/>
</Cell>
<Cell col={1} style={{ margin: 'auto', padding: '0 5px' }}>
<Tooltip
label={
<div>
Here you can specify which users that <br />
should get this variant.
</div>
}
>
<Icon name="info" />
</Tooltip>
</Cell>
</Grid>
<a href="#close" onClick={closeVariant}>
Close
</a>
</td>
<td className={styles.actions}>
<Textfield
floatingLabel
label="Weight"
name="weight"
value={variant.weight}
style={{ width: '40px' }}
disabled
onChange={() => {}}
/>
</td>
<td className={styles.actions}>
<IconButton name="expand_less" onClick={closeVariant} />
<IconButton name="delete" onClick={removeVariant} />
</td>
</tr>
);
}
}
VariantEditComponent.propTypes = {
variant: PropTypes.object,
removeVariant: PropTypes.func,
updateVariant: PropTypes.func,
closeVariant: PropTypes.func,
};
export default VariantEditComponent;

View File

@ -0,0 +1,35 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { IconButton } from 'react-mdl';
import styles from './variant.scss';
import { UPDATE_FEATURE } from '../../../permissions';
class VariantViewComponent extends Component {
render() {
const { variant, editVariant, removeVariant, hasPermission } = this.props;
return (
<tr>
<td onClick={editVariant}>{variant.name}</td>
<td>{variant.weight}</td>
{hasPermission(UPDATE_FEATURE) ? (
<td className={styles.actions}>
<IconButton name="expand_more" onClick={editVariant} />
<IconButton name="delete" onClick={removeVariant} />
</td>
) : (
<td className={styles.actions} />
)}
</tr>
);
}
}
VariantViewComponent.propTypes = {
variant: PropTypes.object,
removeVariant: PropTypes.func,
editVariant: PropTypes.func,
hasPermission: PropTypes.func.isRequired,
};
export default VariantViewComponent;

View File

@ -0,0 +1,22 @@
.variantTable {
width: 100%;
th, td {
text-align: center;
width: 100px;
}
th:first-of-type, td:first-of-type {
text-align: left;
width: 100%;
}
}
th.actions {
text-align: right;
}
td.actions {
text-align: right;
vertical-align: top;
}

View File

@ -1,11 +1,24 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Tabs, Tab, ProgressBar, Button, Card, CardText, CardTitle, CardActions, Textfield, Switch } from 'react-mdl'; import {
Tabs,
Tab,
ProgressBar,
Button,
Card,
CardText,
CardTitle,
CardActions,
Textfield,
Switch,
Badge,
} 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 EditFeatureToggle from './form/form-update-feature-container';
import EditVariants from './variant/update-variant-container';
import ViewFeatureToggle from './form/form-view-feature-container'; import ViewFeatureToggle from './form/form-view-feature-container';
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';
@ -13,7 +26,8 @@ import { CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE } from '../../permission
const TABS = { const TABS = {
strategies: 0, strategies: 0,
view: 1, view: 1,
history: 2, variants: 2,
history: 3,
}; };
export default class ViewFeatureToggleComponent extends React.Component { export default class ViewFeatureToggleComponent extends React.Component {
@ -27,6 +41,7 @@ export default class ViewFeatureToggleComponent extends React.Component {
activeTab: PropTypes.string.isRequired, activeTab: PropTypes.string.isRequired,
featureToggleName: PropTypes.string.isRequired, featureToggleName: PropTypes.string.isRequired,
features: PropTypes.array.isRequired, features: PropTypes.array.isRequired,
betaFlags: PropTypes.array.isRequired,
toggleFeature: PropTypes.func, toggleFeature: PropTypes.func,
removeFeatureToggle: PropTypes.func, removeFeatureToggle: PropTypes.func,
revive: PropTypes.func, revive: PropTypes.func,
@ -60,6 +75,15 @@ export default class ViewFeatureToggleComponent extends React.Component {
); );
} }
return <ViewFeatureToggle featureToggle={featureToggle} />; return <ViewFeatureToggle featureToggle={featureToggle} />;
} else if (TABS[activeTab] === TABS.variants) {
return (
<EditVariants
featureToggle={featureToggle}
features={features}
history={this.props.history}
hasPermission={this.props.hasPermission}
/>
);
} else { } else {
return <MetricComponent featureToggle={featureToggle} />; return <MetricComponent featureToggle={featureToggle} />;
} }
@ -74,6 +98,7 @@ export default class ViewFeatureToggleComponent extends React.Component {
const { const {
featureToggle, featureToggle,
features, features,
betaFlags,
activeTab, activeTab,
revive, revive,
// setValue, // setValue,
@ -83,6 +108,9 @@ export default class ViewFeatureToggleComponent extends React.Component {
hasPermission, hasPermission,
} = this.props; } = this.props;
// TODO: Find better solution for this
const showVariants = betaFlags.includes('unleash.beta.variants');
if (!featureToggle) { if (!featureToggle) {
if (features.length === 0) { if (features.length === 0) {
return <ProgressBar indeterminate />; return <ProgressBar indeterminate />;
@ -217,6 +245,15 @@ export default class ViewFeatureToggleComponent extends React.Component {
> >
<Tab onClick={() => this.goToTab('strategies', featureToggleName)}>Strategies</Tab> <Tab onClick={() => this.goToTab('strategies', featureToggleName)}>Strategies</Tab>
<Tab onClick={() => this.goToTab('view', featureToggleName)}>Metrics</Tab> <Tab onClick={() => this.goToTab('view', featureToggleName)}>Metrics</Tab>
{showVariants ? (
<Tab onClick={() => this.goToTab('variants', featureToggleName)}>
<Badge text="beta" noBackground>
Variants
</Badge>
</Tab>
) : (
[]
)}
<Tab onClick={() => this.goToTab('history', featureToggleName)}>History</Tab> <Tab onClick={() => this.goToTab('history', featureToggleName)}>History</Tab>
</Tabs> </Tabs>
{tabContent} {tabContent}

View File

@ -13,6 +13,11 @@ import { hasPermission } from '../../permissions';
export default connect( export default connect(
(state, props) => ({ (state, props) => ({
features: state.features.toJS(), features: state.features.toJS(),
betaFlags: state.features
.toJS()
.filter(t => t.enabled)
.filter(t => t.name.startsWith('unleash.beta'))
.map(t => t.name),
featureToggle: state.features.toJS().find(toggle => toggle.name === props.featureToggleName), featureToggle: state.features.toJS().find(toggle => toggle.name === props.featureToggleName),
activeTab: props.activeTab, activeTab: props.activeTab,
hasPermission: hasPermission.bind(null, state.user.get('profile')), hasPermission: hasPermission.bind(null, state.user.get('profile')),

View File

@ -102,6 +102,22 @@ export function requestUpdateFeatureToggleStrategies(featureToggle, newStrategie
}; };
} }
export function requestUpdateFeatureToggleVariants(featureToggle, newVariants) {
return dispatch => {
featureToggle.variants = newVariants;
dispatch({ type: START_UPDATE_FEATURE_TOGGLE });
return api
.update(featureToggle)
.then(() => {
const info = `${featureToggle.name} successfully updated!`;
setTimeout(() => dispatch({ type: MUTE_ERROR, error: info }), 1000);
return dispatch({ type: UPDATE_FEATURE_TOGGLE_STRATEGIES, featureToggle, info });
})
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_FEATURE_TOGGLE));
};
}
export function removeFeatureToggle(featureToggleName) { export function removeFeatureToggle(featureToggleName) {
return dispatch => { return dispatch => {
dispatch({ type: START_REMOVE_FEATURE_TOGGLE }); dispatch({ type: START_REMOVE_FEATURE_TOGGLE });

View File

@ -18,7 +18,7 @@ function assertId(state, id) {
} }
function assertList(state, id, key) { function assertList(state, id, key) {
if (!state.getIn(id).has(key)) { if (!state.getIn(id).has(key) || state.getIn(id).get(key) == null) {
return state.setIn(id.concat([key]), new List()); return state.setIn(id.concat([key]), new List());
} }
return state; return state;