mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-31 00:16:47 +01:00
feat: allow custom context fields to define stickiness. (#241)
This commit is contained in:
parent
972eccb7f1
commit
388dfe45d3
@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Chip, Textfield, Card, CardTitle, CardText, CardActions } from 'react-mdl';
|
||||
import { Button, Chip, Textfield, Card, CardTitle, CardText, CardActions, Checkbox } from 'react-mdl';
|
||||
|
||||
import { FormButtons, styles as commonStyles } from '../common';
|
||||
import { trim } from '../common/util';
|
||||
@ -151,6 +151,27 @@ class AddContextComponent extends Component {
|
||||
<Button onClick={this.addLegalValue}>Add</Button>
|
||||
<div>{contextField.legalValues.map(this.renderLegalValue)}</div>
|
||||
</section>
|
||||
<br />
|
||||
<section style={{ padding: '16px' }}>
|
||||
<h6 style={{ marginTop: '0' }}>Custom stickiness (beta)</h6>
|
||||
<p style={{ color: 'rgba(0,0,0,.54)' }}>
|
||||
By enabling stickiness on this context field you can use it together with the
|
||||
flexible-rollout strategy. This will guarantee a consistent behavior for specific values
|
||||
of this context field. PS! Not all client SDK's support this feature yet!{' '}
|
||||
<a
|
||||
href="https://unleash.github.io/docs/activation_strategy#flexiblerollout"
|
||||
target="_blank"
|
||||
>
|
||||
Read more
|
||||
</a>
|
||||
</p>
|
||||
<Checkbox
|
||||
label="Allow stickiness"
|
||||
ripple
|
||||
checked={contextField.stickiness}
|
||||
onChange={() => this.setValue('stickiness', !contextField.stickiness)}
|
||||
/>
|
||||
</section>
|
||||
</section>
|
||||
<CardActions>
|
||||
<FormButtons submitText={submitText} onCancel={this.onCancel} />
|
||||
|
@ -0,0 +1,10 @@
|
||||
import { connect } from 'react-redux';
|
||||
import FlexibleRolloutStrategy from './flexible-rollout-strategy';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
context: state.context.toJS(),
|
||||
});
|
||||
|
||||
const FormAddContainer = connect(mapStateToProps, undefined)(FlexibleRolloutStrategy);
|
||||
|
||||
export default FormAddContainer;
|
@ -1,11 +1,12 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Textfield } from 'react-mdl';
|
||||
import PropTypes from 'prop-types';
|
||||
import strategyInputProps from './strategy-input-props';
|
||||
import Select from '../../common/select';
|
||||
|
||||
import StrategyInputPercentage from './input-percentage';
|
||||
|
||||
const stickinessOptions = [
|
||||
const builtInStickinessOptions = [
|
||||
{ key: 'default', label: 'default' },
|
||||
{ key: 'userId', label: 'userId' },
|
||||
{ key: 'sessionId', label: 'sessionId' },
|
||||
@ -13,16 +14,24 @@ const stickinessOptions = [
|
||||
];
|
||||
|
||||
export default class FlexibleRolloutStrategy extends Component {
|
||||
static propTypes = strategyInputProps;
|
||||
static propTypes = { ...strategyInputProps, context: PropTypes.array };
|
||||
|
||||
onConfiguUpdate = (field, evt) => {
|
||||
onUpdate = (field, evt) => {
|
||||
evt.preventDefault();
|
||||
const value = evt.target.value;
|
||||
this.props.updateParameter(field, value);
|
||||
};
|
||||
|
||||
resolveStickiness = () => {
|
||||
const { context } = this.props;
|
||||
return builtInStickinessOptions.concat(
|
||||
context.filter(c => c.stickiness).map(c => ({ key: c.name, label: c.name }))
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { editable, parameters, index } = this.props;
|
||||
const stickinessOptions = this.resolveStickiness();
|
||||
|
||||
const rollout = parameters.rollout;
|
||||
const stickiness = parameters.stickiness;
|
||||
@ -38,7 +47,7 @@ export default class FlexibleRolloutStrategy extends Component {
|
||||
minLabel="off"
|
||||
maxLabel="on"
|
||||
disabled={!editable}
|
||||
onChange={evt => this.onConfiguUpdate('rollout', evt)}
|
||||
onChange={evt => this.onUpdate('rollout', evt)}
|
||||
id={`${index}-groupId`}
|
||||
/>
|
||||
<div>
|
||||
@ -48,7 +57,7 @@ export default class FlexibleRolloutStrategy extends Component {
|
||||
options={stickinessOptions}
|
||||
value={stickiness}
|
||||
disabled={!editable}
|
||||
onChange={evt => this.onConfiguUpdate('stickiness', evt)}
|
||||
onChange={evt => this.onUpdate('stickiness', evt)}
|
||||
/>
|
||||
|
||||
<Textfield
|
||||
@ -56,7 +65,7 @@ export default class FlexibleRolloutStrategy extends Component {
|
||||
label="groupId"
|
||||
value={groupId}
|
||||
disabled={!editable}
|
||||
onChange={evt => this.onConfiguUpdate('groupId', evt)}
|
||||
onChange={evt => this.onUpdate('groupId', evt)}
|
||||
id={`${index}-groupId`}
|
||||
/>{' '}
|
||||
</div>
|
||||
|
@ -3,7 +3,7 @@ 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 FlexibleRolloutStrategy from './flexible-rollout-strategy-container';
|
||||
import DefaultStrategy from './default-strategy';
|
||||
import GeneralStrategy from './general-strategy';
|
||||
import UserWithIdStrategy from './user-with-id-strategy';
|
||||
|
@ -4,7 +4,6 @@ exports[`renders correctly with with variants 1`] = `
|
||||
<section
|
||||
style={
|
||||
Object {
|
||||
"maxWidth": "700px",
|
||||
"padding": "16px",
|
||||
}
|
||||
}
|
||||
@ -154,7 +153,7 @@ exports[`renders correctly with with variants 1`] = `
|
||||
</tbody>
|
||||
</table>
|
||||
<br />
|
||||
<p>
|
||||
<div>
|
||||
<a
|
||||
href="#add-variant"
|
||||
onClick={[Function]}
|
||||
@ -162,7 +161,59 @@ exports[`renders correctly with with variants 1`] = `
|
||||
>
|
||||
Add variant
|
||||
</a>
|
||||
</p>
|
||||
<section
|
||||
style={
|
||||
Object {
|
||||
"paddingTop": "16px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="mdl-textfield mdl-js-textfield mdl-textfield--floating-label is-dirty is-upgraded"
|
||||
style={
|
||||
Object {
|
||||
"width": "auto",
|
||||
}
|
||||
}
|
||||
>
|
||||
<select
|
||||
className="mdl-textfield__input"
|
||||
disabled={false}
|
||||
onChange={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"background": "none",
|
||||
"width": "auto",
|
||||
}
|
||||
}
|
||||
value="default"
|
||||
>
|
||||
<option
|
||||
value="default"
|
||||
>
|
||||
default
|
||||
</option>
|
||||
</select>
|
||||
<label
|
||||
className="mdl-textfield__label"
|
||||
htmlFor="textfield-contextName"
|
||||
>
|
||||
Stickiness
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<small>
|
||||
By overriding the stickiness you can control which parameter you want to be used in order to ensure consistent traffic allocation across variants.
|
||||
|
||||
<a
|
||||
href="https://unleash.github.io/docs/toggle_variants"
|
||||
target="_blank"
|
||||
>
|
||||
Read more
|
||||
</a>
|
||||
</small>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
@ -170,7 +221,6 @@ exports[`renders correctly with without variants 1`] = `
|
||||
<section
|
||||
style={
|
||||
Object {
|
||||
"maxWidth": "700px",
|
||||
"padding": "16px",
|
||||
}
|
||||
}
|
||||
@ -193,7 +243,7 @@ exports[`renders correctly with without variants 1`] = `
|
||||
No variants defined.
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
<div>
|
||||
<a
|
||||
href="#add-variant"
|
||||
onClick={[Function]}
|
||||
@ -201,7 +251,7 @@ exports[`renders correctly with without variants 1`] = `
|
||||
>
|
||||
Add variant
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
@ -209,7 +259,6 @@ exports[`renders correctly with without variants and no permissions 1`] = `
|
||||
<section
|
||||
style={
|
||||
Object {
|
||||
"maxWidth": "700px",
|
||||
"padding": "16px",
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ test('renders correctly with without variants', () => {
|
||||
addVariant={jest.fn()}
|
||||
removeVariant={jest.fn()}
|
||||
updateVariant={jest.fn()}
|
||||
stickinessOptions={['default']}
|
||||
hasPermission={permission => permission === UPDATE_FEATURE}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
@ -34,6 +35,7 @@ test('renders correctly with without variants and no permissions', () => {
|
||||
addVariant={jest.fn()}
|
||||
removeVariant={jest.fn()}
|
||||
updateVariant={jest.fn()}
|
||||
stickinessOptions={['default']}
|
||||
hasPermission={() => false}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
@ -90,6 +92,7 @@ test('renders correctly with with variants', () => {
|
||||
addVariant={jest.fn()}
|
||||
removeVariant={jest.fn()}
|
||||
updateVariant={jest.fn()}
|
||||
stickinessOptions={['default']}
|
||||
hasPermission={permission => permission === UPDATE_FEATURE}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
|
@ -185,7 +185,7 @@ function AddVariant({ showDialog, closeDialog, save, validateName, editVariant,
|
||||
/>
|
||||
<span>%</span>
|
||||
</Cell>
|
||||
<Cell col={9} className={[styles.flexCenter, styles.marginL10]}>
|
||||
<Cell col={9} className={[styles.flexCenter, styles.marginL10].join(' ')}>
|
||||
<Switch name="weightType" checked={isFixWeight} onChange={setVariantWeightType}>
|
||||
Custom percentage
|
||||
</Switch>
|
||||
|
@ -5,8 +5,9 @@ import VariantViewComponent from './variant-view-component';
|
||||
import styles from './variant.module.scss';
|
||||
import { UPDATE_FEATURE } from '../../../permissions';
|
||||
import AddVariant from './add-variant';
|
||||
import MySelect from '../../common/select';
|
||||
|
||||
const initalState = {
|
||||
const initialState = {
|
||||
showDialog: false,
|
||||
editVariant: undefined,
|
||||
editIndex: -1,
|
||||
@ -15,11 +16,11 @@ const initalState = {
|
||||
class UpdateVariantComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { ...initalState };
|
||||
this.state = { ...initialState };
|
||||
}
|
||||
|
||||
closeDialog = () => {
|
||||
this.setState({ ...initalState });
|
||||
this.setState({ ...initialState });
|
||||
};
|
||||
|
||||
openAddVariant = e => {
|
||||
@ -80,13 +81,45 @@ class UpdateVariantComponent extends Component {
|
||||
</table>
|
||||
);
|
||||
|
||||
renderStickiness = variants => {
|
||||
const { updateStickiness, stickinessOptions } = this.props;
|
||||
|
||||
if (!variants || variants.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = variants[0].stickiness || 'default';
|
||||
const options = stickinessOptions.map(c => ({ key: c, label: c }));
|
||||
|
||||
// guard on stickiness being disabled for context field.
|
||||
if (!stickinessOptions.includes(value)) {
|
||||
options.push({ key: value, label: value });
|
||||
}
|
||||
|
||||
const onChange = event => updateStickiness(event.target.value);
|
||||
|
||||
return (
|
||||
<section style={{ paddingTop: '16px' }}>
|
||||
<MySelect label="Stickiness" options={options} value={value} onChange={onChange} />
|
||||
|
||||
<small>
|
||||
By overriding the stickiness you can control which parameter you want to be used in order to ensure
|
||||
consistent traffic allocation across variants.{' '}
|
||||
<a href="https://unleash.github.io/docs/toggle_variants" target="_blank">
|
||||
Read more
|
||||
</a>
|
||||
</small>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { showDialog, editVariant, editIndex, title } = this.state;
|
||||
const { variants, addVariant, updateVariant } = this.props;
|
||||
const saveVariant = editVariant ? updateVariant.bind(null, editIndex) : addVariant;
|
||||
|
||||
return (
|
||||
<section style={{ padding: '16px', maxWidth: '700px' }}>
|
||||
<section style={{ padding: '16px' }}>
|
||||
<p>
|
||||
Variants allows you to return a variant object if the feature toggle is considered enabled for the
|
||||
current request. When using variants you should use the{' '}
|
||||
@ -96,11 +129,12 @@ class UpdateVariantComponent extends Component {
|
||||
{variants.length > 0 ? this.renderVariants(variants) : <p>No variants defined.</p>}
|
||||
<br />
|
||||
{this.props.hasPermission(UPDATE_FEATURE) ? (
|
||||
<p>
|
||||
<div>
|
||||
<a href="#add-variant" title="Add variant" onClick={this.openAddVariant}>
|
||||
Add variant
|
||||
</a>
|
||||
</p>
|
||||
{this.renderStickiness(variants)}
|
||||
</div>
|
||||
) : null}
|
||||
<AddVariant
|
||||
showDialog={showDialog}
|
||||
@ -120,7 +154,9 @@ UpdateVariantComponent.propTypes = {
|
||||
addVariant: PropTypes.func.isRequired,
|
||||
removeVariant: PropTypes.func.isRequired,
|
||||
updateVariant: PropTypes.func.isRequired,
|
||||
updateStickiness: PropTypes.func.isRequired,
|
||||
hasPermission: PropTypes.func.isRequired,
|
||||
stickinessOptions: PropTypes.array,
|
||||
};
|
||||
|
||||
export default UpdateVariantComponent;
|
||||
|
@ -6,6 +6,7 @@ import { updateWeight } from '../../common/util';
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
variants: ownProps.featureToggle.variants || [],
|
||||
stickinessOptions: ['default', ...state.context.filter(c => c.stickiness).map(c => c.name)],
|
||||
hasPermission: ownProps.hasPermission,
|
||||
});
|
||||
|
||||
@ -33,6 +34,12 @@ const mapDispatchToProps = (dispatch, ownProps) => ({
|
||||
updateWeight(variants, 1000);
|
||||
requestUpdateFeatureToggleVariants(featureToggle, variants)(dispatch);
|
||||
},
|
||||
updateStickiness: stickiness => {
|
||||
const { featureToggle } = ownProps;
|
||||
const currentVariants = featureToggle.variants || [];
|
||||
const variants = currentVariants.map(v => ({ ...v, stickiness }));
|
||||
requestUpdateFeatureToggleVariants(featureToggle, variants)(dispatch);
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(UpdateFeatureToggleComponent);
|
||||
|
@ -6,7 +6,7 @@ Object {
|
||||
"headerBackground": undefined,
|
||||
"links": Array [
|
||||
Object {
|
||||
"href": "https://unleash.github.io",
|
||||
"href": "https://unleash.github.io?source=oss",
|
||||
"icon": "library_books",
|
||||
"title": "User documentation",
|
||||
"value": "User documentation",
|
||||
@ -30,7 +30,7 @@ Object {
|
||||
"headerBackground": "red",
|
||||
"links": Array [
|
||||
Object {
|
||||
"href": "https://unleash.github.io",
|
||||
"href": "https://unleash.github.io?source=oss",
|
||||
"icon": "library_books",
|
||||
"title": "User documentation",
|
||||
"value": "User documentation",
|
||||
@ -54,7 +54,7 @@ Object {
|
||||
"headerBackground": "black",
|
||||
"links": Array [
|
||||
Object {
|
||||
"href": "https://unleash.github.io",
|
||||
"href": "https://unleash.github.io?source=oss",
|
||||
"icon": "library_books",
|
||||
"title": "User documentation",
|
||||
"value": "User documentation",
|
||||
|
@ -19,7 +19,7 @@ const DEFAULT = new $Map({
|
||||
{
|
||||
value: 'User documentation',
|
||||
icon: 'library_books',
|
||||
href: 'https://unleash.github.io',
|
||||
href: 'https://unleash.github.io?source=oss',
|
||||
title: 'User documentation',
|
||||
},
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user