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

Fix/v4 corrections (#287)

* fix: remove project display check

* fix: refetch bootstrap on user change

* fix: remove console log

* fix: update test

* fix: do not allow submit if errors exists

* fix: do not allow strategies to redirect home when name is taken
This commit is contained in:
Fredrik Strand Oseberg 2021-05-05 14:17:25 +02:00 committed by GitHub
parent 549be832bf
commit 9b1a07c5ab
26 changed files with 572 additions and 344 deletions

View File

@ -20,7 +20,7 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
useEffect(() => {
fetchUiBootstrap();
/* eslint-disable-next-line */
}, []);
}, [user.authDetails?.type]);
const renderMainLayoutRoutes = () => {
return routes.filter(route => route.layout === 'main').map(renderRoute);

View File

@ -9,16 +9,13 @@ const ProjectSelect = ({
projects,
currentProjectId,
updateCurrentProject,
...rest
}) => {
const setProject = v => {
const id = typeof v === 'string' ? v.trim() : '';
updateCurrentProject(id);
};
if (!projects || projects.length === 1) {
return null;
}
// TODO fixme
let curentProject = projects.find(i => i.id === currentProjectId);
if (!curentProject) {
@ -57,6 +54,8 @@ const ProjectSelect = ({
];
};
const { updateSetting, fetchProjects, ...passDown } = rest;
return (
<React.Fragment>
<DropdownMenu
@ -66,6 +65,7 @@ const ProjectSelect = ({
callback={handleChangeProject}
renderOptions={renderProjectOptions}
className=""
{...passDown}
/>
</React.Fragment>
);

View File

@ -6,7 +6,7 @@ import { MenuItemWithIcon } from '../../../common';
import DropdownMenu from '../../../common/DropdownMenu/DropdownMenu';
import ProjectSelect from '../../../common/ProjectSelect';
import { useStyles } from './styles';
import classnames from 'classnames';
import useLoading from '../../../../hooks/useLoading';
const sortingOptions = [
{ type: 'name', displayName: 'Name' },
@ -27,6 +27,7 @@ const FeatureToggleListActions = ({
loading,
}) => {
const styles = useStyles();
const ref = useLoading(loading);
const handleSort = e => {
const target = e.target.getAttribute('data-target');
@ -64,14 +65,15 @@ const FeatureToggleListActions = ({
];
return (
<div className={styles.actions}>
<div className={styles.actions} ref={ref}>
<DropdownMenu
id={'metric'}
label={`Last ${settings.showLastHour ? 'hour' : 'minute'}`}
title="Metric interval"
callback={toggleMetrics}
renderOptions={renderMetricsOptions}
className={classnames({ skeleton: loading })}
className=""
data-loading
/>
<DropdownMenu
id={'sorting'}
@ -79,9 +81,14 @@ const FeatureToggleListActions = ({
callback={handleSort}
renderOptions={renderSortingOptions}
title="Sort by"
className={classnames({ skeleton: loading })}
className=""
data-loading
/>
<ProjectSelect
settings={settings}
updateSetting={updateSetting}
data-loading
/>
<ProjectSelect settings={settings} updateSetting={updateSetting} />
</div>
);
};

View File

@ -70,6 +70,7 @@ exports[`renders correctly with one feature 1`] = `
aria-controls="metric"
aria-haspopup="true"
className="MuiButtonBase-root MuiButton-root MuiButton-text"
data-loading={true}
disabled={false}
id="metric"
onBlur={[Function]}
@ -108,6 +109,7 @@ exports[`renders correctly with one feature 1`] = `
aria-controls="sorting"
aria-haspopup="true"
className="MuiButtonBase-root MuiButton-root MuiButton-text"
data-loading={true}
disabled={false}
id="sorting"
onBlur={[Function]}
@ -270,6 +272,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
aria-controls="metric"
aria-haspopup="true"
className="MuiButtonBase-root MuiButton-root MuiButton-text"
data-loading={true}
disabled={false}
id="metric"
onBlur={[Function]}
@ -311,6 +314,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
aria-controls="sorting"
aria-haspopup="true"
className="MuiButtonBase-root MuiButton-root MuiButton-text"
data-loading={true}
disabled={false}
id="sorting"
onBlur={[Function]}

View File

@ -18,6 +18,7 @@ import FeatureTypeSelect from '../feature-type-select-container';
import ProjectSelect from '../project-select-container';
import UpdateDescriptionComponent from '../view/update-description-component';
import {
CREATE_FEATURE,
DELETE_FEATURE,
UPDATE_FEATURE,
} from '../../AccessProvider/permissions';
@ -35,6 +36,7 @@ import ConfirmDialogue from '../../common/Dialogue';
import { useCommonStyles } from '../../../common.styles';
import AccessContext from '../../../contexts/AccessContext';
import { projectFilterGenerator } from '../../../utils/project-filter-generator';
const FeatureView = ({
activeTab,
@ -53,6 +55,7 @@ const FeatureView = ({
featureTags,
fetchTags,
tagTypes,
user,
}) => {
const isFeatureView = !!fetchFeatureToggles;
const [delDialog, setDelDialog] = useState(false);
@ -215,6 +218,7 @@ const FeatureView = ({
const findActiveTab = activeTab =>
tabs.findIndex(tab => tab.name === activeTab);
return (
<Paper
className={commonStyles.fullwidth}
@ -253,6 +257,10 @@ const FeatureView = ({
label="Project"
filled
editable={editable}
filter={projectFilterGenerator(
user,
CREATE_FEATURE
)}
/>
</div>
<FeatureTagComponent

View File

@ -10,15 +10,22 @@ import {
} from '../../../store/feature-toggle/actions';
import FeatureView from './FeatureView';
import { fetchTags, tagFeature, untagFeature } from '../../../store/feature-tags/actions';
import {
fetchTags,
tagFeature,
untagFeature,
} from '../../../store/feature-tags/actions';
export default connect(
(state, props) => ({
features: state.features.toJS(),
featureToggle: state.features.toJS().find(toggle => toggle.name === props.featureToggleName),
featureToggle: state.features
.toJS()
.find(toggle => toggle.name === props.featureToggleName),
featureTags: state.featureTags.toJS(),
tagTypes: state.tagTypes.toJS(),
activeTab: props.activeTab,
user: state.user.toJS(),
}),
{
fetchFeatureToggles,

View File

@ -0,0 +1,140 @@
import { useEffect } from 'react';
import PropTypes from 'prop-types';
import { CardActions, Switch, TextField } from '@material-ui/core';
import FeatureTypeSelect from '../../feature-type-select-container';
import ProjectSelect from '../../project-select-container';
import StrategiesList from '../../strategy/strategies-list-container';
import PageContent from '../../../common/PageContent/PageContent';
import { FormButtons, styles as commonStyles } from '../../../common';
import { trim } from '../../../common/util';
import styles from '../add-feature-component.module.scss';
import {
CF_CREATE_BTN_ID,
CF_DESC_ID,
CF_NAME_ID,
CF_TYPE_ID,
} from '../../../../testIds';
import { CREATE_FEATURE } from '../../../AccessProvider/permissions';
import { projectFilterGenerator } from '../../../../utils/project-filter-generator';
const CreateFeature = ({
input,
errors,
setValue,
validateName,
onSubmit,
onCancel,
user,
}) => {
useEffect(() => {
window.onbeforeunload = () =>
'Data will be lost if you leave the page, are you sure?';
return () => {
window.onbeforeunload = false;
};
}, []);
return (
<PageContent headerContent="Create new feature toggle">
<form onSubmit={onSubmit}>
<div className={styles.formContainer}>
<TextField
size="small"
variant="outlined"
label="Name"
placeholder="Unique-name"
className={styles.nameInput}
name="name"
inputProps={{
'data-test': CF_NAME_ID,
}}
value={input.name}
error={errors.name !== undefined}
helperText={errors.name}
onBlur={v => validateName(v.target.value)}
onChange={v => setValue('name', trim(v.target.value))}
/>
</div>
<div className={styles.formContainer}>
<FeatureTypeSelect
value={input.type}
onChange={v => setValue('type', v.target.value)}
label={'Toggle type'}
id="feature-type-select"
editable
inputProps={{
'data-test': CF_TYPE_ID,
}}
/>
</div>
<section className={styles.formContainer}>
<ProjectSelect
value={input.project}
onChange={v => setValue('project', v.target.value)}
filter={projectFilterGenerator(user, CREATE_FEATURE)}
/>
</section>
<section className={styles.formContainer}>
<TextField
size="small"
variant="outlined"
className={commonStyles.fullwidth}
multiline
rows={4}
label="Description"
placeholder="A short description of the feature toggle"
error={errors.description !== undefined}
helperText={errors.description}
value={input.description}
inputProps={{
'data-test': CF_DESC_ID,
}}
onChange={v => setValue('description', v.target.value)}
/>
</section>
<section className={styles.toggleContainer}>
<Switch
checked={input.enabled}
onChange={() => {
setValue('enabled', !input.enabled);
}}
/>
<p className={styles.toggleText}>
{input.enabled ? 'Enabled' : 'Disabled'} feature toggle
</p>
</section>
<section className={styles.strategiesContainer}>
<StrategiesList
configuredStrategies={input.strategies}
featureToggleName={input.name}
saveStrategies={s => setValue('strategies', s)}
editable
/>
</section>
<CardActions>
<FormButtons
submitText={'Create'}
primaryButtonTestId={CF_CREATE_BTN_ID}
onCancel={onCancel}
/>
</CardActions>
</form>
</PageContent>
);
};
CreateFeature.propTypes = {
input: PropTypes.object,
errors: PropTypes.object,
setValue: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
validateName: PropTypes.func.isRequired,
initCallRequired: PropTypes.bool,
init: PropTypes.func,
};
export default CreateFeature;

View File

@ -1,9 +1,12 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createFeatureToggles, validateName } from '../../../store/feature-toggle/actions';
import AddFeatureComponent from './add-feature-component';
import { loadNameFromHash } from '../../common/util';
import {
createFeatureToggles,
validateName,
} from '../../../../store/feature-toggle/actions';
import CreateFeature from './CreateFeature';
import { loadNameFromHash } from '../../../common/util';
const defaultStrategy = {
name: 'default',
@ -63,11 +66,17 @@ class WrapperComponent extends Component {
const { createFeatureToggles, history } = this.props;
const { featureToggle } = this.state;
if (Object.keys(this.state.errors)) {
return;
}
if (featureToggle.strategies < 1) {
featureToggle.strategies = [defaultStrategy];
}
createFeatureToggles(featureToggle).then(() => history.push(`/features/strategies/${featureToggle.name}`));
createFeatureToggles(featureToggle).then(() =>
history.push(`/features/strategies/${featureToggle.name}`)
);
};
onCancel = evt => {
@ -77,7 +86,7 @@ class WrapperComponent extends Component {
render() {
return (
<AddFeatureComponent
<CreateFeature
onSubmit={this.onSubmit}
onCancel={this.onCancel}
validateName={this.validateName}
@ -85,6 +94,7 @@ class WrapperComponent extends Component {
setStrategies={this.setStrategies}
input={this.state.featureToggle}
errors={this.state.errors}
user={this.props.user}
/>
);
}
@ -97,16 +107,20 @@ WrapperComponent.propTypes = {
const mapDispatchToProps = dispatch => ({
validateName: name => validateName(name)(dispatch),
createFeatureToggles: featureToggle => createFeatureToggles(featureToggle)(dispatch),
createFeatureToggles: featureToggle =>
createFeatureToggles(featureToggle)(dispatch),
});
const mapStateToProps = state => {
const settings = state.settings.toJS().feature || {};
const currentProjectId = resolveCurrentProjectId(settings);
return { currentProjectId };
return { currentProjectId, user: state.user.toJS() };
};
const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(WrapperComponent);
const FormAddContainer = connect(
mapStateToProps,
mapDispatchToProps
)(WrapperComponent);
export default FormAddContainer;

View File

@ -1,120 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { CardActions, Switch, TextField } from '@material-ui/core';
import FeatureTypeSelect from '../feature-type-select-container';
import ProjectSelect from '../project-select-container';
import StrategiesList from '../strategy/strategies-list-container';
import PageContent from '../../common/PageContent/PageContent';
import { FormButtons, styles as commonStyles } from '../../common';
import { trim } from '../../common/util';
import styles from './add-feature-component.module.scss';
import { CF_CREATE_BTN_ID, CF_DESC_ID, CF_NAME_ID, CF_TYPE_ID } from '../../../testIds';
class AddFeatureComponent extends Component {
// static displayName = `AddFeatureComponent-${getDisplayName(Component)}`;
componentDidMount() {
window.onbeforeunload = () => 'Data will be lost if you leave the page, are you sure?';
}
componentWillUnmount() {
window.onbeforeunload = false;
}
render() {
const { input, errors, setValue, validateName, onSubmit, onCancel } = this.props;
return (
<PageContent headerContent="Create new feature toggle">
<form onSubmit={onSubmit}>
<div className={styles.formContainer}>
<TextField
size="small"
variant="outlined"
label="Name"
placeholder="Unique-name"
className={styles.nameInput}
name="name"
inputProps={{
'data-test': CF_NAME_ID,
}}
value={input.name}
error={errors.name !== undefined}
helperText={errors.name}
onBlur={v => validateName(v.target.value)}
onChange={v => setValue('name', trim(v.target.value))}
/>
</div>
<div className={styles.formContainer}>
<FeatureTypeSelect
value={input.type}
onChange={v => setValue('type', v.target.value)}
label={'Toggle type'}
id="feature-type-select"
editable
inputProps={{
'data-test': CF_TYPE_ID,
}}
/>
</div>
<section className={styles.formContainer}>
<ProjectSelect value={input.project} onChange={v => setValue('project', v.target.value)} />
</section>
<section className={styles.formContainer}>
<TextField
size="small"
variant="outlined"
className={commonStyles.fullwidth}
multiline
rows={4}
label="Description"
placeholder="A short description of the feature toggle"
error={errors.description !== undefined}
helperText={errors.description}
value={input.description}
inputProps={{
'data-test': CF_DESC_ID,
}}
onChange={v => setValue('description', v.target.value)}
/>
</section>
<section className={styles.toggleContainer}>
<Switch
checked={input.enabled}
onChange={() => {
setValue('enabled', !input.enabled);
}}
/>
<p className={styles.toggleText}>{input.enabled ? 'Enabled' : 'Disabled'} feature toggle</p>
</section>
<section className={styles.strategiesContainer}>
<StrategiesList
configuredStrategies={input.strategies}
featureToggleName={input.name}
saveStrategies={s => setValue('strategies', s)}
editable
/>
</section>
<CardActions>
<FormButtons submitText={'Create'} primaryButtonTestId={CF_CREATE_BTN_ID} onCancel={onCancel} />
</CardActions>
</form>
</PageContent>
);
}
}
AddFeatureComponent.propTypes = {
input: PropTypes.object,
errors: PropTypes.object,
setValue: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
validateName: PropTypes.func.isRequired,
initCallRequired: PropTypes.bool,
init: PropTypes.func,
};
export default AddFeatureComponent;

View File

@ -9,6 +9,7 @@
.nameInput {
margin-right: 1.5rem;
min-width: 250px;
}
.formContainer {

View File

@ -11,23 +11,43 @@ class ProjectSelectComponent extends Component {
}
render() {
const { value, projects, onChange, enabled } = this.props;
const { value, projects, onChange, enabled, filter } = this.props;
if (!enabled) {
return null;
}
const options = projects.map(t => ({
key: t.id,
label: t.name,
title: t.description,
}));
const formatOption = project => {
return {
key: project.id,
label: project.name,
title: project.description,
};
};
let options;
if (filter) {
options = projects
.filter(project => {
return filter(project);
})
.map(formatOption);
} else {
options = projects.map(formatOption);
}
if (value && !options.find(o => o.key === value)) {
options.push({ key: value, label: value });
}
return <MySelect label="Project" options={options} value={value} onChange={onChange} />;
return (
<MySelect
label="Project"
options={options}
value={value}
onChange={onChange}
/>
);
}
}

View File

@ -188,7 +188,7 @@ const AddVariant = ({
name="name"
placeholder=""
className={commonStyles.fullWidth}
value={data.name}
value={data.name || ''}
error={error.name}
variant="outlined"
size="small"
@ -213,7 +213,7 @@ const AddVariant = ({
),
}}
style={{ marginRight: '0.8rem' }}
value={data.weight}
value={data.weight || ''}
error={error.weight}
type="number"
disabled={!isFixWeight}

View File

@ -77,6 +77,7 @@ test('renders correctly with one feature', () => {
featureToggle={feature}
fetchFeatureToggles={jest.fn()}
history={{}}
user={{ permissions: [] }}
featureTags={[]}
fetchTags={jest.fn()}
untagFeature={jest.fn()}

View File

@ -0,0 +1,113 @@
import PropTypes from 'prop-types';
import { Typography, TextField, Button, Icon } from '@material-ui/core';
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="p">
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={<Icon>add</Icon>}
>
Add parameter
</Button>
<FormButtons
submitText={editMode ? 'Update' : '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

@ -0,0 +1,66 @@
import { TextField, Checkbox, FormControlLabel } from '@material-ui/core';
import PropTypes from 'prop-types';
import MySelect from '../../../../common/select';
import { styles as commonStyles } from '../../../../common';
const paramTypesOptions = [
{ key: 'string', label: 'string' },
{ key: 'percentage', label: 'percentage' },
{ key: 'list', label: 'list' },
{ key: 'number', label: 'number' },
{ key: 'boolean', label: 'boolean' },
];
const StrategyParameter = ({ set, input = {}, index }) => {
const handleTypeChange = event => {
set({ type: event.target.value });
};
return (
<div className={commonStyles.contentSpacing}>
<TextField
style={{ width: '50%', marginRight: '5px' }}
label={`Parameter name ${index + 1}`}
onChange={({ target }) => set({ name: target.value }, true)}
value={input.name || ''}
variant="outlined"
size="small"
/>
<MySelect
label="Type"
options={paramTypesOptions}
value={input.type || 'string'}
onChange={handleTypeChange}
id={`prop-type-${index}-select`}
/>
<TextField
style={{ width: '100%' }}
rows={2}
multiline
label={`Parameter name ${index + 1} description`}
onChange={({ target }) => set({ description: target.value })}
value={input.description || ''}
variant="outlined"
size="small"
/>
<FormControlLabel
control={
<Checkbox
checked={!!input.required}
onChange={() => set({ required: !input.required })}
/>
}
label="Required"
/>
</div>
);
};
StrategyParameter.propTypes = {
input: PropTypes.object,
set: PropTypes.func,
index: PropTypes.number,
};
export default StrategyParameter;

View File

@ -0,0 +1,27 @@
import StrategyParameter from './StrategyParameter/StrategyParameter';
import PropTypes from 'prop-types';
function gerArrayWithEntries(num) {
return Array.from(Array(num));
}
const StrategyParameters = ({ input = [], count = 0, updateParameter }) => (
<div>
{gerArrayWithEntries(count).map((v, i) => (
<StrategyParameter
key={i}
set={v => updateParameter(i, v, true)}
index={i}
input={input[i]}
/>
))}
</div>
);
StrategyParameters.propTypes = {
input: PropTypes.array,
updateParameter: PropTypes.func.isRequired,
count: PropTypes.number,
};
export default StrategyParameters;

View File

@ -3,10 +3,15 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createStrategy, updateStrategy } from '../../store/strategy/actions';
import {
createStrategy,
updateStrategy,
} from '../../../store/strategy/actions';
import AddStrategy from './form-strategy';
import { loadNameFromHash } from '../common/util';
import CreateStrategy from './CreateStrategy';
import { loadNameFromHash } from '../../common/util';
const STRATEGY_EXIST_ERROR = 'Error: Strategy with name';
class WrapperComponent extends Component {
constructor(props) {
@ -18,6 +23,10 @@ class WrapperComponent extends Component {
};
}
clearErrors = () => {
this.setState({ errors: {} });
};
appParameter = () => {
const { strategy } = this.state;
strategy.parameters = [...strategy.parameters, {}];
@ -47,26 +56,49 @@ class WrapperComponent extends Component {
onSubmit = async evt => {
evt.preventDefault();
const { createStrategy, updateStrategy, history, editMode } = this.props;
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,
}));
.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 {
await createStrategy(strategy);
history.push(`/strategies`);
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 ',
},
});
}
}
}
};
@ -84,7 +116,7 @@ class WrapperComponent extends Component {
render() {
return (
<AddStrategy
<CreateStrategy
onSubmit={this.onSubmit}
onCancel={this.onCancel}
setValue={this.setValue}
@ -93,6 +125,7 @@ class WrapperComponent extends Component {
input={this.state.strategy}
errors={this.state.errors}
editMode={this.props.editMode}
clearErrors={this.clearErrors}
/>
);
}
@ -110,11 +143,16 @@ const mapDispatchToProps = { createStrategy, updateStrategy };
const mapStateToProps = (state, props) => {
const { strategy, editMode } = props;
return {
strategy: strategy ? strategy : { name: loadNameFromHash(), description: '', parameters: [] },
strategy: strategy
? strategy
: { name: loadNameFromHash(), description: '', parameters: [] },
editMode,
};
};
const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(WrapperComponent);
const FormAddContainer = connect(
mapStateToProps,
mapDispatchToProps
)(WrapperComponent);
export default FormAddContainer;

View File

@ -1,165 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Typography, TextField, FormControlLabel, Checkbox, Button, Icon } from '@material-ui/core';
import MySelect from '../common/select';
import PageContent from '../common/PageContent/PageContent';
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
import { styles as commonStyles, FormButtons } from '../common';
import { trim } from '../common/util';
function gerArrayWithEntries(num) {
return Array.from(Array(num));
}
const paramTypesOptions = [
{ key: 'string', label: 'string' },
{ key: 'percentage', label: 'percentage' },
{ key: 'list', label: 'list' },
{ key: 'number', label: 'number' },
{ key: 'boolean', label: 'boolean' },
];
const Parameter = ({ set, input = {}, index }) => {
const handleTypeChange = event => {
set({ type: event.target.value });
};
return (
<div className={commonStyles.contentSpacing}>
<TextField
style={{ width: '50%', marginRight: '5px' }}
label={`Parameter name ${index + 1}`}
onChange={({ target }) => set({ name: target.value }, true)}
value={input.name || ''}
variant="outlined"
size="small"
/>
<MySelect
label="Type"
options={paramTypesOptions}
value={input.type || 'string'}
onChange={handleTypeChange}
id={`prop-type-${index}-select`}
/>
<TextField
style={{ width: '100%' }}
rows={2}
multiline
label={`Parameter name ${index + 1} description`}
onChange={({ target }) => set({ description: target.value })}
value={input.description || ''}
variant="outlined"
size="small"
/>
<FormControlLabel
control={<Checkbox checked={!!input.required} onChange={() => set({ required: !input.required })} />}
label="Required"
/>
</div>
);
};
Parameter.propTypes = {
input: PropTypes.object,
set: PropTypes.func,
index: PropTypes.number,
};
const Parameters = ({ input = [], count = 0, updateParameter }) => (
<div>
{gerArrayWithEntries(count).map((v, i) => (
<Parameter key={i} set={v => updateParameter(i, v, true)} index={i} input={input[i]} />
))}
</div>
);
Parameters.propTypes = {
input: PropTypes.array,
updateParameter: PropTypes.func.isRequired,
count: PropTypes.number,
};
class AddStrategy extends Component {
static propTypes = {
input: PropTypes.object,
setValue: PropTypes.func,
appParameter: PropTypes.func,
updateParameter: PropTypes.func,
clear: PropTypes.func,
onCancel: PropTypes.func,
onSubmit: PropTypes.func,
editMode: PropTypes.bool,
};
getHeaderTitle = () => {
const { editMode } = this.props;
if (editMode) return 'Edit strategy';
return 'Create a new strategy';
};
render() {
const { input, setValue, appParameter, onCancel, editMode = false, onSubmit, updateParameter } = this.props;
return (
<PageContent headerContent={this.getHeaderTitle()}>
<ConditionallyRender
condition={editMode}
show={
<Typography variant="p">
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}
onChange={({ target }) => 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"
/>
<Parameters
input={input.parameters}
count={input.parameters.length}
updateParameter={updateParameter}
/>
<Button
onClick={e => {
e.preventDefault();
appParameter();
}}
startIcon={<Icon>add</Icon>}
>
Add parameter
</Button>
<FormButtons submitText={editMode ? 'Update' : 'Create'} onCancel={onCancel} />
</form>
</PageContent>
);
}
}
export default AddStrategy;

View File

@ -2,7 +2,7 @@ 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 './form-container';
import EditStrategy from './CreateStrategy';
import { UPDATE_STRATEGY } from '../AccessProvider/permissions';
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
import TabNav from '../common/TabNav/TabNav';

View File

@ -1,7 +1,13 @@
interface IAuthStatus {
export interface IAuthStatus {
authDetails: IAuthDetails;
showDialog: boolean;
profile?: IUser;
permissions: IPermission[];
}
export interface IPermission {
permission: string;
project: string;
}
interface IAuthDetails {

View File

@ -1,8 +1,10 @@
import React from 'react';
import AddFeatureToggleForm from '../../component/feature/create/add-feature-container';
import CreateFeature from '../../component/feature/create/CreateFeature';
import PropTypes from 'prop-types';
const render = ({ history }) => <AddFeatureToggleForm title="Create feature toggle" history={history} />;
const render = ({ history }) => (
<CreateFeature title="Create feature toggle" history={history} />
);
render.propTypes = {
history: PropTypes.object.isRequired,

View File

@ -1,5 +1,5 @@
import React from 'react';
import AddStrategies from '../../component/strategies/form-container';
import AddStrategies from '../../component/strategies/CreateStrategy';
import PropTypes from 'prop-types';
const render = ({ history }) => <AddStrategies history={history} />;

View File

@ -9,13 +9,28 @@ import {
UPDATE_FEATURE_TOGGLE,
} from '../feature-toggle/actions';
import { ERROR_UPDATING_STRATEGY, ERROR_CREATING_STRATEGY, ERROR_RECEIVE_STRATEGIES } from '../strategy/actions';
import {
ERROR_UPDATING_STRATEGY,
ERROR_CREATING_STRATEGY,
ERROR_RECEIVE_STRATEGIES,
} from '../strategy/actions';
import { ERROR_ADD_CONTEXT_FIELD, ERROR_UPDATE_CONTEXT_FIELD } from '../context/actions';
import {
ERROR_ADD_CONTEXT_FIELD,
ERROR_UPDATE_CONTEXT_FIELD,
} from '../context/actions';
import { ERROR_REMOVING_PROJECT, ERROR_ADD_PROJECT, ERROR_UPDATE_PROJECT } from '../project/actions';
import {
ERROR_REMOVING_PROJECT,
ERROR_ADD_PROJECT,
ERROR_UPDATE_PROJECT,
} from '../project/actions';
import { ERROR_ADD_ADDON_CONFIG, ERROR_UPDATE_ADDON_CONFIG, ERROR_REMOVING_ADDON_CONFIG } from '../addons/actions'
import {
ERROR_ADD_ADDON_CONFIG,
ERROR_UPDATE_ADDON_CONFIG,
ERROR_REMOVING_ADDON_CONFIG,
} from '../addons/actions';
import { UPDATE_APPLICATION_FIELD } from '../application/actions';
@ -54,12 +69,16 @@ const strategies = (state = getInitState(), action) => {
case ERROR_UPDATE_ADDON_CONFIG:
case ERROR_REMOVING_ADDON_CONFIG:
case ERROR_ADD_PROJECT:
console.log(action);
return addErrorIfNotAlreadyInList(state, action.error.message);
case FORBIDDEN:
return addErrorIfNotAlreadyInList(state, action.error.message || '403 Forbidden');
return addErrorIfNotAlreadyInList(
state,
action.error.message || '403 Forbidden'
);
case MUTE_ERROR:
return state.update('list', list => list.remove(list.indexOf(action.error)));
return state.update('list', list =>
list.remove(list.indexOf(action.error))
);
case UPDATE_FEATURE_TOGGLE:
case UPDATE_FEATURE_TOGGLE_STRATEGIES:
case UPDATE_APPLICATION_FIELD:

View File

@ -20,7 +20,9 @@ const features = (state = new List([]), action) => {
return state.push(new $Map(action.featureToggle));
case REMOVE_FEATURE_TOGGLE:
debug(REMOVE_FEATURE_TOGGLE, action);
return state.filter(toggle => toggle.get('name') !== action.featureToggleName);
return state.filter(
toggle => toggle.get('name') !== action.featureToggleName
);
case TOGGLE_FEATURE_TOGGLE:
debug(TOGGLE_FEATURE_TOGGLE, action);
return state.map(toggle => {
@ -62,7 +64,6 @@ const features = (state = new List([]), action) => {
return new List(action.featureToggles.map($Map));
case USER_LOGIN:
case USER_LOGOUT:
console.log('clear toggle store');
debug(USER_LOGOUT, action);
return new List([]);
default:

View File

@ -64,7 +64,10 @@ export function createStrategy(strategy) {
return api
.create(strategy)
.then(() => dispatch(addStrategy(strategy)))
.catch(dispatchError(dispatch, ERROR_CREATING_STRATEGY));
.catch(e => {
dispatchError(dispatch, ERROR_CREATING_STRATEGY);
throw e;
});
};
}

View File

@ -0,0 +1,36 @@
import { ADMIN } from '../component/AccessProvider/permissions';
import IAuthStatus, { IPermission } from '../interfaces/user';
type objectIdx = {
[key: string]: string;
};
export const projectFilterGenerator = (
user: IAuthStatus,
matcherPermission: string
) => {
let admin = false;
const permissionMap: objectIdx = user.permissions.reduce(
(acc: objectIdx, current: IPermission) => {
if (current.permission === ADMIN) {
admin = true;
}
if (current.permission === matcherPermission) {
acc[current.project] = matcherPermission;
}
return acc;
},
{}
);
return (project: string) => {
if (admin) {
return true;
}
if (permissionMap[project]) {
return true;
}
return false;
};
};