mirror of
https://github.com/strapi/strapi.git
synced 2025-08-01 21:36:25 +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}` }
|
{ target: `${tokenName}` }
|
||||||
)}
|
)}
|
||||||
|
name="delete"
|
||||||
noBorder
|
noBorder
|
||||||
icon={<Trash />}
|
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 React from 'react';
|
||||||
import Pencil from '@strapi/icons/Pencil';
|
import Pencil from '@strapi/icons/Pencil';
|
||||||
import { useIntl } from 'react-intl';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Link } from '@strapi/helper-plugin';
|
import DefaultButton from '../DefaultButton';
|
||||||
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};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const UpdateButton = ({ tokenName, tokenId }) => {
|
const UpdateButton = ({ tokenName, tokenId }) => {
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
const {
|
|
||||||
location: { pathname },
|
|
||||||
} = useHistory();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LinkUpdate
|
<DefaultButton tokenName={tokenName} tokenId={tokenId}>
|
||||||
to={`${pathname}/${tokenId}`}
|
|
||||||
title={formatMessage(
|
|
||||||
{
|
|
||||||
id: 'app.component.table.edit',
|
|
||||||
defaultMessage: 'Edit {target}',
|
|
||||||
},
|
|
||||||
{ target: `${tokenName}` }
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Pencil />
|
<Pencil />
|
||||||
</LinkUpdate>
|
</DefaultButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,8 +14,9 @@ import PropTypes from 'prop-types';
|
|||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import DeleteButton from './DeleteButton';
|
import DeleteButton from './DeleteButton';
|
||||||
import UpdateButton from './UpdateButton';
|
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 { formatMessage } = useIntl();
|
||||||
const [{ query }] = useQueryParams();
|
const [{ query }] = useQueryParams();
|
||||||
const [, sortOrder] = query.sort.split(':');
|
const [, sortOrder] = query.sort.split(':');
|
||||||
@ -73,6 +74,9 @@ const TableRows = ({ canDelete, canUpdate, onClickDelete, withBulkActions, rows
|
|||||||
<Td>
|
<Td>
|
||||||
<Flex justifyContent="end">
|
<Flex justifyContent="end">
|
||||||
{canUpdate && <UpdateButton tokenName={apiToken.name} tokenId={apiToken.id} />}
|
{canUpdate && <UpdateButton tokenName={apiToken.name} tokenId={apiToken.id} />}
|
||||||
|
{!canUpdate && canRead && (
|
||||||
|
<ReadButton tokenName={apiToken.name} tokenId={apiToken.id} />
|
||||||
|
)}
|
||||||
{canDelete && (
|
{canDelete && (
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
tokenName={apiToken.name}
|
tokenName={apiToken.name}
|
||||||
@ -92,6 +96,7 @@ const TableRows = ({ canDelete, canUpdate, onClickDelete, withBulkActions, rows
|
|||||||
TableRows.defaultProps = {
|
TableRows.defaultProps = {
|
||||||
canDelete: false,
|
canDelete: false,
|
||||||
canUpdate: false,
|
canUpdate: false,
|
||||||
|
canRead: false,
|
||||||
onClickDelete() {},
|
onClickDelete() {},
|
||||||
rows: [],
|
rows: [],
|
||||||
withBulkActions: false,
|
withBulkActions: false,
|
||||||
@ -100,6 +105,7 @@ TableRows.defaultProps = {
|
|||||||
TableRows.propTypes = {
|
TableRows.propTypes = {
|
||||||
canDelete: PropTypes.bool,
|
canDelete: PropTypes.bool,
|
||||||
canUpdate: PropTypes.bool,
|
canUpdate: PropTypes.bool,
|
||||||
|
canRead: PropTypes.bool,
|
||||||
onClickDelete: PropTypes.func,
|
onClickDelete: PropTypes.func,
|
||||||
rows: PropTypes.array,
|
rows: PropTypes.array,
|
||||||
withBulkActions: PropTypes.bool,
|
withBulkActions: PropTypes.bool,
|
||||||
|
@ -137,15 +137,16 @@ const ApiTokenListView = () => {
|
|||||||
headers={tableHeaders}
|
headers={tableHeaders}
|
||||||
contentType="api-tokens"
|
contentType="api-tokens"
|
||||||
rows={apiTokens}
|
rows={apiTokens}
|
||||||
withBulkActions={canDelete || canUpdate}
|
withBulkActions={canDelete || canUpdate || canRead}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onConfirmDelete={(id) => deleteMutation.mutateAsync(id)}
|
onConfirmDelete={(id) => deleteMutation.mutateAsync(id)}
|
||||||
>
|
>
|
||||||
<TableRows
|
<TableRows
|
||||||
|
canRead={canRead}
|
||||||
canDelete={canDelete}
|
canDelete={canDelete}
|
||||||
canUpdate={canUpdate}
|
canUpdate={canUpdate}
|
||||||
rows={apiTokens}
|
rows={apiTokens}
|
||||||
withBulkActions={canDelete || canUpdate}
|
withBulkActions={canDelete || canUpdate || canRead}
|
||||||
/>
|
/>
|
||||||
</DynamicTable>
|
</DynamicTable>
|
||||||
)}
|
)}
|
||||||
|
@ -15,9 +15,7 @@ jest.mock('@strapi/helper-plugin', () => ({
|
|||||||
...jest.requireActual('@strapi/helper-plugin'),
|
...jest.requireActual('@strapi/helper-plugin'),
|
||||||
useNotification: jest.fn(),
|
useNotification: jest.fn(),
|
||||||
useFocusWhenNavigate: jest.fn(),
|
useFocusWhenNavigate: jest.fn(),
|
||||||
useRBAC: jest.fn(() => ({
|
useRBAC: jest.fn(),
|
||||||
allowedActions: { canCreate: true, canDelete: true, canRead: true, canUpdate: true },
|
|
||||||
})),
|
|
||||||
useGuidedTour: jest.fn(() => ({
|
useGuidedTour: jest.fn(() => ({
|
||||||
startSection: jest.fn(),
|
startSection: jest.fn(),
|
||||||
})),
|
})),
|
||||||
@ -73,6 +71,9 @@ describe('ADMIN | Pages | API TOKENS | ListPage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should show a list of api tokens', async () => {
|
it('should show a list of api tokens', async () => {
|
||||||
|
useRBAC.mockImplementation(() => ({
|
||||||
|
allowedActions: { canCreate: true, canDelete: true, canRead: true, canUpdate: true },
|
||||||
|
}));
|
||||||
const history = createMemoryHistory();
|
const history = createMemoryHistory();
|
||||||
history.push('/settings/api-tokens');
|
history.push('/settings/api-tokens');
|
||||||
const app = makeApp(history);
|
const app = makeApp(history);
|
||||||
@ -631,7 +632,8 @@ describe('ADMIN | Pages | API TOKENS | ListPage', () => {
|
|||||||
fill: #8e8ea9;
|
fill: #8e8ea9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c36:hover svg path {
|
.c36:hover svg path,
|
||||||
|
.c36:focus svg path {
|
||||||
fill: #32324d;
|
fill: #32324d;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -956,6 +958,7 @@ describe('ADMIN | Pages | API TOKENS | ListPage', () => {
|
|||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
aria-labelledby="tooltip-3"
|
aria-labelledby="tooltip-3"
|
||||||
class="c25 c26"
|
class="c25 c26"
|
||||||
|
name="delete"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
type="button"
|
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 () => {
|
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 },
|
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());
|
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 }],
|
update: [{ action: 'admin::webhooks.update', subject: null }],
|
||||||
},
|
},
|
||||||
'api-tokens': {
|
'api-tokens': {
|
||||||
main: [
|
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 },
|
|
||||||
],
|
|
||||||
create: [{ action: 'admin::api-tokens.create', subject: null }],
|
create: [{ action: 'admin::api-tokens.create', subject: null }],
|
||||||
delete: [{ action: 'admin::api-tokens.delete', subject: null }],
|
delete: [{ action: 'admin::api-tokens.delete', subject: null }],
|
||||||
read: [{ action: 'admin::api-tokens.read', 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": "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: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: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": {
|
"dependencies": {
|
||||||
"@babel/core": "7.18.10",
|
"@babel/core": "7.18.10",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user