diff --git a/frontend/src/__mocks__/react-mdl.js b/frontend/src/__mocks__/react-mdl.js index 7052e6d945..fc109db7a9 100644 --- a/frontend/src/__mocks__/react-mdl.js +++ b/frontend/src/__mocks__/react-mdl.js @@ -9,6 +9,8 @@ module.exports = { Cell: 'react-mdl-Cell', Chip: 'react-mdl-Chip', Grid: 'react-mdl-Grid', + Button: 'react-mdl-Button', + FABButton: 'react-mdl-FABButton', Icon: 'react-mdl-Icon', IconButton: 'react-mdl-IconButton', List: 'react-mdl-List', diff --git a/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap b/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap index f395e1b464..39fa96d945 100644 --- a/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap +++ b/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap @@ -5,3 +5,356 @@ exports[`renders correctly if no application 1`] = ` indeterminate={true} /> `; + +exports[`renders correctly with permissions 1`] = ` + + + + + test-app + + + app description + + + + + + +
+ + + Details + + + Edit + + + + +
+ Toggles +
+
+ + + + + + } + subtitle="this is A toggle" + > + + ToggleA + + + + + + + ToggleB + + + + +
+ +
+ Implemented strategies +
+
+ + + + + StrategyA + + + + + + + StrategyB + + + + +
+ +
+ 1 + Instances registered +
+
+ + + + 123.123.123.123 + last seen at + + + 02/23/2017, 3:56:49 PM + + + } + > + instance-1 + + (4.0) + + + +
+
+
+`; + +exports[`renders correctly without permission 1`] = ` + + + + + test-app + + + app description + + + + + + +
+ + + +
+ Toggles +
+
+ + + + + + } + subtitle="this is A toggle" + > + + ToggleA + + + + + + ToggleB + + + +
+ +
+ Implemented strategies +
+
+ + + + + StrategyA + + + + + + StrategyB + + + +
+ +
+ 1 + Instances registered +
+
+ + + + 123.123.123.123 + last seen at + + + 02/23/2017, 3:56:49 PM + + + } + > + instance-1 + + (4.0) + + + +
+
+
+`; diff --git a/frontend/src/component/application/__tests__/application-edit-component-test.js b/frontend/src/component/application/__tests__/application-edit-component-test.js index 892c953030..a2a8198394 100644 --- a/frontend/src/component/application/__tests__/application-edit-component-test.js +++ b/frontend/src/component/application/__tests__/application-edit-component-test.js @@ -2,12 +2,130 @@ import React from 'react'; import ClientApplications from '../application-edit-component'; import renderer from 'react-test-renderer'; +import { MemoryRouter } from 'react-router-dom'; +import { CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION } from '../../../permissions'; jest.mock('react-mdl'); test('renders correctly if no application', () => { const tree = renderer - .create() + .create( + true} + /> + ) + .toJSON(); + + expect(tree).toMatchSnapshot(); +}); + +test('renders correctly without permission', () => { + const tree = renderer + .create( + + false} + /> + + ) + .toJSON(); + + expect(tree).toMatchSnapshot(); +}); + +test('renders correctly with permissions', () => { + const tree = renderer + .create( + + + [CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION].indexOf(permission) !== -1 + } + /> + + ) .toJSON(); expect(tree).toMatchSnapshot(); diff --git a/frontend/src/component/application/application-edit-component.js b/frontend/src/component/application/application-edit-component.js index 5dcc164875..e814a84048 100644 --- a/frontend/src/component/application/application-edit-component.js +++ b/frontend/src/component/application/application-edit-component.js @@ -22,6 +22,7 @@ import { } from 'react-mdl'; import { IconLink, shorten, styles as commonStyles } from '../common'; import { formatFullDateTimeWithLocale } from '../common/util'; +import { CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION } from '../../permissions'; class StatefulTextfield extends Component { static propTypes = { @@ -61,6 +62,7 @@ class ClientApplications extends PureComponent { application: PropTypes.object, location: PropTypes.object, storeApplicationMetaData: PropTypes.func.isRequired, + hasPermission: PropTypes.func.isRequired, }; constructor(props) { @@ -78,7 +80,7 @@ class ClientApplications extends PureComponent { if (!this.props.application) { return ; } - const { application, storeApplicationMetaData } = this.props; + const { application, storeApplicationMetaData, hasPermission } = this.props; const { appName, instances, strategies, seenToggles, url, description, icon = 'apps', color } = application; const content = @@ -92,9 +94,15 @@ class ClientApplications extends PureComponent { ({ name, description, enabled, notFound }, i) => notFound ? ( - - {name} - + {hasPermission(CREATE_FEATURE) ? ( + + {name} + + ) : ( + + {name} + + )} ) : ( @@ -121,9 +129,15 @@ class ClientApplications extends PureComponent { ({ name, description, notFound }, i) => notFound ? ( - - {name} - + {hasPermission(CREATE_STRATEGY) ? ( + + {name} + + ) : ( + + {name} + + )} ) : ( @@ -203,16 +217,20 @@ class ClientApplications extends PureComponent { )}
- this.setState({ activeTab: tabId })} - ripple - tabBarProps={{ style: { width: '100%' } }} - className="mdl-color--grey-100" - > - Details - Edit - + {hasPermission(UPDATE_APPLICATION) ? ( + this.setState({ activeTab: tabId })} + ripple + tabBarProps={{ style: { width: '100%' } }} + className="mdl-color--grey-100" + > + Details + Edit + + ) : ( + '' + )} {content} diff --git a/frontend/src/component/application/application-edit-container.js b/frontend/src/component/application/application-edit-container.js index cb9daba63d..a3d89b54ba 100644 --- a/frontend/src/component/application/application-edit-container.js +++ b/frontend/src/component/application/application-edit-container.js @@ -1,6 +1,7 @@ import { connect } from 'react-redux'; import ApplicationEdit from './application-edit-component'; import { fetchApplication, storeApplicationMetaData } from './../../store/application/actions'; +import { hasPermission } from '../../permissions'; const mapStateToProps = (state, props) => { let application = state.applications.getIn(['apps', props.appName]); @@ -11,6 +12,7 @@ const mapStateToProps = (state, props) => { return { application, location, + hasPermission: hasPermission.bind(null, state.user.get('profile')), }; }; diff --git a/frontend/src/component/archive/view-container.js b/frontend/src/component/archive/view-container.js index 6457722619..f4bd906017 100644 --- a/frontend/src/component/archive/view-container.js +++ b/frontend/src/component/archive/view-container.js @@ -1,6 +1,7 @@ import { connect } from 'react-redux'; import { fetchArchive, revive } from './../../store/archive-actions'; import ViewToggleComponent from './../feature/view-component'; +import { hasPermission } from '../../permissions'; export default connect( (state, props) => ({ @@ -10,6 +11,7 @@ export default connect( .toArray() .find(toggle => toggle.name === props.featureToggleName), activeTab: props.activeTab, + hasPermission: hasPermission.bind(null, state.user.get('profile')), }), { fetchArchive, 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 07294b161d..8e1aa06601 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 @@ -55,3 +55,58 @@ exports[`renders correctly with one feature 1`] = ` `; + +exports[`renders correctly with one feature without permission 1`] = ` + + + + + + + + + + + + Another + + another's description + + + + + + gradualRolloutRandom + + + + +`; 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 new file mode 100644 index 0000000000..d5bbbd92ed --- /dev/null +++ b/frontend/src/component/feature/__tests__/__snapshots__/list-component-test.jsx.snap @@ -0,0 +1,332 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly with one feature 1`] = ` +
+
+ + + + + + +
+ + + + Last minute + + + + + + Last minute + + + + Last hour + + + + By name + + + + + Name + + + Enabled + + + Created + + + Strategies + + + Metrics + + + +
+ + + +
+
+`; + +exports[`renders correctly with one feature without permissions 1`] = ` +
+
+ + +
+ + + + Last minute + + + + + + Last minute + + + + Last hour + + + + By name + + + + + Name + + + Enabled + + + Created + + + Strategies + + + Metrics + + + +
+ + + +
+
+`; diff --git a/frontend/src/component/feature/__tests__/__snapshots__/view-component-test.jsx.snap b/frontend/src/component/feature/__tests__/__snapshots__/view-component-test.jsx.snap new file mode 100644 index 0000000000..5fbbfe5201 --- /dev/null +++ b/frontend/src/component/feature/__tests__/__snapshots__/view-component-test.jsx.snap @@ -0,0 +1,144 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly with one feature 1`] = ` + + + Another + + + + + + + + Disabled + + + + Archive + + +
+ + + Strategies + + + Metrics + + + History + + + +
+`; diff --git a/frontend/src/component/feature/__tests__/feature-list-item-component-test.jsx b/frontend/src/component/feature/__tests__/feature-list-item-component-test.jsx index 20e52e44c3..b403f63695 100644 --- a/frontend/src/component/feature/__tests__/feature-list-item-component-test.jsx +++ b/frontend/src/component/feature/__tests__/feature-list-item-component-test.jsx @@ -3,6 +3,7 @@ import { MemoryRouter } from 'react-router-dom'; import Feature from './../feature-list-item-component'; import renderer from 'react-test-renderer'; +import { UPDATE_FEATURE } from '../../../permissions'; jest.mock('react-mdl'); @@ -32,6 +33,41 @@ test('renders correctly with one feature', () => { metricsLastMinute={featureMetrics.lastMinute[feature.name]} feature={feature} toggleFeature={jest.fn()} + hasPermission={permission => permission === UPDATE_FEATURE} + /> + + ); + + expect(tree).toMatchSnapshot(); +}); + +test('renders correctly with one feature without permission', () => { + const feature = { + name: 'Another', + description: "another's description", + enabled: false, + strategies: [ + { + name: 'gradualRolloutRandom', + parameters: { + percentage: 50, + }, + }, + ], + createdAt: '2018-02-04T20:27:52.127Z', + }; + const featureMetrics = { lastHour: {}, lastMinute: {}, seenApps: {} }; + const settings = { sort: 'name' }; + const tree = renderer.create( + + false} /> ); diff --git a/frontend/src/component/feature/__tests__/list-component-test.jsx b/frontend/src/component/feature/__tests__/list-component-test.jsx new file mode 100644 index 0000000000..b3fd258844 --- /dev/null +++ b/frontend/src/component/feature/__tests__/list-component-test.jsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; + +import FeatureListComponent from './../list-component'; +import renderer from 'react-test-renderer'; +import { CREATE_FEATURE } from '../../../permissions'; + +jest.mock('react-mdl'); +jest.mock('../feature-list-item-component', () => ({ + __esModule: true, + default: 'Feature', +})); + +test('renders correctly with one feature', () => { + const features = [ + { + name: 'Another', + }, + ]; + const featureMetrics = { lastHour: {}, lastMinute: {}, seenApps: {} }; + const settings = { sort: 'name' }; + const tree = renderer.create( + + permission === CREATE_FEATURE} + /> + + ); + + expect(tree).toMatchSnapshot(); +}); + +test('renders correctly with one feature without permissions', () => { + const features = [ + { + name: 'Another', + }, + ]; + const featureMetrics = { lastHour: {}, lastMinute: {}, seenApps: {} }; + const settings = { sort: 'name' }; + const tree = renderer.create( + + false} + /> + + ); + + expect(tree).toMatchSnapshot(); +}); diff --git a/frontend/src/component/feature/__tests__/view-component-test.jsx b/frontend/src/component/feature/__tests__/view-component-test.jsx new file mode 100644 index 0000000000..921549ce29 --- /dev/null +++ b/frontend/src/component/feature/__tests__/view-component-test.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; + +import ViewFeatureToggleComponent from './../view-component'; +import renderer from 'react-test-renderer'; +import { DELETE_FEATURE, UPDATE_FEATURE } from '../../../permissions'; + +jest.mock('react-mdl'); +jest.mock('../form/form-update-feature-container', () => ({ + __esModule: true, + default: 'UpdateFeatureToggleComponent', +})); + +test('renders correctly with one feature', () => { + const feature = { + name: 'Another', + description: "another's description", + enabled: false, + strategies: [ + { + name: 'gradualRolloutRandom', + parameters: { + percentage: 50, + }, + }, + ], + createdAt: '2018-02-04T20:27:52.127Z', + }; + const tree = renderer.create( + + [DELETE_FEATURE, UPDATE_FEATURE].indexOf(permission) !== -1} + /> + + ); + + expect(tree).toMatchSnapshot(); +}); diff --git a/frontend/src/component/feature/feature-list-item-component.jsx b/frontend/src/component/feature/feature-list-item-component.jsx index 9ec3d019b5..41bb2cf11e 100644 --- a/frontend/src/component/feature/feature-list-item-component.jsx +++ b/frontend/src/component/feature/feature-list-item-component.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; import { Switch, Chip, ListItem, ListItemAction, Icon } from 'react-mdl'; import Progress from './progress'; +import { UPDATE_FEATURE } from '../../permissions'; import { calc, styles as commonStyles } from '../common'; import styles from './feature.scss'; @@ -14,6 +15,7 @@ const Feature = ({ metricsLastHour = { yes: 0, no: 0, isFallback: true }, metricsLastMinute = { yes: 0, no: 0, isFallback: true }, revive, + hasPermission, }) => { const { name, description, enabled, strategies } = feature; const { showLastHour = false } = settings; @@ -41,13 +43,17 @@ const Feature = ({
- toggleFeature(name)} - checked={enabled} - /> + {hasPermission(UPDATE_FEATURE) ? ( + toggleFeature(name)} + checked={enabled} + /> + ) : ( + + )} @@ -59,7 +65,7 @@ const Feature = ({ {strategyChips} {summaryChip} - {revive ? ( + {revive && hasPermission(UPDATE_FEATURE) ? ( revive(feature.name)}> @@ -77,6 +83,7 @@ Feature.propTypes = { metricsLastHour: PropTypes.object, metricsLastMinute: PropTypes.object, revive: PropTypes.func, + hasPermission: PropTypes.func.isRequired, }; export default Feature; diff --git a/frontend/src/component/feature/form/__tests__/__snapshots__/strategy-input-list-test.jsx.snap b/frontend/src/component/feature/form/__tests__/__snapshots__/strategy-input-list-test.jsx.snap index b9410f89ce..5cf83c499b 100644 --- a/frontend/src/component/feature/form/__tests__/__snapshots__/strategy-input-list-test.jsx.snap +++ b/frontend/src/component/feature/form/__tests__/__snapshots__/strategy-input-list-test.jsx.snap @@ -1,5 +1,32 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`renders correctly when disabled 1`] = ` +
+

+ featureName +

+ + item1 + + + item2 + + +
+`; + exports[`renders strategy with empty list as param 1`] = `

diff --git a/frontend/src/component/feature/form/__tests__/strategy-input-list-test.jsx b/frontend/src/component/feature/form/__tests__/strategy-input-list-test.jsx index ad49344c7c..5be4471733 100644 --- a/frontend/src/component/feature/form/__tests__/strategy-input-list-test.jsx +++ b/frontend/src/component/feature/form/__tests__/strategy-input-list-test.jsx @@ -79,3 +79,11 @@ it('spy onClose', () => { wrapper.find('react-mdl-Chip').simulate('close', closeMock); expect(onClose).toHaveBeenCalled(); }); + +it('renders correctly when disabled', () => { + const list = ['item1', 'item2']; + const name = 'featureName'; + const tree = renderer.create(); + + expect(tree).toMatchSnapshot(); +}); diff --git a/frontend/src/component/feature/form/strategies-section.jsx b/frontend/src/component/feature/form/strategies-section.jsx index 2006d82210..f4cd730395 100644 --- a/frontend/src/component/feature/form/strategies-section.jsx +++ b/frontend/src/component/feature/form/strategies-section.jsx @@ -8,10 +8,10 @@ import { HeaderTitle } from '../../common'; class StrategiesSectionComponent extends React.Component { static propTypes = { strategies: PropTypes.array.isRequired, - addStrategy: PropTypes.func.isRequired, - removeStrategy: PropTypes.func.isRequired, - updateStrategy: PropTypes.func.isRequired, - fetchStrategies: PropTypes.func.isRequired, + addStrategy: PropTypes.func, + removeStrategy: PropTypes.func, + updateStrategy: PropTypes.func, + fetchStrategies: PropTypes.func, }; componentWillMount() { diff --git a/frontend/src/component/feature/form/strategy-configure.jsx b/frontend/src/component/feature/form/strategy-configure.jsx index b18e5e850a..a15e217a61 100644 --- a/frontend/src/component/feature/form/strategy-configure.jsx +++ b/frontend/src/component/feature/form/strategy-configure.jsx @@ -104,7 +104,12 @@ class StrategyConfigure extends React.Component { } return (

- + {description &&

{description}

}
); diff --git a/frontend/src/component/feature/form/strategy-input-list.jsx b/frontend/src/component/feature/form/strategy-input-list.jsx index 102b61eb6e..cdfd9610f2 100644 --- a/frontend/src/component/feature/form/strategy-input-list.jsx +++ b/frontend/src/component/feature/form/strategy-input-list.jsx @@ -7,6 +7,7 @@ export default class InputList extends Component { name: PropTypes.string.isRequired, list: PropTypes.array.isRequired, setConfig: PropTypes.func.isRequired, + disabled: PropTypes.bool, }; onBlur(e) { @@ -49,35 +50,43 @@ export default class InputList extends Component { } render() { - const { name, list } = this.props; + const { name, list, disabled } = this.props; return (

{name}

{list.map((entryValue, index) => ( - this.onClose(index)}> + this.onClose(index)} + > {entryValue} ))} -
- { - this.textInput = input; - }} - /> - -
+ {disabled ? ( + '' + ) : ( +
+ { + this.textInput = input; + }} + /> + +
+ )}
); } diff --git a/frontend/src/component/feature/list-component.jsx b/frontend/src/component/feature/list-component.jsx index 849542ed51..aef75559f5 100644 --- a/frontend/src/component/feature/list-component.jsx +++ b/frontend/src/component/feature/list-component.jsx @@ -5,6 +5,7 @@ import { Link } from 'react-router-dom'; import { Icon, FABButton, Textfield, Menu, MenuItem, Card, CardActions, List } from 'react-mdl'; import { MenuItemWithIcon, DropdownButton, styles as commonStyles } from '../common'; import styles from './feature.scss'; +import { CREATE_FEATURE } from '../../permissions'; export default class FeatureListComponent extends React.Component { static propTypes = { @@ -19,6 +20,7 @@ export default class FeatureListComponent extends React.Component { toggleFeature: PropTypes.func, settings: PropTypes.object, history: PropTypes.object.isRequired, + hasPermission: PropTypes.func.isRequired, }; componentDidMount() { @@ -46,7 +48,7 @@ export default class FeatureListComponent extends React.Component { } render() { - const { features, toggleFeature, featureMetrics, settings, revive } = this.props; + const { features, toggleFeature, featureMetrics, settings, revive, hasPermission } = this.props; features.forEach(e => { e.reviveName = e.name; }); @@ -62,11 +64,15 @@ export default class FeatureListComponent extends React.Component { label="Search" style={{ width: '100%' }} /> - - - - - + {hasPermission(CREATE_FEATURE) ? ( + + + + + + ) : ( + '' + )}
@@ -119,6 +125,7 @@ export default class FeatureListComponent extends React.Component { feature={feature} toggleFeature={toggleFeature} revive={revive} + hasPermission={hasPermission} /> ))} diff --git a/frontend/src/component/feature/list-container.jsx b/frontend/src/component/feature/list-container.jsx index 65b9128ff2..56dc606639 100644 --- a/frontend/src/component/feature/list-container.jsx +++ b/frontend/src/component/feature/list-container.jsx @@ -4,6 +4,7 @@ import { updateSettingForGroup } from '../../store/settings/actions'; import FeatureListComponent from './list-component'; import { logoutUser } from '../../store/user/actions'; +import { hasPermission } from '../../permissions'; export const mapStateToPropsConfigurable = isFeature => state => { const featureMetrics = state.featureMetrics.toJS(); @@ -68,6 +69,7 @@ export const mapStateToPropsConfigurable = isFeature => state => { features, featureMetrics, settings, + hasPermission: hasPermission.bind(null, state.user.get('profile')), }; }; const mapStateToProps = mapStateToPropsConfigurable(true); diff --git a/frontend/src/component/feature/view-component.jsx b/frontend/src/component/feature/view-component.jsx index 16c2cbf3db..86cbc4799f 100644 --- a/frontend/src/component/feature/view-component.jsx +++ b/frontend/src/component/feature/view-component.jsx @@ -8,6 +8,7 @@ import MetricComponent from './metric-container'; import EditFeatureToggle from './form/form-update-feature-container'; import ViewFeatureToggle from './form/form-view-feature-container'; import { styles as commonStyles } from '../common'; +import { CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE } from '../../permissions'; const TABS = { strategies: 0, @@ -34,6 +35,7 @@ export default class ViewFeatureToggleComponent extends React.Component { editFeatureToggle: PropTypes.func, featureToggle: PropTypes.object, history: PropTypes.object.isRequired, + hasPermission: PropTypes.func.isRequired, }; componentWillMount() { @@ -47,12 +49,12 @@ export default class ViewFeatureToggleComponent extends React.Component { } getTabContent(activeTab) { - const { features, featureToggle, featureToggleName } = this.props; + const { features, featureToggle, featureToggleName, hasPermission } = this.props; if (TABS[activeTab] === TABS.history) { return ; } else if (TABS[activeTab] === TABS.strategies) { - if (this.isFeatureView) { + if (this.isFeatureView && hasPermission(UPDATE_FEATURE)) { return ( ); @@ -78,6 +80,7 @@ export default class ViewFeatureToggleComponent extends React.Component { featureToggleName, toggleFeature, removeFeatureToggle, + hasPermission, } = this.props; if (!featureToggle) { @@ -87,14 +90,18 @@ export default class ViewFeatureToggleComponent extends React.Component { return ( Could not find the toggle{' '} - - {featureToggleName} - + {hasPermission(CREATE_FEATURE) ? ( + + {featureToggleName} + + ) : ( + featureToggleName + )} ); } @@ -115,8 +122,8 @@ export default class ViewFeatureToggleComponent extends React.Component { revive(featureToggle.name); this.props.history.push('/features'); }; - const updateFeatureToggle = () => { - let feature = { ...featureToggle }; + const updateFeatureToggle = e => { + let feature = { ...featureToggle, description: e.target.value }; if (Array.isArray(feature.strategies)) { feature.strategies.forEach(s => { delete s.id; @@ -134,7 +141,7 @@ export default class ViewFeatureToggleComponent extends React.Component { {featureToggle.name} - {this.isFeatureView ? ( + {this.isFeatureView && hasPermission(UPDATE_FEATURE) ? ( - toggleFeature(featureToggle.name)} - > - {featureToggle.enabled ? 'Enabled' : 'Disabled'} - + {hasPermission(UPDATE_FEATURE) ? ( + toggleFeature(featureToggle.name)} + > + {featureToggle.enabled ? 'Enabled' : 'Disabled'} + + ) : ( + + {featureToggle.enabled ? 'Enabled' : 'Disabled'} + + )} {this.isFeatureView ? ( - ) : ( - )} diff --git a/frontend/src/component/feature/view-container.jsx b/frontend/src/component/feature/view-container.jsx index f8a33ae007..4ed95e6309 100644 --- a/frontend/src/component/feature/view-container.jsx +++ b/frontend/src/component/feature/view-container.jsx @@ -8,12 +8,14 @@ import { } from './../../store/feature-actions'; import ViewToggleComponent from './view-component'; +import { hasPermission } from '../../permissions'; export default connect( (state, props) => ({ features: state.features.toJS(), featureToggle: state.features.toJS().find(toggle => toggle.name === props.featureToggleName), activeTab: props.activeTab, + hasPermission: hasPermission.bind(null, state.user.get('profile')), }), { fetchFeatureToggles, diff --git a/frontend/src/component/strategies/__tests__/__snapshots__/list-component-test.jsx.snap b/frontend/src/component/strategies/__tests__/__snapshots__/list-component-test.jsx.snap new file mode 100644 index 0000000000..df427c3075 --- /dev/null +++ b/frontend/src/component/strategies/__tests__/__snapshots__/list-component-test.jsx.snap @@ -0,0 +1,138 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly with one strategy 1`] = ` + + +
+
+
+ Strategies +
+
+
+ +
+
+ + + + + + Another + + + + + + +
+
+`; + +exports[`renders correctly with one strategy without permissions 1`] = ` + + +
+
+
+ Strategies +
+
+ +
+ + + + + + Another + + + + + + +
+
+`; diff --git a/frontend/src/component/strategies/__tests__/__snapshots__/strategy-details-component-test.jsx.snap b/frontend/src/component/strategies/__tests__/__snapshots__/strategy-details-component-test.jsx.snap new file mode 100644 index 0000000000..6949b2efc8 --- /dev/null +++ b/frontend/src/component/strategies/__tests__/__snapshots__/strategy-details-component-test.jsx.snap @@ -0,0 +1,167 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly with one strategy 1`] = ` + + +
+
+
+ Another +
+ + another's description + +
+
+ + + Details + + + Edit + + +
+
+
+ + +
+ Parameters +
+
+ + + + customParam + + + ( + list + ) + + + + +
+ +
+ Applications using this strategy +
+
+ + + + + + appA + + app description + + + + + +
+ +
+ Toggles using this strategy +
+
+ + + + + toggleA + + + + +
+
+
+
+
+
+
+`; diff --git a/frontend/src/component/strategies/__tests__/list-component-test.jsx b/frontend/src/component/strategies/__tests__/list-component-test.jsx new file mode 100644 index 0000000000..f2a2295fdc --- /dev/null +++ b/frontend/src/component/strategies/__tests__/list-component-test.jsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; + +import StrategiesListComponent from '../list-component'; +import renderer from 'react-test-renderer'; +import { CREATE_STRATEGY, DELETE_STRATEGY } from '../../../permissions'; + +jest.mock('react-mdl'); + +test('renders correctly with one strategy', () => { + const strategy = { + name: 'Another', + description: "another's description", + }; + const tree = renderer.create( + + [CREATE_STRATEGY, DELETE_STRATEGY].indexOf(permission) !== -1} + /> + + ); + + expect(tree).toMatchSnapshot(); +}); + +test('renders correctly with one strategy without permissions', () => { + const strategy = { + name: 'Another', + description: "another's description", + }; + const tree = renderer.create( + + false} + /> + + ); + + expect(tree).toMatchSnapshot(); +}); diff --git a/frontend/src/component/strategies/__tests__/strategy-details-component-test.jsx b/frontend/src/component/strategies/__tests__/strategy-details-component-test.jsx new file mode 100644 index 0000000000..65c3beaba3 --- /dev/null +++ b/frontend/src/component/strategies/__tests__/strategy-details-component-test.jsx @@ -0,0 +1,54 @@ +import React from 'react'; + +import StrategyDetails from '../strategy-details-component'; +import renderer from 'react-test-renderer'; +import { UPDATE_STRATEGY } from '../../../permissions'; +import { MemoryRouter } from 'react-router-dom'; + +jest.mock('react-mdl'); + +test('renders correctly with one strategy', () => { + const strategy = { + name: 'Another', + description: "another's description", + editable: true, + parameters: [ + { + name: 'customParam', + type: 'list', + description: 'customList', + required: true, + }, + ], + }; + const applications = [ + { + appName: 'appA', + description: 'app description', + }, + ]; + const toggles = [ + { + name: 'toggleA', + description: 'toggle description', + }, + ]; + const tree = renderer.create( + + [UPDATE_STRATEGY].indexOf(permission) !== -1} + /> + + ); + + expect(tree).toMatchSnapshot(); +}); diff --git a/frontend/src/component/strategies/list-component.jsx b/frontend/src/component/strategies/list-component.jsx index a1fbbc0c85..61bfd6c57f 100644 --- a/frontend/src/component/strategies/list-component.jsx +++ b/frontend/src/component/strategies/list-component.jsx @@ -4,6 +4,7 @@ import { Link } from 'react-router-dom'; import { List, ListItem, ListItemContent, IconButton, Grid, Cell } from 'react-mdl'; import { HeaderTitle } from '../common'; +import { CREATE_STRATEGY, DELETE_STRATEGY } from '../../permissions'; class StrategiesListComponent extends Component { static propTypes = { @@ -11,6 +12,7 @@ class StrategiesListComponent extends Component { fetchStrategies: PropTypes.func.isRequired, removeStrategy: PropTypes.func.isRequired, history: PropTypes.object.isRequired, + hasPermission: PropTypes.func.isRequired, }; componentDidMount() { @@ -18,7 +20,7 @@ class StrategiesListComponent extends Component { } render() { - const { strategies, removeStrategy } = this.props; + const { strategies, removeStrategy, hasPermission } = this.props; return ( @@ -26,12 +28,16 @@ class StrategiesListComponent extends Component { this.props.history.push('/strategies/create')} - title="Add new strategy" - /> + hasPermission(CREATE_STRATEGY) ? ( + this.props.history.push('/strategies/create')} + title="Add new strategy" + /> + ) : ( + '' + ) } /> @@ -43,7 +49,7 @@ class StrategiesListComponent extends Component { {strategy.name} - {strategy.editable === false ? ( + {strategy.editable === false || !hasPermission(DELETE_STRATEGY) ? ( '' ) : ( removeStrategy(strategy)} /> diff --git a/frontend/src/component/strategies/list-container.jsx b/frontend/src/component/strategies/list-container.jsx index aec2cf3791..a71e7431f4 100644 --- a/frontend/src/component/strategies/list-container.jsx +++ b/frontend/src/component/strategies/list-container.jsx @@ -1,12 +1,14 @@ import { connect } from 'react-redux'; import StrategiesListComponent from './list-component.jsx'; import { fetchStrategies, removeStrategy } from './../../store/strategy/actions'; +import { hasPermission } from '../../permissions'; const mapStateToProps = state => { const list = state.strategies.get('list').toArray(); return { strategies: list, + hasPermission: hasPermission.bind(null, state.user.get('profile')), }; }; diff --git a/frontend/src/component/strategies/strategy-details-component.jsx b/frontend/src/component/strategies/strategy-details-component.jsx index 49a7b4a4f5..8110095789 100644 --- a/frontend/src/component/strategies/strategy-details-component.jsx +++ b/frontend/src/component/strategies/strategy-details-component.jsx @@ -4,6 +4,7 @@ import { Tabs, Tab, ProgressBar, Grid, Cell } from 'react-mdl'; import ShowStrategy from './show-strategy-component'; import EditStrategy from './edit-container'; import { HeaderTitle } from '../common'; +import { UPDATE_STRATEGY } from '../../permissions'; const TABS = { view: 0, @@ -21,6 +22,7 @@ export default class StrategyDetails extends Component { fetchApplications: PropTypes.func.isRequired, fetchFeatureToggles: PropTypes.func.isRequired, history: PropTypes.object.isRequired, + hasPermission: PropTypes.func.isRequired, }; componentDidMount() { @@ -66,7 +68,7 @@ export default class StrategyDetails extends Component { - {strategy.editable === false ? ( + {strategy.editable === false || !this.props.hasPermission(UPDATE_STRATEGY) ? ( '' ) : ( diff --git a/frontend/src/component/strategies/strategy-details-container.js b/frontend/src/component/strategies/strategy-details-container.js index 486b6e3a06..95087ba166 100644 --- a/frontend/src/component/strategies/strategy-details-container.js +++ b/frontend/src/component/strategies/strategy-details-container.js @@ -3,6 +3,7 @@ import ShowStrategy from './strategy-details-component'; import { fetchStrategies } from './../../store/strategy/actions'; import { fetchAll } from './../../store/application/actions'; import { fetchFeatureToggles } from './../../store/feature-actions'; +import { hasPermission } from '../../permissions'; const mapStateToProps = (state, props) => { let strategy = state.strategies.get('list').find(n => n.name === props.strategyName); @@ -17,6 +18,7 @@ const mapStateToProps = (state, props) => { applications: applications && applications.toJS(), toggles: toggles && toggles.toJS(), activeTab: props.activeTab, + hasPermission: hasPermission.bind(null, state.user.get('profile')), }; }; diff --git a/frontend/src/data/helper.js b/frontend/src/data/helper.js index f1132843fa..00d18e8fde 100644 --- a/frontend/src/data/helper.js +++ b/frontend/src/data/helper.js @@ -23,12 +23,25 @@ export class AuthenticationError extends Error { } } +export class ForbiddenError extends Error { + constructor(statusCode, body) { + super('You cannot perform this action'); + this.name = 'ForbiddenError'; + this.statusCode = statusCode; + this.body = body; + } +} + export function throwIfNotSuccess(response) { if (!response.ok) { if (response.status === 401) { return new Promise((resolve, reject) => { response.json().then(body => reject(new AuthenticationError(response.status, body))); }); + } else if (response.status === 403) { + return new Promise((resolve, reject) => { + response.json().then(body => reject(new ForbiddenError(response.status, body))); + }); } else if (response.status > 399 && response.status < 404) { return new Promise((resolve, reject) => { response.json().then(body => { diff --git a/frontend/src/permissions.js b/frontend/src/permissions.js new file mode 100644 index 0000000000..70c4b64872 --- /dev/null +++ b/frontend/src/permissions.js @@ -0,0 +1,15 @@ +export const ADMIN = 'ADMIN'; +export const CREATE_FEATURE = 'CREATE_FEATURE'; +export const UPDATE_FEATURE = 'UPDATE_FEATURE'; +export const DELETE_FEATURE = 'DELETE_FEATURE'; +export const CREATE_STRATEGY = 'CREATE_STRATEGY'; +export const UPDATE_STRATEGY = 'UPDATE_STRATEGY'; +export const DELETE_STRATEGY = 'DELETE_STRATEGY'; +export const UPDATE_APPLICATION = 'UPDATE_APPLICATION'; + +export function hasPermission(user, permission) { + return ( + user && + (!user.permissions || user.permissions.indexOf(ADMIN) !== -1 || user.permissions.indexOf(permission) !== -1) + ); +}