From 8ad6f3dc351520550059a696375c7ea7297c2320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Thu, 24 Oct 2019 16:04:39 +0200 Subject: [PATCH] feat: Add support for flexible rollout strategy. (#193) UI part of https://github.com/Unleash/unleash/issues/516 --- ...orm-update-feature-component-test.jsx.snap | 1 + .../form/flexible-rollout-strategy-input.jsx | 86 +++++++++++++++++++ .../form/form-update-feature-component.jsx | 1 + .../src/component/feature/form/select.jsx | 40 +++++++++ .../feature/form/strategies-list.jsx | 11 ++- .../feature/form/strategies-section.jsx | 1 + .../feature/form/strategy-configure.jsx | 25 ++++-- .../form/strategy-input-percentage.jsx | 28 ++++-- .../src/component/feature/form/strategy.scss | 6 +- frontend/src/component/menu/header.jsx | 5 +- frontend/src/data/context-api.js | 13 +++ frontend/src/store/context/actions.js | 18 ++++ frontend/src/store/context/index.js | 18 ++++ frontend/src/store/index.js | 2 + 14 files changed, 238 insertions(+), 17 deletions(-) create mode 100644 frontend/src/component/feature/form/flexible-rollout-strategy-input.jsx create mode 100644 frontend/src/component/feature/form/select.jsx create mode 100644 frontend/src/data/context-api.js create mode 100644 frontend/src/store/context/actions.js create mode 100644 frontend/src/store/context/index.js diff --git a/frontend/src/component/feature/form/__tests__/__snapshots__/form-update-feature-component-test.jsx.snap b/frontend/src/component/feature/form/__tests__/__snapshots__/form-update-feature-component-test.jsx.snap index 5ccf0a9d05..c4ec53dd06 100644 --- a/frontend/src/component/feature/form/__tests__/__snapshots__/form-update-feature-component-test.jsx.snap +++ b/frontend/src/component/feature/form/__tests__/__snapshots__/form-update-feature-component-test.jsx.snap @@ -12,6 +12,7 @@ exports[`render the create feature page 1`] = ` { + 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 ( +
+
+
Rollout
+ handleConfigChange('rollout', evt)} + /> +
+ + {options.map(o => ( + + ))} + + +
+ ); +}; + +Select.propTypes = { + name: PropTypes.string, + value: PropTypes.string, + label: PropTypes.string, + options: PropTypes.array, + style: PropTypes.object, + onChange: PropTypes.func.isRequired, +}; + +export default Select; diff --git a/frontend/src/component/feature/form/strategies-list.jsx b/frontend/src/component/feature/form/strategies-list.jsx index c7dcd709e8..903a1d2736 100644 --- a/frontend/src/component/feature/form/strategies-list.jsx +++ b/frontend/src/component/feature/form/strategies-list.jsx @@ -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 { + ); + } 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 = ( @@ -203,11 +220,7 @@ class StrategyConfigure extends React.Component { {name} {this.props.strategyDefinition.description} - {inputFields && ( - - {inputFields} - - )} + {strategyContent && {strategyContent}} diff --git a/frontend/src/component/feature/form/strategy-input-percentage.jsx b/frontend/src/component/feature/form/strategy-input-percentage.jsx index 4af5b432bc..4652d214a7 100644 --- a/frontend/src/component/feature/form/strategy-input-percentage.jsx +++ b/frontend/src/component/feature/form/strategy-input-percentage.jsx @@ -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 }) => (
-
- {name}: {value}% -
+ + +  {minLabel} + + + {name}: {value}% + + + {maxLabel}  + +
); InputPercentage.propTypes = { name: PropTypes.string, + minLabel: PropTypes.string, + maxLabel: PropTypes.string, value: PropTypes.number, onChange: PropTypes.func.isRequired, }; diff --git a/frontend/src/component/feature/form/strategy.scss b/frontend/src/component/feature/form/strategy.scss index 49e5e56c85..3262854405 100644 --- a/frontend/src/component/feature/form/strategy.scss +++ b/frontend/src/component/feature/form/strategy.scss @@ -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; diff --git a/frontend/src/component/menu/header.jsx b/frontend/src/component/menu/header.jsx index 1e6b6fe394..61ced8aae2 100644 --- a/frontend/src/component/menu/header.jsx +++ b/frontend/src/component/menu/header.jsx @@ -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); diff --git a/frontend/src/data/context-api.js b/frontend/src/data/context-api.js new file mode 100644 index 0000000000..5c949d43b2 --- /dev/null +++ b/frontend/src/data/context-api.js @@ -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, +}; diff --git a/frontend/src/store/context/actions.js b/frontend/src/store/context/actions.js new file mode 100644 index 0000000000..6dab9d4073 --- /dev/null +++ b/frontend/src/store/context/actions.js @@ -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)); +} diff --git a/frontend/src/store/context/index.js b/frontend/src/store/context/index.js new file mode 100644 index 0000000000..279adbf873 --- /dev/null +++ b/frontend/src/store/context/index.js @@ -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; diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index 7bb506be41..3a6abd02b0 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -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;