diff --git a/frontend/src/component/common/select.jsx b/frontend/src/component/common/select.jsx index facc3eefb6..fd193c55a4 100644 --- a/frontend/src/component/common/select.jsx +++ b/frontend/src/component/common/select.jsx @@ -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 (
{ name={name} onChange={onChange} value={value} - style={{ width: 'auto' }} + style={{ width: 'auto', background: filled ? '#f5f5f5' : 'none' }} > {options.map(o => ( - ))} diff --git a/frontend/src/component/feature/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap b/frontend/src/component/feature/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap index 8e1aa06601..9e510db3ea 100644 --- a/frontend/src/component/feature/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap +++ b/frontend/src/component/feature/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap @@ -47,10 +47,8 @@ exports[`renders correctly with one feature 1`] = ` className="listItemStrategies hideLt920" > - gradualRolloutRandom - + className="mdl-color--blue-grey-100" + /> @@ -102,10 +100,8 @@ exports[`renders correctly with one feature without permission 1`] = ` className="listItemStrategies hideLt920" > - gradualRolloutRandom - + className="mdl-color--blue-grey-100" + /> diff --git a/frontend/src/component/feature/__tests__/__snapshots__/list-component-test.jsx.snap b/frontend/src/component/feature/__tests__/__snapshots__/list-component-test.jsx.snap index d5bbbd92ed..a9045b9eac 100644 --- a/frontend/src/component/feature/__tests__/__snapshots__/list-component-test.jsx.snap +++ b/frontend/src/component/feature/__tests__/__snapshots__/list-component-test.jsx.snap @@ -124,6 +124,12 @@ exports[`renders correctly with one feature 1`] = ` > Name + + Type + Name + + Type + - another's description -   - - edit - +
+ another's description +   + + edit + +
+
+ + ({ __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', diff --git a/frontend/src/component/feature/feature-list-item-component.jsx b/frontend/src/component/feature/feature-list-item-component.jsx index e19ab683b6..a0a0435491 100644 --- a/frontend/src/component/feature/feature-list-item-component.jsx +++ b/frontend/src/component/feature/feature-list-item-component.jsx @@ -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) => ( - - {s.name} - - )); - const summaryChip = remainingStrategies > 0 && +{remainingStrategies}; + const typeChip = {type}; const featureUrl = toggleFeature === undefined ? `/archive/strategies/${name}` : `/features/strategies/${name}`; return ( @@ -61,10 +51,7 @@ const Feature = ({ {description}
- - {strategyChips} - {summaryChip} - + {typeChip} {revive && hasPermission(UPDATE_FEATURE) ? ( revive(feature.name)}> diff --git a/frontend/src/component/feature/feature.scss b/frontend/src/component/feature/feature.scss index 05730919e3..bba65140ce 100644 --- a/frontend/src/component/feature/feature.scss +++ b/frontend/src/component/feature/feature.scss @@ -33,3 +33,8 @@ .strategyChip { margin-left: 8px !important; } + +.typeChip { + margin-left: 8px !important; + background: #d3c1ff; +} \ No newline at end of file diff --git a/frontend/src/component/feature/form/__tests__/__snapshots__/form-add-feature-component-test.jsx.snap b/frontend/src/component/feature/form/__tests__/__snapshots__/form-add-feature-component-test.jsx.snap index d721fdbf24..98a882c18e 100644 --- a/frontend/src/component/feature/form/__tests__/__snapshots__/form-add-feature-component-test.jsx.snap +++ b/frontend/src/component/feature/form/__tests__/__snapshots__/form-add-feature-component-test.jsx.snap @@ -23,21 +23,54 @@ exports[`render the create feature page 1`] = `
+ + + + + + + + + + Disabled + + +
- -
-
- - Enabled - -
-
-
({ key: t.id, label: t.name, title: t.description })); + + if (!options.find(o => o.key === value)) { + options.push({ key: value, label: value }); + } + + return ; + } +} + +FeatureTypeSelectComponent.propTypes = { + value: PropTypes.string, + filled: PropTypes.bool, + types: PropTypes.array.isRequired, + fetchFeatureTypes: PropTypes.func, + onChange: PropTypes.func.isRequired, +}; + +export default FeatureTypeSelectComponent; diff --git a/frontend/src/component/feature/form/feature-type-select-container.jsx b/frontend/src/component/feature/form/feature-type-select-container.jsx new file mode 100644 index 0000000000..a58462ec82 --- /dev/null +++ b/frontend/src/component/feature/form/feature-type-select-container.jsx @@ -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; diff --git a/frontend/src/component/feature/form/form-add-feature-component.jsx b/frontend/src/component/feature/form/form-add-feature-component.jsx index 1d357e285c..7f56de3683 100644 --- a/frontend/src/component/feature/form/form-add-feature-component.jsx +++ b/frontend/src/component/feature/form/form-add-feature-component.jsx @@ -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 { Create new feature toggle -
- validateName(v.target.value)} - onChange={v => setValue('name', trim(v.target.value))} - /> + + + validateName(v.target.value)} + onChange={v => setValue('name', trim(v.target.value))} + /> + + + setValue('type', v.target.value)} /> + + + { + setValue('enabled', !input.enabled); + }} + > + {input.enabled ? 'Enabled' : 'Disabled'} + + + +
setValue('description', v.target.value)} /> -
-
- { - setValue('enabled', !input.enabled); - }} - > - Enabled - -
-
-
{input.name ? ( +
{description}  {isFeatureView && hasPermission(UPDATE_FEATURE) ? ( edit ) : null} - +
); } renderEdit() { const { description } = this.state; return ( - +
-
+
); } diff --git a/frontend/src/component/feature/list-component.jsx b/frontend/src/component/feature/list-component.jsx index 55b6f1c634..ea9d5c3e29 100644 --- a/frontend/src/component/feature/list-component.jsx +++ b/frontend/src/component/feature/list-component.jsx @@ -105,6 +105,9 @@ export default class FeatureListComponent extends React.Component { Name + + Type + Enabled diff --git a/frontend/src/component/feature/list-container.jsx b/frontend/src/component/feature/list-container.jsx index 49a1150a9c..54b5733a92 100644 --- a/frontend/src/component/feature/list-container.jsx +++ b/frontend/src/component/feature/list-container.jsx @@ -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; diff --git a/frontend/src/component/feature/view-component.jsx b/frontend/src/component/feature/view-component.jsx index f656347662..37ac1cf42d 100644 --- a/frontend/src/component/feature/view-component.jsx +++ b/frontend/src/component/feature/view-component.jsx @@ -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 ( {featureToggle.name} - + + + + + + response.json()); +} + +export default { + fetchAll, +}; diff --git a/frontend/src/store/error-store.js b/frontend/src/store/error-store.js index 19f61dc77c..c172f1fe9a 100644 --- a/frontend/src/store/error-store.js +++ b/frontend/src/store/error-store.js @@ -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: diff --git a/frontend/src/store/feature-actions.js b/frontend/src/store/feature-actions.js index be60edbbde..5ad8d57bd6 100644 --- a/frontend/src/store/feature-actions.js +++ b/frontend/src/store/feature-actions.js @@ -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)); }; } diff --git a/frontend/src/store/feature-type/actions.js b/frontend/src/store/feature-type/actions.js new file mode 100644 index 0000000000..04eaaeae75 --- /dev/null +++ b/frontend/src/store/feature-type/actions.js @@ -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)); +} diff --git a/frontend/src/store/feature-type/index.js b/frontend/src/store/feature-type/index.js new file mode 100644 index 0000000000..32fdb97851 --- /dev/null +++ b/frontend/src/store/feature-type/index.js @@ -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; diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index f1fd1f157a..d7fc5efe1a 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -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,