1
0
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:
Ivar Conradi Østhus 2021-04-20 19:13:31 +02:00 committed by GitHub
parent 3bf9bd73ae
commit f669f96d49
67 changed files with 742 additions and 715 deletions

View File

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

View File

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

View File

@ -0,0 +1,12 @@
import { Map as $MAp } from 'immutable';
export const createFakeStore = (permissions) => {
return {
getState: () => ({
user:
new $MAp({
permissions
})
}),
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}
/> />
&nbsp; &nbsp;
<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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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",
},
] ]
`; `;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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} />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
import React from 'react';
const AccessContext = React.createContext()
export default AccessContext;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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