mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-04 00:18:01 +01:00
commit
d0c8cf25a3
@ -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]
|
||||||
|
@ -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",
|
||||||
|
@ -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={{}}
|
||||||
|
@ -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>
|
||||||
|
`;
|
@ -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();
|
||||||
|
});
|
@ -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
|
||||||
|
<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;
|
@ -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);
|
@ -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;
|
@ -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;
|
22
frontend/src/component/feature/variant/variant.scss
Normal file
22
frontend/src/component/feature/variant/variant.scss
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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}
|
||||||
|
@ -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')),
|
||||||
|
@ -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 });
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user