1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-31 00:16:47 +01:00

feat: Add support for flexible rollout strategy. (#193)

UI part of https://github.com/Unleash/unleash/issues/516
This commit is contained in:
Ivar Conradi Østhus 2019-10-24 16:04:39 +02:00 committed by ivaosthu
parent d1e0972401
commit 8ad6f3dc35
14 changed files with 238 additions and 17 deletions

View File

@ -12,6 +12,7 @@ exports[`render the create feature page 1`] = `
<StrategiesSection
addStrategy={[MockFunction]}
configuredStrategies={Array []}
featureToggleName="feature"
moveStrategy={[MockFunction]}
removeStrategy={[MockFunction]}
updateStrategy={[MockFunction]}

View File

@ -0,0 +1,86 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Textfield } from 'react-mdl';
import Select from './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,
};
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)}
/>
&nbsp;
<Textfield
floatingLabel
label="groupId"
value={groupId}
onChange={evt => handleConfigChange('groupId', evt)}
/>{' '}
</div>
</div>
);
}
}

View File

@ -34,6 +34,7 @@ class UpdateFeatureComponent extends Component {
<section style={{ padding: '16px' }}>
<StrategiesSection
configuredStrategies={configuredStrategies}
featureToggleName={input.name}
addStrategy={addStrategy}
updateStrategy={updateStrategy}
moveStrategy={moveStrategy}

View File

@ -0,0 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
const Select = ({ name, value, label, options, style, onChange }) => {
const wrapper = Object.assign({ width: 'auto' }, style);
return (
<div
className="mdl-textfield mdl-js-textfield mdl-textfield--floating-label is-dirty is-upgraded"
style={wrapper}
>
<select
className="mdl-textfield__input"
name={name}
onChange={onChange}
value={value}
style={{ width: 'auto' }}
>
{options.map(o => (
<option key={o.key} value={o.key}>
{o.label}
</option>
))}
</select>
<label className="mdl-textfield__label" htmlFor="textfield-conextName">
{label}
</label>
</div>
);
};
Select.propTypes = {
name: PropTypes.string,
value: PropTypes.string,
label: PropTypes.string,
options: PropTypes.array,
style: PropTypes.object,
onChange: PropTypes.func.isRequired,
};
export default Select;

View File

@ -9,13 +9,21 @@ class StrategiesList extends React.Component {
static propTypes = {
strategies: PropTypes.array.isRequired,
configuredStrategies: PropTypes.array.isRequired,
featureToggleName: PropTypes.string.isRequired,
updateStrategy: PropTypes.func,
removeStrategy: PropTypes.func,
moveStrategy: PropTypes.func,
};
render() {
const { strategies, configuredStrategies, moveStrategy, removeStrategy, updateStrategy } = this.props;
const {
strategies,
configuredStrategies,
moveStrategy,
removeStrategy,
updateStrategy,
featureToggleName,
} = this.props;
if (!configuredStrategies || configuredStrategies.length === 0) {
return (
@ -29,6 +37,7 @@ class StrategiesList extends React.Component {
<ConfigureStrategy
index={i}
key={`${strategy.id}-${i}`}
featureToggleName={featureToggleName}
strategy={strategy}
moveStrategy={moveStrategy}
removeStrategy={removeStrategy ? removeStrategy.bind(null, i) : null}

View File

@ -8,6 +8,7 @@ import { HeaderTitle } from '../../common';
class StrategiesSectionComponent extends React.Component {
static propTypes = {
strategies: PropTypes.array.isRequired,
featureToggleName: PropTypes.string.isRequired,
addStrategy: PropTypes.func,
removeStrategy: PropTypes.func,
updateStrategy: PropTypes.func,

View File

@ -16,6 +16,7 @@ import {
import { DragSource, DropTarget } from 'react-dnd';
import { Link } from 'react-router-dom';
import StrategyInputPercentage from './strategy-input-percentage';
import FlexibleRolloutStrategyInput from './flexible-rollout-strategy-input';
import StrategyInputList from './strategy-input-list';
import styles from './strategy.scss';
@ -58,6 +59,7 @@ 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,
@ -93,6 +95,21 @@ class StrategyConfigure extends React.Component {
this.props.removeStrategy();
};
renderStrategContent(strategyDefinition) {
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 this.renderInputFields(strategyDefinition);
}
}
renderInputFields({ parameters }) {
if (parameters && parameters.length > 0) {
return parameters.map(({ name, type, description, required }) => {
@ -193,7 +210,7 @@ class StrategyConfigure extends React.Component {
let item;
if (this.props.strategyDefinition) {
const inputFields = this.renderInputFields(this.props.strategyDefinition);
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' }}>
@ -203,11 +220,7 @@ class StrategyConfigure extends React.Component {
{name}
</CardTitle>
<CardText>{this.props.strategyDefinition.description}</CardText>
{inputFields && (
<CardActions border style={{ padding: '20px' }}>
{inputFields}
</CardActions>
)}
{strategyContent && <CardActions border>{strategyContent}</CardActions>}
<CardMenu className="mdl-color-text--white">
<Link title="View strategy" to={`/strategies/view/${name}`} className={styles.editLink}>

View File

@ -1,25 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Slider } from 'react-mdl';
import { Slider, Grid, Cell } from 'react-mdl';
const labelStyle = {
margin: '20px 0',
textAlign: 'center',
color: '#3f51b5',
fontSize: '12px',
};
const InputPercentage = ({ name, value, onChange }) => (
const infoLabelStyle = {
fontSize: '0.8em',
color: 'gray',
paddingBottom: '-3px',
};
const InputPercentage = ({ name, minLabel, maxLabel, value, onChange }) => (
<div style={{ marginBottom: '20px' }}>
<div style={labelStyle}>
{name}: {value}%
</div>
<Grid noSpacing style={{ margin: '0 15px' }}>
<Cell col={1}>
&nbsp;<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>&nbsp;
</Cell>
</Grid>
<Slider min={0} max={100} defaultValue={value} 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,
};

View File

@ -1,8 +1,8 @@
.item {
flex: 1;
min-width: 300px;
min-width: 400px;
max-width: 100%;
margin: 5px 0px 15px 35px;
margin: 5px 0 5px 5px;
position: relative;
z-index: 1;
};
@ -17,6 +17,7 @@
margin-left: 0;
}
/*
.item:not(:first-child):after {
content: " OR ";
position: absolute;
@ -30,6 +31,7 @@
height: 100%;
z-index: 2;
}
*/
.cardTitle {
color: #fff;

View File

@ -7,16 +7,19 @@ import { DrawerMenu } from './drawer';
import Breadcrum from './breadcrumb';
import ShowUserContainer from '../user/show-user-container';
import { fetchUIConfig } from './../../store/ui-config/actions';
import { fetchContext } from './../../store/context/actions';
class HeaderComponent extends PureComponent {
static propTypes = {
uiConfig: PropTypes.object.isRequired,
fetchUIConfig: PropTypes.func.isRequired,
fetchContext: PropTypes.func.isRequired,
location: PropTypes.object.isRequired,
};
componentDidMount() {
this.props.fetchUIConfig();
this.props.fetchContext();
}
componentWillReceiveProps(nextProps) {
@ -51,5 +54,5 @@ class HeaderComponent extends PureComponent {
export default connect(
state => ({ uiConfig: state.uiConfig.toJS() }),
{ fetchUIConfig }
{ fetchUIConfig, fetchContext }
)(HeaderComponent);

View File

@ -0,0 +1,13 @@
import { throwIfNotSuccess } from './helper';
const URI = 'api/admin/context';
function fetchContext() {
return fetch(URI, { credentials: 'include' })
.then(throwIfNotSuccess)
.then(response => response.json());
}
export default {
fetchContext,
};

View File

@ -0,0 +1,18 @@
import api from '../../data/context-api';
import { dispatchAndThrow } from '../util';
export const RECEIVE_CONTEXT = 'RECEIVE_CONTEXT';
export const ERROR_RECEIVE_CONTEXT = 'ERROR_RECEIVE_CONTEXT';
export const receiveContext = json => ({
type: RECEIVE_CONTEXT,
value: json,
});
export function fetchContext() {
return dispatch =>
api
.fetchContext()
.then(json => dispatch(receiveContext(json)))
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_CONTEXT));
}

View File

@ -0,0 +1,18 @@
import { RECEIVE_CONTEXT } from './actions';
const DEFAULT_CONTEXT_FIELDS = [{ name: 'environment' }, { name: 'userId' }, { name: 'appName' }];
function getInitState() {
return DEFAULT_CONTEXT_FIELDS;
}
const strategies = (state = getInitState(), action) => {
switch (action.type) {
case RECEIVE_CONTEXT:
return action.value;
default:
return state;
}
};
export default strategies;

View File

@ -12,6 +12,7 @@ import user from './user';
import api from './api';
import applications from './application';
import uiConfig from './ui-config';
import context from './context';
const unleashStore = combineReducers({
features,
@ -27,6 +28,7 @@ const unleashStore = combineReducers({
applications,
uiConfig,
api,
context,
});
export default unleashStore;