Merge branch 'features/api-token-v2' of https://github.com/strapi/strapi into features/api-token-v2

This commit is contained in:
Bassel Kanso 2022-08-18 12:20:56 +03:00
commit 4c07dec65f
9 changed files with 137 additions and 51 deletions

View File

@ -0,0 +1,63 @@
import React from 'react';
import { useIntl } from 'react-intl';
import PropTypes from 'prop-types';
import { Link } from '@strapi/helper-plugin';
import { useHistory } from 'react-router-dom';
import styled from 'styled-components';
const MESSAGES_MAP = {
edit: {
id: 'app.component.table.edit',
defaultMessage: 'Edit {target}',
},
read: {
id: 'app.component.table.read',
defaultMessage: 'Read {target}',
},
};
const LinkStyled = styled(Link)`
svg {
path {
fill: ${({ theme }) => theme.colors.neutral500};
}
}
&:hover,
&:focus {
svg {
path {
fill: ${({ theme }) => theme.colors.neutral800};
}
}
}
`;
const DefaultButton = ({ tokenName, tokenId, buttonType, children }) => {
const { formatMessage } = useIntl();
const {
location: { pathname },
} = useHistory();
return (
<LinkStyled
to={`${pathname}/${tokenId}`}
title={formatMessage(MESSAGES_MAP[buttonType], { target: tokenName })}
>
{children}
</LinkStyled>
);
};
DefaultButton.propTypes = {
tokenName: PropTypes.string.isRequired,
tokenId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
buttonType: PropTypes.string,
children: PropTypes.node.isRequired,
};
DefaultButton.defaultProps = {
buttonType: 'edit',
};
export default DefaultButton;

View File

@ -24,6 +24,7 @@ const DeleteButton = ({ tokenName, onClickDelete }) => {
},
{ target: `${tokenName}` }
)}
name="delete"
noBorder
icon={<Trash />}
/>

View File

@ -0,0 +1,19 @@
import React from 'react';
import Eye from '@strapi/icons/Eye';
import PropTypes from 'prop-types';
import DefaultButton from '../DefaultButton';
const ReadButton = ({ tokenName, tokenId }) => {
return (
<DefaultButton tokenName={tokenName} tokenId={tokenId} buttonType="read">
<Eye />
</DefaultButton>
);
};
ReadButton.propTypes = {
tokenName: PropTypes.string.isRequired,
tokenId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
};
export default ReadButton;

View File

@ -1,46 +1,13 @@
import React from 'react';
import Pencil from '@strapi/icons/Pencil';
import { useIntl } from 'react-intl';
import PropTypes from 'prop-types';
import { Link } from '@strapi/helper-plugin';
import { useHistory } from 'react-router-dom';
import styled from 'styled-components';
const LinkUpdate = styled(Link)`
svg {
path {
fill: ${({ theme }) => theme.colors.neutral500};
}
}
&:hover {
svg {
path {
fill: ${({ theme }) => theme.colors.neutral800};
}
}
}
`;
import DefaultButton from '../DefaultButton';
const UpdateButton = ({ tokenName, tokenId }) => {
const { formatMessage } = useIntl();
const {
location: { pathname },
} = useHistory();
return (
<LinkUpdate
to={`${pathname}/${tokenId}`}
title={formatMessage(
{
id: 'app.component.table.edit',
defaultMessage: 'Edit {target}',
},
{ target: `${tokenName}` }
)}
>
<DefaultButton tokenName={tokenName} tokenId={tokenId}>
<Pencil />
</LinkUpdate>
</DefaultButton>
);
};

View File

