Merge fix

This commit is contained in:
Fernando Chavez 2023-01-10 09:42:33 +01:00
commit 65e29e5f61
9 changed files with 208 additions and 119 deletions

View File

@ -0,0 +1,94 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { Loader } from '@strapi/design-system/Loader';
import { Grid } from '@strapi/design-system/Grid';
import { Box } from '@strapi/design-system/Box';
import { Flex } from '@strapi/design-system/Flex';
import { Typography } from '@strapi/design-system/Typography';
import { pxToRem } from '@strapi/helper-plugin';
import getDefaultMessage from '../utils/getActionTypesDefaultMessages';
import ActionItem from './ActionItem';
const ActionBody = ({ status, data, formattedDate }) => {
const { formatMessage } = useIntl();
if (status === 'loading') {
return (
<Flex padding={7} justifyContent="center" alignItems="center">
<Loader />
</Flex>
);
}
const { action, user, payload } = data;
return (
<>
<Box marginBottom={pxToRem(12)}>
<Typography variant="delta" id="title">
{formatMessage({
id: 'Settings.permissions.auditLogs.details',
defaultMessage: 'Log Details',
})}
</Typography>
</Box>
<Grid
gap={4}
gridCols={2}
paddingTop={4}
paddingBottom={4}
paddingLeft={6}
paddingRight={6}
background="neutral100"
hasRadius
>
<ActionItem
actionLabel={formatMessage({
id: 'Settings.permissions.auditLogs.action',
defaultMessage: 'Action',
})}
actionName={formatMessage({
id: `Settings.permissions.auditLogs.${action}`,
defaultMessage: getDefaultMessage(action),
})}
/>
<ActionItem
actionLabel={formatMessage({
id: 'Settings.permissions.auditLogs.date',
defaultMessage: 'Date',
})}
actionName={formattedDate}
/>
<ActionItem
actionLabel={formatMessage({
id: 'Settings.permissions.auditLogs.user',
defaultMessage: 'User',
})}
actionName={user ? user.fullname : '-'}
/>
</Grid>
{/* TODO remove when adding JSON component */}
<Box as="pre" marginTop={4}>
<Typography>{JSON.stringify(payload, null, 2)}</Typography>
</Box>
</>
);
};
ActionBody.defaultProps = {
data: {},
};
ActionBody.propTypes = {
status: PropTypes.oneOf(['idle', 'loading', 'error', 'success']).isRequired,
data: PropTypes.shape({
action: PropTypes.string,
date: PropTypes.string,
payload: PropTypes.object,
user: PropTypes.object,
}),
formattedDate: PropTypes.string.isRequired,
};
export default ActionBody;

View File

@ -0,0 +1,53 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useQuery } from 'react-query';
import { ModalLayout, ModalHeader, ModalBody } from '@strapi/design-system/ModalLayout';
import { Breadcrumbs, Crumb } from '@strapi/design-system/Breadcrumbs';
import { useNotification } from '@strapi/helper-plugin';
import useFormatTimeStamp from '../hooks/useFormatTimeStamp';
import { useFetchClient } from '../../../../../../hooks';
import ActionBody from './ActionBody';
const Modal = ({ handleClose, logId }) => {
const { get } = useFetchClient();
const toggleNotification = useNotification();
const fetchAuditLog = async (id) => {
const { data } = await get(`/admin/audit-logs/${id}`);
return data;
};
const { data, status } = useQuery(['audit-log', logId], () => fetchAuditLog(logId), {
onError() {
toggleNotification({
type: 'warning',
message: { id: 'notification.error', defaultMessage: 'An error occured' },
});
handleClose();
},
});
const formatTimeStamp = useFormatTimeStamp();
const formattedDate = data ? formatTimeStamp(data.date) : '';
return (
<ModalLayout onClose={handleClose} labelledBy="title">
<ModalHeader>
<Breadcrumbs label={formattedDate} id="title">
<Crumb>{formattedDate}</Crumb>
</Breadcrumbs>
</ModalHeader>
<ModalBody>
<ActionBody status={status} data={data} formattedDate={formattedDate} />
</ModalBody>
</ModalLayout>
);
};
Modal.propTypes = {
handleClose: PropTypes.func.isRequired,
logId: PropTypes.number.isRequired,
};
export default Modal;

View File

