mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-18 01:18:23 +02:00
wip: frontend should understand rbac permissions (#269)
* chore: update changelog * 4.0.0-alpha.4 * wip: frontend should understand rbac permissions * move all feature components to hasAccess * fix: remove all change permissions * fix all the tests * fix all the tests x2 * fix snapshot for node 12 * fine tune perms a bit * refactor: rewrite to ts * refactor: use admin constant * fix: import Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
This commit is contained in:
parent
3bf9bd73ae
commit
f669f96d49
@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
The latest version of this document is always available in
|
The latest version of this document is always available in
|
||||||
[releases][releases-url].
|
[releases][releases-url].
|
||||||
|
|
||||||
|
# 4.0.0-alpha.4
|
||||||
|
- fix: overall bugs
|
||||||
|
- feat: user flow
|
||||||
|
- fix: small description for toggles
|
||||||
|
- fix: make admin pages fork for OSS and enterprise
|
||||||
|
|
||||||
# 4.0.0-alpha.3
|
# 4.0.0-alpha.3
|
||||||
- fix: logout redirect logic
|
- fix: logout redirect logic
|
||||||
- fix: redirect from login page if authorized
|
- fix: redirect from login page if authorized
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "unleash-frontend",
|
"name": "unleash-frontend",
|
||||||
"description": "unleash your features",
|
"description": "unleash your features",
|
||||||
"version": "4.0.0-alpha.3",
|
"version": "4.0.0-alpha.4",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"unleash",
|
"unleash",
|
||||||
"feature toggle",
|
"feature toggle",
|
||||||
|
12
frontend/src/accessStoreFake.js
Normal file
12
frontend/src/accessStoreFake.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Map as $MAp } from 'immutable';
|
||||||
|
|
||||||
|
export const createFakeStore = (permissions) => {
|
||||||
|
return {
|
||||||
|
getState: () => ({
|
||||||
|
user:
|
||||||
|
new $MAp({
|
||||||
|
permissions
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
38
frontend/src/component/AccessProvider/AccessProvider.tsx
Normal file
38
frontend/src/component/AccessProvider/AccessProvider.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
import AccessContext from '../../contexts/AccessContext'
|
||||||
|
import { ADMIN } from "./permissions";
|
||||||
|
|
||||||
|
// TODO: Type up redux store
|
||||||
|
interface IAccessProvider {
|
||||||
|
store: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPermission {
|
||||||
|
permission: string;
|
||||||
|
project: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AccessProvider: FC<IAccessProvider> = ({store, children}) => {
|
||||||
|
const hasAccess = (permission: string, project: string) => {
|
||||||
|
const permissions = store.getState().user.get('permissions') || [];
|
||||||
|
|
||||||
|
const result = permissions.some((p: IPermission) => {
|
||||||
|
if(p.permission === ADMIN) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if(p.permission === permission && p.project === project) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const context = { hasAccess };
|
||||||
|
|
||||||
|
return <AccessContext.Provider value={context}>{children}</AccessContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AccessProvider;
|
@ -20,10 +20,6 @@ export const DELETE_TAG = 'DELETE_TAG';
|
|||||||
export const CREATE_ADDON = 'CREATE_ADDON';
|
export const CREATE_ADDON = 'CREATE_ADDON';
|
||||||
export const UPDATE_ADDON = 'UPDATE_ADDON';
|
export const UPDATE_ADDON = 'UPDATE_ADDON';
|
||||||
export const DELETE_ADDON = 'DELETE_ADDON';
|
export const DELETE_ADDON = 'DELETE_ADDON';
|
||||||
|
export const UPDATE_API_TOKEN = 'UPDATE_API_TOKEN';
|
||||||
export function hasPermission(user, permission) {
|
export const CREATE_API_TOKEN = 'CREATE_API_TOKEN';
|
||||||
return (
|
export const DELETE_API_TOKEN = 'DELETE_API_TOKEN';
|
||||||
user &&
|
|
||||||
(!user.permissions || user.permissions.indexOf(ADMIN) !== -1 || user.permissions.indexOf(permission) !== -1)
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,9 +1,10 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ConfiguredAddons from './ConfiguredAddons';
|
import ConfiguredAddons from './ConfiguredAddons';
|
||||||
import AvailableAddons from './AvailableAddons';
|
import AvailableAddons from './AvailableAddons';
|
||||||
import { Avatar, Icon } from '@material-ui/core';
|
import { Avatar, Icon } from '@material-ui/core';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
width: '40px',
|
width: '40px',
|
||||||
@ -29,7 +30,8 @@ const getIcon = name => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddonList = ({ addons, providers, fetchAddons, removeAddon, toggleAddon, history, hasPermission }) => {
|
const AddonList = ({ addons, providers, fetchAddons, removeAddon, toggleAddon, history }) => {
|
||||||
|
const { hasAccess } = useContext(AccessContext);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (addons.length === 0) {
|
if (addons.length === 0) {
|
||||||
fetchAddons();
|
fetchAddons();
|
||||||
@ -45,7 +47,7 @@ const AddonList = ({ addons, providers, fetchAddons, removeAddon, toggleAddon, h
|
|||||||
<ConfiguredAddons
|
<ConfiguredAddons
|
||||||
addons={addons}
|
addons={addons}
|
||||||
toggleAddon={toggleAddon}
|
toggleAddon={toggleAddon}
|
||||||
hasPermission={hasPermission}
|
hasAccess={hasAccess}
|
||||||
removeAddon={removeAddon}
|
removeAddon={removeAddon}
|
||||||
getIcon={getIcon}
|
getIcon={getIcon}
|
||||||
/>
|
/>
|
||||||
@ -53,7 +55,7 @@ const AddonList = ({ addons, providers, fetchAddons, removeAddon, toggleAddon, h
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
<AvailableAddons providers={providers} hasPermission={hasPermission} history={history} getIcon={getIcon} />
|
<AvailableAddons providers={providers} hasAccess={hasAccess} history={history} getIcon={getIcon} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -65,7 +67,6 @@ AddonList.propTypes = {
|
|||||||
removeAddon: PropTypes.func.isRequired,
|
removeAddon: PropTypes.func.isRequired,
|
||||||
toggleAddon: PropTypes.func.isRequired,
|
toggleAddon: PropTypes.func.isRequired,
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AddonList;
|
export default AddonList;
|
||||||
|
@ -2,17 +2,17 @@ import React from 'react';
|
|||||||
import PageContent from '../../../common/PageContent/PageContent';
|
import PageContent from '../../../common/PageContent/PageContent';
|
||||||
import { Button, List, ListItem, ListItemAvatar, ListItemSecondaryAction, ListItemText } from '@material-ui/core';
|
import { Button, List, ListItem, ListItemAvatar, ListItemSecondaryAction, ListItemText } from '@material-ui/core';
|
||||||
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
||||||
import { CREATE_ADDON } from '../../../../permissions';
|
import { CREATE_ADDON } from '../../../AccessProvider/permissions';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const AvailableAddons = ({ providers, getIcon, hasPermission, history }) => {
|
const AvailableAddons = ({ providers, getIcon, hasAccess, history }) => {
|
||||||
const renderProvider = provider => (
|
const renderProvider = provider => (
|
||||||
<ListItem key={provider.name}>
|
<ListItem key={provider.name}>
|
||||||
<ListItemAvatar>{getIcon(provider.name)}</ListItemAvatar>
|
<ListItemAvatar>{getIcon(provider.name)}</ListItemAvatar>
|
||||||
<ListItemText primary={provider.displayName} secondary={provider.description} />
|
<ListItemText primary={provider.displayName} secondary={provider.description} />
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission(CREATE_ADDON)}
|
condition={hasAccess(CREATE_ADDON)}
|
||||||
show={
|
show={
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@ -37,7 +37,7 @@ const AvailableAddons = ({ providers, getIcon, hasPermission, history }) => {
|
|||||||
AvailableAddons.propTypes = {
|
AvailableAddons.propTypes = {
|
||||||
providers: PropTypes.array.isRequired,
|
providers: PropTypes.array.isRequired,
|
||||||
getIcon: PropTypes.func.isRequired,
|
getIcon: PropTypes.func.isRequired,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
hasAccess: PropTypes.func.isRequired,
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -9,12 +9,12 @@ import {
|
|||||||
ListItemText,
|
ListItemText,
|
||||||
} from '@material-ui/core';
|
} from '@material-ui/core';
|
||||||
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
||||||
import { DELETE_ADDON, UPDATE_ADDON } from '../../../../permissions';
|
import { DELETE_ADDON, UPDATE_ADDON } from '../../../AccessProvider/permissions';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import PageContent from '../../../common/PageContent/PageContent';
|
import PageContent from '../../../common/PageContent/PageContent';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const ConfiguredAddons = ({ addons, hasPermission, removeAddon, getIcon, toggleAddon }) => {
|
const ConfiguredAddons = ({ addons, hasAccess, removeAddon, getIcon, toggleAddon }) => {
|
||||||
const onRemoveAddon = addon => () => removeAddon(addon);
|
const onRemoveAddon = addon => () => removeAddon(addon);
|
||||||
const renderAddon = addon => (
|
const renderAddon = addon => (
|
||||||
<ListItem key={addon.id}>
|
<ListItem key={addon.id}>
|
||||||
@ -23,7 +23,7 @@ const ConfiguredAddons = ({ addons, hasPermission, removeAddon, getIcon, toggleA
|
|||||||
primary={
|
primary={
|
||||||
<span>
|
<span>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission(UPDATE_ADDON)}
|
condition={hasAccess(UPDATE_ADDON)}
|
||||||
show={
|
show={
|
||||||
<Link to={`/addons/edit/${addon.id}`}>
|
<Link to={`/addons/edit/${addon.id}`}>
|
||||||
<strong>{addon.provider}</strong>
|
<strong>{addon.provider}</strong>
|
||||||
@ -38,7 +38,7 @@ const ConfiguredAddons = ({ addons, hasPermission, removeAddon, getIcon, toggleA
|
|||||||
/>
|
/>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission(UPDATE_ADDON)}
|
condition={hasAccess(UPDATE_ADDON)}
|
||||||
show={
|
show={
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
@ -50,7 +50,7 @@ const ConfiguredAddons = ({ addons, hasPermission, removeAddon, getIcon, toggleA
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission(DELETE_ADDON)}
|
condition={hasAccess(DELETE_ADDON)}
|
||||||
show={
|
show={
|
||||||
<IconButton size="small" title="Remove addon" onClick={onRemoveAddon(addon)}>
|
<IconButton size="small" title="Remove addon" onClick={onRemoveAddon(addon)}>
|
||||||
<Icon>delete</Icon>
|
<Icon>delete</Icon>
|
||||||
@ -68,7 +68,7 @@ const ConfiguredAddons = ({ addons, hasPermission, removeAddon, getIcon, toggleA
|
|||||||
};
|
};
|
||||||
ConfiguredAddons.propTypes = {
|
ConfiguredAddons.propTypes = {
|
||||||
addons: PropTypes.array.isRequired,
|
addons: PropTypes.array.isRequired,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
hasAccess: PropTypes.func.isRequired,
|
||||||
removeAddon: PropTypes.func.isRequired,
|
removeAddon: PropTypes.func.isRequired,
|
||||||
toggleAddon: PropTypes.func.isRequired,
|
toggleAddon: PropTypes.func.isRequired,
|
||||||
getIcon: PropTypes.func.isRequired,
|
getIcon: PropTypes.func.isRequired,
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import AddonsListComponent from './AddonList';
|
import AddonsListComponent from './AddonList';
|
||||||
import { fetchAddons, removeAddon, updateAddon } from '../../store/addons/actions';
|
import { fetchAddons, removeAddon, updateAddon } from '../../store/addons/actions';
|
||||||
import { hasPermission } from '../../permissions';
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const list = state.addons.toJS();
|
const list = state.addons.toJS();
|
||||||
@ -9,7 +8,6 @@ const mapStateToProps = state => {
|
|||||||
return {
|
return {
|
||||||
addons: list.addons,
|
addons: list.addons,
|
||||||
providers: list.providers,
|
providers: list.providers,
|
||||||
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,19 +4,23 @@ import { ThemeProvider } from '@material-ui/core';
|
|||||||
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 { MemoryRouter } from 'react-router-dom';
|
||||||
import { CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION } from '../../../permissions';
|
import { ADMIN, CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION } from '../../AccessProvider/permissions';
|
||||||
import theme from '../../../themes/main-theme';
|
import theme from '../../../themes/main-theme';
|
||||||
|
|
||||||
|
import { createFakeStore } from '../../../accessStoreFake';
|
||||||
|
import AccessProvider from '../../AccessProvider/AccessProvider';
|
||||||
|
|
||||||
test('renders correctly if no application', () => {
|
test('renders correctly if no application', () => {
|
||||||
const tree = renderer
|
const tree = renderer
|
||||||
.create(
|
.create(
|
||||||
<ClientApplications
|
<AccessProvider store={createFakeStore([{permission: ADMIN}])}>
|
||||||
fetchApplication={() => Promise.resolve({})}
|
<ClientApplications
|
||||||
storeApplicationMetaData={jest.fn()}
|
fetchApplication={() => Promise.resolve({})}
|
||||||
deleteApplication={jest.fn()}
|
storeApplicationMetaData={jest.fn()}
|
||||||
hasPermission={() => true}
|
deleteApplication={jest.fn()}
|
||||||
history={{}}
|
history={{}}
|
||||||
/>
|
/>
|
||||||
|
</AccessProvider>
|
||||||
)
|
)
|
||||||
.toJSON();
|
.toJSON();
|
||||||
|
|
||||||
@ -28,6 +32,7 @@ test('renders correctly without permission', () => {
|
|||||||
.create(
|
.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
|
<AccessProvider store={createFakeStore([])}>
|
||||||
<ClientApplications
|
<ClientApplications
|
||||||
fetchApplication={() => Promise.resolve({})}
|
fetchApplication={() => Promise.resolve({})}
|
||||||
storeApplicationMetaData={jest.fn()}
|
storeApplicationMetaData={jest.fn()}
|
||||||
@ -71,8 +76,8 @@ test('renders correctly without permission', () => {
|
|||||||
description: 'app description',
|
description: 'app description',
|
||||||
}}
|
}}
|
||||||
location={{ locale: 'en-GB' }}
|
location={{ locale: 'en-GB' }}
|
||||||
hasPermission={() => false}
|
|
||||||
/>
|
/>
|
||||||
|
</AccessProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
)
|
)
|
||||||
@ -86,6 +91,7 @@ test('renders correctly with permissions', () => {
|
|||||||
.create(
|
.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
|
<AccessProvider store={createFakeStore([{permission: ADMIN}])}>
|
||||||
<ClientApplications
|
<ClientApplications
|
||||||
fetchApplication={() => Promise.resolve({})}
|
fetchApplication={() => Promise.resolve({})}
|
||||||
storeApplicationMetaData={jest.fn()}
|
storeApplicationMetaData={jest.fn()}
|
||||||
@ -129,10 +135,8 @@ test('renders correctly with permissions', () => {
|
|||||||
description: 'app description',
|
description: 'app description',
|
||||||
}}
|
}}
|
||||||
location={{ locale: 'en-GB' }}
|
location={{ locale: 'en-GB' }}
|
||||||
hasPermission={permission =>
|
|
||||||
[CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION].indexOf(permission) !== -1
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
</AccessProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
)
|
)
|
||||||
|
@ -5,15 +5,18 @@ import PropTypes from 'prop-types';
|
|||||||
import { Avatar, Link, Icon, IconButton, Button, LinearProgress, Typography } from '@material-ui/core';
|
import { Avatar, Link, Icon, IconButton, Button, LinearProgress, Typography } from '@material-ui/core';
|
||||||
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
||||||
import { formatFullDateTimeWithLocale, formatDateWithLocale } from '../common/util';
|
import { formatFullDateTimeWithLocale, formatDateWithLocale } from '../common/util';
|
||||||
import { UPDATE_APPLICATION } from '../../permissions';
|
import { UPDATE_APPLICATION } from '../AccessProvider/permissions';
|
||||||
import ApplicationView from './application-view';
|
import ApplicationView from './application-view';
|
||||||
import ApplicationUpdate from './application-update';
|
import ApplicationUpdate from './application-update';
|
||||||
import TabNav from '../common/TabNav/TabNav';
|
import TabNav from '../common/TabNav/TabNav';
|
||||||
import Dialogue from '../common/Dialogue';
|
import Dialogue from '../common/Dialogue';
|
||||||
import PageContent from '../common/PageContent';
|
import PageContent from '../common/PageContent';
|
||||||
import HeaderTitle from '../common/HeaderTitle';
|
import HeaderTitle from '../common/HeaderTitle';
|
||||||
|
import AccessContext from '../../contexts/AccessContext';
|
||||||
|
|
||||||
class ClientApplications extends PureComponent {
|
class ClientApplications extends PureComponent {
|
||||||
|
static contextType = AccessContext;
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
fetchApplication: PropTypes.func.isRequired,
|
fetchApplication: PropTypes.func.isRequired,
|
||||||
appName: PropTypes.string,
|
appName: PropTypes.string,
|
||||||
@ -21,7 +24,6 @@ class ClientApplications extends PureComponent {
|
|||||||
location: PropTypes.object,
|
location: PropTypes.object,
|
||||||
storeApplicationMetaData: PropTypes.func.isRequired,
|
storeApplicationMetaData: PropTypes.func.isRequired,
|
||||||
deleteApplication: PropTypes.func.isRequired,
|
deleteApplication: PropTypes.func.isRequired,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -60,7 +62,8 @@ class ClientApplications extends PureComponent {
|
|||||||
} else if (!this.props.application) {
|
} else if (!this.props.application) {
|
||||||
return <p>Application ({this.props.appName}) not found</p>;
|
return <p>Application ({this.props.appName}) not found</p>;
|
||||||
}
|
}
|
||||||
const { application, storeApplicationMetaData, hasPermission } = this.props;
|
const { hasAccess } = this.context;
|
||||||
|
const { application, storeApplicationMetaData } = this.props;
|
||||||
const { appName, instances, strategies, seenToggles, url, description, icon = 'apps', createdAt } = application;
|
const { appName, instances, strategies, seenToggles, url, description, icon = 'apps', createdAt } = application;
|
||||||
|
|
||||||
const toggleModal = () => {
|
const toggleModal = () => {
|
||||||
@ -84,7 +87,7 @@ class ClientApplications extends PureComponent {
|
|||||||
strategies={strategies}
|
strategies={strategies}
|
||||||
instances={instances}
|
instances={instances}
|
||||||
seenToggles={seenToggles}
|
seenToggles={seenToggles}
|
||||||
hasPermission={hasPermission}
|
hasAccess={hasAccess}
|
||||||
formatFullDateTime={this.formatFullDateTime}
|
formatFullDateTime={this.formatFullDateTime}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -126,7 +129,7 @@ class ClientApplications extends PureComponent {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission(UPDATE_APPLICATION)}
|
condition={hasAccess(UPDATE_APPLICATION)}
|
||||||
show={
|
show={
|
||||||
<Button color="secondary" title="Delete application" onClick={toggleModal}>
|
<Button color="secondary" title="Delete application" onClick={toggleModal}>
|
||||||
Delete
|
Delete
|
||||||
@ -145,7 +148,7 @@ class ClientApplications extends PureComponent {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission(UPDATE_APPLICATION)}
|
condition={hasAccess(UPDATE_APPLICATION)}
|
||||||
show={
|
show={
|
||||||
<div>
|
<div>
|
||||||
{renderModal()}
|
{renderModal()}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
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, deleteApplication } from './../../store/application/actions';
|
import { fetchApplication, storeApplicationMetaData, deleteApplication } 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]);
|
||||||
@ -12,7 +11,6 @@ const mapStateToProps = (state, props) => {
|
|||||||
return {
|
return {
|
||||||
application,
|
application,
|
||||||
location,
|
location,
|
||||||
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,14 +3,14 @@ import { Link } from 'react-router-dom';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Grid, List, ListItem, ListItemText, ListItemAvatar, Switch, Icon, Typography } from '@material-ui/core';
|
import { Grid, List, ListItem, ListItemText, ListItemAvatar, Switch, Icon, Typography } from '@material-ui/core';
|
||||||
import { shorten } from '../common';
|
import { shorten } from '../common';
|
||||||
import { CREATE_FEATURE, CREATE_STRATEGY } from '../../permissions';
|
import { CREATE_FEATURE, CREATE_STRATEGY } from '../AccessProvider/permissions';
|
||||||
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
function ApplicationView({ seenToggles, hasPermission, strategies, instances, formatFullDateTime }) {
|
function ApplicationView({ seenToggles, hasAccess, strategies, instances, formatFullDateTime }) {
|
||||||
const notFoundListItem = ({ createUrl, name, permission }) => (
|
const notFoundListItem = ({ createUrl, name, permission }) => (
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
key={`not_found_conditional_${name}`}
|
key={`not_found_conditional_${name}`}
|
||||||
condition={hasPermission(permission)}
|
condition={hasAccess(permission)}
|
||||||
show={
|
show={
|
||||||
<ListItem key={`not_found_${name}`}>
|
<ListItem key={`not_found_${name}`}>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
@ -149,7 +149,7 @@ ApplicationView.propTypes = {
|
|||||||
instances: PropTypes.array.isRequired,
|
instances: PropTypes.array.isRequired,
|
||||||
seenToggles: PropTypes.array.isRequired,
|
seenToggles: PropTypes.array.isRequired,
|
||||||
strategies: PropTypes.array.isRequired,
|
strategies: PropTypes.array.isRequired,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
hasAccess: PropTypes.func.isRequired,
|
||||||
formatFullDateTime: PropTypes.func.isRequired,
|
formatFullDateTime: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
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/FeatureView/FeatureView';
|
import ViewToggleComponent from '../feature/FeatureView/FeatureView';
|
||||||
import { hasPermission } from '../../permissions';
|
|
||||||
import { fetchTags } from '../../store/feature-tags/actions';
|
import { fetchTags } from '../../store/feature-tags/actions';
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
@ -14,7 +13,6 @@ export default connect(
|
|||||||
tagTypes: state.tagTypes.toJS(),
|
tagTypes: state.tagTypes.toJS(),
|
||||||
featureTags: state.featureTags.toJS(),
|
featureTags: state.featureTags.toJS(),
|
||||||
activeTab: props.activeTab,
|
activeTab: props.activeTab,
|
||||||
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
fetchArchive,
|
fetchArchive,
|
||||||
|
@ -2,14 +2,16 @@ import PropTypes from 'prop-types';
|
|||||||
import PageContent from '../../common/PageContent/PageContent';
|
import PageContent from '../../common/PageContent/PageContent';
|
||||||
import HeaderTitle from '../../common/HeaderTitle';
|
import HeaderTitle from '../../common/HeaderTitle';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
import { CREATE_CONTEXT_FIELD, DELETE_CONTEXT_FIELD } from '../../../permissions';
|
import { CREATE_CONTEXT_FIELD, DELETE_CONTEXT_FIELD } from '../../AccessProvider/permissions';
|
||||||
import { Icon, IconButton, List, ListItem, ListItemIcon, ListItemText, Tooltip } from '@material-ui/core';
|
import { Icon, IconButton, List, ListItem, ListItemIcon, ListItemText, Tooltip } from '@material-ui/core';
|
||||||
import React, { useState } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useStyles } from './styles';
|
import { useStyles } from './styles';
|
||||||
import ConfirmDialogue from '../../common/Dialogue';
|
import ConfirmDialogue from '../../common/Dialogue';
|
||||||
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
|
||||||
const ContextList = ({ removeContextField, hasPermission, history, contextFields }) => {
|
const ContextList = ({ removeContextField, history, contextFields }) => {
|
||||||
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
||||||
const [name, setName] = useState();
|
const [name, setName] = useState();
|
||||||
|
|
||||||
@ -29,7 +31,7 @@ const ContextList = ({ removeContextField, hasPermission, history, contextFields
|
|||||||
secondary={field.description}
|
secondary={field.description}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission(DELETE_CONTEXT_FIELD)}
|
condition={hasAccess(DELETE_CONTEXT_FIELD)}
|
||||||
show={
|
show={
|
||||||
<Tooltip title="Delete context field">
|
<Tooltip title="Delete context field">
|
||||||
<IconButton
|
<IconButton
|
||||||
@ -48,7 +50,7 @@ const ContextList = ({ removeContextField, hasPermission, history, contextFields
|
|||||||
));
|
));
|
||||||
const headerButton = () => (
|
const headerButton = () => (
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission(CREATE_CONTEXT_FIELD)}
|
condition={hasAccess(CREATE_CONTEXT_FIELD)}
|
||||||
show={
|
show={
|
||||||
<Tooltip title="Add context type">
|
<Tooltip title="Add context type">
|
||||||
<IconButton onClick={() => history.push('/context/create')}>
|
<IconButton onClick={() => history.push('/context/create')}>
|
||||||
@ -88,7 +90,6 @@ ContextList.propTypes = {
|
|||||||
contextFields: PropTypes.array.isRequired,
|
contextFields: PropTypes.array.isRequired,
|
||||||
removeContextField: PropTypes.func.isRequired,
|
removeContextField: PropTypes.func.isRequired,
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ContextList;
|
export default ContextList;
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ContextList from './ContextList';
|
import ContextList from './ContextList';
|
||||||
import { fetchContext, removeContextField } from '../../../store/context/actions';
|
import { fetchContext, removeContextField } from '../../../store/context/actions';
|
||||||
import { hasPermission } from '../../../permissions';
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const list = state.context.toJS();
|
const list = state.context.toJS();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contextFields: list,
|
contextFields: list,
|
||||||
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,24 +3,23 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import { Snackbar, Icon, IconButton } from '@material-ui/core';
|
import { Snackbar, Icon, IconButton } from '@material-ui/core';
|
||||||
|
|
||||||
const ErrorComponent = ({ errors, ...props }) => {
|
const ErrorComponent = ({ errors, muteError }) => {
|
||||||
const showError = errors.length > 0;
|
const showError = errors.length > 0;
|
||||||
const error = showError ? errors[0] : undefined;
|
const error = showError ? errors[0] : undefined;
|
||||||
const muteError = () => props.muteError(error);
|
|
||||||
return (
|
return (
|
||||||
<Snackbar
|
<Snackbar
|
||||||
action={
|
action={
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<IconButton size="small" aria-label="close" color="inherit" onClick={muteError}>
|
<IconButton size="small" aria-label="close" color="inherit">
|
||||||
<Icon>close</Icon>
|
<Icon>close</Icon>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
open={showError}
|
open={showError}
|
||||||
onClose={muteError}
|
onClose={() => muteError(error)}
|
||||||
autoHideDuration={10000}
|
autoHideDuration={10000}
|
||||||
message={
|
message={
|
||||||
<div>
|
<div key={error}>
|
||||||
<Icon>question_answer</Icon>
|
<Icon>question_answer</Icon>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,11 +6,14 @@ const mapDispatchToProps = {
|
|||||||
muteError,
|
muteError,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => {
|
||||||
errors: state.error
|
return {
|
||||||
|
errors: state.error
|
||||||
.get('list')
|
.get('list')
|
||||||
.toArray()
|
.toArray()
|
||||||
.reverse(),
|
.reverse()
|
||||||
});
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ErrorComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(ErrorComponent);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useLayoutEffect } from 'react';
|
import { useContext, useLayoutEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
@ -21,21 +21,24 @@ import HeaderTitle from '../../common/HeaderTitle';
|
|||||||
|
|
||||||
import loadingFeatures from './loadingFeatures';
|
import loadingFeatures from './loadingFeatures';
|
||||||
|
|
||||||
import { CREATE_FEATURE } from '../../../permissions';
|
import { CREATE_FEATURE } from '../../AccessProvider/permissions';
|
||||||
|
|
||||||
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
|
||||||
import { useStyles } from './styles';
|
import { useStyles } from './styles';
|
||||||
|
|
||||||
const FeatureToggleList = ({
|
const FeatureToggleList = ({
|
||||||
fetcher,
|
fetcher,
|
||||||
features,
|
features,
|
||||||
hasPermission,
|
|
||||||
settings,
|
settings,
|
||||||
revive,
|
revive,
|
||||||
|
currentProjectId,
|
||||||
updateSetting,
|
updateSetting,
|
||||||
featureMetrics,
|
featureMetrics,
|
||||||
toggleFeature,
|
toggleFeature,
|
||||||
loading,
|
loading,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const smallScreen = useMediaQuery('(max-width:700px)');
|
const smallScreen = useMediaQuery('(max-width:700px)');
|
||||||
|
|
||||||
@ -66,7 +69,7 @@ const FeatureToggleList = ({
|
|||||||
feature={feature}
|
feature={feature}
|
||||||
toggleFeature={toggleFeature}
|
toggleFeature={toggleFeature}
|
||||||
revive={revive}
|
revive={revive}
|
||||||
hasPermission={hasPermission}
|
hasAccess={hasAccess}
|
||||||
className={'skeleton'}
|
className={'skeleton'}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
@ -86,7 +89,7 @@ const FeatureToggleList = ({
|
|||||||
feature={feature}
|
feature={feature}
|
||||||
toggleFeature={toggleFeature}
|
toggleFeature={toggleFeature}
|
||||||
revive={revive}
|
revive={revive}
|
||||||
hasPermission={hasPermission}
|
hasAccess={hasAccess}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
elseShow={
|
elseShow={
|
||||||
@ -132,39 +135,38 @@ const FeatureToggleList = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission(CREATE_FEATURE)}
|
condition={smallScreen}
|
||||||
show={
|
show={
|
||||||
<ConditionallyRender
|
<Tooltip title="Create feature toggle">
|
||||||
condition={smallScreen}
|
<IconButton
|
||||||
show={
|
component={Link}
|
||||||
<Tooltip title="Create feature toggle">
|
to="/features/create"
|
||||||
<IconButton
|
data-test="add-feature-btn"
|
||||||
component={Link}
|
disabled={!hasAccess(CREATE_FEATURE, currentProjectId)}
|
||||||
to="/features/create"
|
>
|
||||||
data-test="add-feature-btn"
|
<Icon>add</Icon>
|
||||||
>
|
</IconButton>
|
||||||
<Icon>add</Icon>
|
</Tooltip>
|
||||||
</IconButton>
|
}
|
||||||
</Tooltip>
|
elseShow={
|
||||||
}
|
<Button
|
||||||
elseShow={
|
to="/features/create"
|
||||||
<Button
|
data-test="add-feature-btn"
|
||||||
to="/features/create"
|
color="secondary"
|
||||||
data-test="add-feature-btn"
|
variant="contained"
|
||||||
color="secondary"
|
component={Link}
|
||||||
variant="contained"
|
disabled={!hasAccess(CREATE_FEATURE, currentProjectId)}
|
||||||
component={Link}
|
className={classnames({
|
||||||
className={classnames({
|
skeleton: loading,
|
||||||
skeleton: loading,
|
})}
|
||||||
})}
|
>
|
||||||
>
|
Create feature toggle
|
||||||
Create feature toggle
|
</Button>
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -185,8 +187,8 @@ FeatureToggleList.propTypes = {
|
|||||||
toggleFeature: PropTypes.func,
|
toggleFeature: PropTypes.func,
|
||||||
settings: PropTypes.object,
|
settings: PropTypes.object,
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
|
||||||
loading: PropTypes.bool,
|
loading: PropTypes.bool,
|
||||||
|
currentProjectId: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FeatureToggleList;
|
export default FeatureToggleList;
|
||||||
|
@ -10,7 +10,7 @@ import Status from '../../status-component';
|
|||||||
import FeatureToggleListItemChip from './FeatureToggleListItemChip';
|
import FeatureToggleListItemChip from './FeatureToggleListItemChip';
|
||||||
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
import { UPDATE_FEATURE } from '../../../../permissions';
|
import { UPDATE_FEATURE } from '../../../AccessProvider/permissions';
|
||||||
import { calc, styles as commonStyles } from '../../../common';
|
import { calc, styles as commonStyles } from '../../../common';
|
||||||
|
|
||||||
import { useStyles } from './styles';
|
import { useStyles } from './styles';
|
||||||
@ -22,12 +22,12 @@ const FeatureToggleListItem = ({
|
|||||||
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,
|
hasAccess,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
|
||||||
const { name, description, enabled, type, stale, createdAt } = feature;
|
const { name, description, enabled, type, stale, createdAt, project } = feature;
|
||||||
const { showLastHour = false } = settings;
|
const { showLastHour = false } = settings;
|
||||||
const isStale = showLastHour
|
const isStale = showLastHour
|
||||||
? metricsLastHour.isFallback
|
? metricsLastHour.isFallback
|
||||||
@ -64,7 +64,7 @@ const FeatureToggleListItem = ({
|
|||||||
</span>
|
</span>
|
||||||
<span className={styles.listItemToggle}>
|
<span className={styles.listItemToggle}>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission(UPDATE_FEATURE)}
|
condition={hasAccess(UPDATE_FEATURE, project)}
|
||||||
show={
|
show={
|
||||||
<Switch
|
<Switch
|
||||||
disabled={toggleFeature === undefined}
|
disabled={toggleFeature === undefined}
|
||||||
@ -115,7 +115,7 @@ const FeatureToggleListItem = ({
|
|||||||
<FeatureToggleListItemChip type={type} />
|
<FeatureToggleListItemChip type={type} />
|
||||||
</span>
|
</span>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={revive && hasPermission(UPDATE_FEATURE)}
|
condition={revive && hasAccess(UPDATE_FEATURE, project)}
|
||||||
show={
|
show={
|
||||||
<IconButton onClick={() => revive(feature.name)}>
|
<IconButton onClick={() => revive(feature.name)}>
|
||||||
<Icon>undo</Icon>
|
<Icon>undo</Icon>
|
||||||
@ -134,7 +134,7 @@ FeatureToggleListItem.propTypes = {
|
|||||||
metricsLastHour: PropTypes.object,
|
metricsLastHour: PropTypes.object,
|
||||||
metricsLastMinute: PropTypes.object,
|
metricsLastMinute: PropTypes.object,
|
||||||
revive: PropTypes.func,
|
revive: PropTypes.func,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
hasAccess: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(FeatureToggleListItem);
|
export default memo(FeatureToggleListItem);
|
||||||
|
@ -120,8 +120,8 @@ exports[`renders correctly with one feature without permission 1`] = `
|
|||||||
className="MuiSwitch-root"
|
className="MuiSwitch-root"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-disabled={true}
|
aria-disabled={false}
|
||||||
className="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-6 MuiSwitch-switchBase MuiSwitch-colorSecondary PrivateSwitchBase-disabled-8 Mui-disabled Mui-disabled Mui-disabled"
|
className="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-6 MuiSwitch-switchBase MuiSwitch-colorSecondary"
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
onDragLeave={[Function]}
|
onDragLeave={[Function]}
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
@ -133,7 +133,7 @@ exports[`renders correctly with one feature without permission 1`] = `
|
|||||||
onTouchEnd={[Function]}
|
onTouchEnd={[Function]}
|
||||||
onTouchMove={[Function]}
|
onTouchMove={[Function]}
|
||||||
onTouchStart={[Function]}
|
onTouchStart={[Function]}
|
||||||
tabIndex={-1}
|
tabIndex={null}
|
||||||
title="Toggle Another"
|
title="Toggle Another"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -142,7 +142,7 @@ exports[`renders correctly with one feature without permission 1`] = `
|
|||||||
<input
|
<input
|
||||||
checked={false}
|
checked={false}
|
||||||
className="PrivateSwitchBase-input-9 MuiSwitch-input"
|
className="PrivateSwitchBase-input-9 MuiSwitch-input"
|
||||||
disabled={true}
|
disabled={false}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
/>
|
/>
|
||||||
@ -150,6 +150,9 @@ exports[`renders correctly with one feature without permission 1`] = `
|
|||||||
className="MuiSwitch-thumb"
|
className="MuiSwitch-thumb"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
className="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="MuiSwitch-track"
|
className="MuiSwitch-track"
|
||||||
|
@ -144,8 +144,8 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
aria-disabled={false}
|
aria-disabled={true}
|
||||||
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedSecondary"
|
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedSecondary Mui-disabled Mui-disabled"
|
||||||
data-test="add-feature-btn"
|
data-test="add-feature-btn"
|
||||||
href="/features/create"
|
href="/features/create"
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
@ -161,7 +161,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
onTouchMove={[Function]}
|
onTouchMove={[Function]}
|
||||||
onTouchStart={[Function]}
|
onTouchStart={[Function]}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="MuiButton-label"
|
className="MuiButton-label"
|
||||||
@ -186,7 +186,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
"reviveName": "Another",
|
"reviveName": "Another",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hasPermission={[Function]}
|
hasAccess={[Function]}
|
||||||
settings={
|
settings={
|
||||||
Object {
|
Object {
|
||||||
"sort": "name",
|
"sort": "name",
|
||||||
@ -349,6 +349,32 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<a
|
||||||
|
aria-disabled={true}
|
||||||
|
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedSecondary Mui-disabled Mui-disabled"
|
||||||
|
data-test="add-feature-btn"
|
||||||
|
href="/features/create"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onClick={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiButton-label"
|
||||||
|
>
|
||||||
|
Create feature toggle
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -366,7 +392,7 @@ exports[`renders correctly with one feature without permissions 1`] = `
|
|||||||
"reviveName": "Another",
|
"reviveName": "Another",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hasPermission={[Function]}
|
hasAccess={[Function]}
|
||||||
settings={
|
settings={
|
||||||
Object {
|
Object {
|
||||||
"sort": "name",
|
"sort": "name",
|
||||||
|
@ -4,7 +4,6 @@ import { ThemeProvider } from '@material-ui/core';
|
|||||||
|
|
||||||
import FeatureToggleListItem from '../FeatureToggleListItem';
|
import FeatureToggleListItem from '../FeatureToggleListItem';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import { UPDATE_FEATURE } from '../../../../permissions';
|
|
||||||
|
|
||||||
import theme from '../../../../themes/main-theme';
|
import theme from '../../../../themes/main-theme';
|
||||||
|
|
||||||
@ -38,7 +37,7 @@ 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}
|
hasAccess={() => true}
|
||||||
/>
|
/>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
@ -75,7 +74,7 @@ test('renders correctly with one feature without permission', () => {
|
|||||||
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
|
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
|
||||||
feature={feature}
|
feature={feature}
|
||||||
toggleFeature={jest.fn()}
|
toggleFeature={jest.fn()}
|
||||||
hasPermission={() => false}
|
hasAccess={() => true}
|
||||||
/>
|
/>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
|
@ -4,8 +4,12 @@ import { ThemeProvider } from '@material-ui/core';
|
|||||||
|
|
||||||
import FeatureToggleList from '../FeatureToggleList';
|
import FeatureToggleList from '../FeatureToggleList';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import { CREATE_FEATURE } from '../../../../permissions';
|
|
||||||
import theme from '../../../../themes/main-theme';
|
import theme from '../../../../themes/main-theme';
|
||||||
|
import { createFakeStore } from '../../../../accessStoreFake';
|
||||||
|
import { ADMIN, CREATE_FEATURE } from '../../../AccessProvider/permissions';
|
||||||
|
import AccessProvider from '../../../AccessProvider/AccessProvider';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
jest.mock('../FeatureToggleListItem', () => ({
|
jest.mock('../FeatureToggleListItem', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
@ -25,6 +29,7 @@ test('renders correctly with one feature', () => {
|
|||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
|
<AccessProvider store={createFakeStore([{permission: CREATE_FEATURE}])}>
|
||||||
<FeatureToggleList
|
<FeatureToggleList
|
||||||
updateSetting={jest.fn()}
|
updateSetting={jest.fn()}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
@ -33,8 +38,9 @@ test('renders correctly with one feature', () => {
|
|||||||
features={features}
|
features={features}
|
||||||
toggleFeature={jest.fn()}
|
toggleFeature={jest.fn()}
|
||||||
fetcher={jest.fn()}
|
fetcher={jest.fn()}
|
||||||
hasPermission={permission => permission === CREATE_FEATURE}
|
currentProjectId='default'
|
||||||
/>
|
/>
|
||||||
|
</AccessProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@ -53,6 +59,7 @@ test('renders correctly with one feature without permissions', () => {
|
|||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
|
<AccessProvider store={createFakeStore([{permission: CREATE_FEATURE}])}>
|
||||||
<FeatureToggleList
|
<FeatureToggleList
|
||||||
updateSetting={jest.fn()}
|
updateSetting={jest.fn()}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
@ -61,8 +68,9 @@ test('renders correctly with one feature without permissions', () => {
|
|||||||
features={features}
|
features={features}
|
||||||
toggleFeature={jest.fn()}
|
toggleFeature={jest.fn()}
|
||||||
fetcher={jest.fn()}
|
fetcher={jest.fn()}
|
||||||
hasPermission={() => false}
|
currentProjectId='default'
|
||||||
/>
|
/>
|
||||||
|
</AccessProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
@ -3,8 +3,6 @@ import { toggleFeature, fetchFeatureToggles } from '../../../store/feature-toggl
|
|||||||
import { updateSettingForGroup } from '../../../store/settings/actions';
|
import { updateSettingForGroup } from '../../../store/settings/actions';
|
||||||
import FeatureToggleList from './FeatureToggleList';
|
import FeatureToggleList from './FeatureToggleList';
|
||||||
|
|
||||||
import { hasPermission } from '../../../permissions';
|
|
||||||
|
|
||||||
function checkConstraints(strategy, regex) {
|
function checkConstraints(strategy, regex) {
|
||||||
if (!strategy.constraints) {
|
if (!strategy.constraints) {
|
||||||
return;
|
return;
|
||||||
@ -12,6 +10,12 @@ function checkConstraints(strategy, regex) {
|
|||||||
return strategy.constraints.some(c => c.values.some(v => regex.test(v)));
|
return strategy.constraints.some(c => c.values.some(v => regex.test(v)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveCurrentProjectId(settings) {
|
||||||
|
if(!settings.currentProjectId || settings.currentProjectId === '*') {
|
||||||
|
return 'default';
|
||||||
|
} return settings.currentProjectId;
|
||||||
|
}
|
||||||
|
|
||||||
export const mapStateToPropsConfigurable = isFeature => state => {
|
export const mapStateToPropsConfigurable = isFeature => state => {
|
||||||
const featureMetrics = state.featureMetrics.toJS();
|
const featureMetrics = state.featureMetrics.toJS();
|
||||||
const settings = state.settings.toJS().feature || {};
|
const settings = state.settings.toJS().feature || {};
|
||||||
@ -96,9 +100,9 @@ export const mapStateToPropsConfigurable = isFeature => state => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
features,
|
features,
|
||||||
|
currentProjectId: resolveCurrentProjectId(settings),
|
||||||
featureMetrics,
|
featureMetrics,
|
||||||
settings,
|
settings,
|
||||||
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
|
||||||
loading: state.apiCalls.fetchTogglesState.loading,
|
loading: state.apiCalls.fetchTogglesState.loading,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useLayoutEffect, useState } from 'react';
|
import React, { useContext, useEffect, useLayoutEffect, useState } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
@ -18,10 +18,9 @@ import FeatureTypeSelect from '../feature-type-select-container';
|
|||||||
import ProjectSelect from '../project-select-container';
|
import ProjectSelect from '../project-select-container';
|
||||||
import UpdateDescriptionComponent from '../view/update-description-component';
|
import UpdateDescriptionComponent from '../view/update-description-component';
|
||||||
import {
|
import {
|
||||||
CREATE_FEATURE,
|
|
||||||
DELETE_FEATURE,
|
DELETE_FEATURE,
|
||||||
UPDATE_FEATURE,
|
UPDATE_FEATURE,
|
||||||
} from '../../../permissions';
|
} from '../../AccessProvider/permissions';
|
||||||
import StatusComponent from '../status-component';
|
import StatusComponent from '../status-component';
|
||||||
import FeatureTagComponent from '../feature-tag-component';
|
import FeatureTagComponent from '../feature-tag-component';
|
||||||
import StatusUpdateComponent from '../view/status-update-component';
|
import StatusUpdateComponent from '../view/status-update-component';
|
||||||
@ -35,6 +34,7 @@ import styles from './FeatureView.module.scss';
|
|||||||
import ConfirmDialogue from '../../common/Dialogue';
|
import ConfirmDialogue from '../../common/Dialogue';
|
||||||
|
|
||||||
import { useCommonStyles } from '../../../common.styles';
|
import { useCommonStyles } from '../../../common.styles';
|
||||||
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
|
||||||
const FeatureView = ({
|
const FeatureView = ({
|
||||||
activeTab,
|
activeTab,
|
||||||
@ -49,7 +49,6 @@ const FeatureView = ({
|
|||||||
editFeatureToggle,
|
editFeatureToggle,
|
||||||
featureToggle,
|
featureToggle,
|
||||||
history,
|
history,
|
||||||
hasPermission,
|
|
||||||
untagFeature,
|
untagFeature,
|
||||||
featureTags,
|
featureTags,
|
||||||
fetchTags,
|
fetchTags,
|
||||||
@ -58,6 +57,8 @@ const FeatureView = ({
|
|||||||
const isFeatureView = !!fetchFeatureToggles;
|
const isFeatureView = !!fetchFeatureToggles;
|
||||||
const [delDialog, setDelDialog] = useState(false);
|
const [delDialog, setDelDialog] = useState(false);
|
||||||
const commonStyles = useCommonStyles();
|
const commonStyles = useCommonStyles();
|
||||||
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
const { project } = featureToggle || { };
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
@ -76,26 +77,17 @@ const FeatureView = ({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const editable = isFeatureView && hasAccess(UPDATE_FEATURE, project);
|
||||||
|
|
||||||
const getTabComponent = key => {
|
const getTabComponent = key => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'activation':
|
case 'activation':
|
||||||
if (isFeatureView && hasPermission(UPDATE_FEATURE)) {
|
return <UpdateStrategies
|
||||||
return (
|
|
||||||
<UpdateStrategies
|
|
||||||
featureToggle={featureToggle}
|
|
||||||
features={features}
|
|
||||||
history={history}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<UpdateStrategies
|
|
||||||
featureToggle={featureToggle}
|
featureToggle={featureToggle}
|
||||||
features={features}
|
features={features}
|
||||||
history={history}
|
history={history}
|
||||||
editable={false}
|
editable={editable}
|
||||||
/>
|
/>
|
||||||
);
|
|
||||||
case 'metrics':
|
case 'metrics':
|
||||||
return <MetricComponent featureToggle={featureToggle} />;
|
return <MetricComponent featureToggle={featureToggle} />;
|
||||||
case 'variants':
|
case 'variants':
|
||||||
@ -104,7 +96,7 @@ const FeatureView = ({
|
|||||||
featureToggle={featureToggle}
|
featureToggle={featureToggle}
|
||||||
features={features}
|
features={features}
|
||||||
history={history}
|
history={history}
|
||||||
hasPermission={hasPermission}
|
editable={editable}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'log':
|
case 'log':
|
||||||
@ -152,7 +144,7 @@ const FeatureView = ({
|
|||||||
<span>
|
<span>
|
||||||
Could not find the toggle{' '}
|
Could not find the toggle{' '}
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission(CREATE_FEATURE)}
|
condition={editable}
|
||||||
show={
|
show={
|
||||||
<Link
|
<Link
|
||||||
to={{
|
to={{
|
||||||
@ -243,13 +235,14 @@ const FeatureView = ({
|
|||||||
isFeatureView={isFeatureView}
|
isFeatureView={isFeatureView}
|
||||||
description={featureToggle.description}
|
description={featureToggle.description}
|
||||||
update={updateDescription}
|
update={updateDescription}
|
||||||
hasPermission={hasPermission}
|
editable={editable}
|
||||||
/>
|
/>
|
||||||
<div className={styles.selectContainer}>
|
<div className={styles.selectContainer}>
|
||||||
<FeatureTypeSelect
|
<FeatureTypeSelect
|
||||||
value={featureToggle.type}
|
value={featureToggle.type}
|
||||||
onChange={updateType}
|
onChange={updateType}
|
||||||
label="Feature type"
|
label="Feature type"
|
||||||
|
editable={editable}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ProjectSelect
|
<ProjectSelect
|
||||||
@ -257,6 +250,7 @@ const FeatureView = ({
|
|||||||
onChange={updateProject}
|
onChange={updateProject}
|
||||||
label="Project"
|
label="Project"
|
||||||
filled
|
filled
|
||||||
|
editable={editable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FeatureTagComponent
|
<FeatureTagComponent
|
||||||
@ -271,7 +265,7 @@ const FeatureView = ({
|
|||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<span style={{ paddingRight: '24px' }}>
|
<span style={{ paddingRight: '24px' }}>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission(UPDATE_FEATURE)}
|
condition={editable}
|
||||||
show={
|
show={
|
||||||
<>
|
<>
|
||||||
<Switch
|
<Switch
|
||||||
@ -327,7 +321,7 @@ const FeatureView = ({
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={!hasPermission(DELETE_FEATURE)}
|
disabled={!hasAccess(DELETE_FEATURE, project)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDelDialog(true);
|
setDelDialog(true);
|
||||||
}}
|
}}
|
||||||
@ -340,7 +334,7 @@ const FeatureView = ({
|
|||||||
}
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<Button
|
<Button
|
||||||
disabled={!hasPermission(UPDATE_FEATURE)}
|
disabled={!hasAccess(UPDATE_FEATURE, hasAccess)}
|
||||||
onClick={reviveToggle}
|
onClick={reviveToggle}
|
||||||
style={{ flexShrink: 0 }}
|
style={{ flexShrink: 0 }}
|
||||||
>
|
>
|
||||||
@ -383,7 +377,6 @@ FeatureView.propTypes = {
|
|||||||
editFeatureToggle: PropTypes.func,
|
editFeatureToggle: PropTypes.func,
|
||||||
featureToggle: PropTypes.object,
|
featureToggle: PropTypes.object,
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
|
||||||
fetchTags: PropTypes.func,
|
fetchTags: PropTypes.func,
|
||||||
untagFeature: PropTypes.func,
|
untagFeature: PropTypes.func,
|
||||||
featureTags: PropTypes.array,
|
featureTags: PropTypes.array,
|
||||||
|
@ -10,7 +10,6 @@ import {
|
|||||||
} from '../../../store/feature-toggle/actions';
|
} from '../../../store/feature-toggle/actions';
|
||||||
|
|
||||||
import FeatureView from './FeatureView';
|
import FeatureView from './FeatureView';
|
||||||
import { hasPermission } from '../../../permissions';
|
|
||||||
import { fetchTags, tagFeature, untagFeature } from '../../../store/feature-tags/actions';
|
import { fetchTags, tagFeature, untagFeature } from '../../../store/feature-tags/actions';
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
@ -20,7 +19,6 @@ export default connect(
|
|||||||
featureTags: state.featureTags.toJS(),
|
featureTags: state.featureTags.toJS(),
|
||||||
tagTypes: state.tagTypes.toJS(),
|
tagTypes: state.tagTypes.toJS(),
|
||||||
activeTab: props.activeTab,
|
activeTab: props.activeTab,
|
||||||
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
fetchFeatureToggles,
|
fetchFeatureToggles,
|
||||||
|
@ -52,6 +52,7 @@ class AddFeatureComponent extends Component {
|
|||||||
onChange={v => setValue('type', v.target.value)}
|
onChange={v => setValue('type', v.target.value)}
|
||||||
label={'Toggle type'}
|
label={'Toggle type'}
|
||||||
id="feature-type-select"
|
id="feature-type-select"
|
||||||
|
editable
|
||||||
inputProps={{
|
inputProps={{
|
||||||
'data-test': CF_TYPE_ID,
|
'data-test': CF_TYPE_ID,
|
||||||
}}
|
}}
|
||||||
|
@ -12,6 +12,7 @@ class FeatureTypeSelectComponent extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
editable,
|
||||||
value,
|
value,
|
||||||
types,
|
types,
|
||||||
onChange,
|
onChange,
|
||||||
@ -32,11 +33,12 @@ class FeatureTypeSelectComponent extends Component {
|
|||||||
options.push({ key: value, label: value });
|
options.push({ key: value, label: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
return <MySelect options={options} value={value} onChange={onChange} label={label} id={id} {...rest} />;
|
return <MySelect disabled={!editable} options={options} value={value} onChange={onChange} label={label} id={id} {...rest} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FeatureTypeSelectComponent.propTypes = {
|
FeatureTypeSelectComponent.propTypes = {
|
||||||
|
editable: PropTypes.bool.isRequired,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
filled: PropTypes.bool,
|
filled: PropTypes.bool,
|
||||||
types: PropTypes.array.isRequired,
|
types: PropTypes.array.isRequired,
|
||||||
|
@ -596,5 +596,35 @@ exports[`renders correctly with without variants and no permissions 1`] = `
|
|||||||
No variants defined.
|
No variants defined.
|
||||||
</p>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="MuiButtonBase-root MuiButton-root MuiButton-contained addVariantButton MuiButton-containedPrimary"
|
||||||
|
disabled={false}
|
||||||
|
onBlur={[Function]}
|
||||||
|
onClick={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
|
title="Add variant"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiButton-label"
|
||||||
|
>
|
||||||
|
Add variant
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
|
@ -3,7 +3,6 @@ import { ThemeProvider } from '@material-ui/core';
|
|||||||
|
|
||||||
import UpdateVariant from './../update-variant-component';
|
import UpdateVariant from './../update-variant-component';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import { UPDATE_FEATURE } from '../../../../permissions';
|
|
||||||
import { weightTypes } from '../enums';
|
import { weightTypes } from '../enums';
|
||||||
import theme from '../../../../themes/main-theme';
|
import theme from '../../../../themes/main-theme';
|
||||||
|
|
||||||
@ -24,7 +23,7 @@ test('renders correctly with without variants', () => {
|
|||||||
updateVariant={jest.fn()}
|
updateVariant={jest.fn()}
|
||||||
stickinessOptions={['default']}
|
stickinessOptions={['default']}
|
||||||
updateStickiness={jest.fn()}
|
updateStickiness={jest.fn()}
|
||||||
hasPermission={permission => permission === UPDATE_FEATURE}
|
editable
|
||||||
/>
|
/>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
@ -45,7 +44,7 @@ test('renders correctly with without variants and no permissions', () => {
|
|||||||
updateVariant={jest.fn()}
|
updateVariant={jest.fn()}
|
||||||
stickinessOptions={['default']}
|
stickinessOptions={['default']}
|
||||||
updateStickiness={jest.fn()}
|
updateStickiness={jest.fn()}
|
||||||
hasPermission={() => false}
|
editable
|
||||||
/>
|
/>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
@ -105,7 +104,7 @@ test('renders correctly with with variants', () => {
|
|||||||
updateVariant={jest.fn()}
|
updateVariant={jest.fn()}
|
||||||
stickinessOptions={['default']}
|
stickinessOptions={['default']}
|
||||||
updateStickiness={jest.fn()}
|
updateStickiness={jest.fn()}
|
||||||
hasPermission={permission => permission === UPDATE_FEATURE}
|
editable
|
||||||
/>
|
/>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
@ -4,7 +4,6 @@ import classnames from 'classnames';
|
|||||||
|
|
||||||
import VariantViewComponent from './variant-view-component';
|
import VariantViewComponent from './variant-view-component';
|
||||||
import styles from './variant.module.scss';
|
import styles from './variant.module.scss';
|
||||||
import { UPDATE_FEATURE } from '../../../permissions';
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableHead,
|
TableHead,
|
||||||
@ -46,7 +45,7 @@ class UpdateVariantComponent extends Component {
|
|||||||
|
|
||||||
openEditVariant = (e, index, variant) => {
|
openEditVariant = (e, index, variant) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.props.hasPermission(UPDATE_FEATURE)) {
|
if (this.props.editable) {
|
||||||
this.setState({
|
this.setState({
|
||||||
showDialog: true,
|
showDialog: true,
|
||||||
editVariant: variant,
|
editVariant: variant,
|
||||||
@ -73,7 +72,7 @@ class UpdateVariantComponent extends Component {
|
|||||||
variant={variant}
|
variant={variant}
|
||||||
editVariant={e => this.openEditVariant(e, index, variant)}
|
editVariant={e => this.openEditVariant(e, index, variant)}
|
||||||
removeVariant={e => this.onRemoveVariant(e, index)}
|
removeVariant={e => this.onRemoveVariant(e, index)}
|
||||||
hasPermission={this.props.hasPermission}
|
editable={this.props.editable}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -162,7 +161,7 @@ class UpdateVariantComponent extends Component {
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={this.props.hasPermission(UPDATE_FEATURE)}
|
condition={this.props.editable}
|
||||||
show={
|
show={
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
@ -198,7 +197,7 @@ UpdateVariantComponent.propTypes = {
|
|||||||
removeVariant: PropTypes.func.isRequired,
|
removeVariant: PropTypes.func.isRequired,
|
||||||
updateVariant: PropTypes.func.isRequired,
|
updateVariant: PropTypes.func.isRequired,
|
||||||
updateStickiness: PropTypes.func.isRequired,
|
updateStickiness: PropTypes.func.isRequired,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
editable: PropTypes.bool.isRequired,
|
||||||
stickinessOptions: PropTypes.array,
|
stickinessOptions: PropTypes.array,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ import { updateWeight } from '../../common/util';
|
|||||||
const mapStateToProps = (state, ownProps) => ({
|
const mapStateToProps = (state, ownProps) => ({
|
||||||
variants: ownProps.featureToggle.variants || [],
|
variants: ownProps.featureToggle.variants || [],
|
||||||
stickinessOptions: ['default', ...state.context.filter(c => c.stickiness).map(c => c.name)],
|
stickinessOptions: ['default', ...state.context.filter(c => c.stickiness).map(c => c.name)],
|
||||||
hasPermission: ownProps.hasPermission,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, ownProps) => ({
|
const mapDispatchToProps = (dispatch, ownProps) => ({
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { IconButton, Chip, Icon, TableCell, TableRow } from '@material-ui/core';
|
import { IconButton, Chip, Icon, TableCell, TableRow } from '@material-ui/core';
|
||||||
import { UPDATE_FEATURE } from '../../../permissions';
|
|
||||||
import { weightTypes } from './enums';
|
import { weightTypes } from './enums';
|
||||||
|
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
import styles from './variant.module.scss';
|
import styles from './variant.module.scss';
|
||||||
function VariantViewComponent({ variant, editVariant, removeVariant, hasPermission }) {
|
function VariantViewComponent({ variant, editVariant, removeVariant, editable }) {
|
||||||
const { FIX } = weightTypes;
|
const { FIX } = weightTypes;
|
||||||
return (
|
return (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@ -29,7 +28,7 @@ function VariantViewComponent({ variant, editVariant, removeVariant, hasPermissi
|
|||||||
<TableCell>{variant.weight / 10.0} %</TableCell>
|
<TableCell>{variant.weight / 10.0} %</TableCell>
|
||||||
<TableCell>{variant.weightType === FIX ? 'Fix' : 'Variable'}</TableCell>
|
<TableCell>{variant.weightType === FIX ? 'Fix' : 'Variable'}</TableCell>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission(UPDATE_FEATURE)}
|
condition={editable}
|
||||||
show={
|
show={
|
||||||
<TableCell className={styles.actions}>
|
<TableCell className={styles.actions}>
|
||||||
<div className={styles.actionsContainer}>
|
<div className={styles.actionsContainer}>
|
||||||
@ -52,7 +51,7 @@ VariantViewComponent.propTypes = {
|
|||||||
variant: PropTypes.object,
|
variant: PropTypes.object,
|
||||||
removeVariant: PropTypes.func,
|
removeVariant: PropTypes.func,
|
||||||
editVariant: PropTypes.func,
|
editVariant: PropTypes.func,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
editable: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default VariantViewComponent;
|
export default VariantViewComponent;
|
||||||
|
@ -82,6 +82,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
className="selectContainer"
|
className="selectContainer"
|
||||||
>
|
>
|
||||||
<FeatureTypeSelect
|
<FeatureTypeSelect
|
||||||
|
editable={true}
|
||||||
label="Feature type"
|
label="Feature type"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
value="release"
|
value="release"
|
||||||
@ -467,6 +468,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
>
|
>
|
||||||
<UpdateStrategiesComponent
|
<UpdateStrategiesComponent
|
||||||
|
editable={true}
|
||||||
featureToggle={
|
featureToggle={
|
||||||
Object {
|
Object {
|
||||||
"createdAt": "2018-02-04T20:27:52.127Z",
|
"createdAt": "2018-02-04T20:27:52.127Z",
|
||||||
|
@ -7,9 +7,11 @@ import { Provider } from 'react-redux';
|
|||||||
import { ThemeProvider } from '@material-ui/core';
|
import { ThemeProvider } from '@material-ui/core';
|
||||||
import ViewFeatureToggleComponent from '../../FeatureView/FeatureView';
|
import ViewFeatureToggleComponent from '../../FeatureView/FeatureView';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import { DELETE_FEATURE, UPDATE_FEATURE } from '../../../../permissions';
|
import { ADMIN, DELETE_FEATURE, UPDATE_FEATURE } from '../../../AccessProvider/permissions';
|
||||||
|
|
||||||
import theme from '../../../../themes/main-theme';
|
import theme from '../../../../themes/main-theme';
|
||||||
|
import { createFakeStore } from '../../../../accessStoreFake';
|
||||||
|
import AccessProvider from '../../../AccessProvider/AccessProvider';
|
||||||
|
|
||||||
jest.mock('../update-strategies-container', () => ({
|
jest.mock('../update-strategies-container', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
@ -61,6 +63,7 @@ test('renders correctly with one feature', () => {
|
|||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<Provider store={createStore(mockReducer, mockStore)}>
|
<Provider store={createStore(mockReducer, mockStore)}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
|
<AccessProvider store={createFakeStore([{permission: ADMIN}])}>
|
||||||
<ViewFeatureToggleComponent
|
<ViewFeatureToggleComponent
|
||||||
activeTab={'strategies'}
|
activeTab={'strategies'}
|
||||||
featureToggleName="another"
|
featureToggleName="another"
|
||||||
@ -69,10 +72,10 @@ test('renders correctly with one feature', () => {
|
|||||||
fetchFeatureToggles={jest.fn()}
|
fetchFeatureToggles={jest.fn()}
|
||||||
history={{}}
|
history={{}}
|
||||||
featureTags={[]}
|
featureTags={[]}
|
||||||
hasPermission={permission => [DELETE_FEATURE, UPDATE_FEATURE].indexOf(permission) !== -1}
|
|
||||||
fetchTags={jest.fn()}
|
fetchTags={jest.fn()}
|
||||||
untagFeature={jest.fn()}
|
untagFeature={jest.fn()}
|
||||||
/>
|
/>
|
||||||
|
</AccessProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
|
@ -5,8 +5,6 @@ import { Typography, IconButton, FormControl, TextField, Button } from '@materia
|
|||||||
import CreateIcon from '@material-ui/icons/Create';
|
import CreateIcon from '@material-ui/icons/Create';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
import { UPDATE_FEATURE } from '../../../permissions';
|
|
||||||
|
|
||||||
import styles from './update-description-component.module.scss';
|
import styles from './update-description-component.module.scss';
|
||||||
|
|
||||||
export default class UpdateDescriptionComponent extends React.Component {
|
export default class UpdateDescriptionComponent extends React.Component {
|
||||||
@ -19,7 +17,7 @@ export default class UpdateDescriptionComponent extends React.Component {
|
|||||||
isFeatureView: PropTypes.bool.isRequired,
|
isFeatureView: PropTypes.bool.isRequired,
|
||||||
update: PropTypes.func,
|
update: PropTypes.func,
|
||||||
featureToggle: PropTypes.object,
|
featureToggle: PropTypes.object,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
editable: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
onEditMode = (description, evt) => {
|
onEditMode = (description, evt) => {
|
||||||
@ -43,14 +41,13 @@ export default class UpdateDescriptionComponent extends React.Component {
|
|||||||
this.setState({ editMode: false, description: undefined });
|
this.setState({ editMode: false, description: undefined });
|
||||||
};
|
};
|
||||||
|
|
||||||
renderRead({ description, isFeatureView, hasPermission }) {
|
renderRead({ description, editable }) {
|
||||||
const showButton = isFeatureView && hasPermission(UPDATE_FEATURE);
|
|
||||||
return (
|
return (
|
||||||
<FormControl size="small" variant="outlined">
|
<FormControl size="small" variant="outlined">
|
||||||
<Typography>
|
<Typography>
|
||||||
{description || 'No feature toggle description'}
|
{description || 'No feature toggle description'}
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={showButton}
|
condition={editable}
|
||||||
show={
|
show={
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="toggle description edit"
|
aria-label="toggle description edit"
|
||||||
|
@ -85,14 +85,6 @@ Array [
|
|||||||
"title": "Reporting",
|
"title": "Reporting",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"icon": "exit_to_app",
|
|
||||||
"layout": "main",
|
|
||||||
"path": "/logout",
|
|
||||||
"title": "Sign out",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
@ -102,5 +94,13 @@ Array [
|
|||||||
"title": "Admin",
|
"title": "Admin",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"icon": "exit_to_app",
|
||||||
|
"layout": "main",
|
||||||
|
"path": "/logout",
|
||||||
|
"title": "Sign out",
|
||||||
|
"type": "protected",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
@ -293,23 +293,6 @@ export const routes = [
|
|||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/logout',
|
|
||||||
title: 'Sign out',
|
|
||||||
icon: 'exit_to_app',
|
|
||||||
component: LogoutFeatures,
|
|
||||||
type: 'protected',
|
|
||||||
layout: 'main',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/login',
|
|
||||||
title: 'Log in',
|
|
||||||
icon: 'user',
|
|
||||||
component: Login,
|
|
||||||
type: 'unprotected',
|
|
||||||
hidden: true,
|
|
||||||
layout: 'standalone',
|
|
||||||
},
|
|
||||||
// Admin
|
// Admin
|
||||||
{
|
{
|
||||||
path: '/admin/api',
|
path: '/admin/api',
|
||||||
@ -344,6 +327,23 @@ export const routes = [
|
|||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/logout',
|
||||||
|
title: 'Sign out',
|
||||||
|
icon: 'exit_to_app',
|
||||||
|
component: LogoutFeatures,
|
||||||
|
type: 'protected',
|
||||||
|
layout: 'main',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
title: 'Log in',
|
||||||
|
icon: 'user',
|
||||||
|
component: Login,
|
||||||
|
type: 'unprotected',
|
||||||
|
hidden: true,
|
||||||
|
layout: 'standalone',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/new-user',
|
path: '/new-user',
|
||||||
title: 'New user',
|
title: 'New user',
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import HeaderTitle from '../../common/HeaderTitle';
|
import HeaderTitle from '../../common/HeaderTitle';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
import { CREATE_PROJECT, DELETE_PROJECT, UPDATE_PROJECT } from '../../../permissions';
|
import { CREATE_PROJECT, DELETE_PROJECT, UPDATE_PROJECT } from '../../AccessProvider/permissions';
|
||||||
import { Icon, IconButton, List, ListItem, ListItemAvatar, ListItemText, Tooltip } from '@material-ui/core';
|
import { Icon, IconButton, List, ListItem, ListItemAvatar, ListItemText, Tooltip } from '@material-ui/core';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import ConfirmDialogue from '../../common/Dialogue';
|
import ConfirmDialogue from '../../common/Dialogue';
|
||||||
import PageContent from '../../common/PageContent/PageContent';
|
import PageContent from '../../common/PageContent/PageContent';
|
||||||
import { useStyles } from './styles';
|
import { useStyles } from './styles';
|
||||||
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
|
||||||
const ProjectList = ({ projects, fetchProjects, removeProject, history, hasPermission }) => {
|
const ProjectList = ({ projects, fetchProjects, removeProject, history }) => {
|
||||||
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
||||||
const [project, setProject] = useState(undefined);
|
const [project, setProject] = useState(undefined);
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
@ -19,7 +21,7 @@ const ProjectList = ({ projects, fetchProjects, removeProject, history, hasPermi
|
|||||||
|
|
||||||
const addProjectButton = () => (
|
const addProjectButton = () => (
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission(CREATE_PROJECT)}
|
condition={hasAccess(CREATE_PROJECT)}
|
||||||
show={
|
show={
|
||||||
<Tooltip title="Add new project">
|
<Tooltip title="Add new project">
|
||||||
<IconButton aria-label="add-project" onClick={() => history.push('/projects/create')}>
|
<IconButton aria-label="add-project" onClick={() => history.push('/projects/create')}>
|
||||||
@ -68,10 +70,10 @@ const ProjectList = ({ projects, fetchProjects, removeProject, history, hasPermi
|
|||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary={projectLink(project)} secondary={project.description} />
|
<ListItemText primary={projectLink(project)} secondary={project.description} />
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission(UPDATE_PROJECT)}
|
condition={hasAccess(UPDATE_PROJECT, project.name)}
|
||||||
show={mgmAccessButton(project)}
|
show={mgmAccessButton(project)}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender condition={hasPermission(DELETE_PROJECT)} show={deleteProjectButton(project)} />
|
<ConditionallyRender condition={hasAccess(DELETE_PROJECT, project.name)} show={deleteProjectButton(project)} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -106,7 +108,6 @@ ProjectList.propTypes = {
|
|||||||
fetchProjects: PropTypes.func.isRequired,
|
fetchProjects: PropTypes.func.isRequired,
|
||||||
removeProject: PropTypes.func.isRequired,
|
removeProject: PropTypes.func.isRequired,
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProjectList;
|
export default ProjectList;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { fetchProjects, removeProject } from '../../../store/project/actions';
|
import { fetchProjects, removeProject } from '../../../store/project/actions';
|
||||||
import { hasPermission } from '../../../permissions';
|
|
||||||
import ProjectList from './ProjectList';
|
import ProjectList from './ProjectList';
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
@ -8,7 +7,6 @@ const mapStateToProps = state => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
projects,
|
projects,
|
||||||
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||||
|
|
||||||
import { List, ListItem, ListItemAvatar, IconButton, Icon, ListItemText, Button, Tooltip } from '@material-ui/core';
|
import { List, ListItem, ListItemAvatar, IconButton, Icon, ListItemText, Button, Tooltip } from '@material-ui/core';
|
||||||
import { CREATE_STRATEGY, DELETE_STRATEGY } from '../../../permissions';
|
import { CREATE_STRATEGY, DELETE_STRATEGY } from '../../AccessProvider/permissions';
|
||||||
|
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
import PageContent from '../../common/PageContent/PageContent';
|
import PageContent from '../../common/PageContent/PageContent';
|
||||||
import HeaderTitle from '../../common/HeaderTitle';
|
import HeaderTitle from '../../common/HeaderTitle';
|
||||||
|
|
||||||
import { useStyles } from './styles';
|
import { useStyles } from './styles';
|
||||||
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
|
||||||
const StrategiesList = ({
|
const StrategiesList = ({
|
||||||
strategies,
|
strategies,
|
||||||
@ -19,11 +20,11 @@ const StrategiesList = ({
|
|||||||
removeStrategy,
|
removeStrategy,
|
||||||
deprecateStrategy,
|
deprecateStrategy,
|
||||||
reactivateStrategy,
|
reactivateStrategy,
|
||||||
hasPermission,
|
|
||||||
}) => {
|
}) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const smallScreen = useMediaQuery('(max-width:700px)');
|
const smallScreen = useMediaQuery('(max-width:700px)');
|
||||||
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStrategies();
|
fetchStrategies();
|
||||||
@ -32,7 +33,7 @@ const StrategiesList = ({
|
|||||||
|
|
||||||
const headerButton = () => (
|
const headerButton = () => (
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission(CREATE_STRATEGY)}
|
condition={hasAccess(CREATE_STRATEGY)}
|
||||||
show={
|
show={
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={smallScreen}
|
condition={smallScreen}
|
||||||
@ -133,7 +134,7 @@ const StrategiesList = ({
|
|||||||
show={reactivateButton(strategy)}
|
show={reactivateButton(strategy)}
|
||||||
elseShow={deprecateButton(strategy)}
|
elseShow={deprecateButton(strategy)}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender condition={hasPermission(DELETE_STRATEGY)} show={deleteButton(strategy)} />
|
<ConditionallyRender condition={hasAccess(DELETE_STRATEGY)} show={deleteButton(strategy)} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -157,7 +158,6 @@ StrategiesList.propTypes = {
|
|||||||
deprecateStrategy: PropTypes.func.isRequired,
|
deprecateStrategy: PropTypes.func.isRequired,
|
||||||
reactivateStrategy: PropTypes.func.isRequired,
|
reactivateStrategy: PropTypes.func.isRequired,
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
deprecated: PropTypes.bool,
|
deprecated: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
@ -6,14 +6,12 @@ import {
|
|||||||
deprecateStrategy,
|
deprecateStrategy,
|
||||||
reactivateStrategy,
|
reactivateStrategy,
|
||||||
} from '../../../store/strategy/actions';
|
} 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')),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,121 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`renders correctly with one strategy 1`] = `
|
exports[`renders correctly with one strategy 1`] = `
|
||||||
|
<div
|
||||||
|
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="makeStyles-headerContainer-3"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="makeStyles-headerTitleContainer-7"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className=""
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
className="MuiTypography-root makeStyles-headerTitle-8 MuiTypography-h2"
|
||||||
|
>
|
||||||
|
Strategies
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="makeStyles-headerActions-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="makeStyles-bodyContainer-4"
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
className="MuiList-root MuiList-padding"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
className="MuiListItem-root makeStyles-listItem-1 MuiListItem-gutters"
|
||||||
|
disabled={false}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="MuiListItemAvatar-root"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden={true}
|
||||||
|
className="material-icons MuiIcon-root"
|
||||||
|
>
|
||||||
|
extension
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="MuiListItemText-root MuiListItemText-multiline"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="/strategies/view/Another"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<strong>
|
||||||
|
Another
|
||||||
|
</strong>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<p
|
||||||
|
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
||||||
|
>
|
||||||
|
another's description
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
aria-describedby={null}
|
||||||
|
className=""
|
||||||
|
onBlur={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseOver={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
title="Deprecate activation strategy"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="MuiButtonBase-root MuiIconButton-root"
|
||||||
|
disabled={false}
|
||||||
|
onBlur={[Function]}
|
||||||
|
onClick={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiIconButton-label"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden={true}
|
||||||
|
className="material-icons MuiIcon-root"
|
||||||
|
>
|
||||||
|
visibility_off
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`renders correctly with one strategy without permissions 1`] = `
|
||||||
<div
|
<div
|
||||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
>
|
>
|
||||||
@ -182,118 +297,3 @@ exports[`renders correctly with one strategy 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`renders correctly with one strategy without permissions 1`] = `
|
|
||||||
<div
|
|
||||||
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="makeStyles-headerContainer-3"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="makeStyles-headerTitleContainer-7"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className=""
|
|
||||||
>
|
|
||||||
<h2
|
|
||||||
className="MuiTypography-root makeStyles-headerTitle-8 MuiTypography-h2"
|
|
||||||
>
|
|
||||||
Strategies
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="makeStyles-headerActions-9"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="makeStyles-bodyContainer-4"
|
|
||||||
>
|
|
||||||
<ul
|
|
||||||
className="MuiList-root MuiList-padding"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
className="MuiListItem-root makeStyles-listItem-1 MuiListItem-gutters"
|
|
||||||
disabled={false}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="MuiListItemAvatar-root"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden={true}
|
|
||||||
className="material-icons MuiIcon-root"
|
|
||||||
>
|
|
||||||
extension
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="MuiListItemText-root MuiListItemText-multiline"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="/strategies/view/Another"
|
|
||||||
onClick={[Function]}
|
|
||||||
>
|
|
||||||
<strong>
|
|
||||||
Another
|
|
||||||
</strong>
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<p
|
|
||||||
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
|
||||||
>
|
|
||||||
another's description
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
aria-describedby={null}
|
|
||||||
className=""
|
|
||||||
onBlur={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
onMouseLeave={[Function]}
|
|
||||||
onMouseOver={[Function]}
|
|
||||||
onTouchEnd={[Function]}
|
|
||||||
onTouchStart={[Function]}
|
|
||||||
title="Deprecate activation strategy"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="MuiButtonBase-root MuiIconButton-root"
|
|
||||||
disabled={false}
|
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onDragLeave={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
onKeyUp={[Function]}
|
|
||||||
onMouseDown={[Function]}
|
|
||||||
onMouseLeave={[Function]}
|
|
||||||
onMouseUp={[Function]}
|
|
||||||
onTouchEnd={[Function]}
|
|
||||||
onTouchMove={[Function]}
|
|
||||||
onTouchStart={[Function]}
|
|
||||||
tabIndex={0}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="MuiIconButton-label"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden={true}
|
|
||||||
className="material-icons MuiIcon-root"
|
|
||||||
>
|
|
||||||
visibility_off
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="MuiTouchRipple-root"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
@ -35,294 +35,187 @@ exports[`renders correctly with one strategy 1`] = `
|
|||||||
>
|
>
|
||||||
another's description
|
another's description
|
||||||
</h6>
|
</h6>
|
||||||
<div>
|
<section>
|
||||||
<div
|
<div
|
||||||
className="MuiPaper-root makeStyles-tabNav-8 MuiPaper-elevation1 MuiPaper-rounded"
|
className="content"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="MuiTabs-root"
|
className="listcontainer"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="MuiTabs-scroller MuiTabs-fixed"
|
className="MuiGrid-root MuiGrid-container"
|
||||||
onScroll={[Function]}
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"marginBottom": null,
|
|
||||||
"overflow": "hidden",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="MuiTabs-flexContainer MuiTabs-centered"
|
className="MuiGrid-root MuiGrid-item MuiGrid-grid-sm-12 MuiGrid-grid-md-12"
|
||||||
onKeyDown={[Function]}
|
|
||||||
role="tablist"
|
|
||||||
>
|
>
|
||||||
<button
|
<h6>
|
||||||
aria-controls="tabpanel-0"
|
Parameters
|
||||||
aria-selected={true}
|
</h6>
|
||||||
className="MuiButtonBase-root MuiTab-root MuiTab-textColorPrimary Mui-selected"
|
<hr />
|
||||||
disabled={false}
|
<ul
|
||||||
id="tab-0"
|
className="MuiList-root MuiList-padding"
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onDragLeave={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
onKeyUp={[Function]}
|
|
||||||
onMouseDown={[Function]}
|
|
||||||
onMouseLeave={[Function]}
|
|
||||||
onMouseUp={[Function]}
|
|
||||||
onTouchEnd={[Function]}
|
|
||||||
onTouchMove={[Function]}
|
|
||||||
onTouchStart={[Function]}
|
|
||||||
role="tab"
|
|
||||||
tabIndex={0}
|
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<span
|
<li
|
||||||
className="MuiTab-wrapper"
|
className="MuiListItem-root MuiListItem-gutters"
|
||||||
|
disabled={false}
|
||||||
>
|
>
|
||||||
Details
|
<div
|
||||||
</span>
|
aria-describedby={null}
|
||||||
<span
|
className="MuiListItemAvatar-root"
|
||||||
className="MuiTouchRipple-root"
|
onBlur={[Function]}
|
||||||
/>
|
onFocus={[Function]}
|
||||||
</button>
|
onMouseLeave={[Function]}
|
||||||
<button
|
onMouseOver={[Function]}
|
||||||
aria-controls="tabpanel-1"
|
onTouchEnd={[Function]}
|
||||||
aria-selected={false}
|
onTouchStart={[Function]}
|
||||||
className="MuiButtonBase-root MuiTab-root MuiTab-textColorPrimary"
|
title="Required"
|
||||||
disabled={false}
|
|
||||||
id="tab-1"
|
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onDragLeave={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
onKeyUp={[Function]}
|
|
||||||
onMouseDown={[Function]}
|
|
||||||
onMouseLeave={[Function]}
|
|
||||||
onMouseUp={[Function]}
|
|
||||||
onTouchEnd={[Function]}
|
|
||||||
onTouchMove={[Function]}
|
|
||||||
onTouchStart={[Function]}
|
|
||||||
role="tab"
|
|
||||||
tabIndex={-1}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="MuiTab-wrapper"
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="MuiTouchRipple-root"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className="PrivateTabIndicator-root-9 PrivateTabIndicator-colorPrimary-10 MuiTabs-indicator"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"left": 0,
|
|
||||||
"width": 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
aria-labelledby="wrapped-tab-0"
|
|
||||||
hidden={false}
|
|
||||||
id="wrapped-tabpanel-0"
|
|
||||||
role="tabpanel"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="listcontainer"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="MuiGrid-root MuiGrid-container"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="MuiGrid-root MuiGrid-item MuiGrid-grid-sm-12 MuiGrid-grid-md-12"
|
|
||||||
>
|
|
||||||
<h6>
|
|
||||||
Parameters
|
|
||||||
</h6>
|
|
||||||
<hr />
|
|
||||||
<ul
|
|
||||||
className="MuiList-root MuiList-padding"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
className="MuiListItem-root MuiListItem-gutters"
|
|
||||||
disabled={false}
|
|
||||||
>
|
>
|
||||||
<div
|
<span
|
||||||
aria-describedby={null}
|
aria-hidden={true}
|
||||||
className="MuiListItemAvatar-root"
|
className="material-icons MuiIcon-root"
|
||||||
onBlur={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
onMouseLeave={[Function]}
|
|
||||||
onMouseOver={[Function]}
|
|
||||||
onTouchEnd={[Function]}
|
|
||||||
onTouchStart={[Function]}
|
|
||||||
title="Required"
|
|
||||||
>
|
>
|
||||||
<span
|
add
|
||||||
aria-hidden={true}
|
</span>
|
||||||
className="material-icons MuiIcon-root"
|
</div>
|
||||||
>
|
<div
|
||||||
add
|
className="MuiListItemText-root MuiListItemText-multiline"
|
||||||
</span>
|
>
|
||||||
</div>
|
<span
|
||||||
<div
|
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
className="MuiListItemText-root MuiListItemText-multiline"
|
|
||||||
>
|
>
|
||||||
<span
|
<div>
|
||||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
customParam
|
||||||
>
|
|
||||||
<div>
|
|
||||||
customParam
|
|
||||||
|
|
||||||
<small>
|
<small>
|
||||||
(
|
(
|
||||||
list
|
list
|
||||||
)
|
)
|
||||||
</small>
|
</small>
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
<p
|
|
||||||
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
|
||||||
>
|
|
||||||
customList
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="MuiGrid-root MuiGrid-item MuiGrid-grid-sm-12 MuiGrid-grid-md-6"
|
|
||||||
>
|
|
||||||
<h6>
|
|
||||||
Applications using this strategy
|
|
||||||
</h6>
|
|
||||||
<hr />
|
|
||||||
<ul
|
|
||||||
className="MuiList-root MuiList-padding"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
className="MuiListItem-root listItem MuiListItem-gutters"
|
|
||||||
disabled={false}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="MuiListItemAvatar-root"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden={true}
|
|
||||||
className="material-icons MuiIcon-root"
|
|
||||||
>
|
|
||||||
apps
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</span>
|
||||||
<div
|
<p
|
||||||
className="MuiListItemText-root MuiListItemText-multiline"
|
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
||||||
>
|
>
|
||||||
<span
|
customList
|
||||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
</p>
|
||||||
>
|
</div>
|
||||||
<a
|
</li>
|
||||||
className="listLink truncate"
|
</ul>
|
||||||
href="/applications/appA"
|
</div>
|
||||||
onClick={[Function]}
|
<div
|
||||||
>
|
className="MuiGrid-root MuiGrid-item MuiGrid-grid-sm-12 MuiGrid-grid-md-6"
|
||||||
appA
|
>
|
||||||
</a>
|
<h6>
|
||||||
</span>
|
Applications using this strategy
|
||||||
<p
|
</h6>
|
||||||
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
<hr />
|
||||||
>
|
<ul
|
||||||
app description
|
className="MuiList-root MuiList-padding"
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="MuiGrid-root MuiGrid-item MuiGrid-grid-sm-12 MuiGrid-grid-md-6"
|
|
||||||
>
|
>
|
||||||
<h6>
|
<li
|
||||||
Toggles using this strategy
|
className="MuiListItem-root listItem MuiListItem-gutters"
|
||||||
</h6>
|
disabled={false}
|
||||||
<hr />
|
|
||||||
<ul
|
|
||||||
className="MuiList-root truncate MuiList-padding"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"textAlign": "left",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<li
|
<div
|
||||||
className="MuiListItem-root MuiListItem-gutters"
|
className="MuiListItemAvatar-root"
|
||||||
disabled={false}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-describedby={null}
|
className="MuiAvatar-root MuiAvatar-circle MuiAvatar-colorDefault"
|
||||||
className="MuiListItemAvatar-root"
|
|
||||||
onBlur={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
onMouseLeave={[Function]}
|
|
||||||
onMouseOver={[Function]}
|
|
||||||
onTouchEnd={[Function]}
|
|
||||||
onTouchStart={[Function]}
|
|
||||||
title="Disabled"
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
className="material-icons MuiIcon-root"
|
className="material-icons MuiIcon-root"
|
||||||
>
|
>
|
||||||
pause
|
apps
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</div>
|
||||||
className="MuiListItemText-root MuiListItemText-multiline"
|
<div
|
||||||
|
className="MuiListItemText-root MuiListItemText-multiline"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
>
|
>
|
||||||
<span
|
<a
|
||||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
className="listLink truncate"
|
||||||
|
href="/applications/appA"
|
||||||
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<a
|
appA
|
||||||
href="/features/view/toggleA"
|
</a>
|
||||||
onClick={[Function]}
|
</span>
|
||||||
>
|
<p
|
||||||
toggleA
|
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
||||||
</a>
|
>
|
||||||
</span>
|
app description
|
||||||
<p
|
</p>
|
||||||
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="MuiGrid-root MuiGrid-item MuiGrid-grid-sm-12 MuiGrid-grid-md-6"
|
||||||
|
>
|
||||||
|
<h6>
|
||||||
|
Toggles using this strategy
|
||||||
|
</h6>
|
||||||
|
<hr />
|
||||||
|
<ul
|
||||||
|
className="MuiList-root truncate MuiList-padding"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"textAlign": "left",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
className="MuiListItem-root MuiListItem-gutters"
|
||||||
|
disabled={false}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-describedby={null}
|
||||||
|
className="MuiListItemAvatar-root"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseOver={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
title="Disabled"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden={true}
|
||||||
|
className="material-icons MuiIcon-root"
|
||||||
|
>
|
||||||
|
pause
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="MuiListItemText-root MuiListItemText-multiline"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="/features/view/toggleA"
|
||||||
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
toggle description
|
toggleA
|
||||||
</p>
|
</a>
|
||||||
</div>
|
</span>
|
||||||
</li>
|
<p
|
||||||
</ul>
|
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
||||||
</div>
|
>
|
||||||
|
toggle description
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
aria-labelledby="wrapped-tab-1"
|
|
||||||
hidden={true}
|
|
||||||
id="wrapped-tabpanel-1"
|
|
||||||
role="tabpanel"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,8 +4,10 @@ import { ThemeProvider } from '@material-ui/core';
|
|||||||
|
|
||||||
import StrategiesListComponent from '../StrategiesList/StrategiesList';
|
import StrategiesListComponent from '../StrategiesList/StrategiesList';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import { CREATE_STRATEGY, DELETE_STRATEGY } from '../../../permissions';
|
|
||||||
import theme from '../../../themes/main-theme';
|
import theme from '../../../themes/main-theme';
|
||||||
|
import AccessProvider from '../../AccessProvider/AccessProvider';
|
||||||
|
import { createFakeStore } from '../../../accessStoreFake';
|
||||||
|
import { ADMIN } from '../../AccessProvider/permissions';
|
||||||
|
|
||||||
test('renders correctly with one strategy', () => {
|
test('renders correctly with one strategy', () => {
|
||||||
const strategy = {
|
const strategy = {
|
||||||
@ -15,15 +17,16 @@ test('renders correctly with one strategy', () => {
|
|||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<StrategiesListComponent
|
<AccessProvider store={createFakeStore()}>
|
||||||
strategies={[strategy]}
|
<StrategiesListComponent
|
||||||
fetchStrategies={jest.fn()}
|
strategies={[strategy]}
|
||||||
removeStrategy={jest.fn()}
|
fetchStrategies={jest.fn()}
|
||||||
deprecateStrategy={jest.fn()}
|
removeStrategy={jest.fn()}
|
||||||
reactivateStrategy={jest.fn()}
|
deprecateStrategy={jest.fn()}
|
||||||
history={{}}
|
reactivateStrategy={jest.fn()}
|
||||||
hasPermission={permission => [CREATE_STRATEGY, DELETE_STRATEGY].indexOf(permission) !== -1}
|
history={{}}
|
||||||
/>
|
/>
|
||||||
|
</AccessProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@ -39,6 +42,7 @@ test('renders correctly with one strategy without permissions', () => {
|
|||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
|
<AccessProvider store={createFakeStore([{permission: ADMIN}])}>
|
||||||
<StrategiesListComponent
|
<StrategiesListComponent
|
||||||
strategies={[strategy]}
|
strategies={[strategy]}
|
||||||
fetchStrategies={jest.fn()}
|
fetchStrategies={jest.fn()}
|
||||||
@ -46,8 +50,8 @@ test('renders correctly with one strategy without permissions', () => {
|
|||||||
deprecateStrategy={jest.fn()}
|
deprecateStrategy={jest.fn()}
|
||||||
reactivateStrategy={jest.fn()}
|
reactivateStrategy={jest.fn()}
|
||||||
history={{}}
|
history={{}}
|
||||||
hasPermission={() => false}
|
|
||||||
/>
|
/>
|
||||||
|
</AccessProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
@ -2,9 +2,10 @@ import React from 'react';
|
|||||||
import { ThemeProvider } from '@material-ui/core';
|
import { ThemeProvider } from '@material-ui/core';
|
||||||
import StrategyDetails from '../strategy-details-component';
|
import StrategyDetails from '../strategy-details-component';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import { UPDATE_STRATEGY } from '../../../permissions';
|
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import theme from '../../../themes/main-theme';
|
import theme from '../../../themes/main-theme';
|
||||||
|
import { createFakeStore } from '../../../accessStoreFake';
|
||||||
|
import AccessProvider from '../../AccessProvider/AccessProvider';
|
||||||
|
|
||||||
test('renders correctly with one strategy', () => {
|
test('renders correctly with one strategy', () => {
|
||||||
const strategy = {
|
const strategy = {
|
||||||
@ -34,20 +35,21 @@ test('renders correctly with one strategy', () => {
|
|||||||
];
|
];
|
||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
|
<AccessProvider store={createFakeStore()}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<StrategyDetails
|
<StrategyDetails
|
||||||
strategyName={'Another'}
|
strategyName={'Another'}
|
||||||
strategy={strategy}
|
strategy={strategy}
|
||||||
activeTab="view"
|
activeTab="view"
|
||||||
applications={applications}
|
applications={applications}
|
||||||
toggles={toggles}
|
toggles={toggles}
|
||||||
fetchStrategies={jest.fn()}
|
fetchStrategies={jest.fn()}
|
||||||
fetchApplications={jest.fn()}
|
fetchApplications={jest.fn()}
|
||||||
fetchFeatureToggles={jest.fn()}
|
fetchFeatureToggles={jest.fn()}
|
||||||
history={{}}
|
history={{}}
|
||||||
hasPermission={permission => [UPDATE_STRATEGY].indexOf(permission) !== -1}
|
/>
|
||||||
/>
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</AccessProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -3,12 +3,15 @@ import PropTypes from 'prop-types';
|
|||||||
import { Grid, Typography } from '@material-ui/core';
|
import { Grid, Typography } from '@material-ui/core';
|
||||||
import ShowStrategy from './show-strategy-component';
|
import ShowStrategy from './show-strategy-component';
|
||||||
import EditStrategy from './form-container';
|
import EditStrategy from './form-container';
|
||||||
import { UPDATE_STRATEGY } from '../../permissions';
|
import { UPDATE_STRATEGY } from '../AccessProvider/permissions';
|
||||||
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender';
|
||||||
import TabNav from '../common/TabNav/TabNav';
|
import TabNav from '../common/TabNav/TabNav';
|
||||||
import PageContent from '../common/PageContent/PageContent';
|
import PageContent from '../common/PageContent/PageContent';
|
||||||
|
import AccessContext from '../../contexts/AccessContext';
|
||||||
|
|
||||||
export default class StrategyDetails extends Component {
|
export default class StrategyDetails extends Component {
|
||||||
|
static contextType = AccessContext;
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
strategyName: PropTypes.string.isRequired,
|
strategyName: PropTypes.string.isRequired,
|
||||||
toggles: PropTypes.array,
|
toggles: PropTypes.array,
|
||||||
@ -19,7 +22,6 @@ 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() {
|
||||||
@ -52,13 +54,16 @@ export default class StrategyDetails extends Component {
|
|||||||
component: <EditStrategy strategy={this.props.strategy} history={this.props.history} editMode />,
|
component: <EditStrategy strategy={this.props.strategy} history={this.props.history} editMode />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const { hasAccess } = this.context;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent headerContent={strategy.name}>
|
<PageContent headerContent={strategy.name}>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item xs={12} sm={12}>
|
<Grid item xs={12} sm={12}>
|
||||||
<Typography variant="subtitle1">{strategy.description}</Typography>
|
<Typography variant="subtitle1">{strategy.description}</Typography>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={strategy.editable && this.props.hasPermission(UPDATE_STRATEGY)}
|
condition={strategy.editable && hasAccess(UPDATE_STRATEGY)}
|
||||||
show={
|
show={
|
||||||
<div>
|
<div>
|
||||||
<TabNav tabData={tabData} />
|
<TabNav tabData={tabData} />
|
||||||
|
@ -3,7 +3,6 @@ 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-toggle/actions';
|
import { fetchFeatureToggles } from './../../store/feature-toggle/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);
|
||||||
@ -22,7 +21,6 @@ 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')),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
@ -16,17 +16,20 @@ import {
|
|||||||
import HeaderTitle from '../../common/HeaderTitle';
|
import HeaderTitle from '../../common/HeaderTitle';
|
||||||
import PageContent from '../../common/PageContent/PageContent';
|
import PageContent from '../../common/PageContent/PageContent';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
import { CREATE_TAG_TYPE, DELETE_TAG_TYPE } from '../../../permissions';
|
import { CREATE_TAG_TYPE, DELETE_TAG_TYPE } from '../../AccessProvider/permissions';
|
||||||
import Dialogue from '../../common/Dialogue/Dialogue';
|
import Dialogue from '../../common/Dialogue/Dialogue';
|
||||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||||
|
|
||||||
import styles from '../TagType.module.scss';
|
import styles from '../TagType.module.scss';
|
||||||
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
|
||||||
const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType, hasPermission }) => {
|
const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => {
|
||||||
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const [deletion, setDeletion] = useState({ open: false });
|
const [deletion, setDeletion] = useState({ open: false });
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const smallScreen = useMediaQuery('(max-width:700px)');
|
const smallScreen = useMediaQuery('(max-width:700px)');
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTagTypes();
|
fetchTagTypes();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@ -37,7 +40,7 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType, hasPermission })
|
|||||||
title="Tag Types"
|
title="Tag Types"
|
||||||
actions={
|
actions={
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission(CREATE_TAG_TYPE)}
|
condition={hasAccess(CREATE_TAG_TYPE)}
|
||||||
show={
|
show={
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={smallScreen}
|
condition={smallScreen}
|
||||||
@ -93,7 +96,7 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType, hasPermission })
|
|||||||
<Icon>label</Icon>
|
<Icon>label</Icon>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary={link} secondary={tagType.description} />
|
<ListItemText primary={link} secondary={tagType.description} />
|
||||||
<ConditionallyRender condition={hasPermission(DELETE_TAG_TYPE)} show={deleteButton} />
|
<ConditionallyRender condition={hasAccess(DELETE_TAG_TYPE)} show={deleteButton} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -127,7 +130,6 @@ TagTypeList.propTypes = {
|
|||||||
tagTypes: PropTypes.array.isRequired,
|
tagTypes: PropTypes.array.isRequired,
|
||||||
fetchTagTypes: PropTypes.func.isRequired,
|
fetchTagTypes: PropTypes.func.isRequired,
|
||||||
removeTagType: PropTypes.func.isRequired,
|
removeTagType: PropTypes.func.isRequired,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TagTypeList;
|
export default TagTypeList;
|
||||||
|
@ -15,7 +15,6 @@ test('renders correctly for creating', () => {
|
|||||||
title="Add tag type"
|
title="Add tag type"
|
||||||
createTagType={jest.fn()}
|
createTagType={jest.fn()}
|
||||||
validateName={() => Promise.resolve(true)}
|
validateName={() => Promise.resolve(true)}
|
||||||
hasPermission={() => true}
|
|
||||||
tagType={{ name: '', description: '', icon: '' }}
|
tagType={{ name: '', description: '', icon: '' }}
|
||||||
editMode={false}
|
editMode={false}
|
||||||
submit={jest.fn()}
|
submit={jest.fn()}
|
||||||
@ -35,7 +34,6 @@ test('it supports editMode', () => {
|
|||||||
title="Add tag type"
|
title="Add tag type"
|
||||||
createTagType={jest.fn()}
|
createTagType={jest.fn()}
|
||||||
validateName={() => Promise.resolve(true)}
|
validateName={() => Promise.resolve(true)}
|
||||||
hasPermission={() => true}
|
|
||||||
tagType={{ name: '', description: '', icon: '' }}
|
tagType={{ name: '', description: '', icon: '' }}
|
||||||
editMode
|
editMode
|
||||||
submit={jest.fn()}
|
submit={jest.fn()}
|
||||||
|
@ -5,18 +5,25 @@ import renderer from 'react-test-renderer';
|
|||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { ThemeProvider } from '@material-ui/styles';
|
import { ThemeProvider } from '@material-ui/styles';
|
||||||
import theme from '../../../themes/main-theme';
|
import theme from '../../../themes/main-theme';
|
||||||
|
import { createFakeStore } from '../../../accessStoreFake';
|
||||||
|
import AccessProvider from '../../AccessProvider/AccessProvider';
|
||||||
|
|
||||||
|
import { ADMIN, CREATE_TAG_TYPE, UPDATE_TAG_TYPE, DELETE_TAG_TYPE } from '../../AccessProvider/permissions';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
test('renders an empty list correctly', () => {
|
test('renders an empty list correctly', () => {
|
||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<TagTypesList
|
<AccessProvider store={createFakeStore([{permission: ADMIN }])}>
|
||||||
tagTypes={[]}
|
<TagTypesList
|
||||||
fetchTagTypes={jest.fn()}
|
tagTypes={[]}
|
||||||
removeTagType={jest.fn()}
|
fetchTagTypes={jest.fn()}
|
||||||
history={{}}
|
removeTagType={jest.fn()}
|
||||||
hasPermission={() => true}
|
history={{}}
|
||||||
/>
|
/>
|
||||||
|
</AccessProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@ -27,19 +34,24 @@ test('renders a list with elements correctly', () => {
|
|||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<TagTypesList
|
<AccessProvider store={createFakeStore([
|
||||||
tagTypes={[
|
{permission: CREATE_TAG_TYPE },
|
||||||
{
|
{permission: UPDATE_TAG_TYPE },
|
||||||
name: 'simple',
|
{permission: DELETE_TAG_TYPE }
|
||||||
description: 'Some simple description',
|
])}>
|
||||||
icon: '#',
|
<TagTypesList
|
||||||
},
|
tagTypes={[
|
||||||
]}
|
{
|
||||||
fetchTagTypes={jest.fn()}
|
name: 'simple',
|
||||||
removeTagType={jest.fn()}
|
description: 'Some simple description',
|
||||||
history={{}}
|
icon: '#',
|
||||||
hasPermission={() => true}
|
},
|
||||||
/>
|
]}
|
||||||
|
fetchTagTypes={jest.fn()}
|
||||||
|
removeTagType={jest.fn()}
|
||||||
|
history={{}}
|
||||||
|
/>
|
||||||
|
</AccessProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import TagTypesListComponent from './TagTypeList';
|
import TagTypesListComponent from './TagTypeList';
|
||||||
import { fetchTagTypes, removeTagType } from '../../store/tag-type/actions';
|
import { fetchTagTypes, removeTagType } from '../../store/tag-type/actions';
|
||||||
import { hasPermission } from '../../permissions';
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const list = state.tagTypes.toJS();
|
const list = state.tagTypes.toJS();
|
||||||
return {
|
return {
|
||||||
tagTypes: list,
|
tagTypes: list,
|
||||||
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,20 +1,22 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { Button, Icon, IconButton, List, ListItem, ListItemIcon, ListItemText, Tooltip } from '@material-ui/core';
|
import { Button, Icon, IconButton, List, ListItem, ListItemIcon, ListItemText, Tooltip } from '@material-ui/core';
|
||||||
import { CREATE_TAG, DELETE_TAG } from '../../../permissions';
|
import { CREATE_TAG, DELETE_TAG } from '../../AccessProvider/permissions';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
import HeaderTitle from '../../common/HeaderTitle';
|
import HeaderTitle from '../../common/HeaderTitle';
|
||||||
import PageContent from '../../common/PageContent/PageContent';
|
import PageContent from '../../common/PageContent/PageContent';
|
||||||
|
|
||||||
import { useStyles } from './TagList.styles';
|
import { useStyles } from './TagList.styles';
|
||||||
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
|
||||||
const TagList = ({ tags, fetchTags, removeTag, hasPermission }) => {
|
const TagList = ({ tags, fetchTags, removeTag }) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const smallScreen = useMediaQuery('(max-width:700px)');
|
const smallScreen = useMediaQuery('(max-width:700px)');
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTags();
|
fetchTags();
|
||||||
@ -33,7 +35,7 @@ const TagList = ({ tags, fetchTags, removeTag, hasPermission }) => {
|
|||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary={tag.value} secondary={tag.type} />
|
<ListItemText primary={tag.value} secondary={tag.type} />
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission(DELETE_TAG)}
|
condition={hasAccess(DELETE_TAG)}
|
||||||
show={<DeleteButton tagType={tag.type} tagValue={tag.value} />}
|
show={<DeleteButton tagType={tag.type} tagValue={tag.value} />}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@ -52,9 +54,9 @@ const TagList = ({ tags, fetchTags, removeTag, hasPermission }) => {
|
|||||||
tagValue: PropTypes.string,
|
tagValue: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddButton = ({ hasPermission }) => (
|
const AddButton = ({ hasAccess }) => (
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission(CREATE_TAG)}
|
condition={hasAccess(CREATE_TAG)}
|
||||||
show={
|
show={
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={smallScreen}
|
condition={smallScreen}
|
||||||
@ -80,7 +82,7 @@ const TagList = ({ tags, fetchTags, removeTag, hasPermission }) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<PageContent headerContent={<HeaderTitle title="Tags" actions={<AddButton hasPermission={hasPermission} />} />}>
|
<PageContent headerContent={<HeaderTitle title="Tags" actions={<AddButton hasAccess={hasAccess} />} />}>
|
||||||
<List>
|
<List>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={tags.length > 0}
|
condition={tags.length > 0}
|
||||||
@ -100,7 +102,6 @@ TagList.propTypes = {
|
|||||||
tags: PropTypes.array.isRequired,
|
tags: PropTypes.array.isRequired,
|
||||||
fetchTags: PropTypes.func.isRequired,
|
fetchTags: PropTypes.func.isRequired,
|
||||||
removeTag: PropTypes.func.isRequired,
|
removeTag: PropTypes.func.isRequired,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TagList;
|
export default TagList;
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import TagsListComponent from './TagList';
|
import TagsListComponent from './TagList';
|
||||||
import { fetchTags, removeTag } from '../../store/tag/actions';
|
import { fetchTags, removeTag } from '../../store/tag/actions';
|
||||||
import { hasPermission } from '../../permissions';
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const list = state.tags.toJS();
|
const list = state.tags.toJS();
|
||||||
return {
|
return {
|
||||||
tags: list,
|
tags: list,
|
||||||
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
5
frontend/src/contexts/AccessContext.js
Normal file
5
frontend/src/contexts/AccessContext.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const AccessContext = React.createContext()
|
||||||
|
|
||||||
|
export default AccessContext;
|
@ -16,6 +16,7 @@ import MetricsPoller from './metrics-poller';
|
|||||||
import App from './component/App';
|
import App from './component/App';
|
||||||
import ScrollToTop from './component/scroll-to-top';
|
import ScrollToTop from './component/scroll-to-top';
|
||||||
import { writeWarning } from './security-logger';
|
import { writeWarning } from './security-logger';
|
||||||
|
import AccessProvider from './component/AccessProvider/AccessProvider';
|
||||||
|
|
||||||
let composeEnhancers;
|
let composeEnhancers;
|
||||||
|
|
||||||
@ -38,16 +39,18 @@ metricsPoller.start();
|
|||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider store={unleashStore}>
|
<Provider store={unleashStore}>
|
||||||
<HashRouter>
|
<AccessProvider store={unleashStore}>
|
||||||
<ThemeProvider theme={mainTheme}>
|
<HashRouter>
|
||||||
<StylesProvider injectFirst>
|
<ThemeProvider theme={mainTheme}>
|
||||||
<CssBaseline />
|
<StylesProvider injectFirst>
|
||||||
<ScrollToTop>
|
<CssBaseline />
|
||||||
<Route path="/" component={App} />
|
<ScrollToTop>
|
||||||
</ScrollToTop>
|
<Route path="/" component={App} />
|
||||||
</StylesProvider>
|
</ScrollToTop>
|
||||||
</ThemeProvider>
|
</StylesProvider>
|
||||||
</HashRouter>
|
</ThemeProvider>
|
||||||
|
</HashRouter>
|
||||||
|
</AccessProvider>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
document.getElementById('app')
|
document.getElementById('app')
|
||||||
);
|
);
|
||||||
|
@ -2,14 +2,11 @@ import { connect } from 'react-redux';
|
|||||||
|
|
||||||
import Component from './api-key-list';
|
import Component from './api-key-list';
|
||||||
import { fetchApiKeys, removeKey, addKey } from './../../../store/e-api-admin/actions';
|
import { fetchApiKeys, removeKey, addKey } from './../../../store/e-api-admin/actions';
|
||||||
import { hasPermission } from '../../../permissions';
|
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
state => ({
|
state => ({
|
||||||
location: state.settings.toJS().location || {},
|
location: state.settings.toJS().location || {},
|
||||||
unleashUrl: state.uiConfig.toJS().unleashUrl,
|
unleashUrl: state.uiConfig.toJS().unleashUrl,
|
||||||
keys: state.apiAdmin.toJS(),
|
keys: state.apiAdmin.toJS(),
|
||||||
hasPermission: permission => hasPermission(state.user.get('profile'), permission),
|
|
||||||
}),
|
}),
|
||||||
{ fetchApiKeys, removeKey, addKey }
|
{ fetchApiKeys, removeKey, addKey }
|
||||||
)(Component);
|
)(Component);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable no-alert */
|
/* eslint-disable no-alert */
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Icon, Table, TableHead, TableBody, TableRow, TableCell, IconButton } from '@material-ui/core';
|
import { Icon, Table, TableHead, TableBody, TableRow, TableCell, IconButton } from '@material-ui/core';
|
||||||
import { Alert } from '@material-ui/lab';
|
import { Alert } from '@material-ui/lab';
|
||||||
@ -8,8 +8,11 @@ import CreateApiKey from './api-key-create';
|
|||||||
import Secret from './secret';
|
import Secret from './secret';
|
||||||
import ConditionallyRender from '../../../component/common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../../component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import Dialogue from '../../../component/common/Dialogue/Dialogue';
|
import Dialogue from '../../../component/common/Dialogue/Dialogue';
|
||||||
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
import { DELETE_API_TOKEN, CREATE_API_TOKEN } from '../../../component/AccessProvider/permissions';
|
||||||
|
|
||||||
function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, hasPermission, unleashUrl }) {
|
function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, unleashUrl }) {
|
||||||
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const [showDelete, setShowDelete] = useState(false);
|
const [showDelete, setShowDelete] = useState(false);
|
||||||
const [delKey, setDelKey] = useState(undefined);
|
const [delKey, setDelKey] = useState(undefined);
|
||||||
const deleteKey = async () => {
|
const deleteKey = async () => {
|
||||||
@ -55,7 +58,7 @@ function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, hasPermis
|
|||||||
{keys.map(item => (
|
{keys.map(item => (
|
||||||
<TableRow key={item.secret}>
|
<TableRow key={item.secret}>
|
||||||
<TableCell style={{ textAlign: 'left' }}>
|
<TableCell style={{ textAlign: 'left' }}>
|
||||||
{formatFullDateTimeWithLocale(item.created, location.locale)}
|
{formatFullDateTimeWithLocale(item.createdAt, location.locale)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell style={{ textAlign: 'left' }}>{item.username}</TableCell>
|
<TableCell style={{ textAlign: 'left' }}>{item.username}</TableCell>
|
||||||
<TableCell style={{ textAlign: 'left' }}>{item.type}</TableCell>
|
<TableCell style={{ textAlign: 'left' }}>{item.type}</TableCell>
|
||||||
@ -63,7 +66,7 @@ function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, hasPermis
|
|||||||
<Secret value={item.secret} />
|
<Secret value={item.secret} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission('ADMIN')}
|
condition={hasAccess(DELETE_API_TOKEN)}
|
||||||
show={
|
show={
|
||||||
<TableCell style={{ textAlign: 'right' }}>
|
<TableCell style={{ textAlign: 'right' }}>
|
||||||
<IconButton
|
<IconButton
|
||||||
@ -81,23 +84,18 @@ function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, hasPermis
|
|||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
<ConditionallyRender
|
<Dialogue
|
||||||
condition={hasPermission('ADMIN')}
|
open={showDelete}
|
||||||
show={
|
onClick={deleteKey}
|
||||||
<Dialogue
|
onClose={() => {
|
||||||
open={showDelete}
|
setShowDelete(false);
|
||||||
onClick={deleteKey}
|
setDelKey(undefined);
|
||||||
onClose={() => {
|
}}
|
||||||
setShowDelete(false);
|
title="Really delete API key?"
|
||||||
setDelKey(undefined);
|
>
|
||||||
}}
|
<div>Are you sure you want to delete?</div>
|
||||||
title="Really delete API key?"
|
</Dialogue>
|
||||||
>
|
<ConditionallyRender condition={hasAccess(CREATE_API_TOKEN)} show={<CreateApiKey addKey={addKey} />} />
|
||||||
<div>Are you sure you want to delete?</div>
|
|
||||||
</Dialogue>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender condition={hasPermission('ADMIN')} show={<CreateApiKey addKey={addKey} />} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -109,7 +107,6 @@ ApiKeyList.propTypes = {
|
|||||||
addKey: PropTypes.func.isRequired,
|
addKey: PropTypes.func.isRequired,
|
||||||
keys: PropTypes.array.isRequired,
|
keys: PropTypes.array.isRequired,
|
||||||
unleashUrl: PropTypes.string,
|
unleashUrl: PropTypes.string,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ApiKeyList;
|
export default ApiKeyList;
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import GoogleAuth from './google-auth';
|
import GoogleAuth from './google-auth';
|
||||||
import { getGoogleConfig, updateGoogleConfig } from './../../../store/e-admin-auth/actions';
|
import { getGoogleConfig, updateGoogleConfig } from './../../../store/e-admin-auth/actions';
|
||||||
import { hasPermission } from '../../../permissions';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
config: state.authAdmin.get('google'),
|
config: state.authAdmin.get('google'),
|
||||||
unleashUrl: state.uiConfig.toJS().unleashUrl,
|
unleashUrl: state.uiConfig.toJS().unleashUrl,
|
||||||
hasPermission: permission => hasPermission(state.user.get('profile'), permission),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const Container = connect(mapStateToProps, { getGoogleConfig, updateGoogleConfig })(GoogleAuth);
|
const Container = connect(mapStateToProps, { getGoogleConfig, updateGoogleConfig })(GoogleAuth);
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useContext } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button, Grid, Switch, TextField } from '@material-ui/core';
|
import { Button, Grid, Switch, TextField } from '@material-ui/core';
|
||||||
import { Alert } from '@material-ui/lab';
|
import { Alert } from '@material-ui/lab';
|
||||||
import PageContent from '../../../component/common/PageContent/PageContent';
|
import PageContent from '../../../component/common/PageContent/PageContent';
|
||||||
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
import { ADMIN } from '../../../component/AccessProvider/permissions';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -10,9 +12,10 @@ const initialState = {
|
|||||||
unleashHostname: location.hostname,
|
unleashHostname: location.hostname,
|
||||||
};
|
};
|
||||||
|
|
||||||
function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission, unleashUrl }) {
|
function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, unleashUrl }) {
|
||||||
const [data, setData] = useState(initialState);
|
const [data, setData] = useState(initialState);
|
||||||
const [info, setInfo] = useState();
|
const [info, setInfo] = useState();
|
||||||
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getGoogleConfig();
|
getGoogleConfig();
|
||||||
@ -25,7 +28,7 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission
|
|||||||
}
|
}
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
if (!hasPermission('ADMIN')) {
|
if (!hasAccess(ADMIN)) {
|
||||||
return <span>You need admin privileges to access this section.</span>;
|
return <span>You need admin privileges to access this section.</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,7 +196,6 @@ GoogleAuth.propTypes = {
|
|||||||
unleashUrl: PropTypes.string,
|
unleashUrl: PropTypes.string,
|
||||||
getGoogleConfig: PropTypes.func.isRequired,
|
getGoogleConfig: PropTypes.func.isRequired,
|
||||||
updateGoogleConfig: PropTypes.func.isRequired,
|
updateGoogleConfig: PropTypes.func.isRequired,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GoogleAuth;
|
export default GoogleAuth;
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import component from './authentication';
|
import component from './authentication';
|
||||||
import { hasPermission } from '../../../permissions';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
authenticationType: state.uiConfig.toJS().authenticationType,
|
authenticationType: state.uiConfig.toJS().authenticationType,
|
||||||
hasPermission: permission => hasPermission(state.user.get('profile'), permission),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const Container = connect(mapStateToProps, { })(component);
|
const Container = connect(mapStateToProps, { })(component);
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import SamlAuth from './saml-auth';
|
import SamlAuth from './saml-auth';
|
||||||
import { getSamlConfig, updateSamlConfig } from './../../../store/e-admin-auth/actions';
|
import { getSamlConfig, updateSamlConfig } from './../../../store/e-admin-auth/actions';
|
||||||
import { hasPermission } from '../../../permissions';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
config: state.authAdmin.get('saml'),
|
config: state.authAdmin.get('saml'),
|
||||||
unleashUrl: state.uiConfig.toJS().unleashUrl,
|
unleashUrl: state.uiConfig.toJS().unleashUrl,
|
||||||
hasPermission: permission => hasPermission(state.user.get('profile'), permission),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const Container = connect(mapStateToProps, { getSamlConfig, updateSamlConfig })(SamlAuth);
|
const Container = connect(mapStateToProps, { getSamlConfig, updateSamlConfig })(SamlAuth);
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useContext } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button, Grid, Switch, TextField } from '@material-ui/core';
|
import { Button, Grid, Switch, TextField } from '@material-ui/core';
|
||||||
import { Alert } from '@material-ui/lab';
|
import { Alert } from '@material-ui/lab';
|
||||||
import PageContent from '../../../component/common/PageContent/PageContent';
|
import PageContent from '../../../component/common/PageContent/PageContent';
|
||||||
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
import { ADMIN } from '../../../component/AccessProvider/permissions';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -10,9 +12,10 @@ const initialState = {
|
|||||||
unleashHostname: location.hostname,
|
unleashHostname: location.hostname,
|
||||||
};
|
};
|
||||||
|
|
||||||
function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission, unleashUrl }) {
|
function SamlAuth({ config, getSamlConfig, updateSamlConfig, unleashUrl }) {
|
||||||
const [data, setData] = useState(initialState);
|
const [data, setData] = useState(initialState);
|
||||||
const [info, setInfo] = useState();
|
const [info, setInfo] = useState();
|
||||||
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getSamlConfig();
|
getSamlConfig();
|
||||||
@ -26,7 +29,7 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission, unle
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
if (!hasPermission('ADMIN')) {
|
if (!hasAccess(ADMIN)) {
|
||||||
return <Alert severity="error">You need to be a root admin to access this section.</Alert>;
|
return <Alert severity="error">You need to be a root admin to access this section.</Alert>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,7 +191,6 @@ SamlAuth.propTypes = {
|
|||||||
unleash: PropTypes.string,
|
unleash: PropTypes.string,
|
||||||
getSamlConfig: PropTypes.func.isRequired,
|
getSamlConfig: PropTypes.func.isRequired,
|
||||||
updateSamlConfig: PropTypes.func.isRequired,
|
updateSamlConfig: PropTypes.func.isRequired,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SamlAuth;
|
export default SamlAuth;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable no-alert */
|
/* eslint-disable no-alert */
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button, Icon, IconButton, Table, TableBody, TableCell, TableHead, TableRow, Avatar } from '@material-ui/core';
|
import { Button, Icon, IconButton, Table, TableBody, TableCell, TableHead, TableRow, Avatar } from '@material-ui/core';
|
||||||
import { formatDateWithLocale } from '../../../../component/common/util';
|
import { formatDateWithLocale } from '../../../../component/common/util';
|
||||||
@ -8,6 +8,8 @@ import ChangePassword from '../change-password-component';
|
|||||||
import UpdateUser from '../update-user-component';
|
import UpdateUser from '../update-user-component';
|
||||||
import DelUser from '../del-user-component';
|
import DelUser from '../del-user-component';
|
||||||
import ConditionallyRender from '../../../../component/common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../../../component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import AccessContext from '../../../../contexts/AccessContext';
|
||||||
|
import { ADMIN } from '../../../../component/AccessProvider/permissions';
|
||||||
|
|
||||||
function UsersList({
|
function UsersList({
|
||||||
roles,
|
roles,
|
||||||
@ -18,9 +20,9 @@ function UsersList({
|
|||||||
changePassword,
|
changePassword,
|
||||||
users,
|
users,
|
||||||
location,
|
location,
|
||||||
hasPermission,
|
|
||||||
validatePassword,
|
validatePassword,
|
||||||
}) {
|
}) {
|
||||||
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const [showDialog, setDialog] = useState(false);
|
const [showDialog, setDialog] = useState(false);
|
||||||
const [pwDialog, setPwDialog] = useState({ open: false });
|
const [pwDialog, setPwDialog] = useState({ open: false });
|
||||||
const [delDialog, setDelDialog] = useState(false);
|
const [delDialog, setDelDialog] = useState(false);
|
||||||
@ -83,7 +85,7 @@ function UsersList({
|
|||||||
<TableCell>Name</TableCell>
|
<TableCell>Name</TableCell>
|
||||||
<TableCell>Username</TableCell>
|
<TableCell>Username</TableCell>
|
||||||
<TableCell align="center">Role</TableCell>
|
<TableCell align="center">Role</TableCell>
|
||||||
<TableCell align="right">{hasPermission('ADMIN') ? 'Action' : ''}</TableCell>
|
<TableCell align="right">{hasAccess('ADMIN') ? 'Action' : ''}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@ -95,7 +97,7 @@ function UsersList({
|
|||||||
<TableCell style={{ textAlign: 'left' }}>{item.username || item.email}</TableCell>
|
<TableCell style={{ textAlign: 'left' }}>{item.username || item.email}</TableCell>
|
||||||
<TableCell align="center">{renderRole(item.rootRole)}</TableCell>
|
<TableCell align="center">{renderRole(item.rootRole)}</TableCell>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission('ADMIN')}
|
condition={hasAccess(ADMIN)}
|
||||||
show={
|
show={
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<IconButton aria-label="Edit" title="Edit" onClick={openUpdateDialog(item)}>
|
<IconButton aria-label="Edit" title="Edit" onClick={openUpdateDialog(item)}>
|
||||||
@ -117,7 +119,7 @@ function UsersList({
|
|||||||
</Table>
|
</Table>
|
||||||
<br />
|
<br />
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasPermission('ADMIN')}
|
condition={hasAccess(ADMIN)}
|
||||||
show={
|
show={
|
||||||
<Button variant="contained" color="primary" onClick={openDialog}>
|
<Button variant="contained" color="primary" onClick={openDialog}>
|
||||||
Add new user
|
Add new user
|
||||||
@ -168,7 +170,6 @@ UsersList.propTypes = {
|
|||||||
fetchUsers: PropTypes.func.isRequired,
|
fetchUsers: PropTypes.func.isRequired,
|
||||||
removeUser: PropTypes.func.isRequired,
|
removeUser: PropTypes.func.isRequired,
|
||||||
addUser: PropTypes.func.isRequired,
|
addUser: PropTypes.func.isRequired,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
|
||||||
validatePassword: PropTypes.func.isRequired,
|
validatePassword: PropTypes.func.isRequired,
|
||||||
updateUser: PropTypes.func.isRequired,
|
updateUser: PropTypes.func.isRequired,
|
||||||
changePassword: PropTypes.func.isRequired,
|
changePassword: PropTypes.func.isRequired,
|
||||||
|
@ -8,13 +8,11 @@ import {
|
|||||||
updateUser,
|
updateUser,
|
||||||
validatePassword,
|
validatePassword,
|
||||||
} from '../../../../store/e-user-admin/actions';
|
} from '../../../../store/e-user-admin/actions';
|
||||||
import { hasPermission } from '../../../../permissions';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
users: state.userAdmin.toJS(),
|
users: state.userAdmin.toJS(),
|
||||||
roles: state.roles.get('root').toJS() || [],
|
roles: state.roles.get('root').toJS() || [],
|
||||||
location: state.settings.toJS().location || {},
|
location: state.settings.toJS().location || {},
|
||||||
hasPermission: permission => hasPermission(state.user.get('profile'), permission),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const Container = connect(mapStateToProps, {
|
const Container = connect(mapStateToProps, {
|
||||||
|
@ -2,11 +2,12 @@ import { Map as $Map } from 'immutable';
|
|||||||
import { USER_CHANGE_CURRENT, USER_LOGOUT } from './actions';
|
import { USER_CHANGE_CURRENT, USER_LOGOUT } from './actions';
|
||||||
import { AUTH_REQUIRED } from '../util';
|
import { AUTH_REQUIRED } from '../util';
|
||||||
|
|
||||||
const userStore = (state = new $Map(), action) => {
|
const userStore = (state = new $Map({permissions: []}), action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case USER_CHANGE_CURRENT:
|
case USER_CHANGE_CURRENT:
|
||||||
state = state
|
state = state
|
||||||
.set('profile', action.value)
|
.set('profile', action.value.user)
|
||||||
|
.set('permissions', action.value.permissions || [])
|
||||||
.set('showDialog', false)
|
.set('showDialog', false)
|
||||||
.set('authDetails', undefined);
|
.set('authDetails', undefined);
|
||||||
return state;
|
return state;
|
||||||
|
Loading…
Reference in New Issue
Block a user