1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-11 00:08:30 +01:00

feature: Add support for permission system in unleash frontend

refactored permission-based components
added tests for all permission-based components
This commit is contained in:
Benjamin Ludewig 2019-01-16 10:39:58 +01:00
parent ba7ec688eb
commit 85fb0f9b89
32 changed files with 1719 additions and 307 deletions

View File

@ -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',

View File

@ -5,3 +5,356 @@ exports[`renders correctly if no application 1`] = `
indeterminate={true}
/>
`;
exports[`renders correctly with permissions 1`] = `
<react-mdl-Card
className="fullwidth"
shadow={0}
>
<react-mdl-CardTitle
style={
Object {
"paddingRight": "64px",
"paddingTop": "24px",
"wordBreak": "break-all",
}
}
>
<react-mdl-Icon
name="apps"
/>
test-app
</react-mdl-CardTitle>
<react-mdl-CardText>
app description
</react-mdl-CardText>
<react-mdl-CardMenu>
<a
className="mdl-color-text--grey-600"
href="http://example.org"
rel="noopener"
target="_blank"
>
<react-mdl-Icon
name="link"
/>
</a>
</react-mdl-CardMenu>
<hr />
<react-mdl-Tabs
activeTab={0}
className="mdl-color--grey-100"
onChange={[Function]}
ripple={true}
tabBarProps={
Object {
"style": Object {
"width": "100%",
},
}
}
>
<react-mdl-Tab>
Details
</react-mdl-Tab>
<react-mdl-Tab>
Edit
</react-mdl-Tab>
</react-mdl-Tabs>
<react-mdl-Grid
style={
Object {
"margin": 0,
}
}
>
<react-mdl-Cell
col={6}
phone={12}
tablet={4}
>
<h6>
Toggles
</h6>
<hr />
<react-mdl-List>
<react-mdl-ListItem
twoLine={true}
>
<react-mdl-ListItemContent
icon={
<span>
<react-mdl-Switch
checked={true}
disabled={true}
/>
</span>
}
subtitle="this is A toggle"
>
<a
href="/features/view/ToggleA"
onClick={[Function]}
>
ToggleA
</a>
</react-mdl-ListItemContent>
</react-mdl-ListItem>
<react-mdl-ListItem
twoLine={true}
>
<react-mdl-ListItemContent
icon="report"
subtitle="Missing, want to create?"
>
<a
href="/features/create?name=ToggleB"
onClick={[Function]}
>
ToggleB
</a>
</react-mdl-ListItemContent>
</react-mdl-ListItem>
</react-mdl-List>
</react-mdl-Cell>
<react-mdl-Cell
col={6}
phone={12}
tablet={4}
>
<h6>
Implemented strategies
</h6>
<hr />
<react-mdl-List>
<react-mdl-ListItem
twoLine={true}
>
<react-mdl-ListItemContent
icon="extension"
subtitle="A description"
>
<a
href="/strategies/view/StrategyA"
onClick={[Function]}
>
StrategyA
</a>
</react-mdl-ListItemContent>
</react-mdl-ListItem>
<react-mdl-ListItem
twoLine={true}
>
<react-mdl-ListItemContent
icon="report"
subtitle="Missing, want to create?"
>
<a
href="/strategies/create?name=StrategyB"
onClick={[Function]}
>
StrategyB
</a>
</react-mdl-ListItemContent>
</react-mdl-ListItem>
</react-mdl-List>
</react-mdl-Cell>
<react-mdl-Cell
col={12}
tablet={12}
>
<h6>
1
Instances registered
</h6>
<hr />
<react-mdl-List>
<react-mdl-ListItem
twoLine={true}
>
<react-mdl-ListItemContent
icon="timeline"
subtitle={
<span>
123.123.123.123
last seen at
<small>
02/23/2017, 3:56:49 PM
</small>
</span>
}
>
instance-1
(4.0)
</react-mdl-ListItemContent>
</react-mdl-ListItem>
</react-mdl-List>
</react-mdl-Cell>
</react-mdl-Grid>
</react-mdl-Card>
`;
exports[`renders correctly without permission 1`] = `
<react-mdl-Card
className="fullwidth"
shadow={0}
>
<react-mdl-CardTitle
style={
Object {
"paddingRight": "64px",
"paddingTop": "24px",
"wordBreak": "break-all",
}
}
>
<react-mdl-Icon
name="apps"
/>
test-app
</react-mdl-CardTitle>
<react-mdl-CardText>
app description
</react-mdl-CardText>
<react-mdl-CardMenu>
<a
className="mdl-color-text--grey-600"
href="http://example.org"
rel="noopener"
target="_blank"
>
<react-mdl-Icon
name="link"
/>
</a>
</react-mdl-CardMenu>
<hr />
<react-mdl-Grid
style={
Object {
"margin": 0,
}
}
>
<react-mdl-Cell
col={6}
phone={12}
tablet={4}
>
<h6>
Toggles
</h6>
<hr />
<react-mdl-List>
<react-mdl-ListItem
twoLine={true}
>
<react-mdl-ListItemContent
icon={
<span>
<react-mdl-Switch
checked={true}
disabled={true}
/>
</span>
}
subtitle="this is A toggle"
>
<a
href="/features/view/ToggleA"
onClick={[Function]}
>
ToggleA
</a>
</react-mdl-ListItemContent>
</react-mdl-ListItem>
<react-mdl-ListItem
twoLine={true}
>
<react-mdl-ListItemContent
icon="report"
subtitle="Missing"
>
ToggleB
</react-mdl-ListItemContent>
</react-mdl-ListItem>
</react-mdl-List>
</react-mdl-Cell>
<react-mdl-Cell
col={6}
phone={12}
tablet={4}
>
<h6>
Implemented strategies
</h6>
<hr />
<react-mdl-List>
<react-mdl-ListItem
twoLine={true}
>
<react-mdl-ListItemContent
icon="extension"
subtitle="A description"
>
<a
href="/strategies/view/StrategyA"
onClick={[Function]}
>
StrategyA
</a>
</react-mdl-ListItemContent>
</react-mdl-ListItem>
<react-mdl-ListItem
twoLine={true}
>
<react-mdl-ListItemContent
icon="report"
subtitle="Missing"
>
StrategyB
</react-mdl-ListItemContent>
</react-mdl-ListItem>
</react-mdl-List>
</react-mdl-Cell>
<react-mdl-Cell
col={12}
tablet={12}
>
<h6>
1
Instances registered
</h6>
<hr />
<react-mdl-List>
<react-mdl-ListItem
twoLine={true}
>
<react-mdl-ListItemContent
icon="timeline"
subtitle={
<span>
123.123.123.123
last seen at
<small>
02/23/2017, 3:56:49 PM
</small>
</span>
}
>
instance-1
(4.0)
</react-mdl-ListItemContent>
</react-mdl-ListItem>
</react-mdl-List>
</react-mdl-Cell>
</react-mdl-Grid>
</react-mdl-Card>
`;