@ -1,80 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { ModalLayout, ModalHeader, ModalBody } from '@strapi/design-system/ModalLayout';
import { Breadcrumbs, Crumb } from '@strapi/design-system/Breadcrumbs';
import { Grid } from '@strapi/design-system/Grid';
import { Box } from '@strapi/design-system/Box';
import { Typography } from '@strapi/design-system/Typography';
import { pxToRem } from '@strapi/helper-plugin';
import getDefaultMessage from '../utils/getActionTypesDefaultMessages';
import useFormatTimeStamp from '../hooks/useFormatTimeStamp';
import ActionItem from './ActionItem';
const ModalDialog = ({ onToggle, data: { date, user, action } }) => {
const { formatMessage } = useIntl();
const formatTimeStamp = useFormatTimeStamp();
const formattedDate = formatTimeStamp(date);
return (
<ModalLayout onClose={onToggle} labelledBy="title">
<ModalHeader>
<Breadcrumbs label={formattedDate}>
<Crumb>{formattedDate}</Crumb>
</Breadcrumbs>
</ModalHeader>
<ModalBody>
<Box marginBottom={pxToRem(12)}>
<Typography variant="delta" id="title">
{formatMessage({
id: 'Settings.permissions.auditLogs.details',
defaultMessage: 'Log Details',
})}
</Typography>
</Box>
<Grid
gap={4}
gridCols={2}
paddingTop={4}
paddingBottom={4}
paddingLeft={6}
paddingRight={6}
background="neutral100"
hasRadius
>
<ActionItem
actionLabel={formatMessage({
id: 'Settings.permissions.auditLogs.action',
defaultMessage: 'Action',
})}
actionName={formatMessage({
id: `Settings.permissions.auditLogs.${action}`,
defaultMessage: getDefaultMessage(action),
})}
/>
<ActionItem
actionLabel={formatMessage({
id: 'Settings.permissions.auditLogs.date',
defaultMessage: 'Date',
})}
actionName={formattedDate}
/>
<ActionItem
actionLabel={formatMessage({
id: 'Settings.permissions.auditLogs.user',
defaultMessage: 'User',
})}
actionName={user ? user.fullname : '-'}
/>
</Grid>
</ModalBody>
</ModalLayout>
);
};
ModalDialog.propTypes = {
onToggle: PropTypes.func.isRequired,
data: PropTypes.object.isRequired,
};
export default ModalDialog;

View File

