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

Refactor/strategies (#668)

* feat: add useStrategiesApi hook

* refactor: remove redux from strategies component

* refactor: CreateStrategy Component

* fix: remove ts errors

* refactor: change strategy-detail to functional component

* refactor: get strategy name from params

* refactor: use features hook and refactor toggle list link

* refactor: StrategiesList

* refactor: fix delete strategy function

* fix: ts errors

* refactor: CreateStrategy to StrategyForm

* feat: add toast for StrategyForm

* refactor: add StrategyView and delete old component

* refactor: StrategyDetails and clean unused files

* fix: cleanup unused code

* fix: add await

* fix: remove unused stores

Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
This commit is contained in:
Youssef Khedher 2022-02-11 00:08:55 +01:00 committed by GitHub
parent de8b3352e7
commit c2842c81e6
35 changed files with 654 additions and 1395 deletions

View File

@ -1,8 +1,8 @@
import CopyFeatureToggle from '../../page/features/copy';
import { FeatureToggleListContainer } from '../feature/FeatureToggleList/FeatureToggleListContainer';
import CreateStrategies from '../../page/strategies/create';
import StrategyView from '../../page/strategies/show';
import Strategies from '../../page/strategies';
import { StrategyForm } from '../strategies/StrategyForm/StrategyForm';
import { StrategyView } from '../../component/strategies/StrategyView/StrategyView';
import { StrategiesList } from '../strategies/StrategiesList/StrategiesList';
import HistoryPage from '../../page/history';
import HistoryTogglePage from '../../page/history/toggle';
import { ArchiveListContainer } from '../archive/ArchiveListContainer';
@ -243,7 +243,7 @@ export const routes = [
path: '/strategies/create',
title: 'Create',
parent: '/strategies',
component: CreateStrategies,
component: StrategyForm,
type: 'protected',
layout: 'main',
menu: {},
@ -260,7 +260,7 @@ export const routes = [
{
path: '/strategies',
title: 'Strategies',
component: Strategies,
component: StrategiesList,
type: 'protected',
layout: 'main',
menu: { mobile: true, advanced: true },

View File

@ -1,129 +0,0 @@
import PropTypes from 'prop-types';
import { Typography, TextField, Button } from '@material-ui/core';
import { Add } from '@material-ui/icons';
import PageContent from '../../common/PageContent/PageContent';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
import { styles as commonStyles, FormButtons } from '../../common';
import { trim } from '../../common/util';
import StrategyParameters from './StrategyParameters/StrategyParameters';
const CreateStrategy = ({
input,
setValue,
appParameter,
onCancel,
editMode = false,
errors,
onSubmit,
clearErrors,
updateParameter,
}) => {
const getHeaderTitle = () => {
if (editMode) return 'Edit strategy';
return 'Create a new strategy';
};
return (
<PageContent headerContent={getHeaderTitle()}>
<ConditionallyRender
condition={editMode}
show={
<Typography variant="body1">
Be careful! Changing a strategy definition might also
require changes to the implementation in the clients.
</Typography>
}
/>
<form
onSubmit={onSubmit}
className={commonStyles.contentSpacing}
style={{ maxWidth: '400px' }}
>
<TextField
label="Strategy name"
name="name"
placeholder=""
disabled={editMode}
error={Boolean(errors.name)}
helperText={errors.name}
onChange={({ target }) => {
clearErrors();
setValue('name', trim(target.value));
}}
value={input.name}
variant="outlined"
size="small"
/>
<TextField
className={commonStyles.fullwidth}
multiline
rows={2}
label="Description"
name="description"
placeholder=""
onChange={({ target }) =>
setValue('description', target.value)
}
value={input.description}
variant="outlined"
size="small"
/>
<StrategyParameters
input={input.parameters}
count={input.parameters.length}
updateParameter={updateParameter}
/>
<Button
onClick={e => {
e.preventDefault();
appParameter();
}}
startIcon={<Add />}
>
Add parameter
</Button>
<ConditionallyRender
condition={editMode}
show={
<Button
type="submit"
variant="contained"
color="primary"
style={{ display: 'block' }}
>
Update
</Button>
}
elseShow={
<FormButtons
submitText={'Create'}
onCancel={onCancel}
/>
}
/>
</form>
</PageContent>
);
};
CreateStrategy.propTypes = {
input: PropTypes.object,
setValue: PropTypes.func,
appParameter: PropTypes.func,
updateParameter: PropTypes.func,
clear: PropTypes.func,
onCancel: PropTypes.func,
onSubmit: PropTypes.func,
errors: PropTypes.object,
editMode: PropTypes.bool,
clearErrors: PropTypes.func,
};
export default CreateStrategy;

View File

@ -1,154 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {
createStrategy,
updateStrategy,
} from '../../../store/strategy/actions';
import CreateStrategy from './CreateStrategy';
import { loadNameFromUrl } from '../../common/util';
const STRATEGY_EXIST_ERROR = 'Error: Strategy with name';
class WrapperComponent extends Component {
constructor(props) {
super(props);
this.state = {
strategy: this.props.strategy,
errors: {},
dirty: false,
};
}
clearErrors = () => {
this.setState({ errors: {} });
};
appParameter = () => {
const { strategy } = this.state;
strategy.parameters = [...strategy.parameters, {}];
this.setState({ strategy, dirty: true });
};
updateParameter = (index, updated) => {
const { strategy } = this.state;
// 1. Make a shallow copy of the items
let parameters = [...strategy.parameters];
// 2. Make a shallow copy of the item you want to mutate
let item = { ...parameters[index] };
// 3. Replace the property you're intested in
// 4. Put it back into our array. N.B. we *are* mutating the array here, but that's why we made a copy first
parameters[index] = Object.assign({}, item, updated);
// 5. Set the state to our new copy
strategy.parameters = parameters;
this.setState({ strategy });
};
setValue = (field, value) => {
const { strategy } = this.state;
strategy[field] = value;
this.setState({ strategy, dirty: true });
};
onSubmit = async evt => {
evt.preventDefault();
const { createStrategy, updateStrategy, history, editMode } =
this.props;
const { strategy } = this.state;
const parameters = (strategy.parameters || [])
.filter(({ name }) => !!name)
.map(
({
name,
type = 'string',
description = '',
required = false,
}) => ({
name,
type,
description,
required,
})
);
strategy.parameters = parameters;
if (editMode) {
await updateStrategy(strategy);
history.push(`/strategies/view/${strategy.name}`);
} else {
try {
await createStrategy(strategy);
history.push(`/strategies`);
} catch (e) {
if (e.toString().includes(STRATEGY_EXIST_ERROR)) {
this.setState({
errors: {
name: 'A strategy with this name already exists ',
},
});
}
}
}
};
onCancel = evt => {
evt.preventDefault();
const { history, editMode } = this.props;
const { strategy } = this.state;
if (editMode) {
history.push(`/strategies/view/${strategy.name}`);
} else {
history.push('/strategies');
}
};
render() {
return (
<CreateStrategy
onSubmit={this.onSubmit}
onCancel={this.onCancel}
setValue={this.setValue}
updateParameter={this.updateParameter}
appParameter={this.appParameter}
input={this.state.strategy}
errors={this.state.errors}
editMode={this.props.editMode}
clearErrors={this.clearErrors}
/>
);
}
}
WrapperComponent.propTypes = {
history: PropTypes.object.isRequired,
createStrategy: PropTypes.func.isRequired,
updateStrategy: PropTypes.func.isRequired,
strategy: PropTypes.object,
editMode: PropTypes.bool,
};
const mapDispatchToProps = { createStrategy, updateStrategy };
const mapStateToProps = (state, props) => {
const { strategy, editMode } = props;
return {
strategy: strategy
? strategy
: { name: loadNameFromUrl(), description: '', parameters: [] },
editMode,
};
};
const FormAddContainer = connect(
mapStateToProps,
mapDispatchToProps
)(WrapperComponent);
export default FormAddContainer;

View File

@ -1,9 +1,7 @@
import { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useContext, useState } from 'react';
import classnames from 'classnames';
import { Link, useHistory } from 'react-router-dom';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import {
IconButton,
List,
@ -19,42 +17,44 @@ import {
Visibility,
VisibilityOff,
} from '@material-ui/icons';
import {
CREATE_STRATEGY,
DELETE_STRATEGY,
UPDATE_STRATEGY,
} from '../../providers/AccessProvider/permissions';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
import PageContent from '../../common/PageContent/PageContent';
import HeaderTitle from '../../common/HeaderTitle';
import { useStyles } from './styles';
import { useStyles } from './StrategiesList.styles';
import AccessContext from '../../../contexts/AccessContext';
import Dialogue from '../../common/Dialogue';
import { ADD_NEW_STRATEGY_ID } from '../../../testIds';
import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton';
import PermissionButton from '../../common/PermissionButton/PermissionButton';
import { getHumanReadableStrategyName } from '../../../utils/strategy-names';
import useStrategies from '../../../hooks/api/getters/useStrategies/useStrategies';
import useStrategiesApi from '../../../hooks/api/actions/useStrategiesApi/useStrategiesApi';
import useToast from '../../../hooks/useToast';
import { IStrategy } from '../../../interfaces/strategy';
const StrategiesList = ({
strategies,
fetchStrategies,
removeStrategy,
deprecateStrategy,
reactivateStrategy,
}) => {
interface IDialogueMetaData {
show: boolean;
title: string;
onConfirm: () => void;
}
export const StrategiesList = () => {
const history = useHistory();
const styles = useStyles();
const smallScreen = useMediaQuery('(max-width:700px)');
const { hasAccess } = useContext(AccessContext);
const [dialogueMetaData, setDialogueMetaData] = useState({ show: false });
useEffect(() => {
fetchStrategies();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [dialogueMetaData, setDialogueMetaData] = useState<IDialogueMetaData>(
{ show: false, title: '', onConfirm: () => {} }
);
const { strategies, refetchStrategies } = useStrategies();
const { removeStrategy, deprecateStrategy, reactivateStrategy } =
useStrategiesApi();
const { setToastData, setToastApiError } = useToast();
const headerButton = () => (
<ConditionallyRender
@ -77,7 +77,6 @@ const StrategiesList = ({
onClick={() => history.push('/strategies/create')}
color="primary"
permission={CREATE_STRATEGY}
variant="contained"
data-test={ADD_NEW_STRATEGY_ID}
tooltip={'Add new strategy'}
>
@ -99,16 +98,70 @@ const StrategiesList = ({
</Link>
);
const reactivateButton = strategy => (
const onReactivateStrategy = (strategy: IStrategy) => {
setDialogueMetaData({
show: true,
title: 'Really reactivate strategy?',
onConfirm: async () => {
try {
await reactivateStrategy(strategy);
refetchStrategies();
setToastData({
type: 'success',
title: 'Success',
text: 'Strategy reactivated successfully',
});
} catch (e: any) {
setToastApiError(e.toString());
}
},
});
};
const onDeprecateStrategy = (strategy: IStrategy) => {
setDialogueMetaData({
show: true,
title: 'Really deprecate strategy?',
onConfirm: async () => {
try {
await deprecateStrategy(strategy);
refetchStrategies();
setToastData({
type: 'success',
title: 'Success',
text: 'Strategy deprecated successfully',
});
} catch (e: any) {
setToastApiError(e.toString());
}
},
});
};
const onDeleteStrategy = (strategy: IStrategy) => {
setDialogueMetaData({
show: true,
title: 'Really delete strategy?',
onConfirm: async () => {
try {
await removeStrategy(strategy);
refetchStrategies();
setToastData({
type: 'success',
title: 'Success',
text: 'Strategy deleted successfully',
});
} catch (e: any) {
setToastApiError(e.toString());
}
},
});
};
const reactivateButton = (strategy: IStrategy) => (
<Tooltip title="Reactivate activation strategy">
<PermissionIconButton
onClick={() =>
setDialogueMetaData({
show: true,
title: 'Really reactivate strategy?',
onConfirm: () => reactivateStrategy(strategy),
})
}
onClick={() => onReactivateStrategy(strategy)}
permission={UPDATE_STRATEGY}
tooltip={'Reactivate activation strategy'}
>
@ -117,7 +170,7 @@ const StrategiesList = ({
</Tooltip>
);
const deprecateButton = strategy => (
const deprecateButton = (strategy: IStrategy) => (
<ConditionallyRender
condition={strategy.name === 'default'}
show={
@ -132,13 +185,7 @@ const StrategiesList = ({
elseShow={
<div>
<PermissionIconButton
onClick={() =>
setDialogueMetaData({
show: true,
title: 'Really deprecate strategy?',
onConfirm: () => deprecateStrategy(strategy),
})
}
onClick={() => onDeprecateStrategy(strategy)}
permission={UPDATE_STRATEGY}
tooltip={'Deprecate activation strategy'}
>
@ -149,18 +196,12 @@ const StrategiesList = ({
/>
);
const deleteButton = strategy => (
const deleteButton = (strategy: IStrategy) => (
<ConditionallyRender
condition={strategy.editable}
show={
<PermissionIconButton
onClick={() =>
setDialogueMetaData({
show: true,
title: 'Really delete strategy?',
onConfirm: () => removeStrategy(strategy),
})
}
onClick={() => onDeleteStrategy(strategy)}
permission={DELETE_STRATEGY}
tooltip={'Delete strategy'}
>
@ -230,23 +271,10 @@ const StrategiesList = ({
<List>
<ConditionallyRender
condition={strategies.length > 0}
show={strategyList()}
show={<>{strategyList()}</>}
elseShow={<ListItem>No strategies found</ListItem>}
/>
</List>
</PageContent>
);
};
StrategiesList.propTypes = {
strategies: PropTypes.array.isRequired,
fetchStrategies: PropTypes.func.isRequired,
removeStrategy: PropTypes.func.isRequired,
deprecateStrategy: PropTypes.func.isRequired,
reactivateStrategy: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
name: PropTypes.string,
deprecated: PropTypes.bool,
};
export default StrategiesList;

View File

@ -1,36 +0,0 @@
import { connect } from 'react-redux';
import StrategiesList from './StrategiesList.jsx';
import {
fetchStrategies,
removeStrategy,
deprecateStrategy,
reactivateStrategy,
} from '../../../store/strategy/actions';
const mapStateToProps = state => {
const list = state.strategies.get('list').toArray();
return {
strategies: list,
};
};
const mapDispatchToProps = dispatch => ({
removeStrategy: strategy => {
removeStrategy(strategy)(dispatch);
},
deprecateStrategy: strategy => {
deprecateStrategy(strategy)(dispatch);
},
reactivateStrategy: strategy => {
reactivateStrategy(strategy)(dispatch);
},
fetchStrategies: () => fetchStrategies()(dispatch),
});
const StrategiesListContainer = connect(
mapStateToProps,
mapDispatchToProps
)(StrategiesList);
export default StrategiesListContainer;

View File

@ -0,0 +1,198 @@
import React, { useState } from 'react';
import { Typography, TextField, Button } from '@material-ui/core';
import { Add } from '@material-ui/icons';
import PageContent from '../../common/PageContent/PageContent';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
import { styles as commonStyles, FormButtons } from '../../common';
import { trim } from '../../common/util';
import StrategyParameters from './StrategyParameters/StrategyParameters';
import { useHistory } from 'react-router-dom';
import useStrategiesApi from '../../../hooks/api/actions/useStrategiesApi/useStrategiesApi';
import { IStrategy } from '../../../interfaces/strategy';
import useToast from '../../../hooks/useToast';
import useStrategies from '../../../hooks/api/getters/useStrategies/useStrategies';
interface ICustomStrategyParams {
name?: string;
type?: string;
description?: string;
required?: boolean;
}
interface ICustomStrategyErrors {
name?: string;
}
interface IStrategyFormProps {
editMode: boolean;
strategy: IStrategy;
}
export const StrategyForm = ({ editMode, strategy }: IStrategyFormProps) => {
const history = useHistory();
const [name, setName] = useState(strategy?.name || '');
const [description, setDescription] = useState(strategy?.description || '');
const [params, setParams] = useState<ICustomStrategyParams[]>(
strategy?.parameters || []
);
const [errors, setErrors] = useState<ICustomStrategyErrors>({});
const { createStrategy, updateStrategy } = useStrategiesApi();
const { refetchStrategies } = useStrategies();
const { setToastData, setToastApiError } = useToast();
const clearErrors = () => {
setErrors({});
};
const getHeaderTitle = () => {
if (editMode) return 'Edit strategy';
return 'Create a new strategy';
};
const appParameter = () => {
setParams(prev => [...prev, {}]);
};
const updateParameter = (index: number, updated: object) => {
let item = { ...params[index] };
params[index] = Object.assign({}, item, updated);
setParams(prev => [...prev]);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const parameters = (params || [])
.filter(({ name }) => !!name)
.map(
({
name,
type = 'string',
description = '',
required = false,
}) => ({
name,
type,
description,
required,
})
);
setParams(prev => [...parameters]);
if (editMode) {
try {
await updateStrategy({ name, description, parameters: params });
history.push(`/strategies/view/${name}`);
setToastData({
type: 'success',
title: 'Success',
text: 'Successfully updated strategy',
});
refetchStrategies();
} catch (e: any) {
setToastApiError(e.toString());
}
} else {
try {
await createStrategy({ name, description, parameters: params });
history.push(`/strategies`);
setToastData({
type: 'success',
title: 'Success',
text: 'Successfully created new strategy',
});
refetchStrategies();
} catch (e: any) {
const STRATEGY_EXIST_ERROR = 'Error: Strategy with name';
if (e.toString().includes(STRATEGY_EXIST_ERROR)) {
setErrors({
name: 'A strategy with this name already exists',
});
}
}
}
};
const handleCancel = () => history.goBack();
return (
<PageContent headerContent={getHeaderTitle()}>
<ConditionallyRender
condition={editMode}
show={
<Typography variant="body1">
Be careful! Changing a strategy definition might also
require changes to the implementation in the clients.
</Typography>
}
/>
<form
onSubmit={handleSubmit}
className={commonStyles.contentSpacing}
style={{ maxWidth: '400px' }}
>
<TextField
label="Strategy name"
name="name"
placeholder=""
disabled={editMode}
error={Boolean(errors.name)}
helperText={errors.name}
onChange={e => {
clearErrors();
setName(trim(e.target.value));
}}
value={name}
variant="outlined"
size="small"
/>
<TextField
className={commonStyles.fullwidth}
multiline
rows={2}
label="Description"
name="description"
placeholder=""
onChange={e => setDescription(e.target.value)}
value={description}
variant="outlined"
size="small"
/>
<StrategyParameters
input={params}
count={params.length}
updateParameter={updateParameter}
/>
<Button
onClick={e => {
e.preventDefault();
appParameter();
}}
startIcon={<Add />}
>
Add parameter
</Button>
<ConditionallyRender
condition={editMode}
show={
<Button
type="submit"
variant="contained"
color="primary"
style={{ display: 'block' }}
>
Update
</Button>
}
elseShow={
<FormButtons
submitText={'Create'}
onCancel={handleCancel}
/>
}
/>
</form>
</PageContent>
);
};

View File

@ -10,7 +10,7 @@ const StrategyParameters = ({ input = [], count = 0, updateParameter }) => (
{gerArrayWithEntries(count).map((v, i) => (
<StrategyParameter
key={i}
set={v => updateParameter(i, v, true)}
set={v => updateParameter(i, v)}
index={i}
input={input[i]}
/>

View File

@ -0,0 +1,97 @@
import {
Grid,
List,
ListItem,
ListItemAvatar,
ListItemText,
Tooltip,
} from '@material-ui/core';
import { Add, RadioButtonChecked } from '@material-ui/icons';
import { AppsLinkList } from '../../../common';
import ConditionallyRender from '../../../common/ConditionallyRender';
import styles from '../../strategies.module.scss';
import { TogglesLinkList } from '../../TogglesLinkList/TogglesLinkList';
import { IParameter, IStrategy } from '../../../../interfaces/strategy';
import { IApplication } from '../../../../interfaces/application';
import { IFeatureToggle } from '../../../../interfaces/featureToggle';
interface IStrategyDetailsProps {
strategy: IStrategy;
applications: IApplication[];
toggles: IFeatureToggle[];
}
export const StrategyDetails = ({
strategy,
applications,
toggles,
}: IStrategyDetailsProps) => {
const { parameters = [] } = strategy;
const renderParameters = (params: IParameter[]) => {
if (params) {
return params.map(({ name, type, description, required }, i) => (
<ListItem key={`${name}-${i}`}>
<ConditionallyRender
condition={required}
show={
<Tooltip title="Required">
<ListItemAvatar>
<Add />
</ListItemAvatar>
</Tooltip>
}
elseShow={
<Tooltip title="Optional">
<ListItemAvatar>
<RadioButtonChecked />
</ListItemAvatar>
</Tooltip>
}
/>
<ListItemText
primary={
<div>
{name} <small>({type})</small>
</div>
}
secondary={description}
/>
</ListItem>
));
} else {
return <ListItem>(no params)</ListItem>;
}
};
return (
<div className={styles.listcontainer}>
<Grid container>
<ConditionallyRender
condition={strategy.deprecated}
show={
<Grid item>
<h5 style={{ color: '#ff0000' }}>Deprecated</h5>
</Grid>
}
/>
<Grid item sm={12} md={12}>
<h6>Parameters</h6>
<hr />
<List>{renderParameters(parameters)}</List>
</Grid>
<Grid item sm={12} md={6}>
<h6>Applications using this strategy</h6>
<hr />
<AppsLinkList apps={applications} />
</Grid>
<Grid item sm={12} md={6}>
<h6>Toggles using this strategy</h6>
<hr />
<TogglesLinkList toggles={toggles} />
</Grid>
</Grid>
</div>
);
};

View File

@ -0,0 +1,78 @@
import { useContext } from 'react';
import { Grid, Typography } from '@material-ui/core';
import { StrategyForm } from '../StrategyForm/StrategyForm';
import { UPDATE_STRATEGY } from '../../providers/AccessProvider/permissions';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
import TabNav from '../../common/TabNav/TabNav';
import PageContent from '../../common/PageContent/PageContent';
import AccessContext from '../../../contexts/AccessContext';
import useStrategies from '../../../hooks/api/getters/useStrategies/useStrategies';
import { useParams } from 'react-router-dom';
import { useFeatures } from '../../../hooks/api/getters/useFeatures/useFeatures';
import useApplications from '../../../hooks/api/getters/useApplications/useApplications';
import { StrategyDetails } from './StrategyDetails/StrategyDetails';
export const StrategyView = () => {
const { hasAccess } = useContext(AccessContext);
const { strategyName } = useParams<{ strategyName: string }>();
const { strategies } = useStrategies();
const { features } = useFeatures();
const { applications } = useApplications();
const toggles = features.filter(toggle => {
return toggle?.strategies.findIndex(s => s.name === strategyName) > -1;
});
const strategy = strategies.find(n => n.name === strategyName);
const tabData = [
{
label: 'Details',
component: (
<StrategyDetails
strategy={strategy}
toggles={toggles}
applications={applications}
/>
),
},
{
label: 'Edit',
component: <StrategyForm strategy={strategy} editMode />,
},
];
if (!strategy) return null;
return (
<PageContent headerContent={strategy.name}>
<Grid container>
<Grid item xs={12} sm={12}>
<Typography variant="subtitle1">
{strategy.description}
</Typography>
<ConditionallyRender
condition={
strategy.editable && hasAccess(UPDATE_STRATEGY)
}
show={
<div>
<TabNav tabData={tabData} />
</div>
}
elseShow={
<section>
<div className="content">
<StrategyDetails
strategy={strategy}
toggles={toggles}
applications={applications}
/>
</div>
</section>
}
/>
</Grid>
</Grid>
</PageContent>
);
};

View File

@ -0,0 +1,47 @@
import {
List,
ListItem,
ListItemAvatar,
ListItemText,
Tooltip,
} from '@material-ui/core';
import { PlayArrow, Pause } from '@material-ui/icons';
import styles from '../../common/common.module.scss';
import { Link } from 'react-router-dom';
import ConditionallyRender from '../../common/ConditionallyRender';
interface ITogglesLinkListProps {
toggles: [];
}
export const TogglesLinkList = ({ toggles }: ITogglesLinkListProps) => (
<List style={{ textAlign: 'left' }} className={styles.truncate}>
<ConditionallyRender
condition={toggles.length > 0}
show={
<>
{toggles.map(({ name, description = '-', enabled }) => (
<ListItem key={name}>
<Tooltip title={enabled ? 'Enabled' : 'Disabled'}>
<ListItemAvatar>
{enabled ? <PlayArrow /> : <Pause />}
</ListItemAvatar>
</Tooltip>
<ListItemText
primary={
<Link
key={name}
to={`/features/view/${name}`}
>
{name}
</Link>
}
secondary={description}
/>
</ListItem>
))}
</>
}
/>
</List>
);

View File

@ -67,18 +67,18 @@ exports[`renders correctly with one strategy 1`] = `
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
<a
href="/strategies/view/Another"
href="/strategies/view/flexibleRollout"
onClick={[Function]}
>
<strong>
Another
Gradual rollout
</strong>
</a>
</span>
<p
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
>
another's description
Roll out to a percentage of your userbase, and ensure that the experience is the same for the user on each visit.
</p>
</div>
<div>
@ -201,18 +201,18 @@ exports[`renders correctly with one strategy without permissions 1`] = `
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
<a
href="/strategies/view/Another"
href="/strategies/view/flexibleRollout"
onClick={[Function]}
>
<strong>
Another
Gradual rollout
</strong>
</a>
</span>
<p
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
>
another's description
Roll out to a percentage of your userbase, and ensure that the experience is the same for the user on each visit.
</p>
</div>
<div>

View File

@ -1,242 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly with one strategy 1`] = `
<div
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
style={
Object {
"borderRadius": "10px",
"boxShadow": "none",
}
}
>
<div
className="makeStyles-headerContainer-1"
>
<div
className="makeStyles-headerTitleContainer-5"
>
<div
className=""
data-loading={true}
>
<h2
className="MuiTypography-root makeStyles-headerTitle-6 MuiTypography-h2"
>
Another
</h2>
</div>
</div>
</div>
<div
className="makeStyles-bodyContainer-2"
>
<div
className="MuiGrid-root MuiGrid-container"
>
<div
className="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12 MuiGrid-grid-sm-12"
>
<h6
className="MuiTypography-root MuiTypography-subtitle1"
>
another's description
</h6>
<section>
<div
className="content"
>
<div
className="listcontainer"
>
<div
className="MuiGrid-root MuiGrid-container"
>
<div
className="MuiGrid-root MuiGrid-item MuiGrid-grid-sm-12 MuiGrid-grid-md-12"
>
<h6>
Parameters
</h6>
<hr />
<ul
className="MuiList-root MuiList-padding"
>
<li
className="MuiListItem-root MuiListItem-gutters"
disabled={false}
>
<div
aria-describedby={null}
className="MuiListItemAvatar-root"
onBlur={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title="Required"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"
/>
</svg>
</div>
<div
className="MuiListItemText-root MuiListItemText-multiline"
>
<span
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
<div>
customParam
<small>
(
list
)
</small>
</div>
</span>
<p
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
>
customList
</p>
</div>
</li>
</ul>
</div>
<div
className="MuiGrid-root MuiGrid-item MuiGrid-grid-sm-12 MuiGrid-grid-md-6"
>
<h6>
Applications using this strategy
</h6>
<hr />
<ul
className="MuiList-root MuiList-padding"
>
<li
className="MuiListItem-root listItem MuiListItem-gutters"
disabled={false}
>
<div
className="MuiListItemAvatar-root"
>
<div
className="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M4 8h4V4H4v4zm6 12h4v-4h-4v4zm-6 0h4v-4H4v4zm0-6h4v-4H4v4zm6 0h4v-4h-4v4zm6-10v4h4V4h-4zm-6 4h4V4h-4v4zm6 6h4v-4h-4v4zm0 6h4v-4h-4v4z"
/>
</svg>
</div>
</div>
<div
className="MuiListItemText-root MuiListItemText-multiline"
>
<span
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
<a
className="listLink truncate"
href="/applications/appA"
onClick={[Function]}
>
appA
</a>
</span>
<p
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
>
app description
</p>
</div>
</li>
</ul>
</div>
<div
className="MuiGrid-root MuiGrid-item MuiGrid-grid-sm-12 MuiGrid-grid-md-6"
>
<h6>
Toggles using this strategy
</h6>
<hr />
<ul
className="MuiList-root truncate MuiList-padding"
style={
Object {
"textAlign": "left",
}
}
>
<li
className="MuiListItem-root MuiListItem-gutters"
disabled={false}
>
<div
aria-describedby={null}
className="MuiListItemAvatar-root"
onBlur={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title="Disabled"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"
/>
</svg>
</div>
<div
className="MuiListItemText-root MuiListItemText-multiline"
>
<span
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
<a
href="/features/view/toggleA"
onClick={[Function]}
>
toggleA
</a>
</span>
<p
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
>
toggle description
</p>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
</div>
</div>
`;
exports[`renders correctly with one strategy 1`] = `null`;

View File

@ -1,10 +1,12 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { ThemeProvider } from '@material-ui/core';
import StrategiesListComponent from '../StrategiesList/StrategiesList';
import { StrategiesList } from '../StrategiesList/StrategiesList';
import renderer from 'react-test-renderer';
import theme from '../../../themes/main-theme';
import AccessProvider from '../../providers/AccessProvider/AccessProvider';
import { createFakeStore } from '../../../accessStoreFake';
import { ADMIN } from '../../providers/AccessProvider/permissions';
import UIProvider from '../../providers/UIProvider/UIProvider';
test('renders correctly with one strategy', () => {
const strategy = {
@ -14,16 +16,18 @@ test('renders correctly with one strategy', () => {
const tree = renderer.create(
<MemoryRouter>
<ThemeProvider theme={theme}>
<AccessProvider>
<StrategiesListComponent
strategies={[strategy]}
fetchStrategies={jest.fn()}
removeStrategy={jest.fn()}
deprecateStrategy={jest.fn()}
reactivateStrategy={jest.fn()}
history={{}}
/>
</AccessProvider>
<UIProvider>
<AccessProvider store={createFakeStore()}>
<StrategiesList
strategies={[strategy]}
fetchStrategies={jest.fn()}
removeStrategy={jest.fn()}
deprecateStrategy={jest.fn()}
reactivateStrategy={jest.fn()}
history={{}}
/>
</AccessProvider>
</UIProvider>
</ThemeProvider>
</MemoryRouter>
);
@ -39,16 +43,20 @@ test('renders correctly with one strategy without permissions', () => {
const tree = renderer.create(
<MemoryRouter>
<ThemeProvider theme={theme}>
<AccessProvider>
<StrategiesListComponent
strategies={[strategy]}
fetchStrategies={jest.fn()}
removeStrategy={jest.fn()}
deprecateStrategy={jest.fn()}
reactivateStrategy={jest.fn()}
history={{}}
/>
</AccessProvider>
<UIProvider>
<AccessProvider
store={createFakeStore([{ permission: ADMIN }])}
>
<StrategiesList
strategies={[strategy]}
fetchStrategies={jest.fn()}
removeStrategy={jest.fn()}
deprecateStrategy={jest.fn()}
reactivateStrategy={jest.fn()}
history={{}}
/>
</AccessProvider>
</UIProvider>
</ThemeProvider>
</MemoryRouter>
);

View File

@ -1,6 +1,6 @@
import React from 'react';
import { ThemeProvider } from '@material-ui/core';
import StrategyDetails from '../strategy-details-component';
import { StrategyView } from '../StrategyView/StrategyView';
import renderer from 'react-test-renderer';
import { MemoryRouter } from 'react-router-dom';
import theme from '../../../themes/main-theme';
@ -36,7 +36,7 @@ test('renders correctly with one strategy', () => {
<MemoryRouter>
<AccessProvider>
<ThemeProvider theme={theme}>
<StrategyDetails
<StrategyView
strategyName={'Another'}
strategy={strategy}
activeTab="view"

View File

@ -1,100 +0,0 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import {
Grid,
List,
ListItem,
ListItemText,
ListItemAvatar,
Tooltip,
} from '@material-ui/core';
import { Add, RadioButtonChecked } from '@material-ui/icons';
import { TogglesLinkList } from './toggles-link-list';
import { AppsLinkList } from '../common';
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
import styles from './strategies.module.scss';
class ShowStrategyComponent extends PureComponent {
static propTypes = {
toggles: PropTypes.array,
applications: PropTypes.array,
strategy: PropTypes.object.isRequired,
};
renderParameters(params) {
if (params) {
return params.map(({ name, type, description, required }, i) => (
<ListItem key={`${name}-${i}`}>
<ConditionallyRender
condition={required}
show={
<Tooltip title="Required">
<ListItemAvatar>
<Add />
</ListItemAvatar>
</Tooltip>
}
elseShow={
<Tooltip title="Optional">
<ListItemAvatar>
<RadioButtonChecked />
</ListItemAvatar>
</Tooltip>
}
/>
<ListItemText
primary={
<div>
{name} <small>({type})</small>
</div>
}
secondary={description}
/>
</ListItem>
));
} else {
return <ListItem>(no params)</ListItem>;
}
}
render() {
const { strategy, applications, toggles } = this.props;
const { parameters = [] } = strategy;
return (
<div className={styles.listcontainer}>
<Grid container>
<ConditionallyRender
condition={strategy.deprecated}
show={
<Grid item>
<h5 style={{ color: '#ff0000' }}>Deprecated</h5>
</Grid>
}
/>
<Grid item sm={12} md={12}>
<h6>Parameters</h6>
<hr />
<List>{this.renderParameters(parameters)}</List>
</Grid>
<Grid item sm={12} md={6}>
<h6>Applications using this strategy</h6>
<hr />
<AppsLinkList apps={applications} />
</Grid>
<Grid item sm={12} md={6}>
<h6>Toggles using this strategy</h6>
<hr />
<TogglesLinkList toggles={toggles} />
</Grid>
</Grid>
</div>
);
}
}
export default ShowStrategyComponent;

View File

@ -1,103 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Grid, Typography } from '@material-ui/core';
import ShowStrategy from './show-strategy-component';
import EditStrategy from './CreateStrategy';
import { UPDATE_STRATEGY } from '../providers/AccessProvider/permissions';
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
import TabNav from '../common/TabNav/TabNav';
import PageContent from '../common/PageContent/PageContent';
import AccessContext from '../../contexts/AccessContext';
export default class StrategyDetails extends Component {
static contextType = AccessContext;
static propTypes = {
strategyName: PropTypes.string.isRequired,
toggles: PropTypes.array,
applications: PropTypes.array,
activeTab: PropTypes.string.isRequired,
strategy: PropTypes.object,
fetchStrategies: PropTypes.func.isRequired,
fetchApplications: PropTypes.func.isRequired,
fetchFeatureToggles: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
};
componentDidMount() {
if (!this.props.strategy) {
this.props.fetchStrategies();
}
if (!this.props.applications || this.props.applications.length === 0) {
this.props.fetchApplications();
}
if (!this.props.toggles || this.props.toggles.length === 0) {
this.props.fetchFeatureToggles();
}
}
render() {
const strategy = this.props.strategy;
if (!strategy) return null;
const tabData = [
{
label: 'Details',
component: (
<ShowStrategy
strategy={this.props.strategy}
toggles={this.props.toggles}
applications={this.props.applications}
/>
),
},
{
label: 'Edit',
component: (
<EditStrategy
strategy={this.props.strategy}
history={this.props.history}
editMode
/>
),
},
];
const { hasAccess } = this.context;
return (
<PageContent headerContent={strategy.name}>
<Grid container>
<Grid item xs={12} sm={12}>
<Typography variant="subtitle1">
{strategy.description}
</Typography>
<ConditionallyRender
condition={
strategy.editable && hasAccess(UPDATE_STRATEGY)
}
show={
<div>
<TabNav tabData={tabData} />
</div>
}
elseShow={
<section>
<div className="content">
<ShowStrategy
strategy={this.props.strategy}
toggles={this.props.toggles}
applications={
this.props.applications
}
/>
</div>
</section>
}
/>
</Grid>
</Grid>
</PageContent>
);
}
}

View File

@ -1,33 +0,0 @@
import { connect } from 'react-redux';
import ShowStrategy from './strategy-details-component';
import { fetchStrategies } from './../../store/strategy/actions';
import { fetchAll } from './../../store/application/actions';
import { fetchFeatureToggles } from './../../store/feature-toggle/actions';
const mapStateToProps = (state, props) => {
let strategy = state.strategies.get('list').find(n => n.name === props.strategyName);
const applications = state.applications
.get('list')
.filter(app => app.strategies && app.strategies.includes(props.strategyName));
const toggles = state.features.filter(
toggle => toggle.get('strategies').findIndex(s => s.name === props.strategyName) > -1
);
return {
strategy,
strategyName: props.strategyName,
applications: applications && applications.toJS(),
toggles: toggles && toggles.toJS(),
activeTab: props.activeTab,
};
};
const Constainer = connect(mapStateToProps, {
fetchStrategies,
fetchApplications: fetchAll,
fetchFeatureToggles,
})(ShowStrategy);
export default Constainer;

View File

@ -1,42 +0,0 @@
import {
List,
ListItem,
ListItemAvatar,
ListItemText,
Tooltip,
} from '@material-ui/core';
import { PlayArrow, Pause } from '@material-ui/icons';
import styles from '../common/common.module.scss';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import React from 'react';
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
export const TogglesLinkList = ({ toggles }) => (
<List style={{ textAlign: 'left' }} className={styles.truncate}>
<ConditionallyRender
condition={toggles.length > 0}
show={toggles.map(({ name, description = '-', enabled }) => (
<ListItem key={name}>
<Tooltip title={enabled ? 'Enabled' : 'Disabled'}>
<ListItemAvatar>
{enabled ? <PlayArrow /> : <Pause />}
</ListItemAvatar>
</Tooltip>
<ListItemText
primary={
<Link key={name} to={`/features/view/${name}`}>
{name}
</Link>
}
secondary={description}
/>
</ListItem>
))}
/>
</List>
);
TogglesLinkList.propTypes = {
toggles: PropTypes.array,
};

View File

@ -0,0 +1,98 @@
import useAPI from '../useApi/useApi';
export interface ICustomStrategyPayload {
name: string;
description: string;
parameters: object[];
}
const useStrategiesApi = () => {
const { makeRequest, createRequest, errors, loading } = useAPI({
propagateErrors: true,
});
const URI = 'api/admin/strategies';
const createStrategy = async (strategy: ICustomStrategyPayload) => {
const req = createRequest(URI, {
method: 'POST',
body: JSON.stringify(strategy),
});
try {
const res = await makeRequest(req.caller, req.id);
return res;
} catch (e) {
throw e;
}
};
const updateStrategy = async (strategy: ICustomStrategyPayload) => {
const path = `${URI}/${strategy.name}`;
const req = createRequest(path, {
method: 'PUT',
body: JSON.stringify(strategy),
});
try {
const res = await makeRequest(req.caller, req.id);
return res;
} catch (e) {
throw e;
}
};
const removeStrategy = async (strategy: ICustomStrategyPayload) => {
const path = `${URI}/${strategy.name}`;
const req = createRequest(path, { method: 'DELETE' });
try {
const res = await makeRequest(req.caller, req.id);
return res;
} catch (e) {
throw e;
}
};
const deprecateStrategy = async (strategy: ICustomStrategyPayload) => {
const path = `${URI}/${strategy.name}/deprecate`;
const req = createRequest(path, {
method: 'POST',
});
try {
const res = await makeRequest(req.caller, req.id);
return res;
} catch (e) {
throw e;
}
};
const reactivateStrategy = async (strategy: ICustomStrategyPayload) => {
const path = `${URI}/${strategy.name}/reactivate`;
const req = createRequest(path, { method: 'POST' });
try {
const res = await makeRequest(req.caller, req.id);
return res;
} catch (e) {
throw e;
}
};
return {
createStrategy,
updateStrategy,
removeStrategy,
deprecateStrategy,
reactivateStrategy,
errors,
loading,
};
};
export default useStrategiesApi;

View File

@ -42,7 +42,7 @@ const useApplications = (
}, [data, error]);
return {
applications: data?.applications || {},
applications: data?.applications || [],
error,
loading,
refetchApplications,

View File

@ -49,7 +49,7 @@ const useStrategies = (options: SWRConfiguration = {}) => {
);
const [loading, setLoading] = useState(!error && !data);
const refetch = () => {
const refetchStrategies = () => {
mutate(STRATEGIES_CACHE_KEY);
};
@ -61,7 +61,7 @@ const useStrategies = (options: SWRConfiguration = {}) => {
strategies: data?.strategies || [flexibleRolloutStrategy],
error,
loading,
refetch,
refetchStrategies,
};
};

View File

@ -1,11 +0,0 @@
import React from 'react';
import AddStrategies from '../../component/strategies/CreateStrategy';
import PropTypes from 'prop-types';
const render = ({ history }) => <AddStrategies history={history} />;
render.propTypes = {
history: PropTypes.object.isRequired,
};
export default render;

View File

@ -1,11 +0,0 @@
import React from 'react';
import Strategies from '../../component/strategies/StrategiesList';
import PropTypes from 'prop-types';
const render = ({ history }) => <Strategies history={history} />;
render.propTypes = {
history: PropTypes.object.isRequired,
};
export default render;

View File

@ -1,14 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import ShowStrategy from '../../component/strategies/strategy-details-container';
const render = ({ match: { params }, history }) => (
<ShowStrategy strategyName={params.strategyName} activeTab={params.activeTab} history={history} />
);
render.propTypes = {
match: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
};
export default render;

View File

@ -1,58 +0,0 @@
import api from './api';
import { dispatchError } from '../util';
import { MUTE_ERROR } from '../error/actions';
export const RECEIVE_ALL_APPLICATIONS = 'RECEIVE_ALL_APPLICATIONS';
export const ERROR_RECEIVE_ALL_APPLICATIONS = 'ERROR_RECEIVE_ALL_APPLICATIONS';
export const ERROR_UPDATING_APPLICATION_DATA = 'ERROR_UPDATING_APPLICATION_DATA';
export const RECEIVE_APPLICATION = 'RECEIVE_APPLICATION';
export const UPDATE_APPLICATION_FIELD = 'UPDATE_APPLICATION_FIELD';
export const DELETE_APPLICATION = 'DELETE_APPLICATION';
export const ERROR_DELETE_APPLICATION = 'ERROR_DELETE_APPLICATION';
const recieveAllApplications = json => ({
type: RECEIVE_ALL_APPLICATIONS,
value: json,
});
const recieveApplication = json => ({
type: RECEIVE_APPLICATION,
value: json,
});
export function fetchAll() {
return dispatch =>
api
.fetchAll()
.then(json => dispatch(recieveAllApplications(json)))
.catch(dispatchError(dispatch, ERROR_RECEIVE_ALL_APPLICATIONS));
}
export function storeApplicationMetaData(appName, key, value) {
return dispatch =>
api
.storeApplicationMetaData(appName, key, value)
.then(() => {
const info = `${appName} successfully updated!`;
setTimeout(() => dispatch({ type: MUTE_ERROR, error: info }), 1000);
dispatch({ type: UPDATE_APPLICATION_FIELD, appName, key, value, info });
})
.catch(dispatchError(dispatch, ERROR_UPDATING_APPLICATION_DATA));
}
export function fetchApplication(appName) {
return dispatch =>
api
.fetchApplication(appName)
.then(json => dispatch(recieveApplication(json)))
.catch(dispatchError(dispatch, ERROR_RECEIVE_ALL_APPLICATIONS));
}
export function deleteApplication(appName) {
return dispatch =>
api
.deleteApplication(appName)
.then(() => dispatch({ type: DELETE_APPLICATION, appName }))
.catch(dispatchError(dispatch, ERROR_DELETE_APPLICATION));
}

View File

@ -1,52 +0,0 @@
import { formatApiPath } from '../../utils/format-path';
import { throwIfNotSuccess, headers } from '../api-helper';
const URI = formatApiPath('api/admin/metrics/applications');
function fetchAll() {
return fetch(URI, { headers, credentials: 'include' })
.then(throwIfNotSuccess)
.then(response => response.json());
}
function fetchApplication(appName) {
return fetch(`${URI}/${appName}`, { headers, credentials: 'include' })
.then(throwIfNotSuccess)
.then(response => response.json());
}
function fetchApplicationsWithStrategyName(strategyName) {
return fetch(`${URI}?strategyName=${strategyName}`, {
headers,
credentials: 'include',
})
.then(throwIfNotSuccess)
.then(response => response.json());
}
function storeApplicationMetaData(appName, key, value) {
const data = {};
data[key] = value;
return fetch(`${URI}/${appName}`, {
method: 'POST',
headers,
body: JSON.stringify(data),
credentials: 'include',
}).then(throwIfNotSuccess);
}
function deleteApplication(appName) {
return fetch(`${URI}/${appName}`, {
method: 'DELETE',
headers,
credentials: 'include',
}).then(throwIfNotSuccess);
}
export default {
fetchApplication,
fetchAll,
fetchApplicationsWithStrategyName,
storeApplicationMetaData,
deleteApplication,
};

View File

@ -1,26 +0,0 @@
import { fromJS, List, Map } from 'immutable';
import { RECEIVE_ALL_APPLICATIONS, RECEIVE_APPLICATION, UPDATE_APPLICATION_FIELD, DELETE_APPLICATION } from './actions';
function getInitState() {
return fromJS({ list: [], apps: {} });
}
const store = (state = getInitState(), action) => {
switch (action.type) {
case RECEIVE_APPLICATION:
return state.setIn(['apps', action.value.appName], new Map(action.value));
case RECEIVE_ALL_APPLICATIONS:
return state.set('list', new List(action.value.applications));
case UPDATE_APPLICATION_FIELD:
return state.setIn(['apps', action.appName, action.key], action.value);
case DELETE_APPLICATION: {
const index = state.get('list').findIndex(item => item.appName === action.appName);
const result = state.removeIn(['list', index]);
return result.removeIn(['apps', action.appName]);
}
default:
return state;
}
};
export default store;

View File

@ -9,15 +9,6 @@ import {
UPDATE_FEATURE_TOGGLE,
} from '../feature-toggle/actions';
import {
ERROR_UPDATING_STRATEGY,
ERROR_CREATING_STRATEGY,
ERROR_RECEIVE_STRATEGIES,
UPDATE_STRATEGY_SUCCESS,
} from '../strategy/actions';
import { UPDATE_APPLICATION_FIELD } from '../application/actions';
import { FORBIDDEN } from '../util';
const debug = require('debug')('unleash:error-store');
@ -42,9 +33,6 @@ const strategies = (state = getInitState(), action) => {
case ERROR_REMOVE_FEATURE_TOGGLE:
case ERROR_FETCH_FEATURE_TOGGLES:
case ERROR_UPDATE_FEATURE_TOGGLE:
case ERROR_UPDATING_STRATEGY:
case ERROR_CREATING_STRATEGY:
case ERROR_RECEIVE_STRATEGIES:
return addErrorIfNotAlreadyInList(state, action.error.message);
case FORBIDDEN:
return addErrorIfNotAlreadyInList(
@ -60,8 +48,6 @@ const strategies = (state = getInitState(), action) => {
// 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

@ -1,15 +1,11 @@
import { combineReducers } from 'redux';
import features from './feature-toggle';
import strategies from './strategy';
import error from './error';
import applications from './application';
import apiCalls from './api-calls';
const unleashStore = combineReducers({
features,
strategies,
error,
applications,
apiCalls,
});

View File

@ -1,138 +0,0 @@
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';
export const REMOVE_STRATEGY = 'REMOVE_STRATEGY';
export const DEPRECATE_STRATEGY = 'DEPRECATE_STRATEGY';
export const REACTIVATE_STRATEGY = 'REACTIVATE_STRATEGY';
export const REQUEST_STRATEGIES = 'REQUEST_STRATEGIES';
export const START_CREATE_STRATEGY = 'START_CREATE_STRATEGY';
export const START_UPDATE_STRATEGY = 'START_UPDATE_STRATEGY';
export const START_DEPRECATE_STRATEGY = 'START_DEPRECATE_STRATEGY';
export const START_REACTIVATE_STRATEGY = 'START_REACTIVATE_STRATEGY';
export const RECEIVE_STRATEGIES = 'RECEIVE_STRATEGIES';
export const ERROR_RECEIVE_STRATEGIES = 'ERROR_RECEIVE_STRATEGIES';
export const ERROR_CREATING_STRATEGY = 'ERROR_CREATING_STRATEGY';
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,
value: json,
});
const addStrategy = strategy => ({ type: ADD_STRATEGY, strategy });
const createRemoveStrategy = strategy => ({ type: REMOVE_STRATEGY, strategy });
const updatedStrategy = strategy => ({ type: UPDATE_STRATEGY, strategy });
const startRequest = () => ({ type: REQUEST_STRATEGIES });
const startCreate = () => ({ type: START_CREATE_STRATEGY });
const startUpdate = () => ({ type: START_UPDATE_STRATEGY });
const startDeprecate = () => ({ type: START_DEPRECATE_STRATEGY });
const deprecateStrategyEvent = strategy => ({
type: DEPRECATE_STRATEGY,
strategy,
});
const startReactivate = () => ({ type: START_REACTIVATE_STRATEGY });
const reactivateStrategyEvent = strategy => ({
type: REACTIVATE_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());
return api
.fetchAll()
.then(json => dispatch(receiveStrategies(json.strategies)))
.catch(dispatchError(dispatch, ERROR_RECEIVE_STRATEGIES));
};
}
export function createStrategy(strategy) {
return dispatch => {
dispatch(startCreate());
return api
.create(strategy)
.then(() => dispatch(addStrategy(strategy)))
.then(() => {
setInfoMessage('Strategy successfully created.', dispatch);
})
.catch(e => {
dispatchError(dispatch, ERROR_CREATING_STRATEGY);
throw e;
});
};
}
export function updateStrategy(strategy) {
return dispatch => {
dispatch(startUpdate());
return api
.update(strategy)
.then(() => dispatch(updatedStrategy(strategy)))
.then(() => {
setInfoMessage('Strategy successfully updated.', dispatch);
})
.catch(dispatchError(dispatch, ERROR_UPDATING_STRATEGY));
};
}
export function removeStrategy(strategy) {
return dispatch =>
api
.remove(strategy)
.then(() => dispatch(createRemoveStrategy(strategy)))
.then(() => {
setInfoMessage('Strategy successfully deleted.', dispatch);
})
.catch(dispatchError(dispatch, ERROR_REMOVING_STRATEGY));
}
export function getApplicationsWithStrategy(strategyName) {
return applicationApi.fetchApplicationsWithStrategyName(strategyName);
}
export function deprecateStrategy(strategy) {
return dispatch => {
dispatch(startDeprecate());
api.deprecate(strategy)
.then(() => dispatch(deprecateStrategyEvent(strategy)))
.then(() =>
setInfoMessage('Strategy successfully deprecated', dispatch)
)
.catch(dispatchError(dispatch, ERROR_DEPRECATING_STRATEGY));
};
}
export function reactivateStrategy(strategy) {
return dispatch => {
dispatch(startReactivate());
api.reactivate(strategy)
.then(() => dispatch(reactivateStrategyEvent(strategy)))
.then(() =>
setInfoMessage('Strategy successfully reactivated', dispatch)
)
.catch(dispatchError(dispatch, ERROR_REACTIVATING_STRATEGY));
};
}

View File

@ -1,61 +0,0 @@
import { formatApiPath } from '../../utils/format-path';
import { throwIfNotSuccess, headers } from '../api-helper';
const URI = formatApiPath('api/admin/strategies');
function fetchAll() {
return fetch(URI, { credentials: 'include' })
.then(throwIfNotSuccess)
.then(response => response.json());
}
function create(strategy) {
return fetch(URI, {
method: 'POST',
headers,
body: JSON.stringify(strategy),
credentials: 'include',
}).then(throwIfNotSuccess);
}
function update(strategy) {
return fetch(`${URI}/${strategy.name}`, {
method: 'put',
headers,
body: JSON.stringify(strategy),
credentials: 'include',
}).then(throwIfNotSuccess);
}
function remove(strategy) {
return fetch(`${URI}/${strategy.name}`, {
method: 'DELETE',
headers,
credentials: 'include',
}).then(throwIfNotSuccess);
}
function deprecate(strategy) {
return fetch(`${URI}/${strategy.name}/deprecate`, {
method: 'POST',
headers,
credentials: 'include',
}).then(throwIfNotSuccess);
}
function reactivate(strategy) {
return fetch(`${URI}/${strategy.name}/reactivate`, {
method: 'POST',
headers,
credentials: 'include',
}).then(throwIfNotSuccess);
}
export default {
fetchAll,
create,
update,
remove,
deprecate,
reactivate,
};

View File

@ -1,67 +0,0 @@
import { List, Map as $Map } from 'immutable';
import {
RECEIVE_STRATEGIES,
REMOVE_STRATEGY,
ADD_STRATEGY,
UPDATE_STRATEGY,
DEPRECATE_STRATEGY,
REACTIVATE_STRATEGY,
} from './actions';
function getInitState() {
return new $Map({ list: new List() });
}
function removeStrategy(state, action) {
const indexToRemove = state.get('list').indexOf(action.strategy);
if (indexToRemove !== -1) {
return state.update('list', list => list.remove(indexToRemove));
}
return state;
}
function updateStrategy(state, action) {
return state.update('list', list =>
list.map(strategy => {
if (strategy.name === action.strategy.name) {
return action.strategy;
} else {
return strategy;
}
})
);
}
function setDeprecationStatus(state, action, status) {
return state.update('list', list =>
list.map(strategy => {
if (strategy.name === action.strategy.name) {
action.strategy.deprecated = status;
return action.strategy;
} else {
return strategy;
}
})
);
}
const strategies = (state = getInitState(), action) => {
switch (action.type) {
case RECEIVE_STRATEGIES:
return state.set('list', new List(action.value));
case REMOVE_STRATEGY:
return removeStrategy(state, action);
case ADD_STRATEGY:
return state.update('list', list => list.push(action.strategy));
case UPDATE_STRATEGY:
return updateStrategy(state, action);
case DEPRECATE_STRATEGY:
return setDeprecationStatus(state, action, true);
case REACTIVATE_STRATEGY:
return setDeprecationStatus(state, action, false);
default:
return state;
}
};
export default strategies;

View File

@ -1,6 +1,5 @@
import api from './api';
import { dispatchError } from '../util';
import { receiveStrategies } from '../strategy/actions';
export const RECEIVE_BOOTSTRAP = 'RECEIVE_CONFIG';
export const ERROR_RECEIVE_BOOTSTRAP = 'ERROR_RECEIVE_CONFIG';
@ -9,8 +8,6 @@ export function fetchUiBootstrap() {
return dispatch =>
api
.fetchUIBootstrap()
.then(json => {
dispatch(receiveStrategies(json.strategies));
})
.then(json => {})
.catch(dispatchError(dispatch, ERROR_RECEIVE_BOOTSTRAP));
}