View File

@ -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(<ClientApplications fetchApplication={jest.fn()} storeApplicationMetaData={jest.fn()} />)
.create(
<ClientApplications
fetchApplication={jest.fn()}
storeApplicationMetaData={jest.fn()}
hasPermission={() => true}
/>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
test('renders correctly without permission', () => {
const tree = renderer
.create(
<MemoryRouter>
<ClientApplications
fetchApplication={jest.fn()}
storeApplicationMetaData={jest.fn()}
application={{
appName: 'test-app',
instances: [
{
instanceId: 'instance-1',
clientIp: '123.123.123.123',
lastSeen: 1487861809466,
sdkVersion: '4.0',
},
],
strategies: [
{
name: 'StrategyA',
description: 'A description',
},
{
name: 'StrategyB',
description: 'B description',
notFound: true,
},
],
seenToggles: [
{
name: 'ToggleA',
description: 'this is A toggle',
enabled: true,
},
{
name: 'ToggleB',
description: 'this is B toggle',
enabled: false,
notFound: true,
},
],
url: 'http://example.org',
description: 'app description',
}}
location={{ locale: 'en-GB' }}
hasPermission={() => false}
/>
</MemoryRouter>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
test('renders correctly with permissions', () => {
const tree = renderer
.create(
<MemoryRouter>
<ClientApplications
fetchApplication={jest.fn()}
storeApplicationMetaData={jest.fn()}
application={{
appName: 'test-app',
instances: [
{
instanceId: 'instance-1',
clientIp: '123.123.123.123',
lastSeen: 1487861809466,
sdkVersion: '4.0',
},
],
strategies: [
{
name: 'StrategyA',
description: 'A description',
},
{
name: 'StrategyB',
description: 'B description',
notFound: true,
},
],
seenToggles: [
{
name: 'ToggleA',
description: 'this is A toggle',
enabled: true,
},
{
name: 'ToggleB',
description: 'this is B toggle',
enabled: false,
notFound: true,
},
],
url: 'http://example.org',
description: 'app description',
}}
location={{ locale: 'en-GB' }}
hasPermission={permission =>
[CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION].indexOf(permission) !== -1
}
/>
</MemoryRouter>
)
.toJSON();
expect(tree).toMatchSnapshot();

View File

@ -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 <ProgressBar indeterminate />;
}
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 ? (
<PermissionComponent
permission={CREATE_FEATURE}
component={
<ListItem twoLine key={i}>
<ListItemContent
icon={'report'}
subtitle={'Missing, want to create?'}
>
<Link to={`/features/create?name=${name}`}>{name}</Link>
</ListItemContent>
</ListItem>
}
otherwise={
<ListItem twoLine key={i}>
<ListItemContent icon={'report'} subtitle={'Missing'}>
{name}
</ListItemContent>
</ListItem>
}
/>
<ListItem twoLine key={i}>
{hasPermission(CREATE_FEATURE) ? (
<ListItemContent icon={'report'} subtitle={'Missing, want to create?'}>
<Link to={`/features/create?name=${name}`}>{name}</Link>
</ListItemContent>
) : (
<ListItemContent icon={'report'} subtitle={'Missing'}>
{name}
</ListItemContent>
)}
</ListItem>
) : (
<ListItem twoLine key={i}>
<ListItemContent
@ -137,26 +128,17 @@ class ClientApplications extends PureComponent {
{strategies.map(
({ name, description, notFound }, i) =>
notFound ? (
<PermissionComponent
permission={CREATE_STRATEGY}
component={
<ListItem twoLine key={`${name}-${i}`}>
<ListItemContent
icon={'report'}
subtitle={'Missing, want to create?'}
>
<Link to={`/strategies/create?name=${name}`}>{name}</Link>
</ListItemContent>
</ListItem>
}
otherwise={
<ListItem twoLine key={`${name}-${i}`}>
<ListItemContent icon={'report'} subtitle={'Missing'}>
{name}
</ListItemContent>
</ListItem>
}
/>
<ListItem twoLine key={`${name}-${i}`}>
{hasPermission(CREATE_STRATEGY) ? (
<ListItemContent icon={'report'} subtitle={'Missing, want to create?'}>
<Link to={`/strategies/create?name=${name}`}>{name}</Link>
</ListItemContent>
) : (
<ListItemContent icon={'report'} subtitle={'Missing'}>
{name}
</ListItemContent>
)}
</ListItem>
) : (
<ListItem twoLine key={`${name}-${i}`}>
<ListItemContent icon={'extension'} subtitle={shorten(description, 60)}>
@ -235,21 +217,20 @@ class ClientApplications extends PureComponent {
</CardMenu>
)}
<hr />
<PermissionComponent
permission={UPDATE_APPLICATION}
component={
<Tabs
activeTab={this.state.activeTab}
onChange={tabId => this.setState({ activeTab: tabId })}
ripple
tabBarProps={{ style: { width: '100%' } }}
className="mdl-color--grey-100"
>
<Tab>Details</Tab>
<Tab>Edit</Tab>
</Tabs>
}
/>
{hasPermission(UPDATE_APPLICATION) ? (
<Tabs
activeTab={this.state.activeTab}
onChange={tabId => this.setState({ activeTab: tabId })}
ripple
tabBarProps={{ style: { width: '100%' } }}
className="mdl-color--grey-100"
>
<Tab>Details</Tab>
<Tab>Edit</Tab>
</Tabs>
) : (
''
)}
{content}
</Card>

View File

@ -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')),
};
};

View File

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

View File

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

View File

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

View File

@ -79,7 +79,6 @@ exports[`renders correctly with one feature without permission 1`] = `
<react-mdl-Switch
checked={false}
disabled={true}
onChange={[Function]}
title="Toggle Another"
/>
</span>

View File

@ -0,0 +1,332 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly with one feature 1`] = `
<div>
<div
className="toolbar"
>
<react-mdl-Textfield
floatingLabel={true}
label="Search"
onChange={[Function]}
style={
Object {
"width": "100%",
}
}
/>
<a
className="toolbarButton"
href="/features/create"
onClick={[Function]}
>
<react-mdl-FABButton
accent={true}
title="Create feature toggle"
>
<react-mdl-Icon
name="add"
/>
</react-mdl-FABButton>
</a>
</div>
<react-mdl-Card
className="fullwidth"
shadow={0}
style={
Object {
"overflow": "visible",
}
}
>
<react-mdl-CardActions>
<react-mdl-Button
className="dropdownButton"
id="metric"
>
Last minute
<react-mdl-Icon
className="mdl-color-text--grey-600"
name="arrow_drop_down"
/>
</react-mdl-Button>
<react-mdl-Menu
onClick={[Function]}
style={
Object {
"width": "168px",
}
}
target="metric"
>
<react-mdl-MenuItem
data-target="minute"
disabled={true}
style={
Object {
"alignItems": "center",
"display": "flex",
}
}
>
<react-mdl-Icon
name="hourglass_empty"
style={
Object {
"paddingRight": "16px",
}
}
/>
Last minute
</react-mdl-MenuItem>
<react-mdl-MenuItem
data-target="hour"
style={
Object {
"alignItems": "center",
"display": "flex",
}
}
>
<react-mdl-Icon
name="hourglass_full"
style={
Object {
"paddingRight": "16px",
}
}
/>
Last hour
</react-mdl-MenuItem>
</react-mdl-Menu>
<react-mdl-Button
className="dropdownButton"
id="sorting"
>
By name
<react-mdl-Icon
className="mdl-color-text--grey-600"
name="arrow_drop_down"
/>
</react-mdl-Button>
<react-mdl-Menu
onClick={[Function]}
style={
Object {
"width": "168px",
}
}
target="sorting"
>
<react-mdl-MenuItem
data-target="name"
disabled={true}
>
Name
</react-mdl-MenuItem>
<react-mdl-MenuItem
data-target="enabled"
disabled={false}
>
Enabled
</react-mdl-MenuItem>
<react-mdl-MenuItem
data-target="created"
disabled={false}
>
Created
</react-mdl-MenuItem>
<react-mdl-MenuItem
data-target="strategies"
disabled={false}
>
Strategies
</react-mdl-MenuItem>
<react-mdl-MenuItem
data-target="metrics"
disabled={false}
>
Metrics
</react-mdl-MenuItem>
</react-mdl-Menu>
</react-mdl-CardActions>
<hr />
<react-mdl-List>
<Feature
feature={
Object {
"name": "Another",
"reviveName": "Another",
}
}
hasPermission={[Function]}
settings={
Object {
"sort": "name",
}
}
toggleFeature={[MockFunction]}
/>
</react-mdl-List>
</react-mdl-Card>
</div>
`;
exports[`renders correctly with one feature without permissions 1`] = `
<div>
<div
className="toolbar"
>
<react-mdl-Textfield
floatingLabel={true}
label="Search"
onChange={[Function]}
style={
Object {
"width": "100%",
}
}
/>
</div>
<react-mdl-Card
className="fullwidth"
shadow={0}
style={
Object {
"overflow": "visible",
}
}
>
<react-mdl-CardActions>
<react-mdl-Button
className="dropdownButton"
id="metric"
>
Last minute
<react-mdl-Icon
className="mdl-color-text--grey-600"
name="arrow_drop_down"
/>
</react-mdl-Button>
<react-mdl-Menu
onClick={[Function]}
style={
Object {
"width": "168px",
}
}
target="metric"
>
<react-mdl-MenuItem
data-target="minute"
disabled={true}
style={
Object {
"alignItems": "center",
"display": "flex",
}
}
>
<react-mdl-Icon
name="hourglass_empty"
style={
Object {
"paddingRight": "16px",
}
}
/>
Last minute
</react-mdl-MenuItem>
<react-mdl-MenuItem
data-target="hour"
style={
Object {
"alignItems": "center",
"display": "flex",
}
}
>
<react-mdl-Icon
name="hourglass_full"
style={
Object {
"paddingRight": "16px",
}
}
/>
Last hour
</react-mdl-MenuItem>
</react-mdl-Menu>
<react-mdl-Button
className="dropdownButton"
id="sorting"
>
By name
<react-mdl-Icon
className="mdl-color-text--grey-600"
name="arrow_drop_down"
/>
</react-mdl-Button>
<react-mdl-Menu
onClick={[Function]}
style={
Object {
"width": "168px",
}
}
target="sorting"
>
<react-mdl-MenuItem
data-target="name"
disabled={true}
>
Name
</react-mdl-MenuItem>
<react-mdl-MenuItem
data-target="enabled"
disabled={false}
>
Enabled
</react-mdl-MenuItem>
<react-mdl-MenuItem
data-target="created"
disabled={false}
>
Created
</react-mdl-MenuItem>
<react-mdl-MenuItem
data-target="strategies"
disabled={false}
>
Strategies
</react-mdl-MenuItem>
<react-mdl-MenuItem
data-target="metrics"
disabled={false}
>
Metrics
</react-mdl-MenuItem>
</react-mdl-Menu>
</react-mdl-CardActions>
<hr />
<react-mdl-List>
<Feature
feature={
Object {
"name": "Another",
"reviveName": "Another",
}
}
hasPermission={[Function]}
settings={
Object {
"sort": "name",
}
}
toggleFeature={[MockFunction]}
/>
</react-mdl-List>
</react-mdl-Card>
</div>
`;

View File

@ -0,0 +1,144 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly with one feature 1`] = `
<react-mdl-Card
className="fullwidth"
shadow={0}
style={
Object {
"overflow": "visible",
}
}
>
<react-mdl-CardTitle
style={
Object {
"paddingTop": "24px",
"wordBreak": "break-all",
}
}
>
Another
</react-mdl-CardTitle>
<react-mdl-CardText>
<react-mdl-Textfield
floatingLabel={true}
label="Description"
onBlur={[Function]}
onChange={[Function]}
required={true}
rows={1}
style={
Object {
"width": "100%",
}
}
value="another's description"
/>
</react-mdl-CardText>
<react-mdl-CardActions
border={true}
style={
Object {
"alignItems": "center",
"display": "flex",
"justifyContent": "space-between",
}
}
>
<span
style={
Object {
"paddingRight": "24px",
}
}
>
<react-mdl-Switch
checked={false}
disabled={false}
onChange={[Function]}
ripple={true}
>
Disabled
</react-mdl-Switch>
</span>
<react-mdl-Button
disabled={false}
onClick={[Function]}
style={
Object {
"flexShrink": 0,
}
}
>
Archive
</react-mdl-Button>
</react-mdl-CardActions>
<hr />
<react-mdl-Tabs
activeTab={0}
className="mdl-color--grey-100"
ripple={true}
tabBarProps={
Object {
"style": Object {
"width": "100%",
},
}
}
>
<react-mdl-Tab
onClick={[Function]}
>
Strategies
</react-mdl-Tab>
<react-mdl-Tab
onClick={[Function]}
>
Metrics
</react-mdl-Tab>
<react-mdl-Tab
onClick={[Function]}
>
History
</react-mdl-Tab>
</react-mdl-Tabs>
<UpdateFeatureToggleComponent
featureToggle={
Object {
"createdAt": "2018-02-04T20:27:52.127Z",
"description": "another's description",
"enabled": false,
"name": "Another",
"strategies": Array [
Object {
"name": "gradualRolloutRandom",
"parameters": Object {
"percentage": 50,
},
},
],
}
}
features={
Array [
Object {
"createdAt": "2018-02-04T20:27:52.127Z",
"description": "another's description",
"enabled": false,
"name": "Another",
"strategies": Array [
Object {
"name": "gradualRolloutRandom",
"parameters": Object {
"percentage": 50,
},
},
],
},
]
}
history={Object {}}
/>
</react-mdl-Card>
`;

View File

@ -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(
<Provider store={createStore(state => state, store)}>
<MemoryRouter>
<Feature
key={0}
settings={settings}
metricsLastHour={featureMetrics.lastHour[feature.name]}
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
feature={feature}
toggleFeature={jest.fn()}
/>
</MemoryRouter>
</Provider>
<MemoryRouter>
<Feature
key={0}
settings={settings}
metricsLastHour={featureMetrics.lastHour[feature.name]}
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
feature={feature}
toggleFeature={jest.fn()}
hasPermission={permission => permission === UPDATE_FEATURE}
/>
</MemoryRouter>
);
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(
<Provider store={createStore(state => state, store)}>
<MemoryRouter>
<Feature
key={0}
settings={settings}
metricsLastHour={featureMetrics.lastHour[feature.name]}
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
feature={feature}
toggleFeature={jest.fn()}
/>
</MemoryRouter>
</Provider>
<MemoryRouter>
<Feature
key={0}
settings={settings}
metricsLastHour={featureMetrics.lastHour[feature.name]}
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
feature={feature}
toggleFeature={jest.fn()}
hasPermission={() => false}
/>
</MemoryRouter>
);
expect(tree).toMatchSnapshot();

View File

@ -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(
<MemoryRouter>
<FeatureListComponent
updateSetting={jest.fn()}
settings={settings}
history={{}}
featureMetrics={featureMetrics}
features={features}
toggleFeature={jest.fn()}
fetchFeatureToggles={jest.fn()}
hasPermission={permission => permission === CREATE_FEATURE}
/>
</MemoryRouter>
);
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(
<MemoryRouter>
<FeatureListComponent
updateSetting={jest.fn()}
settings={settings}
history={{}}
featureMetrics={featureMetrics}
features={features}
toggleFeature={jest.fn()}
fetchFeatureToggles={jest.fn()}
hasPermission={() => false}
/>
</MemoryRouter>
);
expect(tree).toMatchSnapshot();
});

View File

@ -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(
<MemoryRouter>
<ViewFeatureToggleComponent
activeTab={'strategies'}
featureToggleName="another"
features={[feature]}
featureToggle={feature}
fetchFeatureToggles={jest.fn()}
history={{}}
hasPermission={permission => [DELETE_FEATURE, UPDATE_FEATURE].indexOf(permission) !== -1}
/>
</MemoryRouter>
);
expect(tree).toMatchSnapshot();
});

View File

@ -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 = ({
<Progress strokeWidth={15} percentage={percent} isFallback={isStale} />
</span>
<span className={styles.listItemToggle}>
<PermissionComponent
permission={UPDATE_FEATURE}
component={
<Switch
disabled={toggleFeature === undefined}
title={`Toggle ${name}`}
key="left-actions"
onChange={() => toggleFeature(name)}
checked={enabled}
/>
}
otherwise={
<Switch
disabled
title={`Toggle ${name}`}
key="left-actions"
onChange={() => toggleFeature(name)}
checked={enabled}
/>
}
/>
{hasPermission(UPDATE_FEATURE) ? (
<Switch
disabled={toggleFeature === undefined}
title={`Toggle ${name}`}
key="left-actions"
onChange={() => toggleFeature(name)}
checked={enabled}
/>
) : (
<Switch disabled title={`Toggle ${name}`} key="left-actions" checked={enabled} />
)}
</span>
<span className={['mdl-list__item-primary-content', styles.listItemLink].join(' ')}>
<Link to={featureUrl} className={[commonStyles.listLink, commonStyles.truncate].join(' ')}>
@ -75,16 +65,10 @@ const Feature = ({
{strategyChips}
{summaryChip}
</span>
{revive ? (
<PermissionComponent
permission={UPDATE_FEATURE}
component={
<ListItemAction onClick={() => revive(feature.name)}>
<Icon name="undo" />
</ListItemAction>
}
otherwise={<span />}
/>
{revive && hasPermission(UPDATE_FEATURE) ? (
<ListItemAction onClick={() => revive(feature.name)}>
<Icon name="undo" />
</ListItemAction>
) : (
<span />
)}
@ -99,6 +83,7 @@ Feature.propTypes = {
metricsLastHour: PropTypes.object,
metricsLastMinute: PropTypes.object,
revive: PropTypes.func,
hasPermission: PropTypes.func.isRequired,
};
export default Feature;

View File

@ -1,5 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly when disabled 1`] = `
<div>
<p>
featureName
</p>
<react-mdl-Chip
style={
Object {
"marginRight": "3px",
}
}
>
item1
</react-mdl-Chip>
<react-mdl-Chip
style={
Object {
"marginRight": "3px",
}
}
>
item2
</react-mdl-Chip>
</div>
`;
exports[`renders strategy with empty list as param 1`] = `
<div>
<p>

View File

@ -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(<InputList list={list} name={name} setConfig={jest.fn()} disabled />);
expect(tree).toMatchSnapshot();
});

View File

@ -104,7 +104,12 @@ class StrategyConfigure extends React.Component {
}
return (
<div key={name}>
<StrategyInputList name={name} list={list} setConfig={this.setConfig} />
<StrategyInputList
name={name}
list={list}
disabled={!this.props.updateStrategy}
setConfig={this.setConfig}
/>
{description && <p className={styles.helpText}>{description}</p>}
</div>
);

View File

@ -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 (
<div>
<p>{name}</p>
{list.map((entryValue, index) => (
<Chip key={index + entryValue} style={{ marginRight: '3px' }} onClose={() => this.onClose(index)}>
<Chip
key={index + entryValue}
style={{ marginRight: '3px' }}
onClose={disabled ? undefined : () => this.onClose(index)}
>
{entryValue}
</Chip>
))}
<div style={{ display: 'flex' }}>
<Textfield
name={`${name}_input`}
style={{ width: '100%', flex: 1 }}
floatingLabel
label="Add list entry"
onFocus={this.onFocus.bind(this)}
onBlur={this.onBlur.bind(this)}
ref={input => {
this.textInput = input;
}}
/>
<IconButton
name="add"
raised
style={{ flex: 1, flexGrow: 0, margin: '20px 0 0 10px' }}
onClick={this.setValue}
/>
</div>
{disabled ? (
''
) : (
<div style={{ display: 'flex' }}>
<Textfield
name={`${name}_input`}
style={{ width: '100%', flex: 1 }}
floatingLabel
label="Add list entry"
onFocus={this.onFocus.bind(this)}
onBlur={this.onBlur.bind(this)}
ref={input => {
this.textInput = input;
}}
/>
<IconButton
name="add"
raised
style={{ flex: 1, flexGrow: 0, margin: '20px 0 0 10px' }}
onClick={this.setValue}
/>
</div>
)}
</div>
);
}

View File

@ -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%' }}
/>
<PermissionComponent
permission={CREATE_FEATURE}
component={
<Link to="/features/create" className={styles.toolbarButton}>
<FABButton accent title="Create feature toggle">
<Icon name="add" />
</FABButton>
</Link>
}
/>
{hasPermission(CREATE_FEATURE) ? (
<Link to="/features/create" className={styles.toolbarButton}>
<FABButton accent title="Create feature toggle">
<Icon name="add" />
</FABButton>
</Link>
) : (
''
)}
</div>
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
<CardActions>
@ -126,6 +125,7 @@ export default class FeatureListComponent extends React.Component {
feature={feature}
toggleFeature={toggleFeature}
revive={revive}
hasPermission={hasPermission}
/>
))}
</List>

View File

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

View File

@ -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 <HistoryComponent toggleName={featureToggleName} />;
} else if (TABS[activeTab] === TABS.strategies) {
if (this.isFeatureView) {
if (this.isFeatureView && hasPermission(UPDATE_FEATURE)) {
return (
<PermissionComponent
permission={UPDATE_FEATURE}
component={
<EditFeatureToggle
featureToggle={featureToggle}
features={features}
history={this.props.history}
/>
}
otherwise={<ViewFeatureToggle featureToggle={featureToggle} />}
/>
<EditFeatureToggle featureToggle={featureToggle} features={features} history={this.props.history} />
);
}
return <ViewFeatureToggle featureToggle={featureToggle} />;
@ -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 (
<span>
Could not find the toggle{' '}
<PermissionComponent
permission={CREATE_FEATURE}
component={
<Link
to={{
pathname: '/features/create',
query: { name: featureToggleName },
}}
>
{featureToggleName}
</Link>
}
otherwise={featureToggleName}
/>
{hasPermission(CREATE_FEATURE) ? (
<Link
to={{
pathname: '/features/create',
query: { name: featureToggleName },
}}
>
{featureToggleName}
</Link>
) : (
featureToggleName
)}
</span>
);
}
@ -152,32 +141,16 @@ export default class ViewFeatureToggleComponent extends React.Component {
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
<CardTitle style={{ paddingTop: '24px', wordBreak: 'break-all' }}>{featureToggle.name}</CardTitle>
<CardText>
{this.isFeatureView ? (
<PermissionComponent
permission={UPDATE_FEATURE}
component={
<Textfield
floatingLabel
style={{ width: '100%' }}
rows={1}
label="Description"
required
value={featureToggle.description}
onChange={v => setValue('description', v)}
onBlur={updateFeatureToggle}
/>
}
otherwise={
<Textfield
disabled
floatingLabel
style={{ width: '100%' }}
rows={1}
label="Description"
required
value={featureToggle.description}
/>
}
{this.isFeatureView && hasPermission(UPDATE_FEATURE) ? (
<Textfield
floatingLabel
style={{ width: '100%' }}
rows={1}
label="Description"
required
value={featureToggle.description}
onChange={v => setValue('description', v)}
onBlur={updateFeatureToggle}
/>
) : (
<Textfield
@ -201,49 +174,38 @@ export default class ViewFeatureToggleComponent extends React.Component {
}}
>
<span style={{ paddingRight: '24px' }}>
<PermissionComponent
permission={UPDATE_FEATURE}
component={
<Switch
disabled={!this.isFeatureView}
ripple
checked={featureToggle.enabled}
onChange={() => toggleFeature(featureToggle.name)}
>
{featureToggle.enabled ? 'Enabled' : 'Disabled'}
</Switch>
}
otherwise={
<Switch
disabled
ripple
checked={featureToggle.enabled}
onChange={() => toggleFeature(featureToggle.name)}
>
{featureToggle.enabled ? 'Enabled' : 'Disabled'}
</Switch>
}
/>
{hasPermission(UPDATE_FEATURE) ? (
<Switch
disabled={!this.isFeatureView}
ripple
checked={featureToggle.enabled}
onChange={() => toggleFeature(featureToggle.name)}
>
{featureToggle.enabled ? 'Enabled' : 'Disabled'}
</Switch>
) : (
<Switch disabled ripple checked={featureToggle.enabled}>
{featureToggle.enabled ? 'Enabled' : 'Disabled'}
</Switch>
)}
</span>
{this.isFeatureView ? (
<PermissionComponent
permission={DELETE_FEATURE}
component={
<Button onClick={removeToggle} style={{ flexShrink: 0 }}>
Archive
</Button>
}
/>
<Button
disabled={!hasPermission(DELETE_FEATURE)}
onClick={removeToggle}
style={{ flexShrink: 0 }}
>
Archive
</Button>
) : (
<PermissionComponent
permission={UPDATE_FEATURE}
component={
<Button onClick={reviveToggle} style={{ flexShrink: 0 }}>
Revive
</Button>
}
/>
<Button
disabled={!hasPermission(UPDATE_FEATURE)}
onClick={reviveToggle}
style={{ flexShrink: 0 }}
>
Revive
</Button>
)}
</CardActions>
<hr />

View File

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

View File

@ -0,0 +1,138 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly with one strategy 1`] = `
<react-mdl-Grid
className="mdl-color--white"
>
<react-mdl-Cell
col={12}
>
<div
style={
Object {
"borderBottom": "1px solid #f1f1f1",
"display": "flex",
"marginBottom": "10px",
"padding": "16px 20px ",
}
}
>
<div
style={
Object {
"flex": "2",
}
}
>
<h6
style={
Object {
"margin": 0,
}
}
>
Strategies
</h6>
</div>
<div
style={
Object {
"flex": "1",
"textAlign": "right",
}
}
>
<react-mdl-IconButton
name="add"
onClick={[Function]}
raised={true}
title="Add new strategy"
/>
</div>
</div>
<react-mdl-List>
<react-mdl-ListItem
twoLine={true}
>
<react-mdl-ListItemContent
icon="extension"
subtitle="another's description"
>
<a
href="/strategies/view/Another"
onClick={[Function]}
>
<strong>
Another
</strong>
</a>
</react-mdl-ListItemContent>
<react-mdl-IconButton
name="delete"
onClick={[Function]}
/>
</react-mdl-ListItem>
</react-mdl-List>
</react-mdl-Cell>
</react-mdl-Grid>
`;
exports[`renders correctly with one strategy without permissions 1`] = `
<react-mdl-Grid
className="mdl-color--white"
>
<react-mdl-Cell
col={12}
>
<div
style={
Object {
"borderBottom": "1px solid #f1f1f1",
"display": "flex",
"marginBottom": "10px",
"padding": "16px 20px ",
}
}
>
<div
style={
Object {
"flex": "2",
}
}
>
<h6
style={
Object {
"margin": 0,
}
}
>
Strategies
</h6>
</div>
</div>
<react-mdl-List>
<react-mdl-ListItem
twoLine={true}
>
<react-mdl-ListItemContent
icon="extension"
subtitle="another's description"
>
<a
href="/strategies/view/Another"
onClick={[Function]}
>
<strong>
Another
</strong>
</a>
</react-mdl-ListItemContent>
</react-mdl-ListItem>
</react-mdl-List>
</react-mdl-Cell>
</react-mdl-Grid>
`;

View File

@ -0,0 +1,167 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly with one strategy 1`] = `
<react-mdl-Grid
className="mdl-color--white"
>
<react-mdl-Cell
col={12}
>
<div
style={
Object {
"borderBottom": "1px solid #f1f1f1",
"display": "flex",
"marginBottom": "10px",
"padding": "16px 20px ",
}
}
>
<div
style={
Object {
"flex": "2",
}
}
>
<h6
style={
Object {
"margin": 0,
}
}
>
Another
</h6>
<small>
another's description
</small>
</div>
</div>
<react-mdl-Tabs
ripple={true}
>
<react-mdl-Tab
onClick={[Function]}
>
Details
</react-mdl-Tab>
<react-mdl-Tab
onClick={[Function]}
>
Edit
</react-mdl-Tab>
</react-mdl-Tabs>
<section>
<div
className="content"
>
<div>
<react-mdl-Grid>
<react-mdl-Cell
col={12}
>
<h6>
Parameters
</h6>
<hr />
<react-mdl-List>
<react-mdl-ListItem
title="Required"
twoLine={true}
>
<react-mdl-ListItemContent
avatar="add"
subtitle="customList"
>
customParam
<small>
(
list
)
</small>
</react-mdl-ListItemContent>
</react-mdl-ListItem>
</react-mdl-List>
</react-mdl-Cell>
<react-mdl-Cell
col={6}
tablet={12}
>
<h6>
Applications using this strategy
</h6>
<hr />
<react-mdl-List>
<react-mdl-ListItem
twoLine={true}
>
<span
className="mdl-list__item-primary-content"
style={
Object {
"minWidth": 0,
}
}
>
<react-mdl-Icon
className="mdl-list__item-avatar"
name="apps"
/>
<a
className="listLink truncate"
href="/applications/appA"
onClick={[Function]}
>
appA
<span
className="mdl-list__item-sub-title truncate"
>
app description
</span>
</a>
</span>
</react-mdl-ListItem>
</react-mdl-List>
</react-mdl-Cell>
<react-mdl-Cell
col={6}
tablet={12}
>
<h6>
Toggles using this strategy
</h6>
<hr />
<react-mdl-List
className="truncate"
style={
Object {
"textAlign": "left",
}
}
>
<react-mdl-ListItem
twoLine={true}
>
<react-mdl-ListItemContent
avatar="toggle"
subtitle="toggle description"
>
<a
href="/features/view/toggleA"
onClick={[Function]}
>
toggleA
</a>
</react-mdl-ListItemContent>
</react-mdl-ListItem>
</react-mdl-List>
</react-mdl-Cell>
</react-mdl-Grid>
</div>
</div>
</section>
</react-mdl-Cell>
</react-mdl-Grid>
`;

View File

@ -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(
<MemoryRouter>
<StrategiesListComponent
strategies={[strategy]}
fetchStrategies={jest.fn()}
removeStrategy={jest.fn()}
history={{}}
hasPermission={permission => [CREATE_STRATEGY, DELETE_STRATEGY].indexOf(permission) !== -1}
/>
</MemoryRouter>
);
expect(tree).toMatchSnapshot();
});
test('renders correctly with one strategy without permissions', () => {
const strategy = {
name: 'Another',
description: "another's description",
};
const tree = renderer.create(
<MemoryRouter>
<StrategiesListComponent
strategies={[strategy]}
fetchStrategies={jest.fn()}
removeStrategy={jest.fn()}
history={{}}
hasPermission={() => false}
/>
</MemoryRouter>
);
expect(tree).toMatchSnapshot();
});

View File

@ -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(
<MemoryRouter>
<StrategyDetails
strategyName={'Another'}
strategy={strategy}
activeTab="view"
applications={applications}
toggles={toggles}
fetchStrategies={jest.fn()}
fetchApplications={jest.fn()}
fetchFeatureToggles={jest.fn()}
history={{}}
hasPermission={permission => [UPDATE_STRATEGY].indexOf(permission) !== -1}
/>
</MemoryRouter>
);
expect(tree).toMatchSnapshot();
});

View File

@ -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 (
<Grid className="mdl-color--white">
@ -28,17 +28,16 @@ class StrategiesListComponent extends Component {
<HeaderTitle
title="Strategies"
actions={
<PermissionComponent
permission={CREATE_STRATEGY}
component={
<IconButton
raised
name="add"
onClick={() => this.props.history.push('/strategies/create')}
title="Add new strategy"
/>
}
/>
hasPermission(CREATE_STRATEGY) ? (
<IconButton
raised
name="add"
onClick={() => this.props.history.push('/strategies/create')}
title="Add new strategy"
/>
) : (
''
)
}
/>
<List>
@ -50,15 +49,10 @@ class StrategiesListComponent extends Component {
<strong>{strategy.name}</strong>
</Link>
</ListItemContent>
{strategy.editable === false ? (
{strategy.editable === false || !hasPermission(DELETE_STRATEGY) ? (
''
) : (
<PermissionComponent
permission={DELETE_STRATEGY}
component={
<IconButton name="delete" onClick={() => removeStrategy(strategy)} />
}
/>
<IconButton name="delete" onClick={() => removeStrategy(strategy)} />
)}
</ListItem>
))

View File

@ -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')),
};
};

View File

@ -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 {
<Grid className="mdl-color--white">
<Cell col={12}>
<HeaderTitle title={strategy.name} subtitle={strategy.description} />
{strategy.editable === false ? (
{strategy.editable === false || !this.props.hasPermission(UPDATE_STRATEGY) ? (
''
) : (
<PermissionComponent
permission={UPDATE_STRATEGY}
component={
<Tabs activeTab={activeTabId} ripple>
<Tab onClick={() => this.goToTab('view')}>Details</Tab>
<Tab onClick={() => this.goToTab('edit')}>Edit</Tab>
</Tabs>
}
/>
<Tabs activeTab={activeTabId} ripple>
<Tab onClick={() => this.goToTab('view')}>Details</Tab>
<Tab onClick={() => this.goToTab('edit')}>Edit</Tab>
</Tabs>
)}
<section>

View File

@ -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')),
};
};

View File

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