mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-23 00:16:25 +01:00
feat: add support for toggle type
This commit is contained in:
parent
60705d3993
commit
6395568d55
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const Select = ({ name, value, label, options, style, onChange }) => {
|
const Select = ({ name, value, label, options, style, onChange, filled }) => {
|
||||||
const wrapper = Object.assign({ width: 'auto' }, style);
|
const wrapper = Object.assign({ width: 'auto' }, style);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -13,10 +13,10 @@ const Select = ({ name, value, label, options, style, onChange }) => {
|
|||||||
name={name}
|
name={name}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
style={{ width: 'auto' }}
|
style={{ width: 'auto', background: filled ? '#f5f5f5' : 'none' }}
|
||||||
>
|
>
|
||||||
{options.map(o => (
|
{options.map(o => (
|
||||||
<option key={o.key} value={o.key}>
|
<option key={o.key} value={o.key} title={o.title}>
|
||||||
{o.label}
|
{o.label}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
|
@ -47,10 +47,8 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
className="listItemStrategies hideLt920"
|
className="listItemStrategies hideLt920"
|
||||||
>
|
>
|
||||||
<react-mdl-Chip
|
<react-mdl-Chip
|
||||||
className="strategyChip"
|
className="mdl-color--blue-grey-100"
|
||||||
>
|
/>
|
||||||
gradualRolloutRandom
|
|
||||||
</react-mdl-Chip>
|
|
||||||
</span>
|
</span>
|
||||||
<span />
|
<span />
|
||||||
</react-mdl-ListItem>
|
</react-mdl-ListItem>
|
||||||
@ -102,10 +100,8 @@ exports[`renders correctly with one feature without permission 1`] = `
|
|||||||
className="listItemStrategies hideLt920"
|
className="listItemStrategies hideLt920"
|
||||||
>
|
>
|
||||||
<react-mdl-Chip
|
<react-mdl-Chip
|
||||||
className="strategyChip"
|
className="mdl-color--blue-grey-100"
|
||||||
>
|
/>
|
||||||
gradualRolloutRandom
|
|
||||||
</react-mdl-Chip>
|
|
||||||
</span>
|
</span>
|
||||||
<span />
|
<span />
|
||||||
</react-mdl-ListItem>
|
</react-mdl-ListItem>
|
||||||
|
@ -124,6 +124,12 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
>
|
>
|
||||||
Name
|
Name
|
||||||
</react-mdl-MenuItem>
|
</react-mdl-MenuItem>
|
||||||
|
<react-mdl-MenuItem
|
||||||
|
data-target="type"
|
||||||
|
disabled={false}
|
||||||
|
>
|
||||||
|
Type
|
||||||
|
</react-mdl-MenuItem>
|
||||||
<react-mdl-MenuItem
|
<react-mdl-MenuItem
|
||||||
data-target="enabled"
|
data-target="enabled"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
@ -283,6 +289,12 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
>
|
>
|
||||||
Name
|
Name
|
||||||
</react-mdl-MenuItem>
|
</react-mdl-MenuItem>
|
||||||
|
<react-mdl-MenuItem
|
||||||
|
data-target="type"
|
||||||
|
disabled={false}
|
||||||
|
>
|
||||||
|
Type
|
||||||
|
</react-mdl-MenuItem>
|
||||||
<react-mdl-MenuItem
|
<react-mdl-MenuItem
|
||||||
data-target="enabled"
|
data-target="enabled"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
|
@ -22,14 +22,29 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
|
|
||||||
</react-mdl-CardTitle>
|
</react-mdl-CardTitle>
|
||||||
<react-mdl-CardText>
|
<react-mdl-CardText>
|
||||||
another's description
|
<div>
|
||||||
|
another's description
|
||||||
<a
|
|
||||||
href="#edit"
|
<a
|
||||||
onClick={[Function]}
|
href="#edit"
|
||||||
>
|
onClick={[Function]}
|
||||||
edit
|
>
|
||||||
</a>
|
edit
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</react-mdl-CardText>
|
||||||
|
<react-mdl-CardText
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"paddingTop": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FeatureTypeSelect
|
||||||
|
filled={true}
|
||||||
|
onChange={[Function]}
|
||||||
|
value="release"
|
||||||
|
/>
|
||||||
</react-mdl-CardText>
|
</react-mdl-CardText>
|
||||||
<react-mdl-CardActions
|
<react-mdl-CardActions
|
||||||
border={true}
|
border={true}
|
||||||
@ -137,6 +152,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"type": "release",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
features={
|
features={
|
||||||
@ -154,6 +170,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"type": "release",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -10,12 +10,14 @@ jest.mock('../form/form-update-feature-container', () => ({
|
|||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: 'UpdateFeatureToggleComponent',
|
default: 'UpdateFeatureToggleComponent',
|
||||||
}));
|
}));
|
||||||
|
jest.mock('../form/feature-type-select-container', () => 'FeatureTypeSelect');
|
||||||
|
|
||||||
test('renders correctly with one feature', () => {
|
test('renders correctly with one feature', () => {
|
||||||
const feature = {
|
const feature = {
|
||||||
name: 'Another',
|
name: 'Another',
|
||||||
description: "another's description",
|
description: "another's description",
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
type: 'release',
|
||||||
strategies: [
|
strategies: [
|
||||||
{
|
{
|
||||||
name: 'gradualRolloutRandom',
|
name: 'gradualRolloutRandom',
|
||||||
|
@ -17,7 +17,7 @@ const Feature = ({
|
|||||||
revive,
|
revive,
|
||||||
hasPermission,
|
hasPermission,
|
||||||
}) => {
|
}) => {
|
||||||
const { name, description, enabled, strategies } = feature;
|
const { name, description, enabled, type } = feature;
|
||||||
const { showLastHour = false } = settings;
|
const { showLastHour = false } = settings;
|
||||||
const isStale = showLastHour ? metricsLastHour.isFallback : metricsLastMinute.isFallback;
|
const isStale = showLastHour ? metricsLastHour.isFallback : metricsLastMinute.isFallback;
|
||||||
const percent =
|
const percent =
|
||||||
@ -25,17 +25,7 @@ const Feature = ({
|
|||||||
(showLastHour
|
(showLastHour
|
||||||
? calc(metricsLastHour.yes, metricsLastHour.yes + metricsLastHour.no, 0)
|
? calc(metricsLastHour.yes, metricsLastHour.yes + metricsLastHour.no, 0)
|
||||||
: calc(metricsLastMinute.yes, metricsLastMinute.yes + metricsLastMinute.no, 0));
|
: calc(metricsLastMinute.yes, metricsLastMinute.yes + metricsLastMinute.no, 0));
|
||||||
|
const typeChip = <Chip className="mdl-color--blue-grey-100">{type}</Chip>;
|
||||||
const strategiesToShow = Math.min(strategies.length, 3);
|
|
||||||
const remainingStrategies = strategies.length - strategiesToShow;
|
|
||||||
const strategyChips =
|
|
||||||
strategies &&
|
|
||||||
strategies.slice(0, strategiesToShow).map((s, i) => (
|
|
||||||
<Chip className={styles.strategyChip} key={i}>
|
|
||||||
{s.name}
|
|
||||||
</Chip>
|
|
||||||
));
|
|
||||||
const summaryChip = remainingStrategies > 0 && <Chip className={styles.strategyChip}>+{remainingStrategies}</Chip>;
|
|
||||||
const featureUrl = toggleFeature === undefined ? `/archive/strategies/${name}` : `/features/strategies/${name}`;
|
const featureUrl = toggleFeature === undefined ? `/archive/strategies/${name}` : `/features/strategies/${name}`;
|
||||||
return (
|
return (
|
||||||
<ListItem twoLine>
|
<ListItem twoLine>
|
||||||
@ -61,10 +51,7 @@ const Feature = ({
|
|||||||
<span className={['mdl-list__item-sub-title', commonStyles.truncate].join(' ')}>{description}</span>
|
<span className={['mdl-list__item-sub-title', commonStyles.truncate].join(' ')}>{description}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
<span className={[styles.listItemStrategies, commonStyles.hideLt920].join(' ')}>
|
<span className={[styles.listItemStrategies, commonStyles.hideLt920].join(' ')}>{typeChip}</span>
|
||||||
{strategyChips}
|
|
||||||
{summaryChip}
|
|
||||||
</span>
|
|
||||||
{revive && hasPermission(UPDATE_FEATURE) ? (
|
{revive && hasPermission(UPDATE_FEATURE) ? (
|
||||||
<ListItemAction onClick={() => revive(feature.name)}>
|
<ListItemAction onClick={() => revive(feature.name)}>
|
||||||
<Icon name="undo" />
|
<Icon name="undo" />
|
||||||
|
@ -33,3 +33,8 @@
|
|||||||
.strategyChip {
|
.strategyChip {
|
||||||
margin-left: 8px !important;
|
margin-left: 8px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.typeChip {
|
||||||
|
margin-left: 8px !important;
|
||||||
|
background: #d3c1ff;
|
||||||
|
}
|
@ -23,21 +23,54 @@ exports[`render the create feature page 1`] = `
|
|||||||
<form
|
<form
|
||||||
onSubmit={[MockFunction]}
|
onSubmit={[MockFunction]}
|
||||||
>
|
>
|
||||||
|
<react-mdl-Grid>
|
||||||
|
<react-mdl-Cell
|
||||||
|
col={4}
|
||||||
|
>
|
||||||
|
<react-mdl-Textfield
|
||||||
|
floatingLabel={true}
|
||||||
|
label="Name"
|
||||||
|
name="name"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onChange={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="feature"
|
||||||
|
/>
|
||||||
|
</react-mdl-Cell>
|
||||||
|
<react-mdl-Cell
|
||||||
|
col={2}
|
||||||
|
>
|
||||||
|
<Connect(FeatureTypeSelectComponent)
|
||||||
|
onChange={[Function]}
|
||||||
|
/>
|
||||||
|
</react-mdl-Cell>
|
||||||
|
<react-mdl-Cell
|
||||||
|
col={2}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"paddingTop": "14px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<react-mdl-Switch
|
||||||
|
checked={false}
|
||||||
|
onChange={[Function]}
|
||||||
|
>
|
||||||
|
Disabled
|
||||||
|
</react-mdl-Switch>
|
||||||
|
</react-mdl-Cell>
|
||||||
|
</react-mdl-Grid>
|
||||||
<section
|
<section
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"padding": "16px",
|
"padding": "0 16px",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<react-mdl-Textfield
|
|
||||||
floatingLabel={true}
|
|
||||||
label="Name"
|
|
||||||
name="name"
|
|
||||||
onBlur={[Function]}
|
|
||||||
onChange={[Function]}
|
|
||||||
value="feature"
|
|
||||||
/>
|
|
||||||
<react-mdl-Textfield
|
<react-mdl-Textfield
|
||||||
floatingLabel={true}
|
floatingLabel={true}
|
||||||
label="Description"
|
label="Description"
|
||||||
@ -50,17 +83,6 @@ exports[`render the create feature page 1`] = `
|
|||||||
}
|
}
|
||||||
value="Description"
|
value="Description"
|
||||||
/>
|
/>
|
||||||
<div>
|
|
||||||
<br />
|
|
||||||
<react-mdl-Switch
|
|
||||||
checked={false}
|
|
||||||
onChange={[Function]}
|
|
||||||
>
|
|
||||||
Enabled
|
|
||||||
</react-mdl-Switch>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
<StrategiesSection
|
<StrategiesSection
|
||||||
addStrategy={[MockFunction]}
|
addStrategy={[MockFunction]}
|
||||||
configuredStrategies={Array []}
|
configuredStrategies={Array []}
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import MySelect from '../../common/select';
|
||||||
|
|
||||||
|
class FeatureTypeSelectComponent extends Component {
|
||||||
|
componentDidMount() {
|
||||||
|
if (this.props.fetchFeatureTypes) {
|
||||||
|
this.props.fetchFeatureTypes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { value, types, onChange, filled } = this.props;
|
||||||
|
|
||||||
|
const options = types.map(t => ({ key: t.id, label: t.name, title: t.description }));
|
||||||
|
|
||||||
|
if (!options.find(o => o.key === value)) {
|
||||||
|
options.push({ key: value, label: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
return <MySelect label="Toggle type" options={options} value={value} onChange={onChange} filled={filled} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FeatureTypeSelectComponent.propTypes = {
|
||||||
|
value: PropTypes.string,
|
||||||
|
filled: PropTypes.bool,
|
||||||
|
types: PropTypes.array.isRequired,
|
||||||
|
fetchFeatureTypes: PropTypes.func,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeatureTypeSelectComponent;
|
@ -0,0 +1,11 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import FeatureTypeSelectComponent from './feature-type-select-component';
|
||||||
|
import { fetchFeatureTypes } from './../../../store/feature-type/actions';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
types: state.featureTypes.toJS(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const FormAddContainer = connect(mapStateToProps, { fetchFeatureTypes })(FeatureTypeSelectComponent);
|
||||||
|
|
||||||
|
export default FormAddContainer;
|
@ -1,7 +1,8 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Textfield, Switch, Card, CardTitle, CardActions } from 'react-mdl';
|
import { Textfield, Switch, Card, CardTitle, CardActions, Grid, Cell } from 'react-mdl';
|
||||||
import StrategiesSection from './strategies-section-container';
|
import StrategiesSection from './strategies-section-container';
|
||||||
|
import FeatureTypeSelect from './feature-type-select-container';
|
||||||
|
|
||||||
import { FormButtons } from './../../common';
|
import { FormButtons } from './../../common';
|
||||||
import { styles as commonStyles } from '../../common';
|
import { styles as commonStyles } from '../../common';
|
||||||
@ -37,16 +38,34 @@ class AddFeatureComponent extends Component {
|
|||||||
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||||
<CardTitle style={{ paddingTop: '24px', wordBreak: 'break-all' }}>Create new feature toggle</CardTitle>
|
<CardTitle style={{ paddingTop: '24px', wordBreak: 'break-all' }}>Create new feature toggle</CardTitle>
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<section style={{ padding: '16px' }}>
|
<Grid>
|
||||||
<Textfield
|
<Cell col={4}>
|
||||||
floatingLabel
|
<Textfield
|
||||||
label="Name"
|
floatingLabel
|
||||||
name="name"
|
style={{ width: '100%' }}
|
||||||
value={input.name}
|
label="Name"
|
||||||
error={errors.name}
|
name="name"
|
||||||
onBlur={v => validateName(v.target.value)}
|
value={input.name}
|
||||||
onChange={v => setValue('name', trim(v.target.value))}
|
error={errors.name}
|
||||||
/>
|
onBlur={v => validateName(v.target.value)}
|
||||||
|
onChange={v => setValue('name', trim(v.target.value))}
|
||||||
|
/>
|
||||||
|
</Cell>
|
||||||
|
<Cell col={2}>
|
||||||
|
<FeatureTypeSelect value={input.type} onChange={v => setValue('type', v.target.value)} />
|
||||||
|
</Cell>
|
||||||
|
<Cell col={2} style={{ paddingTop: '14px' }}>
|
||||||
|
<Switch
|
||||||
|
checked={input.enabled}
|
||||||
|
onChange={() => {
|
||||||
|
setValue('enabled', !input.enabled);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{input.enabled ? 'Enabled' : 'Disabled'}
|
||||||
|
</Switch>
|
||||||
|
</Cell>
|
||||||
|
</Grid>
|
||||||
|
<section style={{ padding: '0 16px' }}>
|
||||||
<Textfield
|
<Textfield
|
||||||
floatingLabel
|
floatingLabel
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
@ -56,19 +75,6 @@ class AddFeatureComponent extends Component {
|
|||||||
value={input.description}
|
value={input.description}
|
||||||
onChange={v => setValue('description', v.target.value)}
|
onChange={v => setValue('description', v.target.value)}
|
||||||
/>
|
/>
|
||||||
<div>
|
|
||||||
<br />
|
|
||||||
<Switch
|
|
||||||
checked={input.enabled}
|
|
||||||
onChange={() => {
|
|
||||||
setValue('enabled', !input.enabled);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Enabled
|
|
||||||
</Switch>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{input.name ? (
|
{input.name ? (
|
||||||
<StrategiesSection
|
<StrategiesSection
|
||||||
|
@ -13,7 +13,7 @@ class WrapperComponent extends Component {
|
|||||||
super(props);
|
super(props);
|
||||||
const name = loadNameFromHash();
|
const name = loadNameFromHash();
|
||||||
this.state = {
|
this.state = {
|
||||||
featureToggle: { name, description: '', strategies: [], enabled: true },
|
featureToggle: { name, description: '', type: 'release', strategies: [], enabled: true },
|
||||||
errors: {},
|
errors: {},
|
||||||
dirty: false,
|
dirty: false,
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button, CardText, Textfield } from 'react-mdl';
|
import { Button, Textfield } from 'react-mdl';
|
||||||
|
|
||||||
import { UPDATE_FEATURE } from '../../../permissions';
|
import { UPDATE_FEATURE } from '../../../permissions';
|
||||||
|
|
||||||
@ -40,21 +40,21 @@ export default class UpdateDescriptionComponent extends React.Component {
|
|||||||
|
|
||||||
renderRead({ description, isFeatureView, hasPermission }) {
|
renderRead({ description, isFeatureView, hasPermission }) {
|
||||||
return (
|
return (
|
||||||
<CardText>
|
<div>
|
||||||
{description}
|
{description}
|
||||||
{isFeatureView && hasPermission(UPDATE_FEATURE) ? (
|
{isFeatureView && hasPermission(UPDATE_FEATURE) ? (
|
||||||
<a href="#edit" onClick={this.onEditMode.bind(this, description)}>
|
<a href="#edit" onClick={this.onEditMode.bind(this, description)}>
|
||||||
edit
|
edit
|
||||||
</a>
|
</a>
|
||||||
) : null}
|
) : null}
|
||||||
</CardText>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderEdit() {
|
renderEdit() {
|
||||||
const { description } = this.state;
|
const { description } = this.state;
|
||||||
return (
|
return (
|
||||||
<CardText>
|
<div>
|
||||||
<Textfield
|
<Textfield
|
||||||
floatingLabel
|
floatingLabel
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
@ -72,7 +72,7 @@ export default class UpdateDescriptionComponent extends React.Component {
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardText>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,6 +105,9 @@ export default class FeatureListComponent extends React.Component {
|
|||||||
<MenuItem disabled={settings.sort === 'name'} data-target="name">
|
<MenuItem disabled={settings.sort === 'name'} data-target="name">
|
||||||
Name
|
Name
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem disabled={settings.sort === 'type'} data-target="type">
|
||||||
|
Type
|
||||||
|
</MenuItem>
|
||||||
<MenuItem disabled={settings.sort === 'enabled'} data-target="enabled">
|
<MenuItem disabled={settings.sort === 'enabled'} data-target="enabled">
|
||||||
Enabled
|
Enabled
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -47,6 +47,16 @@ export const mapStateToPropsConfigurable = isFeature => state => {
|
|||||||
});
|
});
|
||||||
} else if (settings.sort === 'strategies') {
|
} else if (settings.sort === 'strategies') {
|
||||||
features = features.sort((a, b) => (a.strategies.length > b.strategies.length ? -1 : 1));
|
features = features.sort((a, b) => (a.strategies.length > b.strategies.length ? -1 : 1));
|
||||||
|
} else if (settings.sort === 'type') {
|
||||||
|
features = features.sort((a, b) => {
|
||||||
|
if (a.type < b.type) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.type > b.type) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
} else if (settings.sort === 'metrics') {
|
} else if (settings.sort === 'metrics') {
|
||||||
const target = settings.showLastHour ? featureMetrics.lastHour : featureMetrics.lastMinute;
|
const target = settings.showLastHour ? featureMetrics.lastHour : featureMetrics.lastMinute;
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Tabs, Tab, ProgressBar, Button, Card, CardTitle, CardActions, Switch } from 'react-mdl';
|
import { Tabs, Tab, ProgressBar, Button, Card, CardTitle, CardActions, Switch, CardText } from 'react-mdl';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import HistoryComponent from '../history/history-list-toggle-container';
|
import HistoryComponent from '../history/history-list-toggle-container';
|
||||||
@ -8,6 +8,7 @@ import MetricComponent from './metric-container';
|
|||||||
import EditFeatureToggle from './form/form-update-feature-container';
|
import EditFeatureToggle from './form/form-update-feature-container';
|
||||||
import EditVariants from './variant/update-variant-container';
|
import EditVariants from './variant/update-variant-container';
|
||||||
import ViewFeatureToggle from './form/form-view-feature-container';
|
import ViewFeatureToggle from './form/form-view-feature-container';
|
||||||
|
import FeatureTypeSelect from './form/feature-type-select-container';
|
||||||
import UpdateDescriptionComponent from './form/update-description-component';
|
import UpdateDescriptionComponent from './form/update-description-component';
|
||||||
import { styles as commonStyles } from '../common';
|
import { styles as commonStyles } from '../common';
|
||||||
import { CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE } from '../../permissions';
|
import { CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE } from '../../permissions';
|
||||||
@ -145,16 +146,33 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
|||||||
|
|
||||||
this.props.editFeatureToggle(feature);
|
this.props.editFeatureToggle(feature);
|
||||||
};
|
};
|
||||||
|
const updateType = evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const type = evt.target.value;
|
||||||
|
let feature = { ...featureToggle, type };
|
||||||
|
if (Array.isArray(feature.strategies)) {
|
||||||
|
feature.strategies.forEach(s => {
|
||||||
|
delete s.id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.editFeatureToggle(feature);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
<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
|
<CardText>
|
||||||
isFeatureView={this.isFeatureView}
|
<UpdateDescriptionComponent
|
||||||
description={featureToggle.description}
|
isFeatureView={this.isFeatureView}
|
||||||
update={updateDescription}
|
description={featureToggle.description}
|
||||||
hasPermission={hasPermission}
|
update={updateDescription}
|
||||||
/>
|
hasPermission={hasPermission}
|
||||||
|
/>
|
||||||
|
</CardText>
|
||||||
|
<CardText style={{ paddingTop: 0 }}>
|
||||||
|
<FeatureTypeSelect value={featureToggle.type} onChange={updateType} filled />
|
||||||
|
</CardText>
|
||||||
|
|
||||||
<CardActions
|
<CardActions
|
||||||
border
|
border
|
||||||
|
13
frontend/src/data/feature-type-api.js
Normal file
13
frontend/src/data/feature-type-api.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { throwIfNotSuccess } from './helper';
|
||||||
|
|
||||||
|
const URI = 'api/admin/feature-types';
|
||||||
|
|
||||||
|
function fetchAll() {
|
||||||
|
return fetch(URI, { credentials: 'include' })
|
||||||
|
.then(throwIfNotSuccess)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fetchAll,
|
||||||
|
};
|
@ -6,6 +6,7 @@ import {
|
|||||||
ERROR_REMOVE_FEATURE_TOGGLE,
|
ERROR_REMOVE_FEATURE_TOGGLE,
|
||||||
ERROR_UPDATE_FEATURE_TOGGLE,
|
ERROR_UPDATE_FEATURE_TOGGLE,
|
||||||
UPDATE_FEATURE_TOGGLE_STRATEGIES,
|
UPDATE_FEATURE_TOGGLE_STRATEGIES,
|
||||||
|
UPDATE_FEATURE_TOGGLE,
|
||||||
} from './feature-actions';
|
} from './feature-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';
|
||||||
@ -46,6 +47,7 @@ const strategies = (state = getInitState(), action) => {
|
|||||||
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_STRATEGIES:
|
case UPDATE_FEATURE_TOGGLE_STRATEGIES:
|
||||||
return addErrorIfNotAlreadyInList(state, action.info);
|
return addErrorIfNotAlreadyInList(state, action.info);
|
||||||
default:
|
default:
|
||||||
|
@ -82,7 +82,11 @@ export function requestUpdateFeatureToggle(featureToggle) {
|
|||||||
|
|
||||||
return api
|
return api
|
||||||
.update(featureToggle)
|
.update(featureToggle)
|
||||||
.then(() => dispatch({ type: UPDATE_FEATURE_TOGGLE, featureToggle }))
|
.then(() => {
|
||||||
|
const info = `${featureToggle.name} successfully updated!`;
|
||||||
|
setTimeout(() => dispatch({ type: MUTE_ERROR, error: info }), 1000);
|
||||||
|
dispatch({ type: UPDATE_FEATURE_TOGGLE, featureToggle, info });
|
||||||
|
})
|
||||||
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_FEATURE_TOGGLE));
|
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_FEATURE_TOGGLE));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
15
frontend/src/store/feature-type/actions.js
Normal file
15
frontend/src/store/feature-type/actions.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import api from '../../data/feature-type-api';
|
||||||
|
import { dispatchAndThrow } from '../util';
|
||||||
|
|
||||||
|
export const RECEIVE_FEATURE_TYPES = 'RECEIVE_FEATURE_TYPES';
|
||||||
|
export const ERROR_RECEIVE_FEATURE_TYPES = 'ERROR_RECEIVE_FEATURE_TYPES';
|
||||||
|
|
||||||
|
const receiveFeatureTypes = value => ({ type: RECEIVE_FEATURE_TYPES, value });
|
||||||
|
|
||||||
|
export function fetchFeatureTypes() {
|
||||||
|
return dispatch =>
|
||||||
|
api
|
||||||
|
.fetchAll()
|
||||||
|
.then(json => dispatch(receiveFeatureTypes(json)))
|
||||||
|
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_FEATURE_TYPES));
|
||||||
|
}
|
19
frontend/src/store/feature-type/index.js
Normal file
19
frontend/src/store/feature-type/index.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { List } from 'immutable';
|
||||||
|
import { RECEIVE_FEATURE_TYPES } from './actions';
|
||||||
|
|
||||||
|
const DEFAULT_FEATURE_TYPES = [{ id: 'release', name: 'Release', inital: true }];
|
||||||
|
|
||||||
|
function getInitState() {
|
||||||
|
return new List(DEFAULT_FEATURE_TYPES);
|
||||||
|
}
|
||||||
|
|
||||||
|
const strategies = (state = getInitState(), action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case RECEIVE_FEATURE_TYPES:
|
||||||
|
return new List(action.value.types);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default strategies;
|
@ -1,5 +1,6 @@
|
|||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
import features from './feature-store';
|
import features from './feature-store';
|
||||||
|
import featureTypes from './feature-type';
|
||||||
import featureMetrics from './feature-metrics-store';
|
import featureMetrics from './feature-metrics-store';
|
||||||
import strategies from './strategy';
|
import strategies from './strategy';
|
||||||
import input from './input-store';
|
import input from './input-store';
|
||||||
@ -15,6 +16,7 @@ import context from './context';
|
|||||||
|
|
||||||
const unleashStore = combineReducers({
|
const unleashStore = combineReducers({
|
||||||
features,
|
features,
|
||||||
|
featureTypes,
|
||||||
featureMetrics,
|
featureMetrics,
|
||||||
strategies,
|
strategies,
|
||||||
input,
|
input,
|
||||||
|
Loading…
Reference in New Issue
Block a user