Add loading state

Signed-off-by: soupette <cyril.lpz@gmail.com>
This commit is contained in:
soupette 2020-06-08 16:51:42 +02:00 committed by Alexandre Bodin
parent 372951327d
commit 65ff67ac53
11 changed files with 801 additions and 33 deletions

View File

@ -57,27 +57,6 @@ const LeftMenuLinkContainer = ({ plugins }) => {
emptyLinksListMessage: messages.noPluginsInstalled.id, emptyLinksListMessage: messages.noPluginsInstalled.id,
links: pluginsLinks, links: pluginsLinks,
}, },
// general: {
// searchable: false,
// name: 'general',
// links: [
// {
// icon: 'list',
// label: messages.listPlugins.id,
// destination: '/list-plugins',
// },
// {
// icon: 'shopping-basket',
// label: messages.installNewPlugin.id,
// destination: '/marketplace',
// },
// {
// icon: 'cog',
// label: messages.settings.id,
// destination: SETTINGS_BASE_URL,
// },
// ],
// },
}; };
return Object.keys(menu).map(current => ( return Object.keys(menu).map(current => (

View File

@ -0,0 +1,46 @@
import React from 'react';
import { createPortal } from 'react-dom';
import { LoadingIndicatorPage } from 'strapi-helper-plugin';
import PropTypes from 'prop-types';
import styled from 'styled-components';
// No need to create a component here
const Wrapper = styled.div`
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1140;
/* This color is not in the theme */
background: #fff;
`;
const MOUNT_NODE = document.getElementById('app') || document.createElement('div');
/*
*
* This component is used to show a global loader while permissions are being checked
* it prevents from lifting the state up in order to avoid setting more logic into the Admin container
* this way we can show a global loader without modifying the Admin code
*
*/
const Loader = ({ show }) => {
if (show) {
return createPortal(
<Wrapper>
<LoadingIndicatorPage />
</Wrapper>,
MOUNT_NODE
);
}
return null;
};
Loader.propTypes = {
show: PropTypes.bool.isRequired,
};
export default Loader;

View File

@ -0,0 +1,13 @@
import styled from 'styled-components';
const LoaderWrapper = styled.div`
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1140;
background: #fff;
`;
export default LoaderWrapper;

View File

@ -7,7 +7,7 @@
import React, { useContext, useEffect, useReducer } from 'react'; import React, { useContext, useEffect, useReducer } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { UserContext } from 'strapi-helper-plugin'; import { UserContext, hasPermissions } from 'strapi-helper-plugin';
import { import {
LeftMenuLinksSection, LeftMenuLinksSection,
LeftMenuFooter, LeftMenuFooter,
@ -15,28 +15,40 @@ import {
LeftMenuLinkContainer, LeftMenuLinkContainer,
LinksContainer, LinksContainer,
} from '../../components/LeftMenu'; } from '../../components/LeftMenu';
import { hasPermissions } from '../../utils';
import reducer, { initialState } from './reducer'; import reducer, { initialState } from './reducer';
import Loader from './Loader';
import Wrapper from './Wrapper'; import Wrapper from './Wrapper';
const LeftMenu = ({ version, plugins }) => { const LeftMenu = ({ version, plugins }) => {
const location = useLocation(); const location = useLocation();
const permissions = useContext(UserContext); const permissions = useContext(UserContext);
const [{ generalSectionLinks }, dispatch] = useReducer(reducer, initialState); const [{ generalSectionLinks, isLoading }, dispatch] = useReducer(reducer, initialState);
const filteredLinks = generalSectionLinks.filter(link => link.isDisplayed); const filteredLinks = generalSectionLinks.filter(link => link.isDisplayed);
useEffect(() => { useEffect(() => {
const getLinksPermissions = async () => { const getLinksPermissions = async () => {
generalSectionLinks.forEach(async (_, i) => { const checkPermissions = async (index, permissionsToCheck) => {
const hasPermission = await hasPermissions(permissions, generalSectionLinks[i].permissions); const hasPermission = await hasPermissions(permissions, permissionsToCheck);
return { index, hasPermission };
};
const generalSectionLinksArrayOfPromises = generalSectionLinks.map((_, index) =>
checkPermissions(index, generalSectionLinks[index].permissions)
);
const results = await Promise.all(generalSectionLinksArrayOfPromises);
results.forEach(result => {
dispatch({ dispatch({
type: 'SET_LINK_PERMISSION', type: 'SET_LINK_PERMISSION',
index: i, ...result,
hasPermission,
}); });
}); });
dispatch({
type: 'TOGGLE_IS_LOADING',
});
}; };
getLinksPermissions(); getLinksPermissions();
@ -45,6 +57,7 @@ const LeftMenu = ({ version, plugins }) => {
return ( return (
<Wrapper> <Wrapper>
<Loader show={isLoading} />
<LeftMenuHeader /> <LeftMenuHeader />
<LinksContainer> <LinksContainer>
<LeftMenuLinkContainer plugins={plugins} /> <LeftMenuLinkContainer plugins={plugins} />

View File

@ -53,6 +53,7 @@ const initialState = {
], ],
}, },
], ],
isLoading: true,
}; };
const reducer = (state, action) => const reducer = (state, action) =>
@ -62,6 +63,10 @@ const reducer = (state, action) =>
set(draftState, ['generalSectionLinks', action.index, 'isDisplayed'], action.hasPermission); set(draftState, ['generalSectionLinks', action.index, 'isDisplayed'], action.hasPermission);
break; break;
} }
case 'TOGGLE_IS_LOADING': {
draftState.isLoading = !state.isLoading;
break;
}
default: default:
return draftState; return draftState;
} }

View File

@ -0,0 +1,108 @@
import reducer from '../reducer';
describe('ADMIN | LeftMenu | reducer', () => {
describe('DEFAULT_ACTION', () => {
it('should return the initialState', () => {
const state = {
test: true,
};
expect(reducer(state, {})).toEqual(state);
});
});
describe('TOGGLE_IS_LOADING', () => {
it('should change the isLoading property correctly', () => {
const state = {
isLoading: true,
ok: true,
};
const expected = {
isLoading: false,
ok: true,
};
const action = {
type: 'TOGGLE_IS_LOADING',
};
expect(reducer(state, action)).toEqual(expected);
});
});
describe('SET_LINK_PERMISSION', () => {
it('should set the isDisplayed property correctly', () => {
const state = {
isLoading: true,
generalSectionLinks: [
{
icon: 'list',
label: 'app.components.LeftMenuLinkContainer.listPlugins',
destination: '/list-plugins',
isDisplayed: false,
permissions: [
{ action: 'admin::marketplace.read', subject: null },
{ action: 'admin::marketplace.plugins.uninstall', subject: null },
],
},
{
icon: 'shopping-basket',
label: 'app.components.LeftMenuLinkContainer.installNewPlugin',
destination: '/marketplace',
isDisplayed: false,
permissions: [
{ action: 'admin::marketplace.read', subject: null },
{ action: 'admin::marketplace.plugins.install', subject: null },
],
},
{
icon: 'cog',
label: 'app.components.LeftMenuLinkContainer.settings',
isDisplayed: false,
destination: '/test',
permissions: [],
},
],
};
const action = {
type: 'SET_LINK_PERMISSION',
index: 1,
hasPermission: true,
};
const expected = {
isLoading: true,
generalSectionLinks: [
{
icon: 'list',
label: 'app.components.LeftMenuLinkContainer.listPlugins',
destination: '/list-plugins',
isDisplayed: false,
permissions: [
{ action: 'admin::marketplace.read', subject: null },
{ action: 'admin::marketplace.plugins.uninstall', subject: null },
],
},
{
icon: 'shopping-basket',
label: 'app.components.LeftMenuLinkContainer.installNewPlugin',
destination: '/marketplace',
isDisplayed: true,
permissions: [
{ action: 'admin::marketplace.read', subject: null },
{ action: 'admin::marketplace.plugins.install', subject: null },
],
},
{
icon: 'cog',
label: 'app.components.LeftMenuLinkContainer.settings',
isDisplayed: false,
destination: '/test',
permissions: [],
},
],
};
expect(reducer(state, action)).toEqual(expected);
});
});
});

View File

@ -2,4 +2,3 @@ export { default as checkFormValidity } from './checkFormValidity';
export { default as formatAPIErrors } from './formatAPIErrors'; export { default as formatAPIErrors } from './formatAPIErrors';
export { default as roleTabsLabel } from './roleTabsLabel'; export { default as roleTabsLabel } from './roleTabsLabel';
export { default as fakePermissionsData } from './fakePermissionsData'; export { default as fakePermissionsData } from './fakePermissionsData';
export { default as hasPermissions } from './hasPermissions';

View File

@ -97,6 +97,7 @@ export { default as cleanData } from './utils/cleanData';
export { default as difference } from './utils/difference'; export { default as difference } from './utils/difference';
export { default as dateFormats } from './utils/dateFormats'; export { default as dateFormats } from './utils/dateFormats';
export { default as dateToUtcTime } from './utils/dateToUtcTime'; export { default as dateToUtcTime } from './utils/dateToUtcTime';
export { default as hasPermissions } from './utils/hasPermissions';
export { default as translatedErrors } from './utils/translatedErrors'; export { default as translatedErrors } from './utils/translatedErrors';
export { darken } from './utils/colors'; export { darken } from './utils/colors';
export { default as getFileExtension } from './utils/getFileExtension'; export { default as getFileExtension } from './utils/getFileExtension';

View File

@ -1,7 +1,7 @@
import { transform } from 'lodash'; import { transform } from 'lodash';
const hasPermissions = async (userPermissions, permissions) => { const findMatchingPermissions = (userPermissions, permissions) => {
const matchingPermissions = transform( return transform(
userPermissions, userPermissions,
(result, value) => { (result, value) => {
const associatedPermission = permissions.find( const associatedPermission = permissions.find(
@ -14,13 +14,23 @@ const hasPermissions = async (userPermissions, permissions) => {
}, },
[] []
); );
};
const shouldCheckPermissions = permissions =>
permissions.some(perm => perm.conditions && perm.conditions.length);
const hasPermissions = async (userPermissions, permissions) => {
if (!permissions.length) { if (!permissions.length) {
return true; return true;
} }
if (matchingPermissions.some(perm => perm.conditions && perm.conditions.length)) { const matchingPermissions = findMatchingPermissions(userPermissions, permissions);
// TODO test when API ready
if (shouldCheckPermissions(matchingPermissions)) {
// TODO
console.log('should do something'); console.log('should do something');
const hasPermission = await new Promise(resolve => const hasPermission = await new Promise(resolve =>
setTimeout(() => { setTimeout(() => {
resolve(true); resolve(true);
@ -34,3 +44,4 @@ const hasPermissions = async (userPermissions, permissions) => {
}; };
export default hasPermissions; export default hasPermissions;
export { findMatchingPermissions, shouldCheckPermissions };

View File

@ -0,0 +1,114 @@
import hasPermissions, { findMatchingPermissions, shouldCheckPermissions } from '../hasPermissions';
import hasPermissionsTestData from './hasPermissionsTestData';
describe('STRAPI-HELPER_PLUGIN | utils ', () => {
describe('findMatchingPermissions', () => {
it('should return an empty array if both arguments are empty', () => {
expect(findMatchingPermissions([], [])).toHaveLength(0);
expect(findMatchingPermissions([], [])).toEqual([]);
});
it('should return an empty array if there is no permissions that matches', () => {
const data = hasPermissionsTestData.userPermissions.user1;
expect(findMatchingPermissions(data, [])).toHaveLength(0);
expect(findMatchingPermissions(data, [])).toEqual([]);
});
it('should return an array with the matched permissions', () => {
const data = hasPermissionsTestData.userPermissions.user1;
const dataToCheck = hasPermissionsTestData.permissionsToCheck.listPlugins;
expect(findMatchingPermissions(data, dataToCheck)).toHaveLength(2);
expect(findMatchingPermissions(data, dataToCheck)).toEqual([
{
action: 'admin::marketplace.read',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::marketplace.plugins.uninstall',
subject: null,
fields: null,
conditions: ['customCondition'],
},
]);
});
});
describe('hasPermissions', () => {
it('should return true if there is no permissions', async () => {
const data = hasPermissionsTestData.userPermissions.user1;
const result = await hasPermissions(data, []);
expect(result).toBeTruthy();
});
it('should return true if there is at least one permissions that matches the user one', async () => {
const data = hasPermissionsTestData.userPermissions.user1;
const dataToCheck = hasPermissionsTestData.permissionsToCheck.marketplace;
const result = await hasPermissions(data, dataToCheck);
expect(result).toBeTruthy();
});
it('should return false no permission is matching', async () => {
const data = hasPermissionsTestData.userPermissions.user1;
const dataToCheck = [
{
action: 'something',
subject: 'something',
},
];
const result = await hasPermissions(data, dataToCheck);
expect(result).toBeFalsy();
});
});
describe('shouldCheckPermissions', () => {
it('should return false if there is no data', () => {
expect(shouldCheckPermissions([])).toBeFalsy();
});
it('should return false if there is no condition in the array of permissions', () => {
const data = [
{
action: 'admin::marketplace.read',
subject: null,
fields: null,
conditions: [],
},
];
expect(shouldCheckPermissions(data)).toBeFalsy();
});
it('should return true if there is at least one item that has a condition in the array of permissions', () => {
const data = [
{
action: 'admin::marketplace.read',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::marketplace.plugins.uninstall',
subject: null,
fields: null,
conditions: ['customCondition'],
},
{
action: 'admin::marketplace.plugins.install',
subject: null,
fields: null,
conditions: null,
},
];
expect(shouldCheckPermissions(data)).toBeTruthy();
});
});
});

View File

@ -0,0 +1,479 @@
const hasPermissionsTestData = {
userPermissions: {
user1: [
// Admin marketplace
{
action: 'admin::marketplace.read',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::marketplace.plugins.install',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::marketplace.plugins.uninstall',
subject: null,
fields: null,
conditions: ['customCondition'],
},
// Admin webhooks
{
action: 'admin::webhooks.create',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::webhooks.read',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::webhooks.update',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::webhooks.delete',
subject: null,
fields: null,
conditions: [],
},
// Admin users
{
action: 'admin::users.create',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::users.read',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::users.update',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::users.delete',
subject: null,
fields: null,
conditions: [],
},
// Admin roles
{
action: 'admin::roles.create',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::roles.read',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::roles.update',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::roles.delete',
subject: null,
fields: null,
conditions: [],
},
// Content type builder
{
action: 'plugins::content-type-builder.read',
subject: null,
fields: null,
conditions: null,
},
// Documentation plugin
{
action: 'plugins::documentation.read',
subject: null,
fields: null,
conditions: null,
},
{
action: 'plugins::documentation.settings.update',
subject: null,
fields: null,
conditions: null,
},
{
action: 'plugins::documentation.settings.regenerate',
subject: null,
fields: null,
conditions: null,
},
// Upload plugin
{
action: 'plugins::upload.read',
subject: null,
fields: null,
conditions: null,
},
{
action: 'plugins::upload.assets.create',
subject: null,
fields: null,
conditions: null,
},
{
action: 'plugins::upload.assets.update',
subject: null,
fields: null,
conditions: null,
},
{
action: 'plugins::upload.assets.dowload',
subject: null,
fields: null,
conditions: null,
},
{
action: 'plugins::upload.assets.copy-link',
subject: null,
fields: null,
conditions: null,
},
// Users-permissions
{
action: 'plugins::users-permissions.roles.create',
subject: null,
fields: null,
conditions: null,
},
{
action: 'plugins::users-permissions.roles.read',
subject: null,
fields: null,
conditions: null,
},
{
action: 'plugins::users-permissions.roles.update',
subject: null,
fields: null,
conditions: null,
},
{
action: 'plugins::users-permissions.roles.delete',
subject: null,
fields: null,
conditions: null,
},
{
action: 'plugins::users-permissions.email-templates.read',
subject: null,
fields: null,
conditions: null,
},
{
action: 'plugins::users-permissions.email-templates.update',
subject: null,
fields: null,
conditions: null,
},
{
action: 'plugins::users-permissions.providers.read',
subject: null,
fields: null,
conditions: null,
},
{
action: 'plugins::users-permissions.providers.update',
subject: null,
fields: null,
conditions: null,
},
{
action: 'plugins::users-permissions.advanced-settings.read',
subject: null,
fields: null,
conditions: null,
},
{
action: 'plugins::users-permissions.advanced-settings.update',
subject: null,
fields: null,
conditions: null,
},
],
user2: [
// Admin marketplace
// {
// action: 'admin::marketplace.read',
// subject: null,
// fields: null,
// conditions: [],
// },
{
action: 'admin::marketplace.plugins.install',
subject: null,
fields: null,
conditions: ['some condition'],
},
// {
// action: 'admin::marketplace.plugins.uninstall',
// subject: null,
// fields: null,
// conditions: [],
// },
// Admin webhooks
{
action: 'admin::webhooks.create',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::webhooks.read',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::webhooks.update',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::webhooks.delete',
subject: null,
fields: null,
conditions: [],
},
// Admin users
{
action: 'admin::users.create',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::users.read',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::users.update',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::users.delete',
subject: null,
fields: null,
conditions: [],
},
// Admin roles
{
action: 'admin::roles.create',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::roles.read',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::roles.update',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::roles.delete',
subject: null,
fields: null,
conditions: [],
},
// Content type builder
{
action: 'plugins::content-type-builder.read',
subject: null,
fields: null,
conditions: [],
},
// Documentation plugin
// {
// action: 'plugins::documentation.read',
// subject: null,
// fields: null,
// conditions:[],
// },
// {
// action: 'plugins::documentation.settings.update',
// subject: null,
// fields: null,
// conditions:[],
// },
// {
// action: 'plugins::documentation.settings.regenerate',
// subject: null,
// fields: null,
// conditions:[],
// },
// Upload plugin
{
action: 'plugins::upload.read',
subject: null,
fields: null,
conditions: [],
},
{
action: 'plugins::upload.assets.create',
subject: null,
fields: null,
conditions: [],
},
{
action: 'plugins::upload.assets.update',
subject: null,
fields: null,
conditions: [],
},
{
action: 'plugins::upload.assets.dowload',
subject: null,
fields: null,
conditions: [],
},
{
action: 'plugins::upload.assets.copy-link',
subject: null,
fields: null,
conditions: [],
},
// Users-permissions
{
action: 'plugins::users-permissions.roles.create',
subject: null,
fields: null,
conditions: [],
},
{
action: 'plugins::users-permissions.roles.read',
subject: null,
fields: null,
conditions: [],
},
{
action: 'plugins::users-permissions.roles.update',
subject: null,
fields: null,
conditions: [],
},
{
action: 'plugins::users-permissions.roles.delete',
subject: null,
fields: null,
conditions: [],
},
{
action: 'plugins::users-permissions.email-templates.read',
subject: null,
fields: null,
conditions: [],
},
{
action: 'plugins::users-permissions.email-templates.update',
subject: null,
fields: null,
conditions: [],
},
{
action: 'plugins::users-permissions.providers.read',
subject: null,
fields: null,
conditions: [],
},
{
action: 'plugins::users-permissions.providers.update',
subject: null,
fields: null,
conditions: [],
},
{
action: 'plugins::users-permissions.advanced-settings.read',
subject: null,
fields: null,
conditions: [],
},
{
action: 'plugins::users-permissions.advanced-settings.update',
subject: null,
fields: null,
conditions: [],
},
],
},
permissionsToCheck: {
listPlugins: [
{ action: 'admin::marketplace.read', subject: null },
{ action: 'admin::marketplace.plugins.uninstall', subject: null },
],
marketplace: [
{ action: 'admin::marketplace.read', subject: null },
{ action: 'admin::marketplace.plugins.install', subject: null },
],
settings: [
// webhooks
{ action: 'admin::webhook.create', subject: null },
{ action: 'admin::webhook.read', subject: null },
{ action: 'admin::webhook.update', subject: null },
{ action: 'admin::webhook.delete', subject: null },
// users
{ action: 'admin::users.create', subject: null },
{ action: 'admin::users.read', subject: null },
{ action: 'admin::users.update', subject: null },
{ action: 'admin::users.delete', subject: null },
// roles
{ action: 'admin::roles.create', subject: null },
{ action: 'admin::roles.update', subject: null },
{ action: 'admin::roles.read', subject: null },
{ action: 'admin::roles.delete', subject: null },
// media library
{ action: 'plugins::upload.read', subject: null },
{ action: 'plugins::upload.assets.create', subject: null },
{ action: 'plugins::upload.assets.update', subject: null },
],
},
};
export default hasPermissionsTestData;