1
0
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:
Ivar Conradi Østhus 2021-02-16 12:22:33 +01:00 committed by GitHub
parent 972eccb7f1
commit 388dfe45d3
11 changed files with 161 additions and 26 deletions

View File

@ -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} />

View File

@ -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;

View File

@ -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)}
/>
&nbsp;
<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>

View File

@ -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';

View File

@ -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",
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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} />
&nbsp;&nbsp;
<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;

View File

@ -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);

View File

@ -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",

View File

@ -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',
},
{