mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-27 01:19:00 +02:00
Merge pull request #164 from Kouzukii/master
Add support for permission system in unleash frontend
This commit is contained in:
commit
e3d6108784
2
frontend/src/__mocks__/react-mdl.js
vendored
2
frontend/src/__mocks__/react-mdl.js
vendored
@ -9,6 +9,8 @@ module.exports = {
|
|||||||
Cell: 'react-mdl-Cell',
|
Cell: 'react-mdl-Cell',
|
||||||
Chip: 'react-mdl-Chip',
|
Chip: 'react-mdl-Chip',
|
||||||
Grid: 'react-mdl-Grid',
|
Grid: 'react-mdl-Grid',
|
||||||
|
Button: 'react-mdl-Button',
|
||||||
|
FABButton: 'react-mdl-FABButton',
|
||||||
Icon: 'react-mdl-Icon',
|
Icon: 'react-mdl-Icon',
|
||||||
IconButton: 'react-mdl-IconButton',
|
IconButton: 'react-mdl-IconButton',
|
||||||
List: 'react-mdl-List',
|
List: 'react-mdl-List',
|
||||||
|
@ -5,3 +5,356 @@ exports[`renders correctly if no application 1`] = `
|
|||||||
indeterminate={true}
|
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>
|
||||||
|
`;
|
||||||
|
@ -2,12 +2,130 @@ import React from 'react';
|
|||||||
|
|
||||||
import ClientApplications from '../application-edit-component';
|
import ClientApplications from '../application-edit-component';
|
||||||
import renderer from 'react-test-renderer';
|
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');
|
jest.mock('react-mdl');
|
||||||
|
|
||||||
test('renders correctly if no application', () => {
|
test('renders correctly if no application', () => {
|
||||||
const tree = renderer
|
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();
|
.toJSON();
|
||||||
|
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
} from 'react-mdl';
|
} from 'react-mdl';
|
||||||
import { IconLink, shorten, styles as commonStyles } from '../common';
|
import { IconLink, shorten, styles as commonStyles } from '../common';
|
||||||
import { formatFullDateTimeWithLocale } from '../common/util';
|
import { formatFullDateTimeWithLocale } from '../common/util';
|
||||||
|
import { CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION } from '../../permissions';
|
||||||
|
|
||||||
class StatefulTextfield extends Component {
|
class StatefulTextfield extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -61,6 +62,7 @@ class ClientApplications extends PureComponent {
|
|||||||
application: PropTypes.object,
|
application: PropTypes.object,
|
||||||
location: PropTypes.object,
|
location: PropTypes.object,
|
||||||
storeApplicationMetaData: PropTypes.func.isRequired,
|
storeApplicationMetaData: PropTypes.func.isRequired,
|
||||||
|
hasPermission: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -78,7 +80,7 @@ class ClientApplications extends PureComponent {
|
|||||||
if (!this.props.application) {
|
if (!this.props.application) {
|
||||||
return <ProgressBar indeterminate />;
|
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 { appName, instances, strategies, seenToggles, url, description, icon = 'apps', color } = application;
|
||||||
|
|
||||||
const content =
|
const content =
|
||||||
@ -92,9 +94,15 @@ class ClientApplications extends PureComponent {
|
|||||||
({ name, description, enabled, notFound }, i) =>
|
({ name, description, enabled, notFound }, i) =>
|
||||||
notFound ? (
|
notFound ? (
|
||||||
<ListItem twoLine key={i}>
|
<ListItem twoLine key={i}>
|
||||||
<ListItemContent icon={'report'} subtitle={'Missing, want to create?'}>
|
{hasPermission(CREATE_FEATURE) ? (
|
||||||
<Link to={`/features/create?name=${name}`}>{name}</Link>
|
<ListItemContent icon={'report'} subtitle={'Missing, want to create?'}>
|
||||||
</ListItemContent>
|
<Link to={`/features/create?name=${name}`}>{name}</Link>
|
||||||
|
</ListItemContent>
|
||||||
|
) : (
|
||||||
|
<ListItemContent icon={'report'} subtitle={'Missing'}>
|
||||||
|
{name}
|
||||||
|
</ListItemContent>
|
||||||
|
)}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
) : (
|
) : (
|
||||||
<ListItem twoLine key={i}>
|
<ListItem twoLine key={i}>
|
||||||
@ -121,9 +129,15 @@ class ClientApplications extends PureComponent {
|
|||||||
({ name, description, notFound }, i) =>
|
({ name, description, notFound }, i) =>
|
||||||
notFound ? (
|
notFound ? (
|
||||||
<ListItem twoLine key={`${name}-${i}`}>
|
<ListItem twoLine key={`${name}-${i}`}>
|
||||||
<ListItemContent icon={'report'} subtitle={'Missing, want to create?'}>
|
{hasPermission(CREATE_STRATEGY) ? (
|
||||||
<Link to={`/strategies/create?name=${name}`}>{name}</Link>
|
<ListItemContent icon={'report'} subtitle={'Missing, want to create?'}>
|
||||||
</ListItemContent>
|
<Link to={`/strategies/create?name=${name}`}>{name}</Link>
|
||||||
|
</ListItemContent>
|
||||||
|
) : (
|
||||||
|
<ListItemContent icon={'report'} subtitle={'Missing'}>
|
||||||
|
{name}
|
||||||
|
</ListItemContent>
|
||||||
|
)}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
) : (
|
) : (
|
||||||
<ListItem twoLine key={`${name}-${i}`}>
|
<ListItem twoLine key={`${name}-${i}`}>
|
||||||
@ -203,16 +217,20 @@ class ClientApplications extends PureComponent {
|
|||||||
</CardMenu>
|
</CardMenu>
|
||||||
)}
|
)}
|
||||||
<hr />
|
<hr />
|
||||||
<Tabs
|
{hasPermission(UPDATE_APPLICATION) ? (
|
||||||
activeTab={this.state.activeTab}
|
<Tabs
|
||||||
onChange={tabId => this.setState({ activeTab: tabId })}
|
activeTab={this.state.activeTab}
|
||||||
ripple
|
onChange={tabId => this.setState({ activeTab: tabId })}
|
||||||
tabBarProps={{ style: { width: '100%' } }}
|
ripple
|
||||||
className="mdl-color--grey-100"
|
tabBarProps={{ style: { width: '100%' } }}
|
||||||
>
|
className="mdl-color--grey-100"
|
||||||
<Tab>Details</Tab>
|
>
|
||||||
<Tab>Edit</Tab>
|
<Tab>Details</Tab>
|
||||||
</Tabs>
|
<Tab>Edit</Tab>
|
||||||
|
</Tabs>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
|
||||||
{content}
|
{content}
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ApplicationEdit from './application-edit-component';
|
import ApplicationEdit from './application-edit-component';
|
||||||
import { fetchApplication, storeApplicationMetaData } from './../../store/application/actions';
|
import { fetchApplication, storeApplicationMetaData } from './../../store/application/actions';
|
||||||
|
import { hasPermission } from '../../permissions';
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => {
|
const mapStateToProps = (state, props) => {
|
||||||
let application = state.applications.getIn(['apps', props.appName]);
|
let application = state.applications.getIn(['apps', props.appName]);
|
||||||
@ -11,6 +12,7 @@ const mapStateToProps = (state, props) => {
|
|||||||
return {
|
return {
|
||||||
application,
|
application,
|
||||||
location,
|
location,
|
||||||
|
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { fetchArchive, revive } from './../../store/archive-actions';
|
import { fetchArchive, revive } from './../../store/archive-actions';
|
||||||
import ViewToggleComponent from './../feature/view-component';
|
import ViewToggleComponent from './../feature/view-component';
|
||||||
|
import { hasPermission } from '../../permissions';
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
(state, props) => ({
|
(state, props) => ({
|
||||||
@ -10,6 +11,7 @@ export default connect(
|
|||||||
.toArray()
|
.toArray()
|
||||||
.find(toggle => toggle.name === props.featureToggleName),
|
.find(toggle => toggle.name === props.featureToggleName),
|
||||||
activeTab: props.activeTab,
|
activeTab: props.activeTab,
|
||||||
|
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
fetchArchive,
|
fetchArchive,
|
||||||
|
@ -55,3 +55,58 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
<span />
|
<span />
|
||||||
</react-mdl-ListItem>
|
</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>
|
||||||
|
`;
|
||||||
|
@ -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>
|
||||||
|
`;
|
@ -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>
|
||||||
|
`;
|
@ -3,6 +3,7 @@ import { MemoryRouter } from 'react-router-dom';
|
|||||||
|
|
||||||
import Feature from './../feature-list-item-component';
|
import Feature from './../feature-list-item-component';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
|
import { UPDATE_FEATURE } from '../../../permissions';
|
||||||
|
|
||||||
jest.mock('react-mdl');
|
jest.mock('react-mdl');
|
||||||
|
|
||||||
@ -32,6 +33,41 @@ test('renders correctly with one feature', () => {
|
|||||||
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
|
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
|
||||||
feature={feature}
|
feature={feature}
|
||||||
toggleFeature={jest.fn()}
|
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>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
@ -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();
|
||||||
|
});
|
@ -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();
|
||||||
|
});
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Switch, Chip, ListItem, ListItemAction, Icon } from 'react-mdl';
|
import { Switch, Chip, ListItem, ListItemAction, Icon } from 'react-mdl';
|
||||||
import Progress from './progress';
|
import Progress from './progress';
|
||||||
|
import { UPDATE_FEATURE } from '../../permissions';
|
||||||
import { calc, styles as commonStyles } from '../common';
|
import { calc, styles as commonStyles } from '../common';
|
||||||
|
|
||||||
import styles from './feature.scss';
|
import styles from './feature.scss';
|
||||||
@ -14,6 +15,7 @@ const Feature = ({
|
|||||||
metricsLastHour = { yes: 0, no: 0, isFallback: true },
|
metricsLastHour = { yes: 0, no: 0, isFallback: true },
|
||||||
metricsLastMinute = { yes: 0, no: 0, isFallback: true },
|
metricsLastMinute = { yes: 0, no: 0, isFallback: true },
|
||||||
revive,
|
revive,
|
||||||
|
hasPermission,
|
||||||
}) => {
|
}) => {
|
||||||
const { name, description, enabled, strategies } = feature;
|
const { name, description, enabled, strategies } = feature;
|
||||||
const { showLastHour = false } = settings;
|
const { showLastHour = false } = settings;
|
||||||
@ -41,13 +43,17 @@ const Feature = ({
|
|||||||
<Progress strokeWidth={15} percentage={percent} isFallback={isStale} />
|
<Progress strokeWidth={15} percentage={percent} isFallback={isStale} />
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.listItemToggle}>
|
<span className={styles.listItemToggle}>
|
||||||
<Switch
|
{hasPermission(UPDATE_FEATURE) ? (
|
||||||
disabled={toggleFeature === undefined}
|
<Switch
|
||||||
title={`Toggle ${name}`}
|
disabled={toggleFeature === undefined}
|
||||||
key="left-actions"
|
title={`Toggle ${name}`}
|
||||||
onChange={() => toggleFeature(name)}
|
key="left-actions"
|
||||||
checked={enabled}
|
onChange={() => toggleFeature(name)}
|
||||||
/>
|
checked={enabled}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Switch disabled title={`Toggle ${name}`} key="left-actions" checked={enabled} />
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className={['mdl-list__item-primary-content', styles.listItemLink].join(' ')}>
|
<span className={['mdl-list__item-primary-content', styles.listItemLink].join(' ')}>
|
||||||
<Link to={featureUrl} className={[commonStyles.listLink, commonStyles.truncate].join(' ')}>
|
<Link to={featureUrl} className={[commonStyles.listLink, commonStyles.truncate].join(' ')}>
|
||||||
@ -59,7 +65,7 @@ const Feature = ({
|
|||||||
{strategyChips}
|
{strategyChips}
|
||||||
{summaryChip}
|
{summaryChip}
|
||||||
</span>
|
</span>
|
||||||
{revive ? (
|
{revive && hasPermission(UPDATE_FEATURE) ? (
|
||||||
<ListItemAction onClick={() => revive(feature.name)}>
|
<ListItemAction onClick={() => revive(feature.name)}>
|
||||||
<Icon name="undo" />
|
<Icon name="undo" />
|
||||||
</ListItemAction>
|
</ListItemAction>
|
||||||
@ -77,6 +83,7 @@ Feature.propTypes = {
|
|||||||
metricsLastHour: PropTypes.object,
|
metricsLastHour: PropTypes.object,
|
||||||
metricsLastMinute: PropTypes.object,
|
metricsLastMinute: PropTypes.object,
|
||||||
revive: PropTypes.func,
|
revive: PropTypes.func,
|
||||||
|
hasPermission: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Feature;
|
export default Feature;
|
||||||
|
@ -1,5 +1,32 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// 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`] = `
|
exports[`renders strategy with empty list as param 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
|
@ -79,3 +79,11 @@ it('spy onClose', () => {
|
|||||||
wrapper.find('react-mdl-Chip').simulate('close', closeMock);
|
wrapper.find('react-mdl-Chip').simulate('close', closeMock);
|
||||||
expect(onClose).toHaveBeenCalled();
|
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();
|
||||||
|
});
|
||||||
|
@ -8,10 +8,10 @@ import { HeaderTitle } from '../../common';
|
|||||||
class StrategiesSectionComponent extends React.Component {
|
class StrategiesSectionComponent extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
strategies: PropTypes.array.isRequired,
|
strategies: PropTypes.array.isRequired,
|
||||||
addStrategy: PropTypes.func.isRequired,
|
addStrategy: PropTypes.func,
|
||||||
removeStrategy: PropTypes.func.isRequired,
|
removeStrategy: PropTypes.func,
|
||||||
updateStrategy: PropTypes.func.isRequired,
|
updateStrategy: PropTypes.func,
|
||||||
fetchStrategies: PropTypes.func.isRequired,
|
fetchStrategies: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
|
@ -104,7 +104,12 @@ class StrategyConfigure extends React.Component {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div key={name}>
|
<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>}
|
{description && <p className={styles.helpText}>{description}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -7,6 +7,7 @@ export default class InputList extends Component {
|
|||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
list: PropTypes.array.isRequired,
|
list: PropTypes.array.isRequired,
|
||||||
setConfig: PropTypes.func.isRequired,
|
setConfig: PropTypes.func.isRequired,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
onBlur(e) {
|
onBlur(e) {
|
||||||
@ -49,35 +50,43 @@ export default class InputList extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { name, list } = this.props;
|
const { name, list, disabled } = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>{name}</p>
|
<p>{name}</p>
|
||||||
{list.map((entryValue, index) => (
|
{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}
|
{entryValue}
|
||||||
</Chip>
|
</Chip>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div style={{ display: 'flex' }}>
|
{disabled ? (
|
||||||
<Textfield
|
''
|
||||||
name={`${name}_input`}
|
) : (
|
||||||
style={{ width: '100%', flex: 1 }}
|
<div style={{ display: 'flex' }}>
|
||||||
floatingLabel
|
<Textfield
|
||||||
label="Add list entry"
|
name={`${name}_input`}
|
||||||
onFocus={this.onFocus.bind(this)}
|
style={{ width: '100%', flex: 1 }}
|
||||||
onBlur={this.onBlur.bind(this)}
|
floatingLabel
|
||||||
ref={input => {
|
label="Add list entry"
|
||||||
this.textInput = input;
|
onFocus={this.onFocus.bind(this)}
|
||||||
}}
|
onBlur={this.onBlur.bind(this)}
|
||||||
/>
|
ref={input => {
|
||||||
<IconButton
|
this.textInput = input;
|
||||||
name="add"
|
}}
|
||||||
raised
|
/>
|
||||||
style={{ flex: 1, flexGrow: 0, margin: '20px 0 0 10px' }}
|
<IconButton
|
||||||
onClick={this.setValue}
|
name="add"
|
||||||
/>
|
raised
|
||||||
</div>
|
style={{ flex: 1, flexGrow: 0, margin: '20px 0 0 10px' }}
|
||||||
|
onClick={this.setValue}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import { Link } from 'react-router-dom';
|
|||||||
import { Icon, FABButton, Textfield, Menu, MenuItem, Card, CardActions, List } from 'react-mdl';
|
import { Icon, FABButton, Textfield, Menu, MenuItem, Card, CardActions, List } from 'react-mdl';
|
||||||
import { MenuItemWithIcon, DropdownButton, styles as commonStyles } from '../common';
|
import { MenuItemWithIcon, DropdownButton, styles as commonStyles } from '../common';
|
||||||
import styles from './feature.scss';
|
import styles from './feature.scss';
|
||||||
|
import { CREATE_FEATURE } from '../../permissions';
|
||||||
|
|
||||||
export default class FeatureListComponent extends React.Component {
|
export default class FeatureListComponent extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -19,6 +20,7 @@ export default class FeatureListComponent extends React.Component {
|
|||||||
toggleFeature: PropTypes.func,
|
toggleFeature: PropTypes.func,
|
||||||
settings: PropTypes.object,
|
settings: PropTypes.object,
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
|
hasPermission: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -46,7 +48,7 @@ export default class FeatureListComponent extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { features, toggleFeature, featureMetrics, settings, revive } = this.props;
|
const { features, toggleFeature, featureMetrics, settings, revive, hasPermission } = this.props;
|
||||||
features.forEach(e => {
|
features.forEach(e => {
|
||||||
e.reviveName = e.name;
|
e.reviveName = e.name;
|
||||||
});
|
});
|
||||||
@ -62,11 +64,15 @@ export default class FeatureListComponent extends React.Component {
|
|||||||
label="Search"
|
label="Search"
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
/>
|
/>
|
||||||
<Link to="/features/create" className={styles.toolbarButton}>
|
{hasPermission(CREATE_FEATURE) ? (
|
||||||
<FABButton accent title="Create feature toggle">
|
<Link to="/features/create" className={styles.toolbarButton}>
|
||||||
<Icon name="add" />
|
<FABButton accent title="Create feature toggle">
|
||||||
</FABButton>
|
<Icon name="add" />
|
||||||
</Link>
|
</FABButton>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||||
<CardActions>
|
<CardActions>
|
||||||
@ -119,6 +125,7 @@ export default class FeatureListComponent extends React.Component {
|
|||||||
feature={feature}
|
feature={feature}
|
||||||
toggleFeature={toggleFeature}
|
toggleFeature={toggleFeature}
|
||||||
revive={revive}
|
revive={revive}
|
||||||
|
hasPermission={hasPermission}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
|
@ -4,6 +4,7 @@ import { updateSettingForGroup } from '../../store/settings/actions';
|
|||||||
|
|
||||||
import FeatureListComponent from './list-component';
|
import FeatureListComponent from './list-component';
|
||||||
import { logoutUser } from '../../store/user/actions';
|
import { logoutUser } from '../../store/user/actions';
|
||||||
|
import { hasPermission } from '../../permissions';
|
||||||
|
|
||||||
export const mapStateToPropsConfigurable = isFeature => state => {
|
export const mapStateToPropsConfigurable = isFeature => state => {
|
||||||
const featureMetrics = state.featureMetrics.toJS();
|
const featureMetrics = state.featureMetrics.toJS();
|
||||||
@ -68,6 +69,7 @@ export const mapStateToPropsConfigurable = isFeature => state => {
|
|||||||
features,
|
features,
|
||||||
featureMetrics,
|
featureMetrics,
|
||||||
settings,
|
settings,
|
||||||
|
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const mapStateToProps = mapStateToPropsConfigurable(true);
|
const mapStateToProps = mapStateToPropsConfigurable(true);
|
||||||
|
@ -8,6 +8,7 @@ import MetricComponent from './metric-container';
|
|||||||
import EditFeatureToggle from './form/form-update-feature-container';
|
import EditFeatureToggle from './form/form-update-feature-container';
|
||||||
import ViewFeatureToggle from './form/form-view-feature-container';
|
import ViewFeatureToggle from './form/form-view-feature-container';
|
||||||
import { styles as commonStyles } from '../common';
|
import { styles as commonStyles } from '../common';
|
||||||
|
import { CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE } from '../../permissions';
|
||||||
|
|
||||||
const TABS = {
|
const TABS = {
|
||||||
strategies: 0,
|
strategies: 0,
|
||||||
@ -34,6 +35,7 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
|||||||
editFeatureToggle: PropTypes.func,
|
editFeatureToggle: PropTypes.func,
|
||||||
featureToggle: PropTypes.object,
|
featureToggle: PropTypes.object,
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
|
hasPermission: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
@ -47,12 +49,12 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTabContent(activeTab) {
|
getTabContent(activeTab) {
|
||||||
const { features, featureToggle, featureToggleName } = this.props;
|
const { features, featureToggle, featureToggleName, hasPermission } = this.props;
|
||||||
|
|
||||||
if (TABS[activeTab] === TABS.history) {
|
if (TABS[activeTab] === TABS.history) {
|
||||||
return <HistoryComponent toggleName={featureToggleName} />;
|
return <HistoryComponent toggleName={featureToggleName} />;
|
||||||
} else if (TABS[activeTab] === TABS.strategies) {
|
} else if (TABS[activeTab] === TABS.strategies) {
|
||||||
if (this.isFeatureView) {
|
if (this.isFeatureView && hasPermission(UPDATE_FEATURE)) {
|
||||||
return (
|
return (
|
||||||
<EditFeatureToggle featureToggle={featureToggle} features={features} history={this.props.history} />
|
<EditFeatureToggle featureToggle={featureToggle} features={features} history={this.props.history} />
|
||||||
);
|
);
|
||||||
@ -78,6 +80,7 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
|||||||
featureToggleName,
|
featureToggleName,
|
||||||
toggleFeature,
|
toggleFeature,
|
||||||
removeFeatureToggle,
|
removeFeatureToggle,
|
||||||
|
hasPermission,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!featureToggle) {
|
if (!featureToggle) {
|
||||||
@ -87,14 +90,18 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
Could not find the toggle{' '}
|
Could not find the toggle{' '}
|
||||||
<Link
|
{hasPermission(CREATE_FEATURE) ? (
|
||||||
to={{
|
<Link
|
||||||
pathname: '/features/create',
|
to={{
|
||||||
query: { name: featureToggleName },
|
pathname: '/features/create',
|
||||||
}}
|
query: { name: featureToggleName },
|
||||||
>
|
}}
|
||||||
{featureToggleName}
|
>
|
||||||
</Link>
|
{featureToggleName}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
featureToggleName
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -115,8 +122,8 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
|||||||
revive(featureToggle.name);
|
revive(featureToggle.name);
|
||||||
this.props.history.push('/features');
|
this.props.history.push('/features');
|
||||||
};
|
};
|
||||||
const updateFeatureToggle = () => {
|
const updateFeatureToggle = e => {
|
||||||
let feature = { ...featureToggle };
|
let feature = { ...featureToggle, description: e.target.value };
|
||||||
if (Array.isArray(feature.strategies)) {
|
if (Array.isArray(feature.strategies)) {
|
||||||
feature.strategies.forEach(s => {
|
feature.strategies.forEach(s => {
|
||||||
delete s.id;
|
delete s.id;
|
||||||
@ -134,7 +141,7 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
|||||||
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||||
<CardTitle style={{ paddingTop: '24px', wordBreak: 'break-all' }}>{featureToggle.name}</CardTitle>
|
<CardTitle style={{ paddingTop: '24px', wordBreak: 'break-all' }}>{featureToggle.name}</CardTitle>
|
||||||
<CardText>
|
<CardText>
|
||||||
{this.isFeatureView ? (
|
{this.isFeatureView && hasPermission(UPDATE_FEATURE) ? (
|
||||||
<Textfield
|
<Textfield
|
||||||
floatingLabel
|
floatingLabel
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
@ -167,22 +174,36 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ paddingRight: '24px' }}>
|
<span style={{ paddingRight: '24px' }}>
|
||||||
<Switch
|
{hasPermission(UPDATE_FEATURE) ? (
|
||||||
disabled={!this.isFeatureView}
|
<Switch
|
||||||
ripple
|
disabled={!this.isFeatureView}
|
||||||
checked={featureToggle.enabled}
|
ripple
|
||||||
onChange={() => toggleFeature(featureToggle.name)}
|
checked={featureToggle.enabled}
|
||||||
>
|
onChange={() => toggleFeature(featureToggle.name)}
|
||||||
{featureToggle.enabled ? 'Enabled' : 'Disabled'}
|
>
|
||||||
</Switch>
|
{featureToggle.enabled ? 'Enabled' : 'Disabled'}
|
||||||
|
</Switch>
|
||||||
|
) : (
|
||||||
|
<Switch disabled ripple checked={featureToggle.enabled}>
|
||||||
|
{featureToggle.enabled ? 'Enabled' : 'Disabled'}
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{this.isFeatureView ? (
|
{this.isFeatureView ? (
|
||||||
<Button onClick={removeToggle} style={{ flexShrink: 0 }}>
|
<Button
|
||||||
|
disabled={!hasPermission(DELETE_FEATURE)}
|
||||||
|
onClick={removeToggle}
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
>
|
||||||
Archive
|
Archive
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button onClick={reviveToggle} style={{ flexShrink: 0 }}>
|
<Button
|
||||||
|
disabled={!hasPermission(UPDATE_FEATURE)}
|
||||||
|
onClick={reviveToggle}
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
>
|
||||||
Revive
|
Revive
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
@ -8,12 +8,14 @@ import {
|
|||||||
} from './../../store/feature-actions';
|
} from './../../store/feature-actions';
|
||||||
|
|
||||||
import ViewToggleComponent from './view-component';
|
import ViewToggleComponent from './view-component';
|
||||||
|
import { hasPermission } from '../../permissions';
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
(state, props) => ({
|
(state, props) => ({
|
||||||
features: state.features.toJS(),
|
features: state.features.toJS(),
|
||||||
featureToggle: state.features.toJS().find(toggle => toggle.name === props.featureToggleName),
|
featureToggle: state.features.toJS().find(toggle => toggle.name === props.featureToggleName),
|
||||||
activeTab: props.activeTab,
|
activeTab: props.activeTab,
|
||||||
|
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
fetchFeatureToggles,
|
fetchFeatureToggles,
|
||||||
|
@ -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>
|
||||||
|
`;
|
@ -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>
|
||||||
|
`;
|
@ -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();
|
||||||
|
});
|
@ -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();
|
||||||
|
});
|
@ -4,6 +4,7 @@ import { Link } from 'react-router-dom';
|
|||||||
|
|
||||||
import { List, ListItem, ListItemContent, IconButton, Grid, Cell } from 'react-mdl';
|
import { List, ListItem, ListItemContent, IconButton, Grid, Cell } from 'react-mdl';
|
||||||
import { HeaderTitle } from '../common';
|
import { HeaderTitle } from '../common';
|
||||||
|
import { CREATE_STRATEGY, DELETE_STRATEGY } from '../../permissions';
|
||||||
|
|
||||||
class StrategiesListComponent extends Component {
|
class StrategiesListComponent extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -11,6 +12,7 @@ class StrategiesListComponent extends Component {
|
|||||||
fetchStrategies: PropTypes.func.isRequired,
|
fetchStrategies: PropTypes.func.isRequired,
|
||||||
removeStrategy: PropTypes.func.isRequired,
|
removeStrategy: PropTypes.func.isRequired,
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
|
hasPermission: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -18,7 +20,7 @@ class StrategiesListComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { strategies, removeStrategy } = this.props;
|
const { strategies, removeStrategy, hasPermission } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid className="mdl-color--white">
|
<Grid className="mdl-color--white">
|
||||||
@ -26,12 +28,16 @@ class StrategiesListComponent extends Component {
|
|||||||
<HeaderTitle
|
<HeaderTitle
|
||||||
title="Strategies"
|
title="Strategies"
|
||||||
actions={
|
actions={
|
||||||
<IconButton
|
hasPermission(CREATE_STRATEGY) ? (
|
||||||
raised
|
<IconButton
|
||||||
name="add"
|
raised
|
||||||
onClick={() => this.props.history.push('/strategies/create')}
|
name="add"
|
||||||
title="Add new strategy"
|
onClick={() => this.props.history.push('/strategies/create')}
|
||||||
/>
|
title="Add new strategy"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<List>
|
<List>
|
||||||
@ -43,7 +49,7 @@ class StrategiesListComponent extends Component {
|
|||||||
<strong>{strategy.name}</strong>
|
<strong>{strategy.name}</strong>
|
||||||
</Link>
|
</Link>
|
||||||
</ListItemContent>
|
</ListItemContent>
|
||||||
{strategy.editable === false ? (
|
{strategy.editable === false || !hasPermission(DELETE_STRATEGY) ? (
|
||||||
''
|
''
|
||||||
) : (
|
) : (
|
||||||
<IconButton name="delete" onClick={() => removeStrategy(strategy)} />
|
<IconButton name="delete" onClick={() => removeStrategy(strategy)} />
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import StrategiesListComponent from './list-component.jsx';
|
import StrategiesListComponent from './list-component.jsx';
|
||||||
import { fetchStrategies, removeStrategy } from './../../store/strategy/actions';
|
import { fetchStrategies, removeStrategy } from './../../store/strategy/actions';
|
||||||
|
import { hasPermission } from '../../permissions';
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const list = state.strategies.get('list').toArray();
|
const list = state.strategies.get('list').toArray();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
strategies: list,
|
strategies: list,
|
||||||
|
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { Tabs, Tab, ProgressBar, Grid, Cell } from 'react-mdl';
|
|||||||
import ShowStrategy from './show-strategy-component';
|
import ShowStrategy from './show-strategy-component';
|
||||||
import EditStrategy from './edit-container';
|
import EditStrategy from './edit-container';
|
||||||
import { HeaderTitle } from '../common';
|
import { HeaderTitle } from '../common';
|
||||||
|
import { UPDATE_STRATEGY } from '../../permissions';
|
||||||
|
|
||||||
const TABS = {
|
const TABS = {
|
||||||
view: 0,
|
view: 0,
|
||||||
@ -21,6 +22,7 @@ export default class StrategyDetails extends Component {
|
|||||||
fetchApplications: PropTypes.func.isRequired,
|
fetchApplications: PropTypes.func.isRequired,
|
||||||
fetchFeatureToggles: PropTypes.func.isRequired,
|
fetchFeatureToggles: PropTypes.func.isRequired,
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
|
hasPermission: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -66,7 +68,7 @@ export default class StrategyDetails extends Component {
|
|||||||
<Grid className="mdl-color--white">
|
<Grid className="mdl-color--white">
|
||||||
<Cell col={12}>
|
<Cell col={12}>
|
||||||
<HeaderTitle title={strategy.name} subtitle={strategy.description} />
|
<HeaderTitle title={strategy.name} subtitle={strategy.description} />
|
||||||
{strategy.editable === false ? (
|
{strategy.editable === false || !this.props.hasPermission(UPDATE_STRATEGY) ? (
|
||||||
''
|
''
|
||||||
) : (
|
) : (
|
||||||
<Tabs activeTab={activeTabId} ripple>
|
<Tabs activeTab={activeTabId} ripple>
|
||||||
|
@ -3,6 +3,7 @@ import ShowStrategy from './strategy-details-component';
|
|||||||
import { fetchStrategies } from './../../store/strategy/actions';
|
import { fetchStrategies } from './../../store/strategy/actions';
|
||||||
import { fetchAll } from './../../store/application/actions';
|
import { fetchAll } from './../../store/application/actions';
|
||||||
import { fetchFeatureToggles } from './../../store/feature-actions';
|
import { fetchFeatureToggles } from './../../store/feature-actions';
|
||||||
|
import { hasPermission } from '../../permissions';
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => {
|
const mapStateToProps = (state, props) => {
|
||||||
let strategy = state.strategies.get('list').find(n => n.name === props.strategyName);
|
let strategy = state.strategies.get('list').find(n => n.name === props.strategyName);
|
||||||
@ -17,6 +18,7 @@ const mapStateToProps = (state, props) => {
|
|||||||
applications: applications && applications.toJS(),
|
applications: applications && applications.toJS(),
|
||||||
toggles: toggles && toggles.toJS(),
|
toggles: toggles && toggles.toJS(),
|
||||||
activeTab: props.activeTab,
|
activeTab: props.activeTab,
|
||||||
|
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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) {
|
export function throwIfNotSuccess(response) {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
response.json().then(body => reject(new AuthenticationError(response.status, body)));
|
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) {
|
} else if (response.status > 399 && response.status < 404) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
response.json().then(body => {
|
response.json().then(body => {
|
||||||
|
15
frontend/src/permissions.js
Normal file
15
frontend/src/permissions.js
Normal 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)
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user