mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
Feat: clone feature toggle configuration (#201)
Create a new feature toggle by cloning the config of an existing feature toggle. This feature alos moves away from the input store for the "create feature toggle form".
This commit is contained in:
parent
ff8ce52347
commit
19443c651f
@ -2,7 +2,8 @@
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"],
|
||||
"plugins": [
|
||||
["@babel/plugin-proposal-decorators", { "legacy": true }],
|
||||
"@babel/plugin-proposal-class-properties"
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-runtime"
|
||||
],
|
||||
"env": {
|
||||
"test": {
|
||||
|
@ -44,8 +44,10 @@
|
||||
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
||||
"@babel/plugin-proposal-decorators": "^7.6.0",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.6.0",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.6.2",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"array-move": "^2.2.1",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-jest": "^24.9.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
|
@ -19,6 +19,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
}
|
||||
>
|
||||
Another
|
||||
|
||||
</react-mdl-CardTitle>
|
||||
<react-mdl-CardText>
|
||||
another's description
|
||||
@ -56,17 +57,36 @@ exports[`renders correctly with one feature 1`] = `
|
||||
Disabled
|
||||
</react-mdl-Switch>
|
||||
</span>
|
||||
<react-mdl-Button
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"flexShrink": 0,
|
||||
<div>
|
||||
<a
|
||||
href="/features/copy/Another"
|
||||
onClick={[Function]}
|
||||
title="Create new feature toggle by cloning configuration"
|
||||
>
|
||||
<react-mdl-Button
|
||||
style={
|
||||
Object {
|
||||
"flexShrink": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
Clone
|
||||
</react-mdl-Button>
|
||||
</a>
|
||||
<react-mdl-Button
|
||||
accent={true}
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"flexShrink": 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
>
|
||||
Archive
|
||||
</react-mdl-Button>
|
||||
title="Archive feature toggle"
|
||||
>
|
||||
Archive
|
||||
</react-mdl-Button>
|
||||
</div>
|
||||
</react-mdl-CardActions>
|
||||
<hr />
|
||||
<react-mdl-Tabs
|
||||
|
@ -18,9 +18,11 @@ exports[`render the create feature page 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
Create feature toggle
|
||||
Create new feature toggle
|
||||
</react-mdl-CardTitle>
|
||||
<form>
|
||||
<form
|
||||
onSubmit={[MockFunction]}
|
||||
>
|
||||
<section
|
||||
style={
|
||||
Object {
|
||||
@ -29,20 +31,17 @@ exports[`render the create feature page 1`] = `
|
||||
}
|
||||
>
|
||||
<react-mdl-Textfield
|
||||
error={Object {}}
|
||||
floatingLabel={true}
|
||||
label="Name"
|
||||
name="name"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
value="feature"
|
||||
/>
|
||||
<react-mdl-Textfield
|
||||
floatingLabel={true}
|
||||
label="Description"
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
rows={1}
|
||||
style={
|
||||
Object {
|
||||
@ -59,11 +58,13 @@ exports[`render the create feature page 1`] = `
|
||||
>
|
||||
Enabled
|
||||
</react-mdl-Switch>
|
||||
<hr />
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
<StrategiesSection
|
||||
addStrategy={[MockFunction]}
|
||||
configuredStrategies={Array []}
|
||||
featureToggleName="feature"
|
||||
moveStrategy={[MockFunction]}
|
||||
removeStrategy={[MockFunction]}
|
||||
updateStrategy={[MockFunction]}
|
||||
|
@ -8,13 +8,14 @@ jest.mock('../strategies-section-container', () => 'StrategiesSection');
|
||||
it('render the create feature page', () => {
|
||||
let input = {
|
||||
name: 'feature',
|
||||
nameError: {},
|
||||
description: 'Description',
|
||||
enabled: false,
|
||||
};
|
||||
let errors = {};
|
||||
const tree = shallow(
|
||||
<AddFeatureComponent
|
||||
input={input}
|
||||
errors={errors}
|
||||
onSubmit={jest.fn()}
|
||||
setValue={jest.fn()}
|
||||
addStrategy={jest.fn()}
|
||||
@ -32,10 +33,10 @@ it('render the create feature page', () => {
|
||||
|
||||
let input = {
|
||||
name: 'feature',
|
||||
nameError: {},
|
||||
description: 'Description',
|
||||
enabled: false,
|
||||
};
|
||||
let errors = {};
|
||||
|
||||
let validateName = jest.fn();
|
||||
let setValue = jest.fn();
|
||||
@ -56,6 +57,7 @@ let eventMock = {
|
||||
const buildComponent = (setValue, validateName) => (
|
||||
<AddFeatureComponent
|
||||
input={input}
|
||||
errors={errors}
|
||||
onSubmit={onSubmit}
|
||||
setValue={setValue}
|
||||
addStrategy={addStrategy}
|
||||
|
@ -8,7 +8,7 @@ jest.mock('../strategies-section-container', () => 'StrategiesSection');
|
||||
it('render the create feature page', () => {
|
||||
let input = {
|
||||
name: 'feature',
|
||||
nameError: {},
|
||||
errors: {},
|
||||
description: 'Description',
|
||||
enabled: false,
|
||||
};
|
||||
|
@ -5,27 +5,18 @@ import StrategiesSection from './strategies-section-container';
|
||||
|
||||
import { FormButtons } from './../../common';
|
||||
import { styles as commonStyles } from '../../common';
|
||||
|
||||
const trim = value => {
|
||||
if (value && value.trim) {
|
||||
return value.trim();
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
import { trim } from './util';
|
||||
|
||||
class AddFeatureComponent extends Component {
|
||||
// static displayName = `AddFeatureComponent-${getDisplayName(Component)}`;
|
||||
componentWillMount() {
|
||||
// TODO unwind this stuff
|
||||
if (this.props.initCallRequired === true) {
|
||||
this.props.init(this.props.input);
|
||||
}
|
||||
componentDidMount() {
|
||||
window.onbeforeunload = () => 'Data will be lost if you leave the page, are you sure?';
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
input,
|
||||
errors,
|
||||
setValue,
|
||||
validateName,
|
||||
addStrategy,
|
||||
@ -36,26 +27,19 @@ class AddFeatureComponent extends Component {
|
||||
onCancel,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
name, // eslint-disable-line
|
||||
nameError,
|
||||
description,
|
||||
enabled,
|
||||
} = input;
|
||||
const configuredStrategies = input.strategies || [];
|
||||
|
||||
return (
|
||||
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||
<CardTitle style={{ paddingTop: '24px', wordBreak: 'break-all' }}>Create feature toggle</CardTitle>
|
||||
<form onSubmit={onSubmit(input)}>
|
||||
<CardTitle style={{ paddingTop: '24px', wordBreak: 'break-all' }}>Create new feature toggle</CardTitle>
|
||||
<form onSubmit={onSubmit}>
|
||||
<section style={{ padding: '16px' }}>
|
||||
<Textfield
|
||||
floatingLabel
|
||||
label="Name"
|
||||
name="name"
|
||||
required
|
||||
value={name}
|
||||
error={nameError}
|
||||
value={input.name}
|
||||
error={errors.name}
|
||||
onBlur={v => validateName(v.target.value)}
|
||||
onChange={v => setValue('name', trim(v.target.value))}
|
||||
/>
|
||||
@ -64,30 +48,34 @@ class AddFeatureComponent extends Component {
|
||||
style={{ width: '100%' }}
|
||||
rows={1}
|
||||
label="Description"
|
||||
required
|
||||
value={description}
|
||||
error={errors.description}
|
||||
value={input.description}
|
||||
onChange={v => setValue('description', v.target.value)}
|
||||
/>
|
||||
<div>
|
||||
<br />
|
||||
<Switch
|
||||
checked={enabled}
|
||||
checked={input.enabled}
|
||||
onChange={() => {
|
||||
setValue('enabled', !enabled);
|
||||
setValue('enabled', !input.enabled);
|
||||
}}
|
||||
>
|
||||
Enabled
|
||||
</Switch>
|
||||
<hr />
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<StrategiesSection
|
||||
configuredStrategies={configuredStrategies}
|
||||
addStrategy={addStrategy}
|
||||
updateStrategy={updateStrategy}
|
||||
moveStrategy={moveStrategy}
|
||||
removeStrategy={removeStrategy}
|
||||
/>
|
||||
{input.name ? (
|
||||
<StrategiesSection
|
||||
configuredStrategies={configuredStrategies}
|
||||
featureToggleName={input.name}
|
||||
addStrategy={addStrategy}
|
||||
updateStrategy={updateStrategy}
|
||||
moveStrategy={moveStrategy}
|
||||
removeStrategy={removeStrategy}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<br />
|
||||
</section>
|
||||
@ -102,6 +90,7 @@ class AddFeatureComponent extends Component {
|
||||
|
||||
AddFeatureComponent.propTypes = {
|
||||
input: PropTypes.object,
|
||||
errors: PropTypes.object,
|
||||
setValue: PropTypes.func.isRequired,
|
||||
addStrategy: PropTypes.func.isRequired,
|
||||
removeStrategy: PropTypes.func.isRequired,
|
||||
|
@ -1,78 +1,122 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import arrayMove from 'array-move';
|
||||
import { createFeatureToggles, validateName } from './../../../store/feature-actions';
|
||||
import { createMapper, createActions } from './../../input-helpers';
|
||||
import AddFeatureComponent from './form-add-feature-component';
|
||||
|
||||
const defaultStrategy = { name: 'default' };
|
||||
|
||||
const ID = 'add-feature-toggle';
|
||||
const mapStateToProps = createMapper({
|
||||
id: ID,
|
||||
getDefault() {
|
||||
let name;
|
||||
class WrapperComponent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
featureToggle: { strategies: [], enabled: true },
|
||||
errors: {},
|
||||
dirty: false,
|
||||
};
|
||||
}
|
||||
|
||||
setValue = (field, value) => {
|
||||
const { featureToggle } = this.state;
|
||||
featureToggle[field] = value;
|
||||
this.setState({ featureToggle, dirty: true });
|
||||
};
|
||||
|
||||
validateName = async featureToggleName => {
|
||||
const { errors } = this.state;
|
||||
try {
|
||||
[, name] = document.location.hash.match(/name=([a-z0-9-_.]+)/i);
|
||||
} catch (e) {
|
||||
// hide error
|
||||
await validateName(featureToggleName);
|
||||
errors.name = undefined;
|
||||
} catch (err) {
|
||||
errors.name = err.message;
|
||||
}
|
||||
return { name };
|
||||
},
|
||||
});
|
||||
const prepare = (methods, dispatch, ownProps) => {
|
||||
methods.onSubmit = input => e => {
|
||||
e.preventDefault();
|
||||
|
||||
input.createdAt = new Date();
|
||||
this.setState({ errors });
|
||||
};
|
||||
|
||||
if (Array.isArray(input.strategies)) {
|
||||
input.strategies.forEach(s => {
|
||||
addStrategy = strat => {
|
||||
strat.id = Math.round(Math.random() * 10000000);
|
||||
const { featureToggle } = this.state;
|
||||
const strategies = featureToggle.strategies.concat(strat);
|
||||
featureToggle.strategies = strategies;
|
||||
this.setState({ featureToggle, dirty: true });
|
||||
};
|
||||
|
||||
moveStrategy = (index, toIndex) => {
|
||||
const { featureToggle } = this.state;
|
||||
const strategies = arrayMove(featureToggle.strategies, index, toIndex);
|
||||
featureToggle.strategies = strategies;
|
||||
this.setState({ featureToggle, dirty: true });
|
||||
};
|
||||
|
||||
removeStrategy = index => {
|
||||
const { featureToggle } = this.state;
|
||||
const strategies = featureToggle.strategies.filter((_, i) => i !== index);
|
||||
featureToggle.strategies = strategies;
|
||||
this.setState({ featureToggle, dirty: true });
|
||||
};
|
||||
|
||||
updateStrategy = (index, strat) => {
|
||||
const { featureToggle } = this.state;
|
||||
const strategies = featureToggle.strategies.concat();
|
||||
strategies[index] = strat;
|
||||
featureToggle.strategies = strategies;
|
||||
this.setState({ featureToggle, dirty: true });
|
||||
};
|
||||
|
||||
onSubmit = evt => {
|
||||
evt.preventDefault();
|
||||
const { createFeatureToggles, history } = this.props;
|
||||
const { featureToggle } = this.state;
|
||||
featureToggle.createdAt = new Date();
|
||||
|
||||
if (Array.isArray(featureToggle.strategies)) {
|
||||
featureToggle.strategies.forEach(s => {
|
||||
delete s.id;
|
||||
});
|
||||
} else {
|
||||
input.strategies = [defaultStrategy];
|
||||
featureToggle.strategies = [defaultStrategy];
|
||||
}
|
||||
|
||||
createFeatureToggles(input)(dispatch)
|
||||
.then(() => methods.clear())
|
||||
.then(() => ownProps.history.push(`/features/strategies/${input.name}`));
|
||||
createFeatureToggles(featureToggle).then(() => history.push(`/features/strategies/${featureToggle.name}`));
|
||||
};
|
||||
|
||||
methods.onCancel = evt => {
|
||||
onCancel = evt => {
|
||||
evt.preventDefault();
|
||||
methods.clear();
|
||||
ownProps.history.push('/features');
|
||||
this.props.history.push('/features');
|
||||
};
|
||||
|
||||
methods.addStrategy = v => {
|
||||
v.id = Math.round(Math.random() * 10000000);
|
||||
methods.pushToList('strategies', v);
|
||||
};
|
||||
|
||||
methods.updateStrategy = (index, n) => {
|
||||
methods.updateInList('strategies', index, n);
|
||||
};
|
||||
|
||||
methods.moveStrategy = (index, toIndex) => {
|
||||
methods.moveItem('strategies', index, toIndex);
|
||||
};
|
||||
|
||||
methods.removeStrategy = index => {
|
||||
methods.removeFromList('strategies', index);
|
||||
};
|
||||
|
||||
methods.validateName = featureToggleName => {
|
||||
validateName(featureToggleName)
|
||||
.then(() => methods.setValue('nameError', undefined))
|
||||
.catch(err => methods.setValue('nameError', err.message));
|
||||
};
|
||||
|
||||
return methods;
|
||||
render() {
|
||||
return (
|
||||
<AddFeatureComponent
|
||||
onSubmit={this.onSubmit}
|
||||
onCancel={this.onCancel}
|
||||
addStrategy={this.addStrategy}
|
||||
updateStrategy={this.updateStrategy}
|
||||
removeStrategy={this.removeStrategy}
|
||||
moveStrategy={this.moveStrategy}
|
||||
validateName={this.validateName}
|
||||
setValue={this.setValue}
|
||||
input={this.state.featureToggle}
|
||||
errors={this.state.errors}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
WrapperComponent.propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
createFeatureToggles: PropTypes.func.isRequired,
|
||||
};
|
||||
const actions = createActions({ id: ID, prepare });
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
validateName: name => validateName(name)(dispatch),
|
||||
createFeatureToggles: featureToggle => createFeatureToggles(featureToggle)(dispatch),
|
||||
});
|
||||
|
||||
const FormAddContainer = connect(
|
||||
mapStateToProps,
|
||||
actions
|
||||
)(AddFeatureComponent);
|
||||
() => {},
|
||||
mapDispatchToProps
|
||||
)(WrapperComponent);
|
||||
|
||||
export default FormAddContainer;
|
||||
|
@ -0,0 +1,139 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Button, Icon, Textfield, Checkbox, Card, CardTitle, CardActions } from 'react-mdl';
|
||||
|
||||
import { styles as commonStyles } from '../../common';
|
||||
|
||||
import { trim } from './util';
|
||||
|
||||
class CopyFeatureComponent extends Component {
|
||||
// static displayName = `AddFeatureComponent-${getDisplayName(Component)}`;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = { newToggleName: '', replaceGroupId: true };
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
// TODO unwind this stuff
|
||||
if (this.props.copyToggle) {
|
||||
this.setState({ featureToggle: this.props.copyToggle });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.copyToggle) {
|
||||
this.refs.name.inputRef.focus();
|
||||
} else {
|
||||
this.props.fetchFeatureToggles();
|
||||
}
|
||||
}
|
||||
|
||||
setValue = evt => {
|
||||
const value = trim(evt.target.value);
|
||||
this.setState({ newToggleName: value });
|
||||
};
|
||||
|
||||
toggleReplaceGroupId = () => {
|
||||
const { replaceGroupId } = !!this.state;
|
||||
this.setState({ replaceGroupId });
|
||||
};
|
||||
|
||||
onValidateName = async () => {
|
||||
const { newToggleName } = this.state;
|
||||
try {
|
||||
await this.props.validateName(newToggleName);
|
||||
this.setState({ nameError: undefined });
|
||||
} catch (err) {
|
||||
this.setState({ nameError: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
onSubmit = evt => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { nameError, newToggleName, replaceGroupId } = this.state;
|
||||
if (nameError) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { copyToggle, history } = this.props;
|
||||
|
||||
copyToggle.name = newToggleName;
|
||||
|
||||
if (replaceGroupId) {
|
||||
copyToggle.strategies.forEach(s => {
|
||||
if (s.parameters.groupId) {
|
||||
s.parameters.groupId = newToggleName;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.props.createFeatureToggle(copyToggle).then(() => history.push(`/features/strategies/${copyToggle.name}`));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { copyToggle } = this.props;
|
||||
|
||||
if (!copyToggle) return <span>Toggle not found</span>;
|
||||
|
||||
const { newToggleName, nameError, replaceGroupId } = this.state;
|
||||
|
||||
return (
|
||||
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||
<CardTitle style={{ paddingTop: '24px', wordBreak: 'break-all' }}>
|
||||
Copy {copyToggle.name}
|
||||
</CardTitle>
|
||||
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<section style={{ padding: '16px' }}>
|
||||
<p>
|
||||
You are about to create a new feature toggle by cloning the configuration of feature
|
||||
toggle
|
||||
<Link to={`/features/strategies/${copyToggle.name}`}>{copyToggle.name}</Link>. You must give
|
||||
the new feature toggle a unique name before you can proceed.
|
||||
</p>
|
||||
<Textfield
|
||||
floatingLabel
|
||||
label="Feature toggle name"
|
||||
name="name"
|
||||
value={newToggleName}
|
||||
error={nameError}
|
||||
onBlur={this.onValidateName}
|
||||
onChange={this.setValue}
|
||||
ref="name"
|
||||
/>
|
||||
<br />
|
||||
<br />
|
||||
<Checkbox
|
||||
checked={replaceGroupId}
|
||||
label="Replace groupId"
|
||||
onChange={this.toggleReplaceGroupId}
|
||||
/>
|
||||
<br />
|
||||
</section>
|
||||
<CardActions>
|
||||
<Button type="submit" ripple raised primary>
|
||||
<Icon name="file_copy" />
|
||||
Copy feature toggle
|
||||
</Button>
|
||||
<br />
|
||||
</CardActions>
|
||||
</form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CopyFeatureComponent.propTypes = {
|
||||
copyToggle: PropTypes.object,
|
||||
history: PropTypes.object.isRequired,
|
||||
createFeatureToggle: PropTypes.func.isRequired,
|
||||
fetchFeatureToggles: PropTypes.func.isRequired,
|
||||
validateName: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CopyFeatureComponent;
|
@ -0,0 +1,21 @@
|
||||
import { connect } from 'react-redux';
|
||||
import CopyFeatureComponent from './form-copy-feature-component';
|
||||
import { createFeatureToggles, validateName, fetchFeatureToggles } from './../../../store/feature-actions';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
history: props.history,
|
||||
copyToggle: state.features.toJS().find(toggle => toggle.name === props.copyToggleName),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
validateName,
|
||||
createFeatureToggle: featureToggle => createFeatureToggles(featureToggle)(dispatch),
|
||||
fetchFeatureToggles: () => fetchFeatureToggles()(dispatch),
|
||||
});
|
||||
|
||||
const FormAddContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(CopyFeatureComponent);
|
||||
|
||||
export default FormAddContainer;
|
@ -3,8 +3,9 @@ import StrategiesSectionComponent from './strategies-section';
|
||||
import { fetchStrategies } from '../../../store/strategy/actions';
|
||||
|
||||
const StrategiesSection = connect(
|
||||
state => ({
|
||||
(state, ownProps) => ({
|
||||
strategies: state.strategies.get('list').toArray(),
|
||||
configuredStrategies: ownProps.configuredStrategies,
|
||||
}),
|
||||
{ fetchStrategies }
|
||||
)(StrategiesSectionComponent);
|
||||
|
7
frontend/src/component/feature/form/util.js
Normal file
7
frontend/src/component/feature/form/util.js
Normal file
@ -0,0 +1,7 @@
|
||||
export const trim = value => {
|
||||
if (value && value.trim) {
|
||||
return value.trim();
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
};
|
@ -147,7 +147,7 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
||||
|
||||
return (
|
||||
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||
<CardTitle style={{ wordBreak: 'break-all', paddingBottom: 0 }}>{featureToggle.name}</CardTitle>
|
||||
<CardTitle style={{ wordBreak: 'break-all', paddingBottom: 0 }}>{featureToggle.name} </CardTitle>
|
||||
<UpdateDescriptionComponent
|
||||
isFeatureView={this.isFeatureView}
|
||||
description={featureToggle.description}
|
||||
@ -181,13 +181,23 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
||||
</span>
|
||||
|
||||
{this.isFeatureView ? (
|
||||
<Button
|
||||
disabled={!hasPermission(DELETE_FEATURE)}
|
||||
onClick={removeToggle}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
Archive
|
||||
</Button>
|
||||
<div>
|
||||
<Link
|
||||
to={`/features/copy/${featureToggle.name}`}
|
||||
title="Create new feature toggle by cloning configuration"
|
||||
>
|
||||
<Button style={{ flexShrink: 0 }}>Clone</Button>
|
||||
</Link>
|
||||
<Button
|
||||
disabled={!hasPermission(DELETE_FEATURE)}
|
||||
onClick={removeToggle}
|
||||
title="Archive feature toggle"
|
||||
accent
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
Archive
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
disabled={!hasPermission(UPDATE_FEATURE)}
|
||||
|
@ -49,6 +49,12 @@ Array [
|
||||
"path": "/features/create",
|
||||
"title": "Create",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"parent": "/features",
|
||||
"path": "/features/copy/:copyToggle",
|
||||
"title": "Copy",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"parent": "/features",
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { routes, baseRoutes, getRoute } from '../routes';
|
||||
|
||||
test('returns all defined routes', () => {
|
||||
expect(routes.length).toEqual(13);
|
||||
expect(routes.length).toEqual(14);
|
||||
expect(routes).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import CreateFeatureToggle from '../../page/features/create';
|
||||
import CopyFeatureToggle from '../../page/features/copy';
|
||||
import ViewFeatureToggle from '../../page/features/show';
|
||||
import Features from '../../page/features';
|
||||
import CreateStrategies from '../../page/strategies/create';
|
||||
@ -15,6 +16,7 @@ import LogoutFeatures from '../../page/user/logout';
|
||||
export const routes = [
|
||||
// Features
|
||||
{ path: '/features/create', parent: '/features', title: 'Create', component: CreateFeatureToggle },
|
||||
{ path: '/features/copy/:copyToggle', parent: '/features', title: 'Copy', component: CopyFeatureToggle },
|
||||
{ path: '/features/:activeTab/:name', parent: '/features', title: ':name', component: ViewFeatureToggle },
|
||||
{ path: '/features', title: 'Feature Toggles', icon: 'list', component: Features },
|
||||
|
||||
|
14
frontend/src/page/features/copy.js
Normal file
14
frontend/src/page/features/copy.js
Normal file
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import CopyFeatureToggleForm from '../../component/feature/form/form-copy-feature-container';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const render = ({ history, match: { params } }) => (
|
||||
<CopyFeatureToggleForm title="Copy feature toggle" history={history} copyToggleName={params.copyToggle} />
|
||||
);
|
||||
|
||||
render.propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
match: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default render;
|
@ -4,6 +4,7 @@ import { dispatchAndThrow } from './util';
|
||||
import { MUTE_ERROR } from './error-actions';
|
||||
|
||||
export const ADD_FEATURE_TOGGLE = 'ADD_FEATURE_TOGGLE';
|
||||
export const COPY_FEATURE_TOGGLE = 'COPY_FEATURE_TOGGLE';
|
||||
export const REMOVE_FEATURE_TOGGLE = 'REMOVE_FEATURE_TOGGLE';
|
||||
export const UPDATE_FEATURE_TOGGLE = 'UPDATE_FEATURE_TOGGLE';
|
||||
export const TOGGLE_FEATURE_TOGGLE = 'TOGGLE_FEATURE_TOGGLE';
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
ADD_FEATURE_TOGGLE,
|
||||
RECEIVE_FEATURE_TOGGLES,
|
||||
UPDATE_FEATURE_TOGGLE,
|
||||
UPDATE_FEATURE_TOGGLE_STRATEGIES,
|
||||
REMOVE_FEATURE_TOGGLE,
|
||||
TOGGLE_FEATURE_TOGGLE,
|
||||
} from './feature-actions';
|
||||
@ -28,6 +29,15 @@ const features = (state = new List([]), action) => {
|
||||
return toggle;
|
||||
}
|
||||
});
|
||||
case UPDATE_FEATURE_TOGGLE_STRATEGIES:
|
||||
debug(UPDATE_FEATURE_TOGGLE_STRATEGIES, action);
|
||||
return state.map(toggle => {
|
||||
if (toggle.get('name') === action.featureToggle.name) {
|
||||
return new $Map(action.featureToggle);
|
||||
} else {
|
||||
return toggle;
|
||||
}
|
||||
});
|
||||
case UPDATE_FEATURE_TOGGLE:
|
||||
debug(UPDATE_FEATURE_TOGGLE, action);
|
||||
return state.map(toggle => {
|
||||
|
@ -137,6 +137,13 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.0.0"
|
||||
|
||||
"@babel/helper-module-imports@^7.7.4":
|
||||
version "7.7.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.7.4.tgz#e5a92529f8888bf319a6376abfbd1cebc491ad91"
|
||||
integrity sha512-dGcrX6K9l8258WFjyDLJwuVKxR4XZfU0/vTUgOQYWEnRD8mgr+p4d6fCUMq/ys0h4CCt/S5JhbvtyErjWouAUQ==
|
||||
dependencies:
|
||||
"@babel/types" "^7.7.4"
|
||||
|
||||
"@babel/helper-module-transforms@^7.1.0", "@babel/helper-module-transforms@^7.4.4":
|
||||
version "7.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.5.5.tgz#f84ff8a09038dcbca1fd4355661a500937165b4a"
|
||||
@ -585,6 +592,16 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
|
||||
"@babel/plugin-transform-runtime@^7.7.6":
|
||||
version "7.7.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.7.6.tgz#4f2b548c88922fb98ec1c242afd4733ee3e12f61"
|
||||
integrity sha512-tajQY+YmXR7JjTwRvwL4HePqoL3DYxpYXIHKVvrOIvJmeHe2y1w4tz5qz9ObUDC9m76rCzIMPyn4eERuwA4a4A==
|
||||
dependencies:
|
||||
"@babel/helper-module-imports" "^7.7.4"
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
resolve "^1.8.1"
|
||||
semver "^5.5.1"
|
||||
|
||||
"@babel/plugin-transform-shorthand-properties@^7.2.0":
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz#6333aee2f8d6ee7e28615457298934a3b46198f0"
|
||||
@ -738,6 +755,15 @@
|
||||
lodash "^4.17.13"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@babel/types@^7.7.4":
|
||||
version "7.7.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.7.4.tgz#516570d539e44ddf308c07569c258ff94fde9193"
|
||||
integrity sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==
|
||||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
lodash "^4.17.13"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@cnakazawa/watch@^1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef"
|
||||
@ -1407,6 +1433,11 @@ array-includes@^3.0.3:
|
||||
define-properties "^1.1.2"
|
||||
es-abstract "^1.7.0"
|
||||
|
||||
array-move@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/array-move/-/array-move-2.2.1.tgz#16d5b68cb949c43e8821d63e4622f3a3336f254d"
|
||||
integrity sha512-qQpEHBnVT6HAFgEVUwRdHVd8TYJThrZIT5wSXpEUTPwBaYhPLclw12mEpyUvRWVdl1VwPOqnIy6LqTFN3cSeUQ==
|
||||
|
||||
array-union@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
|
||||
@ -7703,6 +7734,13 @@ resolve@^1.10.0, resolve@^1.12.0, resolve@^1.3.2:
|
||||
dependencies:
|
||||
path-parse "^1.0.6"
|
||||
|
||||
resolve@^1.8.1:
|
||||
version "1.14.2"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.14.2.tgz#dbf31d0fa98b1f29aa5169783b9c290cb865fea2"
|
||||
integrity sha512-EjlOBLBO1kxsUxsKjLt7TAECyKW6fOh1VRkykQkKGzcBbjjPIxBqGh0jf7GJ3k/f5mxMqW3htMD3WdTUVtW8HQ==
|
||||
dependencies:
|
||||
path-parse "^1.0.6"
|
||||
|
||||
restore-cursor@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
|
||||
@ -7903,7 +7941,7 @@ selfsigned@^1.10.7:
|
||||
dependencies:
|
||||
node-forge "0.9.0"
|
||||
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0:
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||
|
Loading…
Reference in New Issue
Block a user