From 85fb0f9b8998cd200d7540225a95a056c8effb8e Mon Sep 17 00:00:00 2001 From: Benjamin Ludewig Date: Wed, 16 Jan 2019 10:39:58 +0100 Subject: [PATCH] feature: Add support for permission system in unleash frontend refactored permission-based components added tests for all permission-based components --- frontend/src/__mocks__/react-mdl.js | 2 + .../application-edit-component-test.js.snap | 353 ++++++++++++++++++ .../application-edit-component-test.js | 120 +++++- .../application/application-edit-component.js | 95 ++--- .../application/application-edit-container.js | 2 + .../src/component/archive/view-container.js | 2 + .../component/common/permission-component.jsx | 21 -- .../component/common/permission-container.js | 8 - .../feature-list-item-component-test.jsx.snap | 1 - .../list-component-test.jsx.snap | 332 ++++++++++++++++ .../view-component-test.jsx.snap | 144 +++++++ .../feature-list-item-component-test.jsx | 51 ++- .../feature/__tests__/list-component-test.jsx | 64 ++++ .../feature/__tests__/view-component-test.jsx | 44 +++ .../feature/feature-list-item-component.jsx | 49 +-- .../strategy-input-list-test.jsx.snap | 27 ++ .../__tests__/strategy-input-list-test.jsx | 8 + .../feature/form/strategy-configure.jsx | 7 +- .../feature/form/strategy-input-list.jsx | 51 +-- .../src/component/feature/list-component.jsx | 24 +- .../src/component/feature/list-container.jsx | 2 + .../src/component/feature/view-component.jsx | 148 +++----- .../src/component/feature/view-container.jsx | 2 + .../list-component-test.jsx.snap | 138 +++++++ .../strategy-details-component-test.jsx.snap | 167 +++++++++ .../__tests__/list-component-test.jsx | 48 +++ .../strategy-details-component-test.jsx | 54 +++ .../component/strategies/list-component.jsx | 34 +- .../component/strategies/list-container.jsx | 2 + .../strategies/strategy-details-component.jsx | 17 +- .../strategies/strategy-details-container.js | 2 + frontend/src/permissions.js | 7 + 32 files changed, 1719 insertions(+), 307 deletions(-) delete mode 100644 frontend/src/component/common/permission-component.jsx delete mode 100644 frontend/src/component/common/permission-container.js create mode 100644 frontend/src/component/feature/__tests__/__snapshots__/list-component-test.jsx.snap create mode 100644 frontend/src/component/feature/__tests__/__snapshots__/view-component-test.jsx.snap create mode 100644 frontend/src/component/feature/__tests__/list-component-test.jsx create mode 100644 frontend/src/component/feature/__tests__/view-component-test.jsx create mode 100644 frontend/src/component/strategies/__tests__/__snapshots__/list-component-test.jsx.snap create mode 100644 frontend/src/component/strategies/__tests__/__snapshots__/strategy-details-component-test.jsx.snap create mode 100644 frontend/src/component/strategies/__tests__/list-component-test.jsx create mode 100644 frontend/src/component/strategies/__tests__/strategy-details-component-test.jsx 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..3349746ef9 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 65a034af42..e814a84048 100644 --- a/frontend/src/component/application/application-edit-component.js +++ b/frontend/src/component/application/application-edit-component.js @@ -23,7 +23,6 @@ import { import { IconLink, shorten, styles as commonStyles } from '../common'; import { formatFullDateTimeWithLocale } from '../common/util'; import { CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION } from '../../permissions'; -import PermissionComponent from '../common/permission-container'; class StatefulTextfield extends Component { static propTypes = { @@ -63,6 +62,7 @@ class ClientApplications extends PureComponent { application: PropTypes.object, location: PropTypes.object, storeApplicationMetaData: PropTypes.func.isRequired, + hasPermission: PropTypes.func.isRequired, }; constructor(props) { @@ -80,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 = @@ -93,26 +93,17 @@ class ClientApplications extends PureComponent { {seenToggles.map( ({ name, description, enabled, notFound }, i) => notFound ? ( - - - {name} - - - } - otherwise={ - - - {name} - - - } - /> + + {hasPermission(CREATE_FEATURE) ? ( + + {name} + + ) : ( + + {name} + + )} + ) : ( notFound ? ( - - - {name} - - - } - otherwise={ - - - {name} - - - } - /> + + {hasPermission(CREATE_STRATEGY) ? ( + + {name} + + ) : ( + + {name} + + )} + ) : ( @@ -235,21 +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/common/permission-component.jsx b/frontend/src/component/common/permission-component.jsx deleted file mode 100644 index ad9000818f..0000000000 --- a/frontend/src/component/common/permission-component.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import PropTypes from 'prop-types'; -import { ADMIN } from '../../permissions'; - -const PermissionComponent = ({ user, permission, component, otherwise }) => { - if ( - user && - (!user.permissions || user.permissions.indexOf(ADMIN) !== -1 || user.permissions.indexOf(permission) !== -1) - ) { - return component; - } - return otherwise || ''; -}; - -PermissionComponent.propTypes = { - user: PropTypes.object, - component: PropTypes.node, - otherwise: PropTypes.node, - permission: PropTypes.string, -}; - -export default PermissionComponent; diff --git a/frontend/src/component/common/permission-container.js b/frontend/src/component/common/permission-container.js deleted file mode 100644 index f4cf8d9d1e..0000000000 --- a/frontend/src/component/common/permission-container.js +++ /dev/null @@ -1,8 +0,0 @@ -import { connect } from 'react-redux'; -import PermissionComponent from './permission-component'; - -const mapStateToProps = state => ({ user: state.user.get('profile') }); - -const Container = connect(mapStateToProps)(PermissionComponent); - -export default Container; 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 808dfb1cf9..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 @@ -79,7 +79,6 @@ exports[`renders correctly with one feature without permission 1`] = ` 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 3dafb195b3..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 @@ -1,8 +1,5 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { createStore } from 'redux'; -import { Provider } from 'react-redux'; -import { Map as $Map } from 'immutable'; import Feature from './../feature-list-item-component'; import renderer from 'react-test-renderer'; @@ -25,22 +22,20 @@ test('renders correctly with one feature', () => { ], createdAt: '2018-02-04T20:27:52.127Z', }; - const store = { user: new $Map({ profile: { permissions: [UPDATE_FEATURE] } }) }; const featureMetrics = { lastHour: {}, lastMinute: {}, seenApps: {} }; const settings = { sort: 'name' }; const tree = renderer.create( - state, store)}> - - - - + + permission === UPDATE_FEATURE} + /> + ); expect(tree).toMatchSnapshot(); @@ -61,22 +56,20 @@ test('renders correctly with one feature without permission', () => { ], createdAt: '2018-02-04T20:27:52.127Z', }; - const store = { user: new $Map({ profile: { permissions: [] } }) }; const featureMetrics = { lastHour: {}, lastMinute: {}, seenApps: {} }; const settings = { sort: 'name' }; const tree = renderer.create( - state, store)}> - - - - + + false} + /> + ); expect(tree).toMatchSnapshot(); 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 ae7feace7d..41bb2cf11e 100644 --- a/frontend/src/component/feature/feature-list-item-component.jsx +++ b/frontend/src/component/feature/feature-list-item-component.jsx @@ -3,7 +3,6 @@ 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 PermissionComponent from '../common/permission-container'; import { UPDATE_FEATURE } from '../../permissions'; import { calc, styles as commonStyles } from '../common'; @@ -16,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; @@ -43,27 +43,17 @@ const Feature = ({ - toggleFeature(name)} - checked={enabled} - /> - } - otherwise={ - toggleFeature(name)} - checked={enabled} - /> - } - /> + {hasPermission(UPDATE_FEATURE) ? ( + toggleFeature(name)} + checked={enabled} + /> + ) : ( + + )} @@ -75,16 +65,10 @@ const Feature = ({ {strategyChips} {summaryChip} - {revive ? ( - revive(feature.name)}> - - - } - otherwise={} - /> + {revive && hasPermission(UPDATE_FEATURE) ? ( + revive(feature.name)}> + + ) : ( )} @@ -99,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/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 294941f8e5..aef75559f5 100644 --- a/frontend/src/component/feature/list-component.jsx +++ b/frontend/src/component/feature/list-component.jsx @@ -6,7 +6,6 @@ import { Icon, FABButton, Textfield, Menu, MenuItem, Card, CardActions, List } f import { MenuItemWithIcon, DropdownButton, styles as commonStyles } from '../common'; import styles from './feature.scss'; import { CREATE_FEATURE } from '../../permissions'; -import PermissionComponent from '../common/permission-container'; export default class FeatureListComponent extends React.Component { static propTypes = { @@ -21,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() { @@ -48,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; }); @@ -64,16 +64,15 @@ export default class FeatureListComponent extends React.Component { label="Search" style={{ width: '100%' }} /> - - - - - - } - /> + {hasPermission(CREATE_FEATURE) ? ( + + + + + + ) : ( + '' + )}
@@ -126,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 35ea0a285c..86cbc4799f 100644 --- a/frontend/src/component/feature/view-component.jsx +++ b/frontend/src/component/feature/view-component.jsx @@ -9,7 +9,6 @@ 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'; -import PermissionComponent from '../common/permission-container'; const TABS = { strategies: 0, @@ -36,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() { @@ -49,24 +49,14 @@ 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 ( - - } - otherwise={} - /> + ); } return ; @@ -90,6 +80,7 @@ export default class ViewFeatureToggleComponent extends React.Component { featureToggleName, toggleFeature, removeFeatureToggle, + hasPermission, } = this.props; if (!featureToggle) { @@ -99,20 +90,18 @@ export default class ViewFeatureToggleComponent extends React.Component { return ( Could not find the toggle{' '} - - {featureToggleName} - - } - otherwise={featureToggleName} - /> + {hasPermission(CREATE_FEATURE) ? ( + + {featureToggleName} + + ) : ( + featureToggleName + )} ); } @@ -152,32 +141,16 @@ export default class ViewFeatureToggleComponent extends React.Component { {featureToggle.name} - {this.isFeatureView ? ( - setValue('description', v)} - onBlur={updateFeatureToggle} - /> - } - otherwise={ - - } + {this.isFeatureView && hasPermission(UPDATE_FEATURE) ? ( + setValue('description', v)} + onBlur={updateFeatureToggle} /> ) : ( - toggleFeature(featureToggle.name)} - > - {featureToggle.enabled ? 'Enabled' : 'Disabled'} - - } - otherwise={ - toggleFeature(featureToggle.name)} - > - {featureToggle.enabled ? 'Enabled' : 'Disabled'} - - } - /> + {hasPermission(UPDATE_FEATURE) ? ( + toggleFeature(featureToggle.name)} + > + {featureToggle.enabled ? 'Enabled' : 'Disabled'} + + ) : ( + + {featureToggle.enabled ? 'Enabled' : 'Disabled'} + + )} {this.isFeatureView ? ( - - Archive - - } - /> + ) : ( - - Revive - - } - /> + )}
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 bf9749ffc5..61bfd6c57f 100644 --- a/frontend/src/component/strategies/list-component.jsx +++ b/frontend/src/component/strategies/list-component.jsx @@ -5,7 +5,6 @@ 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'; -import PermissionComponent from '../common/permission-container'; class StrategiesListComponent extends Component { static propTypes = { @@ -13,6 +12,7 @@ class StrategiesListComponent extends Component { fetchStrategies: PropTypes.func.isRequired, removeStrategy: PropTypes.func.isRequired, history: PropTypes.object.isRequired, + hasPermission: PropTypes.func.isRequired, }; componentDidMount() { @@ -20,7 +20,7 @@ class StrategiesListComponent extends Component { } render() { - const { strategies, removeStrategy } = this.props; + const { strategies, removeStrategy, hasPermission } = this.props; return ( @@ -28,17 +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" + /> + ) : ( + '' + ) } /> @@ -50,15 +49,10 @@ class StrategiesListComponent extends Component { {strategy.name}
- {strategy.editable === false ? ( + {strategy.editable === false || !hasPermission(DELETE_STRATEGY) ? ( '' ) : ( - removeStrategy(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 fb01bec8d1..8110095789 100644 --- a/frontend/src/component/strategies/strategy-details-component.jsx +++ b/frontend/src/component/strategies/strategy-details-component.jsx @@ -5,7 +5,6 @@ import ShowStrategy from './show-strategy-component'; import EditStrategy from './edit-container'; import { HeaderTitle } from '../common'; import { UPDATE_STRATEGY } from '../../permissions'; -import PermissionComponent from '../common/permission-container'; const TABS = { view: 0, @@ -23,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() { @@ -68,18 +68,13 @@ export default class StrategyDetails extends Component { - {strategy.editable === false ? ( + {strategy.editable === false || !this.props.hasPermission(UPDATE_STRATEGY) ? ( '' ) : ( - - this.goToTab('view')}>Details - this.goToTab('edit')}>Edit - - } - /> + + this.goToTab('view')}>Details + this.goToTab('edit')}>Edit + )}
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/permissions.js b/frontend/src/permissions.js index 58daa194e3..70c4b64872 100644 --- a/frontend/src/permissions.js +++ b/frontend/src/permissions.js @@ -6,3 +6,10 @@ 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) + ); +}