1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-18 00:19:49 +01:00

feat: add support for toggle type

This commit is contained in:
Ivar Conradi Østhus 2020-08-05 21:36:28 +02:00
parent 60705d3993
commit 6395568d55
22 changed files with 270 additions and 93 deletions

View File

@ -1,7 +1,7 @@
import React from 'react';
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);
return (
<div
@ -13,10 +13,10 @@ const Select = ({ name, value, label, options, style, onChange }) => {
name={name}
onChange={onChange}
value={value}
style={{ width: 'auto' }}
style={{ width: 'auto', background: filled ? '#f5f5f5' : 'none' }}
>
{options.map(o => (
<option key={o.key} value={o.key}>
<option key={o.key} value={o.key} title={o.title}>
{o.label}
</option>
))}

View File

@ -47,10 +47,8 @@ exports[`renders correctly with one feature 1`] = `
className="listItemStrategies hideLt920"
>
<react-mdl-Chip
className="strategyChip"
>
gradualRolloutRandom
</react-mdl-Chip>
className="mdl-color--blue-grey-100"
/>
</span>
<span />
</react-mdl-ListItem>
@ -102,10 +100,8 @@ exports[`renders correctly with one feature without permission 1`] = `
className="listItemStrategies hideLt920"
>
<react-mdl-Chip
className="strategyChip"
>
gradualRolloutRandom
</react-mdl-Chip>
className="mdl-color--blue-grey-100"
/>
</span>
<span />
</react-mdl-ListItem>

View File

@ -124,6 +124,12 @@ exports[`renders correctly with one feature 1`] = `
>
Name
</react-mdl-MenuItem>
<react-mdl-MenuItem
data-target="type"
disabled={false}
>
Type
</react-mdl-MenuItem>
<react-mdl-MenuItem
data-target="enabled"
disabled={false}
@ -283,6 +289,12 @@ exports[`renders correctly with one feature without permissions 1`] = `
>
Name
</react-mdl-MenuItem>
<react-mdl-MenuItem
data-target="type"
disabled={false}
>
Type
</react-mdl-MenuItem>
<react-mdl-MenuItem
data-target="enabled"
disabled={false}

View File

@ -22,14 +22,29 @@ exports[`renders correctly with one feature 1`] = `
</react-mdl-CardTitle>
<react-mdl-CardText>
another's description
 
<a
href="#edit"
onClick={[Function]}
>
edit
</a>
<div>
another's description
 
<a
href="#edit"
onClick={[Function]}
>
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-CardActions
border={true}
@ -137,6 +152,7 @@ exports[`renders correctly with one feature 1`] = `
},
},
],
"type": "release",
}
}
features={
@ -154,6 +170,7 @@ exports[`renders correctly with one feature 1`] = `
},
},
],
"type": "release",
},
]
}

View File

@ -10,12 +10,14 @@ jest.mock('../form/form-update-feature-container', () => ({
__esModule: true,
default: 'UpdateFeatureToggleComponent',
}));
jest.mock('../form/feature-type-select-container', () => 'FeatureTypeSelect');
test('renders correctly with one feature', () => {
const feature = {
name: 'Another',
description: "another's description",
enabled: false,
type: 'release',
strategies: [
{
name: 'gradualRolloutRandom',

View File

@ -17,7 +17,7 @@ const Feature = ({
revive,
hasPermission,
}) => {
const { name, description, enabled, strategies } = feature;
const { name, description, enabled, type } = feature;
const { showLastHour = false } = settings;
const isStale = showLastHour ? metricsLastHour.isFallback : metricsLastMinute.isFallback;
const percent =
@ -25,17 +25,7 @@ const Feature = ({
(showLastHour
? calc(metricsLastHour.yes, metricsLastHour.yes + metricsLastHour.no, 0)
: calc(metricsLastMinute.yes, metricsLastMinute.yes + metricsLastMinute.no, 0));
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 typeChip = <Chip className="mdl-color--blue-grey-100">{type}</Chip>;
const featureUrl = toggleFeature === undefined ? `/archive/strategies/${name}` : `/features/strategies/${name}`;
return (
<ListItem twoLine>
@ -61,10 +51,7 @@ const Feature = ({
<span className={['mdl-list__item-sub-title', commonStyles.truncate].join(' ')}>{description}</span>
</Link>
</span>
<span className={[styles.listItemStrategies, commonStyles.hideLt920].join(' ')}>
{strategyChips}
{summaryChip}
</span>
<span className={[styles.listItemStrategies, commonStyles.hideLt920].join(' ')}>{typeChip}</span>
{revive && hasPermission(UPDATE_FEATURE) ? (
<ListItemAction onClick={() => revive(feature.name)}>
<Icon name="undo" />

View File

@ -33,3 +33,8 @@
.strategyChip {
margin-left: 8px !important;
}
.typeChip {
margin-left: 8px !important;
background: #d3c1ff;
}

View File

@ -23,21 +23,54 @@ exports[`render the create feature page 1`] = `
<form
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
style={
Object {
"padding": "16px",
"padding": "0 16px",
}
}
>
<react-mdl-Textfield
floatingLabel={true}
label="Name"
name="name"
onBlur={[Function]}
onChange={[Function]}
value="feature"
/>
<react-mdl-Textfield
floatingLabel={true}
label="Description"
@ -50,17 +83,6 @@ exports[`render the create feature page 1`] = `
}
value="Description"
/>
<div>
<br />
<react-mdl-Switch
checked={false}
onChange={[Function]}
>
Enabled
</react-mdl-Switch>
<br />
<br />
</div>
<StrategiesSection
addStrategy={[MockFunction]}
configuredStrategies={Array []}

View File

@ -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;

View File

@ -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;

View File

@ -1,7 +1,8 @@
import React, { Component } from 'react';
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 FeatureTypeSelect from './feature-type-select-container';
import { FormButtons } 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' }}>
<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"
value={input.name}
error={errors.name}
onBlur={v => validateName(v.target.value)}
onChange={v => setValue('name', trim(v.target.value))}
/>
<Grid>
<Cell col={4}>
<Textfield
floatingLabel
style={{ width: '100%' }}
label="Name"
name="name"
value={input.name}
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
floatingLabel
style={{ width: '100%' }}
@ -56,19 +75,6 @@ class AddFeatureComponent extends Component {
value={input.description}
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 ? (
<StrategiesSection

View File

@ -13,7 +13,7 @@ class WrapperComponent extends Component {
super(props);
const name = loadNameFromHash();
this.state = {
featureToggle: { name, description: '', strategies: [], enabled: true },
featureToggle: { name, description: '', type: 'release', strategies: [], enabled: true },
errors: {},
dirty: false,
};

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, CardText, Textfield } from 'react-mdl';
import { Button, Textfield } from 'react-mdl';
import { UPDATE_FEATURE } from '../../../permissions';
@ -40,21 +40,21 @@ export default class UpdateDescriptionComponent extends React.Component {
renderRead({ description, isFeatureView, hasPermission }) {
return (
<CardText>
<div>
{description}&nbsp;
{isFeatureView && hasPermission(UPDATE_FEATURE) ? (
<a href="#edit" onClick={this.onEditMode.bind(this, description)}>
edit
</a>
) : null}
</CardText>
</div>
);
}
renderEdit() {
const { description } = this.state;
return (
<CardText>
<div>
<Textfield
floatingLabel
style={{ width: '100%' }}
@ -72,7 +72,7 @@ export default class UpdateDescriptionComponent extends React.Component {
Cancel
</Button>
</div>
</CardText>
</div>
);
}

View File

@ -105,6 +105,9 @@ export default class FeatureListComponent extends React.Component {
<MenuItem disabled={settings.sort === 'name'} data-target="name">
Name
</MenuItem>
<MenuItem disabled={settings.sort === 'type'} data-target="type">
Type
</MenuItem>
<MenuItem disabled={settings.sort === 'enabled'} data-target="enabled">
Enabled
</MenuItem>

View File

@ -47,6 +47,16 @@ export const mapStateToPropsConfigurable = isFeature => state => {
});
} else if (settings.sort === 'strategies') {
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') {
const target = settings.showLastHour ? featureMetrics.lastHour : featureMetrics.lastMinute;

View File

@ -1,6 +1,6 @@
import React from 'react';
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 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 EditVariants from './variant/update-variant-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 { styles as commonStyles } from '../common';
import { CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE } from '../../permissions';
@ -145,16 +146,33 @@ export default class ViewFeatureToggleComponent extends React.Component {
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 (
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
<CardTitle style={{ wordBreak: 'break-all', paddingBottom: 0 }}>{featureToggle.name} </CardTitle>
<UpdateDescriptionComponent
isFeatureView={this.isFeatureView}
description={featureToggle.description}
update={updateDescription}
hasPermission={hasPermission}
/>
<CardText>
<UpdateDescriptionComponent
isFeatureView={this.isFeatureView}
description={featureToggle.description}
update={updateDescription}
hasPermission={hasPermission}
/>
</CardText>
<CardText style={{ paddingTop: 0 }}>
<FeatureTypeSelect value={featureToggle.type} onChange={updateType} filled />
</CardText>
<CardActions
border

View 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,
};

View File

@ -6,6 +6,7 @@ import {
ERROR_REMOVE_FEATURE_TOGGLE,
ERROR_UPDATE_FEATURE_TOGGLE,
UPDATE_FEATURE_TOGGLE_STRATEGIES,
UPDATE_FEATURE_TOGGLE,
} from './feature-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');
case MUTE_ERROR:
return state.update('list', list => list.remove(list.indexOf(action.error)));
case UPDATE_FEATURE_TOGGLE:
case UPDATE_FEATURE_TOGGLE_STRATEGIES:
return addErrorIfNotAlreadyInList(state, action.info);
default:

View File

@ -82,7 +82,11 @@ export function requestUpdateFeatureToggle(featureToggle) {
return api
.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));
};
}

View 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));
}

View 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;

View File

@ -1,5 +1,6 @@
import { combineReducers } from 'redux';
import features from './feature-store';
import featureTypes from './feature-type';
import featureMetrics from './feature-metrics-store';
import strategies from './strategy';
import input from './input-store';
@ -15,6 +16,7 @@ import context from './context';
const unleashStore = combineReducers({
features,
featureTypes,
featureMetrics,
strategies,
input,