mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-14 01:16:17 +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:
parent
549be832bf
commit
9b1a07c5ab
@ -20,7 +20,7 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUiBootstrap();
|
fetchUiBootstrap();
|
||||||
/* eslint-disable-next-line */
|
/* eslint-disable-next-line */
|
||||||
}, []);
|
}, [user.authDetails?.type]);
|
||||||
|
|
||||||
const renderMainLayoutRoutes = () => {
|
const renderMainLayoutRoutes = () => {
|
||||||
return routes.filter(route => route.layout === 'main').map(renderRoute);
|
return routes.filter(route => route.layout === 'main').map(renderRoute);
|
||||||
|
@ -9,16 +9,13 @@ const ProjectSelect = ({
|
|||||||
projects,
|
projects,
|
||||||
currentProjectId,
|
currentProjectId,
|
||||||
updateCurrentProject,
|
updateCurrentProject,
|
||||||
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const setProject = v => {
|
const setProject = v => {
|
||||||
const id = typeof v === 'string' ? v.trim() : '';
|
const id = typeof v === 'string' ? v.trim() : '';
|
||||||
updateCurrentProject(id);
|
updateCurrentProject(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!projects || projects.length === 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO fixme
|
// TODO fixme
|
||||||
let curentProject = projects.find(i => i.id === currentProjectId);
|
let curentProject = projects.find(i => i.id === currentProjectId);
|
||||||
if (!curentProject) {
|
if (!curentProject) {
|
||||||
@ -57,6 +54,8 @@ const ProjectSelect = ({
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { updateSetting, fetchProjects, ...passDown } = rest;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
@ -66,6 +65,7 @@ const ProjectSelect = ({
|
|||||||
callback={handleChangeProject}
|
callback={handleChangeProject}
|
||||||
renderOptions={renderProjectOptions}
|
renderOptions={renderProjectOptions}
|
||||||
className=""
|
className=""
|
||||||
|
{...passDown}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
@ -6,7 +6,7 @@ import { MenuItemWithIcon } from '../../../common';
|
|||||||
import DropdownMenu from '../../../common/DropdownMenu/DropdownMenu';
|
import DropdownMenu from '../../../common/DropdownMenu/DropdownMenu';
|
||||||
import ProjectSelect from '../../../common/ProjectSelect';
|
import ProjectSelect from '../../../common/ProjectSelect';
|
||||||
import { useStyles } from './styles';
|
import { useStyles } from './styles';
|
||||||
import classnames from 'classnames';
|
import useLoading from '../../../../hooks/useLoading';
|
||||||
|
|
||||||
const sortingOptions = [
|
const sortingOptions = [
|
||||||
{ type: 'name', displayName: 'Name' },
|
{ type: 'name', displayName: 'Name' },
|
||||||
@ -27,6 +27,7 @@ const FeatureToggleListActions = ({
|
|||||||
loading,
|
loading,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
const ref = useLoading(loading);
|
||||||
|
|
||||||
const handleSort = e => {
|
const handleSort = e => {
|
||||||
const target = e.target.getAttribute('data-target');
|
const target = e.target.getAttribute('data-target');
|
||||||
@ -64,14 +65,15 @@ const FeatureToggleListActions = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions} ref={ref}>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
id={'metric'}
|
id={'metric'}
|
||||||
label={`Last ${settings.showLastHour ? 'hour' : 'minute'}`}
|
label={`Last ${settings.showLastHour ? 'hour' : 'minute'}`}
|
||||||
title="Metric interval"
|
title="Metric interval"
|
||||||
callback={toggleMetrics}
|
callback={toggleMetrics}
|
||||||
renderOptions={renderMetricsOptions}
|
renderOptions={renderMetricsOptions}
|
||||||
className={classnames({ skeleton: loading })}
|
className=""
|
||||||
|
data-loading
|
||||||
/>
|
/>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
id={'sorting'}
|
id={'sorting'}
|
||||||
@ -79,9 +81,14 @@ const FeatureToggleListActions = ({
|
|||||||
callback={handleSort}
|
callback={handleSort}
|
||||||
renderOptions={renderSortingOptions}
|
renderOptions={renderSortingOptions}
|
||||||
title="Sort by"
|
title="Sort by"
|
||||||
className={classnames({ skeleton: loading })}
|
className=""
|
||||||
|
data-loading
|
||||||
|
/>
|
||||||
|
<ProjectSelect
|
||||||
|
settings={settings}
|
||||||
|
updateSetting={updateSetting}
|
||||||
|
data-loading
|
||||||
/>
|
/>
|
||||||
<ProjectSelect settings={settings} updateSetting={updateSetting} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -70,6 +70,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
aria-controls="metric"
|
aria-controls="metric"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
||||||
|
data-loading={true}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
id="metric"
|
id="metric"
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
@ -108,6 +109,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
aria-controls="sorting"
|
aria-controls="sorting"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
||||||
|
data-loading={true}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
id="sorting"
|
id="sorting"
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
@ -270,6 +272,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
aria-controls="metric"
|
aria-controls="metric"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
||||||
|
data-loading={true}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
id="metric"
|
id="metric"
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
@ -311,6 +314,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
aria-controls="sorting"
|
aria-controls="sorting"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
className="MuiButtonBase-root MuiButton-root MuiButton-text"
|
||||||
|
data-loading={true}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
id="sorting"
|
id="sorting"
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
|
@ -18,6 +18,7 @@ import FeatureTypeSelect from '../feature-type-select-container';
|
|||||||
import ProjectSelect from '../project-select-container';
|
import ProjectSelect from '../project-select-container';
|
||||||
import UpdateDescriptionComponent from '../view/update-description-component';
|
import UpdateDescriptionComponent from '../view/update-description-component';
|
||||||
import {
|
import {
|
||||||
|
CREATE_FEATURE,
|
||||||
DELETE_FEATURE,
|
DELETE_FEATURE,
|
||||||
UPDATE_FEATURE,
|
UPDATE_FEATURE,
|
||||||
} from '../../AccessProvider/permissions';
|
} from '../../AccessProvider/permissions';
|
||||||
@ -35,6 +36,7 @@ import ConfirmDialogue from '../../common/Dialogue';
|
|||||||
|
|
||||||
import { useCommonStyles } from '../../../common.styles';
|
import { useCommonStyles } from '../../../common.styles';
|
||||||
import AccessContext from '../../../contexts/AccessContext';
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
import { projectFilterGenerator } from '../../../utils/project-filter-generator';
|
||||||
|
|
||||||
const FeatureView = ({
|
const FeatureView = ({
|
||||||
activeTab,
|
activeTab,
|
||||||
@ -53,6 +55,7 @@ const FeatureView = ({
|
|||||||
featureTags,
|
featureTags,
|
||||||
fetchTags,
|
fetchTags,
|
||||||
tagTypes,
|
tagTypes,
|
||||||
|
user,
|
||||||
}) => {
|
}) => {
|
||||||
const isFeatureView = !!fetchFeatureToggles;
|
const isFeatureView = !!fetchFeatureToggles;
|
||||||
const [delDialog, setDelDialog] = useState(false);
|
const [delDialog, setDelDialog] = useState(false);
|
||||||
@ -215,6 +218,7 @@ const FeatureView = ({
|
|||||||
|
|
||||||
const findActiveTab = activeTab =>
|
const findActiveTab = activeTab =>
|
||||||
tabs.findIndex(tab => tab.name === activeTab);
|
tabs.findIndex(tab => tab.name === activeTab);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
className={commonStyles.fullwidth}
|
className={commonStyles.fullwidth}
|
||||||
@ -253,6 +257,10 @@ const FeatureView = ({
|
|||||||
label="Project"
|
label="Project"
|
||||||
filled
|
filled
|
||||||
editable={editable}
|
editable={editable}
|
||||||
|
filter={projectFilterGenerator(
|
||||||
|
user,
|
||||||
|
CREATE_FEATURE
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FeatureTagComponent
|
<FeatureTagComponent
|
||||||
|
@ -10,15 +10,22 @@ import {
|
|||||||
} from '../../../store/feature-toggle/actions';
|
} from '../../../store/feature-toggle/actions';
|
||||||
|
|
||||||
import FeatureView from './FeatureView';
|
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(
|
export default connect(
|
||||||
(state, props) => ({
|
(state, props) => ({
|
||||||
features: state.features.toJS(),
|
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(),
|
featureTags: state.featureTags.toJS(),
|
||||||
tagTypes: state.tagTypes.toJS(),
|
tagTypes: state.tagTypes.toJS(),
|
||||||
activeTab: props.activeTab,
|
activeTab: props.activeTab,
|
||||||
|
user: state.user.toJS(),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
fetchFeatureToggles,
|
fetchFeatureToggles,
|
||||||
|
@ -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;
|
@ -1,9 +1,12 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createFeatureToggles, validateName } from '../../../store/feature-toggle/actions';
|
import {
|
||||||
import AddFeatureComponent from './add-feature-component';
|
createFeatureToggles,
|
||||||
import { loadNameFromHash } from '../../common/util';
|
validateName,
|
||||||
|
} from '../../../../store/feature-toggle/actions';
|
||||||
|
import CreateFeature from './CreateFeature';
|
||||||
|
import { loadNameFromHash } from '../../../common/util';
|
||||||
|
|
||||||
const defaultStrategy = {
|
const defaultStrategy = {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
@ -63,11 +66,17 @@ class WrapperComponent extends Component {
|
|||||||
const { createFeatureToggles, history } = this.props;
|
const { createFeatureToggles, history } = this.props;
|
||||||
const { featureToggle } = this.state;
|
const { featureToggle } = this.state;
|
||||||
|
|
||||||
|
if (Object.keys(this.state.errors)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (featureToggle.strategies < 1) {
|
if (featureToggle.strategies < 1) {
|
||||||
featureToggle.strategies = [defaultStrategy];
|
featureToggle.strategies = [defaultStrategy];
|
||||||
}
|
}
|
||||||
|
|
||||||
createFeatureToggles(featureToggle).then(() => history.push(`/features/strategies/${featureToggle.name}`));
|
createFeatureToggles(featureToggle).then(() =>
|
||||||
|
history.push(`/features/strategies/${featureToggle.name}`)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
onCancel = evt => {
|
onCancel = evt => {
|
||||||
@ -77,7 +86,7 @@ class WrapperComponent extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<AddFeatureComponent
|
<CreateFeature
|
||||||
onSubmit={this.onSubmit}
|
onSubmit={this.onSubmit}
|
||||||
onCancel={this.onCancel}
|
onCancel={this.onCancel}
|
||||||
validateName={this.validateName}
|
validateName={this.validateName}
|
||||||
@ -85,6 +94,7 @@ class WrapperComponent extends Component {
|
|||||||
setStrategies={this.setStrategies}
|
setStrategies={this.setStrategies}
|
||||||
input={this.state.featureToggle}
|
input={this.state.featureToggle}
|
||||||
errors={this.state.errors}
|
errors={this.state.errors}
|
||||||
|
user={this.props.user}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -97,16 +107,20 @@ WrapperComponent.propTypes = {
|
|||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
validateName: name => validateName(name)(dispatch),
|
validateName: name => validateName(name)(dispatch),
|
||||||
createFeatureToggles: featureToggle => createFeatureToggles(featureToggle)(dispatch),
|
createFeatureToggles: featureToggle =>
|
||||||
|
createFeatureToggles(featureToggle)(dispatch),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const settings = state.settings.toJS().feature || {};
|
const settings = state.settings.toJS().feature || {};
|
||||||
const currentProjectId = resolveCurrentProjectId(settings);
|
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;
|
export default FormAddContainer;
|
@ -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;
|
|
@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
.nameInput {
|
.nameInput {
|
||||||
margin-right: 1.5rem;
|
margin-right: 1.5rem;
|
||||||
|
min-width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.formContainer {
|
.formContainer {
|
||||||
|
@ -11,23 +11,43 @@ class ProjectSelectComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { value, projects, onChange, enabled } = this.props;
|
const { value, projects, onChange, enabled, filter } = this.props;
|
||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = projects.map(t => ({
|
const formatOption = project => {
|
||||||
key: t.id,
|
return {
|
||||||
label: t.name,
|
key: project.id,
|
||||||
title: t.description,
|
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)) {
|
if (value && !options.find(o => o.key === value)) {
|
||||||
options.push({ key: value, label: 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,7 +188,7 @@ const AddVariant = ({
|
|||||||
name="name"
|
name="name"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
className={commonStyles.fullWidth}
|
className={commonStyles.fullWidth}
|
||||||
value={data.name}
|
value={data.name || ''}
|
||||||
error={error.name}
|
error={error.name}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
@ -213,7 +213,7 @@ const AddVariant = ({
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
style={{ marginRight: '0.8rem' }}
|
style={{ marginRight: '0.8rem' }}
|
||||||
value={data.weight}
|
value={data.weight || ''}
|
||||||
error={error.weight}
|
error={error.weight}
|
||||||
type="number"
|
type="number"
|
||||||
disabled={!isFixWeight}
|
disabled={!isFixWeight}
|
||||||
|
@ -77,6 +77,7 @@ test('renders correctly with one feature', () => {
|
|||||||
featureToggle={feature}
|
featureToggle={feature}
|
||||||
fetchFeatureToggles={jest.fn()}
|
fetchFeatureToggles={jest.fn()}
|
||||||
history={{}}
|
history={{}}
|
||||||
|
user={{ permissions: [] }}
|
||||||
featureTags={[]}
|
featureTags={[]}
|
||||||
fetchTags={jest.fn()}
|
fetchTags={jest.fn()}
|
||||||
untagFeature={jest.fn()}
|
untagFeature={jest.fn()}
|
||||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -3,10 +3,15 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import { connect } from 'react-redux';
|
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 CreateStrategy from './CreateStrategy';
|
||||||
import { loadNameFromHash } from '../common/util';
|
import { loadNameFromHash } from '../../common/util';
|
||||||
|
|
||||||
|
const STRATEGY_EXIST_ERROR = 'Error: Strategy with name';
|
||||||
|
|
||||||
class WrapperComponent extends Component {
|
class WrapperComponent extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -18,6 +23,10 @@ class WrapperComponent extends Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearErrors = () => {
|
||||||
|
this.setState({ errors: {} });
|
||||||
|
};
|
||||||
|
|
||||||
appParameter = () => {
|
appParameter = () => {
|
||||||
const { strategy } = this.state;
|
const { strategy } = this.state;
|
||||||
strategy.parameters = [...strategy.parameters, {}];
|
strategy.parameters = [...strategy.parameters, {}];
|
||||||
@ -47,26 +56,49 @@ class WrapperComponent extends Component {
|
|||||||
|
|
||||||
onSubmit = async evt => {
|
onSubmit = async evt => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const { createStrategy, updateStrategy, history, editMode } = this.props;
|
const {
|
||||||
|
createStrategy,
|
||||||
|
updateStrategy,
|
||||||
|
history,
|
||||||
|
editMode,
|
||||||
|
} = this.props;
|
||||||
const { strategy } = this.state;
|
const { strategy } = this.state;
|
||||||
|
|
||||||
const parameters = (strategy.parameters || [])
|
const parameters = (strategy.parameters || [])
|
||||||
.filter(({ name }) => !!name)
|
.filter(({ name }) => !!name)
|
||||||
.map(({ name, type = 'string', description = '', required = false }) => ({
|
.map(
|
||||||
|
({
|
||||||
|
name,
|
||||||
|
type = 'string',
|
||||||
|
description = '',
|
||||||
|
required = false,
|
||||||
|
}) => ({
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
description,
|
description,
|
||||||
required,
|
required,
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
|
|
||||||
strategy.parameters = parameters;
|
strategy.parameters = parameters;
|
||||||
|
|
||||||
if (editMode) {
|
if (editMode) {
|
||||||
await updateStrategy(strategy);
|
await updateStrategy(strategy);
|
||||||
|
|
||||||
history.push(`/strategies/view/${strategy.name}`);
|
history.push(`/strategies/view/${strategy.name}`);
|
||||||
} else {
|
} else {
|
||||||
|
try {
|
||||||
await createStrategy(strategy);
|
await createStrategy(strategy);
|
||||||
history.push(`/strategies`);
|
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() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<AddStrategy
|
<CreateStrategy
|
||||||
onSubmit={this.onSubmit}
|
onSubmit={this.onSubmit}
|
||||||
onCancel={this.onCancel}
|
onCancel={this.onCancel}
|
||||||
setValue={this.setValue}
|
setValue={this.setValue}
|
||||||
@ -93,6 +125,7 @@ class WrapperComponent extends Component {
|
|||||||
input={this.state.strategy}
|
input={this.state.strategy}
|
||||||
errors={this.state.errors}
|
errors={this.state.errors}
|
||||||
editMode={this.props.editMode}
|
editMode={this.props.editMode}
|
||||||
|
clearErrors={this.clearErrors}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -110,11 +143,16 @@ const mapDispatchToProps = { createStrategy, updateStrategy };
|
|||||||
const mapStateToProps = (state, props) => {
|
const mapStateToProps = (state, props) => {
|
||||||
const { strategy, editMode } = props;
|
const { strategy, editMode } = props;
|
||||||
return {
|
return {
|
||||||
strategy: strategy ? strategy : { name: loadNameFromHash(), description: '', parameters: [] },
|
strategy: strategy
|
||||||
|
? strategy
|
||||||
|
: { name: loadNameFromHash(), description: '', parameters: [] },
|
||||||
editMode,
|
editMode,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(WrapperComponent);
|
const FormAddContainer = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(WrapperComponent);
|
||||||
|
|
||||||
export default FormAddContainer;
|
export default FormAddContainer;
|
@ -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;
|
|
@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Grid, Typography } from '@material-ui/core';
|
import { Grid, Typography } from '@material-ui/core';
|
||||||
import ShowStrategy from './show-strategy-component';
|
import ShowStrategy from './show-strategy-component';
|
||||||
import EditStrategy from './form-container';
|
import EditStrategy from './CreateStrategy';
|
||||||
import { UPDATE_STRATEGY } from '../AccessProvider/permissions';
|
import { UPDATE_STRATEGY } from '../AccessProvider/permissions';
|
||||||
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
||||||
import TabNav from '../common/TabNav/TabNav';
|
import TabNav from '../common/TabNav/TabNav';
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
interface IAuthStatus {
|
export interface IAuthStatus {
|
||||||
authDetails: IAuthDetails;
|
authDetails: IAuthDetails;
|
||||||
showDialog: boolean;
|
showDialog: boolean;
|
||||||
profile?: IUser;
|
profile?: IUser;
|
||||||
|
permissions: IPermission[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPermission {
|
||||||
|
permission: string;
|
||||||
|
project: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAuthDetails {
|
interface IAuthDetails {
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import React from 'react';
|
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';
|
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 = {
|
render.propTypes = {
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AddStrategies from '../../component/strategies/form-container';
|
import AddStrategies from '../../component/strategies/CreateStrategy';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const render = ({ history }) => <AddStrategies history={history} />;
|
const render = ({ history }) => <AddStrategies history={history} />;
|
||||||
|
@ -9,13 +9,28 @@ import {
|
|||||||
UPDATE_FEATURE_TOGGLE,
|
UPDATE_FEATURE_TOGGLE,
|
||||||
} from '../feature-toggle/actions';
|
} 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';
|
import { UPDATE_APPLICATION_FIELD } from '../application/actions';
|
||||||
|
|
||||||
@ -54,12 +69,16 @@ const strategies = (state = getInitState(), action) => {
|
|||||||
case ERROR_UPDATE_ADDON_CONFIG:
|
case ERROR_UPDATE_ADDON_CONFIG:
|
||||||
case ERROR_REMOVING_ADDON_CONFIG:
|
case ERROR_REMOVING_ADDON_CONFIG:
|
||||||
case ERROR_ADD_PROJECT:
|
case ERROR_ADD_PROJECT:
|
||||||
console.log(action);
|
|
||||||
return addErrorIfNotAlreadyInList(state, action.error.message);
|
return addErrorIfNotAlreadyInList(state, action.error.message);
|
||||||
case FORBIDDEN:
|
case FORBIDDEN:
|
||||||
return addErrorIfNotAlreadyInList(state, action.error.message || '403 Forbidden');
|
return addErrorIfNotAlreadyInList(
|
||||||
|
state,
|
||||||
|
action.error.message || '403 Forbidden'
|
||||||
|
);
|
||||||
case MUTE_ERROR:
|
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:
|
||||||
case UPDATE_FEATURE_TOGGLE_STRATEGIES:
|
case UPDATE_FEATURE_TOGGLE_STRATEGIES:
|
||||||
case UPDATE_APPLICATION_FIELD:
|
case UPDATE_APPLICATION_FIELD:
|
||||||
|
@ -20,7 +20,9 @@ const features = (state = new List([]), action) => {
|
|||||||
return state.push(new $Map(action.featureToggle));
|
return state.push(new $Map(action.featureToggle));
|
||||||
case REMOVE_FEATURE_TOGGLE:
|
case REMOVE_FEATURE_TOGGLE:
|
||||||
debug(REMOVE_FEATURE_TOGGLE, action);
|
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:
|
case TOGGLE_FEATURE_TOGGLE:
|
||||||
debug(TOGGLE_FEATURE_TOGGLE, action);
|
debug(TOGGLE_FEATURE_TOGGLE, action);
|
||||||
return state.map(toggle => {
|
return state.map(toggle => {
|
||||||
@ -62,7 +64,6 @@ const features = (state = new List([]), action) => {
|
|||||||
return new List(action.featureToggles.map($Map));
|
return new List(action.featureToggles.map($Map));
|
||||||
case USER_LOGIN:
|
case USER_LOGIN:
|
||||||
case USER_LOGOUT:
|
case USER_LOGOUT:
|
||||||
console.log('clear toggle store');
|
|
||||||
debug(USER_LOGOUT, action);
|
debug(USER_LOGOUT, action);
|
||||||
return new List([]);
|
return new List([]);
|
||||||
default:
|
default:
|
||||||
|
@ -64,7 +64,10 @@ export function createStrategy(strategy) {
|
|||||||
return api
|
return api
|
||||||
.create(strategy)
|
.create(strategy)
|
||||||
.then(() => dispatch(addStrategy(strategy)))
|
.then(() => dispatch(addStrategy(strategy)))
|
||||||
.catch(dispatchError(dispatch, ERROR_CREATING_STRATEGY));
|
.catch(e => {
|
||||||
|
dispatchError(dispatch, ERROR_CREATING_STRATEGY);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
36
frontend/src/utils/project-filter-generator.ts
Normal file
36
frontend/src/utils/project-filter-generator.ts
Normal 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;
|
||||||
|
};
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user