mirror of
https://github.com/strapi/strapi.git
synced 2025-07-31 04:45:54 +00:00
Merge branch 'features/api-token-v2' of https://github.com/strapi/strapi into features/api-token-v2
This commit is contained in:
commit
4c07dec65f
@ -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;
|
@ -24,6 +24,7 @@ const DeleteButton = ({ tokenName, onClickDelete }) => {
|
||||
},
|
||||
{ target: `${tokenName}` }
|
||||
)}
|
||||
name="delete"
|
||||
noBorder
|
||||
icon={<Trash />}
|
||||
/>
|
||||
|
@ -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;
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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 }],
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user