1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00

Fix/feedback on create (#292)

* fix: copy feature toggle instead of setting newVariants on the reference

* fix: remove console log

* fix: update messages

* fix: give feedback on strategy actions

* fix: do not allow feature toggle to be created with empty name

* fix: disable delete if only one strategy is applied

* fix: archive view

* fix: set name field on add variant required

* fix: set required on feature toggle name
This commit is contained in:
Fredrik Strand Oseberg 2021-05-10 13:22:22 +02:00 committed by GitHub
parent 06d7f9b609
commit 2f1848f6fd
22 changed files with 347 additions and 132 deletions

View File

@ -23,7 +23,6 @@ Now you should be able to review rendering information in the console. If you do
You need to first start the unleash-api on port 4242
before you can start working on unleash-frontend.
Start webpack-dev-server with hot-reload:
```bash

View File

@ -26,8 +26,6 @@ const ContextList = ({ removeContextField, history, contextFields }) => {
const [showDelDialogue, setShowDelDialogue] = useState(false);
const [name, setName] = useState();
console.log(contextFields);
const styles = useStyles();
const contextList = () =>
contextFields.map(field => (

View File

@ -36,6 +36,7 @@ const FeatureToggleList = ({
updateSetting,
featureMetrics,
toggleFeature,
archive,
loading,
}) => {
const { hasAccess } = useContext(AccessContext);
@ -93,12 +94,24 @@ const FeatureToggleList = ({
/>
))}
elseShow={
<ConditionallyRender
condition={archive}
show={
<ListItem className={styles.emptyStateListItem}>
No features available. Get started by adding a new
feature toggle.
<Link to="/features/create">Add your first toggle</Link>
No archived features.
</ListItem>
}
elseShow={
<ListItem className={styles.emptyStateListItem}>
No features available. Get started by adding a
new feature toggle.
<Link to="/features/create">
Add your first toggle
</Link>
</ListItem>
}
/>
}
/>
);
};
@ -134,7 +147,9 @@ const FeatureToggleList = ({
/>
}
/>
<ConditionallyRender
condition={!archive}
show={
<ConditionallyRender
condition={smallScreen}
show={
@ -175,6 +190,8 @@ const FeatureToggleList = ({
</Button>
}
/>
}
/>
</div>
}
/>

View File

@ -1,5 +1,8 @@
import { connect } from 'react-redux';
import { toggleFeature, fetchFeatureToggles } from '../../../store/feature-toggle/actions';
import {
toggleFeature,
fetchFeatureToggles,
} from '../../../store/feature-toggle/actions';
import { updateSettingForGroup } from '../../../store/settings/actions';
import FeatureToggleList from './FeatureToggleList';
@ -13,16 +16,21 @@ function checkConstraints(strategy, regex) {
function resolveCurrentProjectId(settings) {
if (!settings.currentProjectId || settings.currentProjectId === '*') {
return 'default';
} return settings.currentProjectId;
}
return settings.currentProjectId;
}
export const mapStateToPropsConfigurable = isFeature => state => {
const featureMetrics = state.featureMetrics.toJS();
const settings = state.settings.toJS().feature || {};
let features = isFeature ? state.features.toJS() : state.archive.get('list').toArray();
let features = isFeature
? state.features.toJS()
: state.archive.get('list').toArray();
if (settings.currentProjectId && settings.currentProjectId !== '*') {
features = features.filter(f => f.project === settings.currentProjectId);
features = features.filter(
f => f.project === settings.currentProjectId
);
}
if (settings.filter) {
@ -33,8 +41,11 @@ export const mapStateToPropsConfigurable = isFeature => state => {
feature.strategies.some(s => checkConstraints(s, regex)) ||
regex.test(feature.name) ||
regex.test(feature.description) ||
feature.strategies.some(s => s && s.name && regex.test(s.name)) ||
(settings.filter.length > 1 && regex.test(JSON.stringify(feature)))
feature.strategies.some(
s => s && s.name && regex.test(s.name)
) ||
(settings.filter.length > 1 &&
regex.test(JSON.stringify(feature)))
);
} catch (e) {
// Invalid filter regex
@ -56,9 +67,13 @@ export const mapStateToPropsConfigurable = isFeature => state => {
a.stale === b.stale ? 0 : a.stale ? -1 : 1
);
} else if (settings.sort === 'created') {
features = features.sort((a, b) => (new Date(a.createdAt) > new Date(b.createdAt) ? -1 : 1));
features = features.sort((a, b) =>
new Date(a.createdAt) > new Date(b.createdAt) ? -1 : 1
);
} else if (settings.sort === 'Last seen') {
features = features.sort((a, b) => (new Date(a.lastSeenAt) > new Date(b.lastSeenAt) ? -1 : 1));
features = features.sort((a, b) =>
new Date(a.lastSeenAt) > new Date(b.lastSeenAt) ? -1 : 1
);
} else if (settings.sort === 'name') {
features = features.sort((a, b) => {
if (a.name < b.name) {
@ -70,7 +85,9 @@ export const mapStateToPropsConfigurable = isFeature => state => {
return 0;
});
} else if (settings.sort === 'strategies') {
features = features.sort((a, b) => (a.strategies.length > b.strategies.length ? -1 : 1));
features = features.sort((a, b) =>
a.strategies.length > b.strategies.length ? -1 : 1
);
} else if (settings.sort === 'type') {
features = features.sort((a, b) => {
if (a.type < b.type) {
@ -82,7 +99,9 @@ export const mapStateToPropsConfigurable = isFeature => state => {
return 0;
});
} else if (settings.sort === 'metrics') {
const target = settings.showLastHour ? featureMetrics.lastHour : featureMetrics.lastMinute;
const target = settings.showLastHour
? featureMetrics.lastHour
: featureMetrics.lastMinute;
features = features.sort((a, b) => {
if (!target[a.name]) {
@ -102,6 +121,7 @@ export const mapStateToPropsConfigurable = isFeature => state => {
features,
currentProjectId: resolveCurrentProjectId(settings),
featureMetrics,
archive: !isFeature,
settings,
loading: state.apiCalls.fetchTogglesState.loading,
};
@ -113,6 +133,9 @@ const mapDispatchToProps = {
updateSetting: updateSettingForGroup('feature'),
};
const FeatureToggleListContainer = connect(mapStateToProps, mapDispatchToProps)(FeatureToggleList);
const FeatureToggleListContainer = connect(
mapStateToProps,
mapDispatchToProps
)(FeatureToggleList);
export default FeatureToggleListContainer;

View File

@ -45,6 +45,7 @@ const CreateFeature = ({
size="small"
variant="outlined"
label="Name"
required
placeholder="Unique-name"
className={styles.nameInput}
name="name"

View File

@ -61,7 +61,7 @@ class WrapperComponent extends Component {
this.setState({ errors });
};
onSubmit = evt => {
onSubmit = async evt => {
evt.preventDefault();
const { createFeatureToggles, history } = this.props;
const { featureToggle } = this.state;
@ -76,10 +76,17 @@ class WrapperComponent extends Component {
featureToggle.strategies = [defaultStrategy];
}
createFeatureToggles(featureToggle).then(() =>
try {
await createFeatureToggles(featureToggle).then(() =>
history.push(`/features/strategies/${featureToggle.name}`)
);
} catch (e) {
if (e.toString().includes('not allowed to be empty')) {
this.setState({
errors: { name: 'Name is not allowed to be empty' },
});
}
}
};
onCancel = evt => {

View File

@ -16,6 +16,8 @@ import { styles as commonStyles } from '../../common';
import styles from './copy-feature-component.module.scss';
import { trim } from '../../common/util';
import ConditionallyRender from '../../common/ConditionallyRender';
import { Alert } from '@material-ui/lab';
class CopyFeatureComponent extends Component {
// static displayName = `AddFeatureComponent-${getDisplayName(Component)}`;
@ -62,7 +64,7 @@ class CopyFeatureComponent extends Component {
}
};
onSubmit = evt => {
onSubmit = async evt => {
evt.preventDefault();
const { nameError, newToggleName, replaceGroupId } = this.state;
@ -82,11 +84,15 @@ class CopyFeatureComponent extends Component {
});
}
try {
this.props
.createFeatureToggle(copyToggle)
.then(() =>
history.push(`/features/strategies/${copyToggle.name}`)
);
} catch (e) {
this.setState({ apiError: e });
}
};
render() {
@ -104,7 +110,10 @@ class CopyFeatureComponent extends Component {
<div className={styles.header}>
<h1>Copy&nbsp;{copyToggle.name}</h1>
</div>
<ConditionallyRender
condition={this.state.apiError}
show={<Alert severity="error">{this.state.apiError}</Alert>}
/>
<section className={styles.content}>
<p className={styles.text}>
You are about to create a new feature toggle by cloning

View File

@ -14,6 +14,7 @@ const StrategyCard = ({
connectDragPreview,
connectDragSource,
removeStrategy,
disableDelete,
editStrategy,
connectDropTarget,
}) => {
@ -28,9 +29,13 @@ const StrategyCard = ({
connectDragSource={connectDragSource}
removeStrategy={removeStrategy}
editStrategy={editStrategy}
disableDelete={disableDelete}
/>
<CardContent>
<StrategyCardContent strategy={strategy} strategyDefinition={strategyDefinition} />
<StrategyCardContent
strategy={strategy}
strategyDefinition={strategyDefinition}
/>
</CardContent>
</Card>
</span>

View File

@ -11,12 +11,14 @@ import {
import { useStyles } from './StrategyCardHeader.styles.js';
import { ReactComponent as ReorderIcon } from '../../../../../assets/icons/reorder.svg';
import ConditionallyRender from '../../../../common/ConditionallyRender/ConditionallyRender';
const StrategyCardHeader = ({
name,
connectDragSource,
removeStrategy,
editStrategy,
disableDelete,
}) => {
const styles = useStyles();
@ -51,13 +53,40 @@ const StrategyCardHeader = ({
</Tooltip>
</span>
)}
<ConditionallyRender
condition={disableDelete}
show={
<Tooltip title="One strategy must always be applied. You can not delete this strategy.">
<span>
<IconButton
onClick={removeStrategy}
disabled={disableDelete}
>
<Icon
className={
styles.strateyCardHeaderIcon
}
>
delete
</Icon>
</IconButton>
</span>
</Tooltip>
}
elseShow={
<Tooltip title="Delete strategy">
<IconButton onClick={removeStrategy}>
<Icon className={styles.strateyCardHeaderIcon}>
<Icon
className={
styles.strateyCardHeaderIcon
}
>
delete
</Icon>
</IconButton>
</Tooltip>
}
/>
</div>
</>
}

View File

@ -124,6 +124,8 @@ const StrategiesList = props => {
return strategies.find(s => s.name === strategyName);
};
const disableDelete = editableStrategies.length === 1;
const cards = editableStrategies.map((strategy, i) => (
<StrategyCard
key={i}
@ -133,6 +135,7 @@ const StrategiesList = props => {
moveStrategy={moveStrategy}
editStrategy={() => setEditStrategyIndex(i)}
index={i}
disableDelete={disableDelete}
movable
/>
));

View File

@ -86,10 +86,10 @@ const AddVariant = ({
};
const submit = async e => {
setError({});
e.preventDefault();
const validationError = validateName(data.name);
if (validationError) {
setError(validationError);
return;
@ -112,9 +112,13 @@ const AddVariant = ({
clear();
closeDialog();
} catch (error) {
if (error.message.includes('duplicate value')) {
setError({ name: 'A variant with that name already exists.' });
} else {
const msg = error.message || 'Could not add variant';
setError({ general: msg });
}
}
};
const onPayload = e => {
@ -188,9 +192,11 @@ const AddVariant = ({
name="name"
placeholder=""
className={commonStyles.fullWidth}
helperText={error.name}
value={data.name || ''}
error={error.name}
error={Boolean(error.name)}
variant="outlined"
required
size="small"
type="name"
onChange={setVariantValue}
@ -214,7 +220,7 @@ const AddVariant = ({
}}
style={{ marginRight: '0.8rem' }}
value={data.weight || ''}
error={error.weight}
error={Boolean(error.weight)}
type="number"
disabled={!isFixWeight}
onChange={setVariantValue}

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
@ -63,12 +63,16 @@ class UpdateVariantComponent extends Component {
onRemoveVariant = (e, index) => {
e.preventDefault();
try {
this.props.removeVariant(index);
} catch (e) {
console.log('An exception was caught.');
}
};
renderVariant = (variant, index) => (
<VariantViewComponent
key={index}
key={variant.name}
variant={variant}
editVariant={e => this.openEditVariant(e, index, variant)}
removeVariant={e => this.onRemoveVariant(e, index)}

View File

@ -6,7 +6,10 @@ 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)],
stickinessOptions: [
'default',
...state.context.filter(c => c.stickiness).map(c => c.name),
],
});
const mapDispatchToProps = (dispatch, ownProps) => ({
@ -15,11 +18,15 @@ const mapDispatchToProps = (dispatch, ownProps) => ({
const currentVariants = featureToggle.variants || [];
const variants = [...currentVariants, variant];
updateWeight(variants, 1000);
return requestUpdateFeatureToggleVariants(featureToggle, variants)(dispatch);
return requestUpdateFeatureToggleVariants(
featureToggle,
variants
)(dispatch);
},
removeVariant: index => {
const { featureToggle } = ownProps;
const currentVariants = featureToggle.variants || [];
const variants = currentVariants.filter((v, i) => i !== index);
if (variants.length > 0) {
updateWeight(variants, 1000);
@ -29,7 +36,9 @@ const mapDispatchToProps = (dispatch, ownProps) => ({
updateVariant: (index, variant) => {
const { featureToggle } = ownProps;
const currentVariants = featureToggle.variants || [];
const variants = currentVariants.map((v, i) => (i === index ? variant : v));
const variants = currentVariants.map((v, i) =>
i === index ? variant : v
);
updateWeight(variants, 1000);
requestUpdateFeatureToggleVariants(featureToggle, variants)(dispatch);
},
@ -41,4 +50,7 @@ const mapDispatchToProps = (dispatch, ownProps) => ({
},
});
export default connect(mapStateToProps, mapDispatchToProps)(UpdateFeatureToggleComponent);
export default connect(
mapStateToProps,
mapDispatchToProps
)(UpdateFeatureToggleComponent);

View File

@ -88,10 +88,25 @@ const CreateStrategy = ({
Add parameter
</Button>
<ConditionallyRender
condition={editMode}
show={
<Button
type="submit"
variant="contained"
color="primary"
style={{ display: 'block' }}
>
Update
</Button>
}
elseShow={
<FormButtons
submitText={editMode ? 'Update' : 'Create'}
submitText={'Create'}
onCancel={onCancel}
/>
}
/>
</form>
</PageContent>
);

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect } from 'react';
import { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Link, useHistory } from 'react-router-dom';
@ -25,6 +25,7 @@ import HeaderTitle from '../../common/HeaderTitle';
import { useStyles } from './styles';
import AccessContext from '../../../contexts/AccessContext';
import Dialogue from '../../common/Dialogue';
const StrategiesList = ({
strategies,
@ -37,6 +38,7 @@ const StrategiesList = ({
const styles = useStyles();
const smallScreen = useMediaQuery('(max-width:700px)');
const { hasAccess } = useContext(AccessContext);
const [dialogueMetaData, setDialogueMetaData] = useState({ show: false });
useEffect(() => {
fetchStrategies();
@ -86,7 +88,15 @@ const StrategiesList = ({
const reactivateButton = strategy => (
<Tooltip title="Reactivate activation strategy">
<IconButton onClick={() => reactivateStrategy(strategy)}>
<IconButton
onClick={() =>
setDialogueMetaData({
show: true,
title: 'Really reactivate strategy?',
onConfirm: () => reactivateStrategy(strategy),
})
}
>
<Icon>visibility</Icon>
</IconButton>
</Tooltip>
@ -107,7 +117,16 @@ const StrategiesList = ({
elseShow={
<Tooltip title="Deprecate activation strategy">
<div>
<IconButton onClick={() => deprecateStrategy(strategy)}>
<IconButton
onClick={() =>
setDialogueMetaData({
show: true,
title: 'Really deprecate strategy?',
onConfirm: () =>
deprecateStrategy(strategy),
})
}
>
<Icon>visibility_off</Icon>
</IconButton>
</div>
@ -121,7 +140,15 @@ const StrategiesList = ({
condition={strategy.editable}
show={
<Tooltip title="Delete strategy">
<IconButton onClick={() => removeStrategy(strategy)}>
<IconButton
onClick={() =>
setDialogueMetaData({
show: true,
title: 'Really delete strategy?',
onConfirm: () => removeStrategy(strategy),
})
}
>
<Icon>delete</Icon>
</IconButton>
</Tooltip>
@ -167,12 +194,25 @@ const StrategiesList = ({
</ListItem>
));
const onDialogConfirm = () => {
dialogueMetaData?.onConfirm();
setDialogueMetaData(prev => ({ ...prev, show: false }));
};
return (
<PageContent
headerContent={
<HeaderTitle title="Strategies" actions={headerButton()} />
}
>
<Dialogue
open={dialogueMetaData.show}
onClick={onDialogConfirm}
title={dialogueMetaData?.title}
onClose={() =>
setDialogueMetaData(prev => ({ ...prev, show: false }))
}
/>
<List>
<ConditionallyRender
condition={strategies.length > 0}

View File

@ -17,26 +17,20 @@ const mapStateToProps = state => {
const mapDispatchToProps = dispatch => ({
removeStrategy: strategy => {
// eslint-disable-next-line no-alert
if (window.confirm('Are you sure you want to remove this strategy?')) {
removeStrategy(strategy)(dispatch);
}
},
deprecateStrategy: strategy => {
// eslint-disable-next-line no-alert
if (window.confirm('Are you sure you want to deprecate this strategy?')) {
deprecateStrategy(strategy)(dispatch);
}
},
reactivateStrategy: strategy => {
// eslint-disable-next-line no-alert
if (window.confirm('Are you sure you want to reactivate this strategy?')) {
reactivateStrategy(strategy)(dispatch);
}
},
fetchStrategies: () => fetchStrategies()(dispatch),
});
const StrategiesListContainer = connect(mapStateToProps, mapDispatchToProps)(StrategiesList);
const StrategiesListContainer = connect(
mapStateToProps,
mapDispatchToProps
)(StrategiesList);
export default StrategiesListContainer;

View File

@ -14,9 +14,7 @@ class AuthenticationCustomComponent extends React.Component {
<p>{authDetails.message}</p>
<CardActions style={{ textAlign: 'center' }}>
<a href={authDetails.path}>
<Button raised colored>
Sign In
</Button>
<Button>Sign In</Button>
</a>
</CardActions>
</div>

View File

@ -1,7 +1,9 @@
const defaultErrorMessage = 'Unexpected exception when talking to unleash-api';
function extractJoiMsg(body) {
return body.details.length > 0 ? body.details[0].message : defaultErrorMessage;
return body.details.length > 0
? body.details[0].message
: defaultErrorMessage;
}
function extractLegacyMsg(body) {
return body && body.length > 0 ? body[0].msg : defaultErrorMessage;
@ -35,7 +37,9 @@ export class ForbiddenError extends Error {
export class NotFoundError extends Error {
constructor(statusCode) {
super('The requested resource could not be found but may be available in the future');
super(
'The requested resource could not be found but may be available in the future'
);
this.name = 'NotFoundError';
this.statusCode = statusCode;
}
@ -45,11 +49,19 @@ export function throwIfNotSuccess(response) {
if (!response.ok) {
if (response.status === 401) {
return new Promise((resolve, reject) => {
response.json().then(body => reject(new AuthenticationError(response.status, body)));
response
.json()
.then(body =>
reject(new AuthenticationError(response.status, body))
);
});
} else if (response.status === 403) {
return new Promise((resolve, reject) => {
response.json().then(body => reject(new ForbiddenError(response.status, body)));
response
.json()
.then(body =>
reject(new ForbiddenError(response.status, body))
);
});
} else if (response.status === 404) {
return new Promise((resolve, reject) => {
@ -57,12 +69,18 @@ export function throwIfNotSuccess(response) {
});
} else if (response.status > 399 && response.status < 501) {
return new Promise((resolve, reject) => {
response.json().then(body => {
const errorMsg = body && body.isJoi ? extractJoiMsg(body) : extractLegacyMsg(body);
response
.json()
.then(body => {
const errorMsg =
body && body.isJoi
? extractJoiMsg(body)
: extractLegacyMsg(body);
let error = new Error(errorMsg);
error.statusCode = response.status;
reject(error);
}).catch(() => reject(new Error(defaultErrorMessage)))
})
.catch(() => reject(new Error(defaultErrorMessage)));
});
} else {
return Promise.reject(new ServiceError(response.status));

View File

@ -13,6 +13,7 @@ import {
ERROR_UPDATING_STRATEGY,
ERROR_CREATING_STRATEGY,
ERROR_RECEIVE_STRATEGIES,
UPDATE_STRATEGY_SUCCESS,
} from '../strategy/actions';
import {
@ -79,9 +80,13 @@ const strategies = (state = getInitState(), action) => {
return state.update('list', list =>
list.remove(list.indexOf(action.error))
);
// This reducer controls not only errors, but general information and success
// messages. This can be a little misleading, given it's naming. We should
// revise how this works in a future update.
case UPDATE_FEATURE_TOGGLE:
case UPDATE_FEATURE_TOGGLE_STRATEGIES:
case UPDATE_APPLICATION_FIELD:
case UPDATE_STRATEGY_SUCCESS:
return addErrorIfNotAlreadyInList(state, action.info);
default:
return state;

View File

@ -110,7 +110,10 @@ export function createFeatureToggles(featureToggle) {
featureToggle: createdFeature,
});
})
.catch(dispatchError(dispatch, ERROR_CREATING_FEATURE_TOGGLE));
.catch(e => {
dispatchError(dispatch, ERROR_CREATING_FEATURE_TOGGLE);
throw e;
});
};
}
@ -189,24 +192,28 @@ export function requestUpdateFeatureToggleStrategies(
export function requestUpdateFeatureToggleVariants(featureToggle, newVariants) {
return dispatch => {
featureToggle.variants = newVariants;
const newFeature = { ...featureToggle };
newFeature.variants = newVariants;
dispatch({ type: START_UPDATE_FEATURE_TOGGLE });
return api
.update(featureToggle)
.update(newFeature)
.then(() => {
const info = `${featureToggle.name} successfully updated!`;
const info = `${newFeature.name} successfully updated!`;
setTimeout(
() => dispatch({ type: MUTE_ERROR, error: info }),
1000
);
return dispatch({
type: UPDATE_FEATURE_TOGGLE_STRATEGIES,
featureToggle,
featureToggle: newFeature,
info,
});
})
.catch(dispatchError(dispatch, ERROR_UPDATE_FEATURE_TOGGLE));
.catch(e => {
dispatchError(dispatch, ERROR_UPDATE_FEATURE_TOGGLE);
throw e;
});
};
}

View File

@ -50,14 +50,14 @@ function validate(featureToggle) {
function update(featureToggle) {
return validateToggle(featureToggle)
.then(() =>
fetch(`${URI}/${featureToggle.name}`, {
.then(() => {
return fetch(`${URI}/${featureToggle.name}`, {
method: 'PUT',
headers,
credentials: 'include',
body: JSON.stringify(featureToggle),
});
})
)
.then(throwIfNotSuccess);
}

View File

@ -1,6 +1,7 @@
import api from './api';
import applicationApi from '../application/api';
import { dispatchError } from '../util';
import { MUTE_ERROR } from '../error/actions';
export const ADD_STRATEGY = 'ADD_STRATEGY';
export const UPDATE_STRATEGY = 'UPDATE_STRATEGY';
@ -19,6 +20,7 @@ export const ERROR_UPDATING_STRATEGY = 'ERROR_UPDATING_STRATEGY';
export const ERROR_REMOVING_STRATEGY = 'ERROR_REMOVING_STRATEGY';
export const ERROR_DEPRECATING_STRATEGY = 'ERROR_DEPRECATING_STRATEGY';
export const ERROR_REACTIVATING_STRATEGY = 'ERROR_REACTIVATING_STRATEGY';
export const UPDATE_STRATEGY_SUCCESS = 'UPDATE_STRATEGY_SUCCESS';
export const receiveStrategies = json => ({
type: RECEIVE_STRATEGIES,
@ -46,6 +48,14 @@ const reactivateStrategyEvent = strategy => ({
strategy,
});
const setInfoMessage = (info, dispatch) => {
dispatch({
type: UPDATE_STRATEGY_SUCCESS,
info: info,
});
setTimeout(() => dispatch({ type: MUTE_ERROR, error: info }), 1500);
};
export function fetchStrategies() {
return dispatch => {
dispatch(startRequest());
@ -64,6 +74,9 @@ export function createStrategy(strategy) {
return api
.create(strategy)
.then(() => dispatch(addStrategy(strategy)))
.then(() => {
setInfoMessage('Strategy successfully created.', dispatch);
})
.catch(e => {
dispatchError(dispatch, ERROR_CREATING_STRATEGY);
throw e;
@ -78,6 +91,9 @@ export function updateStrategy(strategy) {
return api
.update(strategy)
.then(() => dispatch(updatedStrategy(strategy)))
.then(() => {
setInfoMessage('Strategy successfully updated.', dispatch);
})
.catch(dispatchError(dispatch, ERROR_UPDATING_STRATEGY));
};
}
@ -87,6 +103,9 @@ export function removeStrategy(strategy) {
api
.remove(strategy)
.then(() => dispatch(createRemoveStrategy(strategy)))
.then(() => {
setInfoMessage('Strategy successfully deleted.', dispatch);
})
.catch(dispatchError(dispatch, ERROR_REMOVING_STRATEGY));
}
@ -99,6 +118,9 @@ export function deprecateStrategy(strategy) {
dispatch(startDeprecate());
api.deprecate(strategy)
.then(() => dispatch(deprecateStrategyEvent(strategy)))
.then(() =>
setInfoMessage('Strategy successfully deprecated', dispatch)
)
.catch(dispatchError(dispatch, ERROR_DEPRECATING_STRATEGY));
};
}
@ -108,6 +130,9 @@ export function reactivateStrategy(strategy) {
dispatch(startReactivate());
api.reactivate(strategy)
.then(() => dispatch(reactivateStrategyEvent(strategy)))
.then(() =>
setInfoMessage('Strategy successfully reactivated', dispatch)
)
.catch(dispatchError(dispatch, ERROR_REACTIVATING_STRATEGY));
};
}