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