1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-10 01:16:39 +02:00

Merge pull request #164 from Kouzukii/master

Add support for permission system in unleash frontend
This commit is contained in:
Ivar Conradi Østhus 2019-01-18 19:34:43 +01:00 committed by GitHub
commit e3d6108784
32 changed files with 1795 additions and 90 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: '2017-02-23T15:56:49',
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: '2017-02-23T15:56:49',
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

@ -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 <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 =
@ -92,9 +94,15 @@ class ClientApplications extends PureComponent {
({ name, description, enabled, notFound }, i) =>
notFound ? (
<ListItem twoLine key={i}>
<ListItemContent icon={'report'} subtitle={'Missing, want to create?'}>
<Link to={`/features/create?name=${name}`}>{name}</Link>
</ListItemContent>
{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}>
@ -121,9 +129,15 @@ class ClientApplications extends PureComponent {
({ name, description, notFound }, i) =>
notFound ? (
<ListItem twoLine key={`${name}-${i}`}>
<ListItemContent icon={'report'} subtitle={'Missing, want to create?'}>
<Link to={`/strategies/create?name=${name}`}>{name}</Link>
</ListItemContent>
{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}`}>
@ -203,16 +217,20 @@ class ClientApplications extends PureComponent {
</CardMenu>
)}
<hr />
<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

@ -55,3 +55,58 @@ exports[`renders correctly with one feature 1`] = `
<span />
</react-mdl-ListItem>
`;
exports[`renders correctly with one feature without permission 1`] = `
<react-mdl-ListItem
twoLine={true}
>
<span
className="listItemMetric"
>
<svg
className="mdl-color-text--grey-300"
viewBox="0 0 24 24"
>
<path
d="M17.3,18C19,16.5 20,14.4 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12C4,14.4 5,16.5 6.7,18C8.2,16.7 10,16 12,16C14,16 15.9,16.7 17.3,18M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M7,9A1,1 0 0,1 8,10A1,1 0 0,1 7,11A1,1 0 0,1 6,10A1,1 0 0,1 7,9M10,6A1,1 0 0,1 11,7A1,1 0 0,1 10,8A1,1 0 0,1 9,7A1,1 0 0,1 10,6M17,9A1,1 0 0,1 18,10A1,1 0 0,1 17,11A1,1 0 0,1 16,10A1,1 0 0,1 17,9M14.4,6.1C14.9,6.3 15.1,6.9 15,7.4L13.6,10.8C13.8,11.1 14,11.5 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12C10,11 10.7,10.1 11.7,10L13.1,6.7C13.3,6.1 13.9,5.9 14.4,6.1Z"
fill="currentColor"
/>
</svg>
</span>
<span
className="listItemToggle"
>
<react-mdl-Switch
checked={false}
disabled={true}
title="Toggle Another"
/>
</span>
<span
className="mdl-list__item-primary-content listItemLink"
>
<a
className="listLink truncate"
href="/features/strategies/Another"
onClick={[Function]}
>
Another
<span
className="mdl-list__item-sub-title truncate"
>
another's description
</span>
</a>
</span>
<span
className="listItemStrategies hideLt920"
>
<react-mdl-Chip
className="strategyChip"
>
gradualRolloutRandom
</react-mdl-Chip>
</span>
<span />
</react-mdl-ListItem>
`;

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

@ -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}
/>
</MemoryRouter>
);
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(
<MemoryRouter>
<Feature
key={0}
settings={settings}
metricsLastHour={featureMetrics.lastHour[feature.name]}
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
feature={feature}
toggleFeature={jest.fn()}
hasPermission={() => false}
/>
</MemoryRouter>
);

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,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 = ({
<Progress strokeWidth={15} percentage={percent} isFallback={isStale} />
</span>
<span className={styles.listItemToggle}>
<Switch
disabled={toggleFeature === undefined}
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(' ')}>
@ -59,7 +65,7 @@ const Feature = ({
{strategyChips}
{summaryChip}
</span>
{revive ? (
{revive && hasPermission(UPDATE_FEATURE) ? (
<ListItemAction onClick={() => revive(feature.name)}>
<Icon name="undo" />
</ListItemAction>
@ -77,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

@ -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() {

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

@ -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%' }}
/>
<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>
@ -119,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

@ -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 <HistoryComponent toggleName={featureToggleName} />;
} else if (TABS[activeTab] === TABS.strategies) {
if (this.isFeatureView) {
if (this.isFeatureView && hasPermission(UPDATE_FEATURE)) {
return (
<EditFeatureToggle featureToggle={featureToggle} features={features} history={this.props.history} />
);
@ -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 (
<span>
Could not find the toggle{' '}
<Link
to={{
pathname: '/features/create',
query: { name: featureToggleName },
}}
>
{featureToggleName}
</Link>
{hasPermission(CREATE_FEATURE) ? (
<Link
to={{
pathname: '/features/create',
query: { name: featureToggleName },
}}
>
{featureToggleName}
</Link>
) : (
featureToggleName
)}
</span>
);
}
@ -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 {
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
<CardTitle style={{ paddingTop: '24px', wordBreak: 'break-all' }}>{featureToggle.name}</CardTitle>
<CardText>
{this.isFeatureView ? (
{this.isFeatureView && hasPermission(UPDATE_FEATURE) ? (
<Textfield
floatingLabel
style={{ width: '100%' }}
@ -167,22 +174,36 @@ export default class ViewFeatureToggleComponent extends React.Component {
}}
>
<span style={{ paddingRight: '24px' }}>
<Switch
disabled={!this.isFeatureView}
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 ? (
<Button onClick={removeToggle} style={{ flexShrink: 0 }}>
<Button
disabled={!hasPermission(DELETE_FEATURE)}
onClick={removeToggle}
style={{ flexShrink: 0 }}
>
Archive
</Button>
) : (
<Button onClick={reviveToggle} style={{ flexShrink: 0 }}>
<Button
disabled={!hasPermission(UPDATE_FEATURE)}
onClick={reviveToggle}
style={{ flexShrink: 0 }}
>
Revive
</Button>
)}

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

@ -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 (
<Grid className="mdl-color--white">
@ -26,12 +28,16 @@ class StrategiesListComponent extends Component {
<HeaderTitle
title="Strategies"
actions={
<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>
@ -43,7 +49,7 @@ class StrategiesListComponent extends Component {
<strong>{strategy.name}</strong>
</Link>
</ListItemContent>
{strategy.editable === false ? (
{strategy.editable === false || !hasPermission(DELETE_STRATEGY) ? (
''
) : (
<IconButton name="delete" onClick={() => removeStrategy(strategy)} />

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

@ -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 {
<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) ? (
''
) : (
<Tabs activeTab={activeTabId} ripple>

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

@ -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 => {

View File

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