@ -10,7 +10,7 @@ import { onRowClick, stopPropagation } from '@strapi/helper-plugin';
import useFormatTimeStamp from '../hooks/useFormatTimeStamp'; import useFormatTimeStamp from '../hooks/useFormatTimeStamp';
import getDefaultMessage from '../utils/getActionTypesDefaultMessages'; import getDefaultMessage from '../utils/getActionTypesDefaultMessages';
const TableRows = ({ headers, rows, onModalToggle }) => { const TableRows = ({ headers, rows, onOpenModal }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const formatTimeStamp = useFormatTimeStamp(); const formatTimeStamp = useFormatTimeStamp();
@ -36,7 +36,7 @@ const TableRows = ({ headers, rows, onModalToggle }) => {
<Tr <Tr
key={data.id} key={data.id}
{...onRowClick({ {...onRowClick({
fn: () => onModalToggle(data.id), fn: () => onOpenModal(data.id),
})} })}
> >
{headers.map(({ key, name, cellFormatter }) => { {headers.map(({ key, name, cellFormatter }) => {
@ -55,7 +55,7 @@ const TableRows = ({ headers, rows, onModalToggle }) => {
<Td {...stopPropagation}> <Td {...stopPropagation}>
<Flex justifyContent="end"> <Flex justifyContent="end">
<IconButton <IconButton
onClick={() => onModalToggle(data.id)} onClick={() => onOpenModal(data.id)}
aria-label={formatMessage( aria-label={formatMessage(
{ id: 'app.component.table.view', defaultMessage: '{target} details' }, { id: 'app.component.table.view', defaultMessage: '{target} details' },
{ target: `${data.action} action` } { target: `${data.action} action` }
@ -79,7 +79,7 @@ TableRows.defaultProps = {
TableRows.propTypes = { TableRows.propTypes = {
headers: PropTypes.array.isRequired, headers: PropTypes.array.isRequired,
rows: PropTypes.array, rows: PropTypes.array,
onModalToggle: PropTypes.func.isRequired, onOpenModal: PropTypes.func.isRequired,
}; };
export default TableRows; export default TableRows;

View File

@ -60,14 +60,14 @@ const rows = [
}, },
]; ];
const onModalToggle = jest.fn(); const onModalOpen = jest.fn();
// eslint-disable-next-line react/prop-types // eslint-disable-next-line react/prop-types
const App = ( const App = (
<ThemeProvider theme={lightTheme}> <ThemeProvider theme={lightTheme}>
<IntlProvider locale="en" messages={{}} defaultLocale="en" textComponent="span"> <IntlProvider locale="en" messages={{}} defaultLocale="en" textComponent="span">
<Router history={history}> <Router history={history}>
<TableRows headers={headers} rows={rows} onModalToggle={onModalToggle} /> <TableRows headers={headers} rows={rows} onOpenModal={onModalOpen} />
</Router> </Router>
</IntlProvider> </IntlProvider>
</ThemeProvider> </ThemeProvider>
@ -94,13 +94,13 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView | Dynamic Table | Table Rows', (
const label = screen.getByText(/update action details/i); const label = screen.getByText(/update action details/i);
const viewDetailsButton = label.closest('button'); const viewDetailsButton = label.closest('button');
fireEvent.click(viewDetailsButton); fireEvent.click(viewDetailsButton);
expect(onModalToggle).toHaveBeenCalled(); expect(onModalOpen).toHaveBeenCalled();
}); });
it('should open a modal when clicked on a row', () => { it('should open a modal when clicked on a row', () => {
render(App); render(App);
const rows = document.querySelectorAll('tr'); const rows = document.querySelectorAll('tr');
fireEvent.click(rows[0]); fireEvent.click(rows[0]);
expect(onModalToggle).toHaveBeenCalled(); expect(onModalOpen).toHaveBeenCalled();
}); });
}); });

View File

@ -15,13 +15,11 @@ import adminPermissions from '../../../../../permissions';
import { useFetchClient } from '../../../../../hooks'; import { useFetchClient } from '../../../../../hooks';
import TableRows from './TableRows'; import TableRows from './TableRows';
import tableHeaders from './utils/tableHeaders'; import tableHeaders from './utils/tableHeaders';
import ModalDialog from './ModalDialog';
import PaginationFooter from './PaginationFooter'; import PaginationFooter from './PaginationFooter';
import Modal from './Modal';
const ListView = () => { const ListView = () => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [isModalOpen, setIsModalOpen] = useState(false);
const [detailsActionData, setDetailsActionData] = useState(null);
const toggleNotification = useNotification(); const toggleNotification = useNotification();
const { const {
allowedActions: { canRead }, allowedActions: { canRead },
@ -31,14 +29,14 @@ const ListView = () => {
useFocusWhenNavigate(); useFocusWhenNavigate();
const fetchData = async ({ queryKey }) => { const fetchAuditLogsPage = async ({ queryKey }) => {
const search = queryKey[1]; const search = queryKey[1];
const { data } = await get(`/admin/audit-logs${search}`); const { data } = await get(`/admin/audit-logs${search}`);
return data; return data;
}; };
const { data, isLoading } = useQuery(['auditLogs', search], fetchData, { const { data, isLoading } = useQuery(['auditLogs', search], fetchAuditLogsPage, {
enabled: canRead, enabled: canRead,
keepPreviousData: true, keepPreviousData: true,
retry: false, retry: false,
@ -64,14 +62,7 @@ const ListView = () => {
}, },
})); }));
const handleToggle = (id) => { const [modalLogId, setModalLogId] = useState(null);
setIsModalOpen((prev) => !prev);
if (data.results && id) {
const actionData = data.results.find((action) => action.id === id);
setDetailsActionData(actionData);
}
};
return ( return (
<Main aria-busy={isLoading}> <Main aria-busy={isLoading}>
@ -91,11 +82,15 @@ const ListView = () => {
withBulkActions withBulkActions
isLoading={isLoading} isLoading={isLoading}
> >
<TableRows headers={headers} rows={data?.results || []} onModalToggle={handleToggle} /> <TableRows
headers={headers}
rows={data?.results || []}
onOpenModal={(id) => setModalLogId(id)}
/>
</DynamicTable> </DynamicTable>
<PaginationFooter pagination={data?.pagination} /> <PaginationFooter pagination={data?.pagination} />
</ContentLayout> </ContentLayout>
{isModalOpen && <ModalDialog onToggle={handleToggle} data={detailsActionData} />} {modalLogId && <Modal handleClose={() => setModalLogId(null)} logId={modalLogId} />}
</Main> </Main>
); );
}; };

View File

@ -2,13 +2,13 @@ import React from 'react';
import { Router } from 'react-router-dom'; import { Router } from 'react-router-dom';
import { IntlProvider } from 'react-intl'; import { IntlProvider } from 'react-intl';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { render, screen, waitFor } from '@testing-library/react'; import { render, screen, waitFor, within } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from 'react-query'; import { QueryClient, QueryClientProvider } from 'react-query';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { ThemeProvider, lightTheme } from '@strapi/design-system'; import { ThemeProvider, lightTheme } from '@strapi/design-system';
import { TrackingProvider } from '@strapi/helper-plugin'; import { TrackingProvider } from '@strapi/helper-plugin';
import ListView from '../index'; import ListView from '../index';
import { TEST_DATA, getBigTestData } from './utils/data'; import { TEST_PAGE_DATA, TEST_SINGLE_DATA, getBigTestPageData } from './utils/data';
const history = createMemoryHistory(); const history = createMemoryHistory();
const user = userEvent.setup(); const user = userEvent.setup();
@ -87,7 +87,7 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView', () => {
it('should show a list of audit logs with all actions', async () => { it('should show a list of audit logs with all actions', async () => {
mockUseQuery.mockReturnValue({ mockUseQuery.mockReturnValue({
data: { data: {
results: TEST_DATA, results: TEST_PAGE_DATA,
}, },
isLoading: false, isLoading: false,
}); });
@ -104,20 +104,31 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView', () => {
it('should open a modal when clicked on a table row and close modal when clicked', async () => { it('should open a modal when clicked on a table row and close modal when clicked', async () => {
mockUseQuery.mockReturnValue({ mockUseQuery.mockReturnValue({
data: { data: {
results: TEST_DATA, results: TEST_PAGE_DATA,
}, },
isLoading: false, isLoading: false,
}); });
render(App);
const { container } = render(App);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
const rows = container.querySelector('tbody').querySelectorAll('tr'); mockUseQuery.mockReturnValue({
await user.click(rows[0]); data: TEST_SINGLE_DATA,
expect(screen.getByRole('dialog')).toBeInTheDocument(); status: 'success',
});
const label = screen.getByText(/close the modal/i); const auditLogRow = screen.getByText('Create role').closest('tr');
const closeButton = label.closest('button'); await user.click(auditLogRow);
const modal = screen.getByRole('dialog');
expect(modal).toBeInTheDocument();
const modalContainer = within(modal);
expect(modalContainer.getByText('Create role')).toBeInTheDocument();
expect(modalContainer.getByText('test user')).toBeInTheDocument();
expect(modalContainer.getAllByText('December 22, 2022, 16:11:03')).toHaveLength(3);
const closeButton = modalContainer.getByText(/close the modal/i).closest('button');
await user.click(closeButton); await user.click(closeButton);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
}); });
@ -125,7 +136,7 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView', () => {
it('should show pagination and be on page 1 on first render', async () => { it('should show pagination and be on page 1 on first render', async () => {
mockUseQuery.mockReturnValue({ mockUseQuery.mockReturnValue({
data: { data: {
results: getBigTestData(15), results: getBigTestPageData(15),
pagination: { pagination: {
page: 1, page: 1,
pageSize: 10, pageSize: 10,
@ -147,7 +158,7 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView', () => {
it('paginates the results', async () => { it('paginates the results', async () => {
mockUseQuery.mockReturnValue({ mockUseQuery.mockReturnValue({
data: { data: {
results: getBigTestData(35), results: getBigTestPageData(35),
pagination: { pagination: {
page: 1, page: 1,
pageSize: 10, pageSize: 10,
@ -191,7 +202,7 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView', () => {
mockUseQuery.mockReturnValue({ mockUseQuery.mockReturnValue({
data: { data: {
results: getBigTestData(20), results: getBigTestPageData(20),
pagination: { pagination: {
page: 1, page: 1,
pageSize: 20, pageSize: 20,

View File

@ -1,4 +1,4 @@
export const TEST_DATA = [ const TEST_PAGE_DATA = [
{ {
id: 1, id: 1,
action: 'role.create', action: 'role.create',
@ -37,15 +37,31 @@ export const TEST_DATA = [
}, },
]; ];
export const getBigTestData = (quantity) => { const TEST_SINGLE_DATA = {
id: 1,
action: 'role.create',
date: '2022-12-22T16:11:03.126Z',
payload: {
meta: 'data',
},
user: {
id: 1,
fullname: 'test user',
email: 'test@test.com',
},
};
const getBigTestPageData = (quantity) => {
const data = []; const data = [];
for (let i = 0; i < quantity; i++) { for (let i = 0; i < quantity; i++) {
data.push({ data.push({
...TEST_DATA[i % TEST_DATA.length], ...TEST_PAGE_DATA[i % TEST_PAGE_DATA.length],
id: i + 1, id: i + 1,
}); });
} }
return data; return data;
}; };
export { TEST_PAGE_DATA, TEST_SINGLE_DATA, getBigTestPageData };