@ -14,8 +14,9 @@ import PropTypes from 'prop-types';
import { useHistory } from 'react-router-dom';
import DeleteButton from './DeleteButton';
import UpdateButton from './UpdateButton';
import ReadButton from './ReadButton';
const TableRows = ({ canDelete, canUpdate, onClickDelete, withBulkActions, rows }) => {
const TableRows = ({ canDelete, canUpdate, canRead, onClickDelete, withBulkActions, rows }) => {
const { formatMessage } = useIntl();
const [{ query }] = useQueryParams();
const [, sortOrder] = query.sort.split(':');
@ -73,6 +74,9 @@ const TableRows = ({ canDelete, canUpdate, onClickDelete, withBulkActions, rows
<Td>
<Flex justifyContent="end">
{canUpdate && <UpdateButton tokenName={apiToken.name} tokenId={apiToken.id} />}
{!canUpdate && canRead && (
<ReadButton tokenName={apiToken.name} tokenId={apiToken.id} />
)}
{canDelete && (
<DeleteButton
tokenName={apiToken.name}
@ -92,6 +96,7 @@ const TableRows = ({ canDelete, canUpdate, onClickDelete, withBulkActions, rows
TableRows.defaultProps = {
canDelete: false,
canUpdate: false,
canRead: false,
onClickDelete() {},
rows: [],
withBulkActions: false,
@ -100,6 +105,7 @@ TableRows.defaultProps = {
TableRows.propTypes = {
canDelete: PropTypes.bool,
canUpdate: PropTypes.bool,
canRead: PropTypes.bool,
onClickDelete: PropTypes.func,
rows: PropTypes.array,
withBulkActions: PropTypes.bool,

View File

@ -137,15 +137,16 @@ const ApiTokenListView = () => {
headers={tableHeaders}
contentType="api-tokens"
rows={apiTokens}
withBulkActions={canDelete || canUpdate}
withBulkActions={canDelete || canUpdate || canRead}
isLoading={isLoading}
onConfirmDelete={(id) => deleteMutation.mutateAsync(id)}
>
<TableRows
canRead={canRead}
canDelete={canDelete}
canUpdate={canUpdate}
rows={apiTokens}
withBulkActions={canDelete || canUpdate}
withBulkActions={canDelete || canUpdate || canRead}
/>
</DynamicTable>
)}

View File

@ -15,9 +15,7 @@ jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
useNotification: jest.fn(),
useFocusWhenNavigate: jest.fn(),
useRBAC: jest.fn(() => ({
allowedActions: { canCreate: true, canDelete: true, canRead: true, canUpdate: true },
})),
useRBAC: jest.fn(),
useGuidedTour: jest.fn(() => ({
startSection: jest.fn(),
})),
@ -73,6 +71,9 @@ describe('ADMIN | Pages | API TOKENS | ListPage', () => {
});
it('should show a list of api tokens', async () => {
useRBAC.mockImplementation(() => ({
allowedActions: { canCreate: true, canDelete: true, canRead: true, canUpdate: true },
}));
const history = createMemoryHistory();
history.push('/settings/api-tokens');
const app = makeApp(history);
@ -631,7 +632,8 @@ describe('ADMIN | Pages | API TOKENS | ListPage', () => {
fill: #8e8ea9;
}
.c36:hover svg path {
.c36:hover svg path,
.c36:focus svg path {
fill: #32324d;
}
@ -956,6 +958,7 @@ describe('ADMIN | Pages | API TOKENS | ListPage', () => {
aria-disabled="false"
aria-labelledby="tooltip-3"
class="c25 c26"
name="delete"
tabindex="-1"
type="button"
>
@ -988,7 +991,7 @@ describe('ADMIN | Pages | API TOKENS | ListPage', () => {
});
it('should not show the create button when the user does not have the rights to create', async () => {
useRBAC.mockImplementationOnce(() => ({
useRBAC.mockImplementation(() => ({
allowedActions: { canCreate: false, canDelete: true, canRead: true, canUpdate: true },
}));
@ -999,4 +1002,34 @@ describe('ADMIN | Pages | API TOKENS | ListPage', () => {
await waitFor(() => expect(queryByTestId('create-api-token-button')).not.toBeInTheDocument());
});
it('should show the delete button when the user have the rights to delete', async () => {
useRBAC.mockImplementation(() => ({
allowedActions: { canCreate: false, canDelete: true, canRead: true, canUpdate: false },
}));
const history = createMemoryHistory();
history.push('/settings/api-tokens');
const app = makeApp(history);
const { container } = render(app);
await waitFor(() => {
expect(container.querySelector('button[name="delete"]')).toBeInTheDocument();
});
});
it('should show the read button when the user have the rights to read and not to update', async () => {
useRBAC.mockImplementation(() => ({
allowedActions: { canCreate: false, canDelete: true, canRead: true, canUpdate: false },
}));
const history = createMemoryHistory();
history.push('/settings/api-tokens');
const app = makeApp(history);
const { container } = render(app);
await waitFor(() => {
expect(container.querySelector('a[title*="Read"]')).toBeInTheDocument();
});
});
});

View File

@ -76,12 +76,7 @@ const permissions = {
update: [{ action: 'admin::webhooks.update', subject: null }],
},
'api-tokens': {
main: [
{ action: 'admin::api-tokens.create', subject: null },
{ action: 'admin::api-tokens.read', subject: null },
{ action: 'admin::api-tokens.update', subject: null },
{ action: 'admin::api-tokens.delete', subject: null },
],
main: [],
create: [{ action: 'admin::api-tokens.create', subject: null }],
delete: [{ action: 'admin::api-tokens.delete', subject: null }],
read: [{ action: 'admin::api-tokens.read', subject: null }],

View File

@ -34,7 +34,8 @@
"test:front": "cross-env IS_EE=true jest --config ./jest.config.front.js",
"test:front:watch": "cross-env IS_EE=true jest --config ./jest.config.front.js --watchAll",
"test:front:ce": "cross-env IS_EE=false jest --config ./jest.config.front.js",
"test:front:watch:ce": "cross-env IS_EE=false jest --config ./jest.config.front.js --watchAll"
"test:front:watch:ce": "cross-env IS_EE=false jest --config ./jest.config.front.js --watchAll",
"test:front:ce:cov": "cross-env IS_EE=false jest --config ./jest.config.front.js --coverage --collectCoverageFrom='<rootDir>/packages/core/admin/admin/**/*.js' --coverageDirectory='<rootDir>/packages/core/admin/coverage'"
},
"dependencies": {
"@babel/core": "7.18.10",