mirror of
https://github.com/strapi/strapi.git
synced 2025-11-02 02:44:55 +00:00
Merge pull request #19020 from strapi/chore/convert-audit-logs-ee-pages
chore(admin): convert auditLogs page to TS
This commit is contained in:
commit
4e6961c7b8
@ -5,10 +5,9 @@ export const ROUTES_EE: Route[] = [
|
||||
? [
|
||||
{
|
||||
async Component() {
|
||||
// @ts-expect-error – No types, yet.
|
||||
const component = await import('./pages/AuditLogs/ProtectedListPage');
|
||||
const { ProtectedAuditLogsListPage } = await import('./pages/AuditLogs/ListPage');
|
||||
|
||||
return component;
|
||||
return ProtectedAuditLogsListPage;
|
||||
},
|
||||
to: '/settings/audit-logs',
|
||||
exact: true,
|
||||
|
||||
@ -0,0 +1,148 @@
|
||||
import {
|
||||
ActionLayout,
|
||||
Box,
|
||||
ContentLayout,
|
||||
HeaderLayout,
|
||||
Layout,
|
||||
Main,
|
||||
} from '@strapi/design-system';
|
||||
import {
|
||||
AnErrorOccurred,
|
||||
DynamicTable,
|
||||
SettingsPageTitle,
|
||||
useFocusWhenNavigate,
|
||||
useQueryParams,
|
||||
useRBAC,
|
||||
CheckPagePermissions,
|
||||
} from '@strapi/helper-plugin';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { Filters } from '../../../../../../../admin/src/pages/Settings/components/Filters';
|
||||
import { selectAdminPermissions } from '../../../../../../../admin/src/selectors';
|
||||
import { SanitizedAdminUserForAuditLogs } from '../../../../../../../shared/contracts/audit-logs';
|
||||
|
||||
import { Modal } from './components/Modal';
|
||||
import { PaginationFooter } from './components/PaginationFooter';
|
||||
import { TableHeader, TableRows } from './components/TableRows';
|
||||
import { useAuditLogsData } from './hooks/useAuditLogsData';
|
||||
import { getDisplayedFilters } from './utils/getDisplayedFilters';
|
||||
|
||||
export const ListView = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const permissions = useSelector(selectAdminPermissions);
|
||||
|
||||
const {
|
||||
allowedActions: { canRead: canReadAuditLogs, canReadUsers },
|
||||
} = useRBAC({
|
||||
...permissions.settings?.auditLogs,
|
||||
readUsers: permissions.settings?.users.read || [],
|
||||
});
|
||||
|
||||
const [{ query }, setQuery] = useQueryParams<{ id?: string | null }>();
|
||||
const { auditLogs, users, isLoading, hasError } = useAuditLogsData({
|
||||
canReadAuditLogs,
|
||||
canReadUsers,
|
||||
});
|
||||
|
||||
useFocusWhenNavigate();
|
||||
|
||||
const displayedFilters = getDisplayedFilters({ formatMessage, users, canReadUsers });
|
||||
|
||||
const title = formatMessage({
|
||||
id: 'global.auditLogs',
|
||||
defaultMessage: 'Audit Logs',
|
||||
});
|
||||
|
||||
const headers = [
|
||||
{
|
||||
name: 'action',
|
||||
key: 'action',
|
||||
metadatas: {
|
||||
label: formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.action',
|
||||
defaultMessage: 'Action',
|
||||
}),
|
||||
sortable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
key: 'date',
|
||||
metadatas: {
|
||||
label: formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.date',
|
||||
defaultMessage: 'Date',
|
||||
}),
|
||||
sortable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'user',
|
||||
name: 'user',
|
||||
metadatas: {
|
||||
label: formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.user',
|
||||
defaultMessage: 'User',
|
||||
}),
|
||||
sortable: false,
|
||||
},
|
||||
// In this case, the passed parameter cannot and shouldn't be something else than User
|
||||
cellFormatter: (user) => (user ? (user as SanitizedAdminUserForAuditLogs).displayName : ''),
|
||||
},
|
||||
] satisfies TableHeader[];
|
||||
|
||||
if (hasError) {
|
||||
return (
|
||||
<Layout>
|
||||
<ContentLayout>
|
||||
<Box paddingTop={8}>
|
||||
<AnErrorOccurred />
|
||||
</Box>
|
||||
</ContentLayout>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Main aria-busy={isLoading}>
|
||||
<SettingsPageTitle name={title} />
|
||||
<HeaderLayout
|
||||
title={title}
|
||||
subtitle={formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.listview.header.subtitle',
|
||||
defaultMessage: 'Logs of all the activities that happened in your environment',
|
||||
})}
|
||||
/>
|
||||
{/* @ts-expect-error – TODO: fix the way filters work and are passed around, this will be a headache. */}
|
||||
<ActionLayout startActions={<Filters displayedFilters={displayedFilters} />} />
|
||||
<ContentLayout>
|
||||
<DynamicTable
|
||||
contentType="Audit logs"
|
||||
headers={headers}
|
||||
rows={auditLogs?.results || []}
|
||||
withBulkActions
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<TableRows
|
||||
headers={headers}
|
||||
rows={auditLogs?.results || []}
|
||||
onOpenModal={(id) => setQuery({ id: `${id}` })}
|
||||
/>
|
||||
</DynamicTable>
|
||||
{auditLogs?.pagination && <PaginationFooter pagination={auditLogs.pagination} />}
|
||||
</ContentLayout>
|
||||
{query?.id && <Modal handleClose={() => setQuery({ id: null }, 'remove')} logId={query.id} />}
|
||||
</Main>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProtectedAuditLogsListPage = () => {
|
||||
const permissions = useSelector(selectAdminPermissions);
|
||||
|
||||
return (
|
||||
<CheckPagePermissions permissions={permissions.settings?.auditLogs?.main}>
|
||||
<ListView />
|
||||
</CheckPagePermissions>
|
||||
);
|
||||
};
|
||||
@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Combobox, ComboboxOption } from '@strapi/design-system';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const ComboboxFilter = ({ value, options, onChange }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const ariaLabel = formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.filter.aria-label',
|
||||
defaultMessage: 'Search and select an option to filter',
|
||||
});
|
||||
|
||||
return (
|
||||
<Combobox aria-label={ariaLabel} value={value} onChange={onChange}>
|
||||
{options.map(({ label, customValue }) => {
|
||||
return (
|
||||
<ComboboxOption key={customValue} value={customValue}>
|
||||
{label}
|
||||
</ComboboxOption>
|
||||
);
|
||||
})}
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
|
||||
ComboboxFilter.defaultProps = {
|
||||
value: null,
|
||||
};
|
||||
|
||||
ComboboxFilter.propTypes = {
|
||||
value: PropTypes.string,
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.string.isRequired,
|
||||
customValue: PropTypes.string.isRequired,
|
||||
}).isRequired
|
||||
).isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ComboboxFilter;
|
||||
@ -1,107 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Box, Flex, Grid, JSONInput, Loader, Typography } from '@strapi/design-system';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
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>Loading content...</Loader>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
const { action, user, payload } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box marginBottom={3}>
|
||||
<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}
|
||||
marginBottom={4}
|
||||
background="neutral100"
|
||||
hasRadius
|
||||
>
|
||||
<ActionItem
|
||||
actionLabel={formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.action',
|
||||
defaultMessage: 'Action',
|
||||
})}
|
||||
actionName={formatMessage(
|
||||
{
|
||||
id: `Settings.permissions.auditLogs.${action}`,
|
||||
defaultMessage: getDefaultMessage(action),
|
||||
},
|
||||
{ model: payload?.model }
|
||||
)}
|
||||
/>
|
||||
<ActionItem
|
||||
actionLabel={formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.date',
|
||||
defaultMessage: 'Date',
|
||||
})}
|
||||
actionName={formattedDate}
|
||||
/>
|
||||
<ActionItem
|
||||
actionLabel={formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.user',
|
||||
defaultMessage: 'User',
|
||||
})}
|
||||
actionName={user?.displayName || '-'}
|
||||
/>
|
||||
<ActionItem
|
||||
actionLabel={formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.userId',
|
||||
defaultMessage: 'User ID',
|
||||
})}
|
||||
actionName={user?.id.toString() || '-'}
|
||||
/>
|
||||
</Grid>
|
||||
<JSONInput
|
||||
value={JSON.stringify(payload, null, 2)}
|
||||
disabled
|
||||
label={formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.payload',
|
||||
defaultMessage: 'Payload',
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Flex, Typography } from '@strapi/design-system';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ActionItem = ({ actionLabel, actionName }) => {
|
||||
return (
|
||||
<Flex direction="column" alignItems="baseline" gap={1}>
|
||||
<Typography textColor="neutral600" variant="sigma">
|
||||
{actionLabel}
|
||||
</Typography>
|
||||
<Typography textColor="neutral600">{actionName}</Typography>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
ActionItem.propTypes = {
|
||||
actionLabel: PropTypes.string.isRequired,
|
||||
actionName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ActionItem;
|
||||
@ -1,62 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ModalBody, ModalHeader, ModalLayout } from '@strapi/design-system';
|
||||
import { Breadcrumbs, Crumb } from '@strapi/design-system/v2';
|
||||
import { useFetchClient, useNotification } from '@strapi/helper-plugin';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import useFormatTimeStamp from '../hooks/useFormatTimeStamp';
|
||||
|
||||
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}`);
|
||||
|
||||
if (!data) {
|
||||
throw new Error('Audit log not found');
|
||||
}
|
||||
|
||||
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>
|
||||
{/**
|
||||
* TODO: this is not semantically correct and should be amended.
|
||||
*/}
|
||||
<Breadcrumbs label={formattedDate} id="title">
|
||||
<Crumb isCurrent>{formattedDate}</Crumb>
|
||||
</Breadcrumbs>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<ActionBody status={status} data={data} formattedDate={formattedDate} />
|
||||
</ModalBody>
|
||||
</ModalLayout>
|
||||
);
|
||||
};
|
||||
|
||||
Modal.propTypes = {
|
||||
handleClose: PropTypes.func.isRequired,
|
||||
logId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Box, Flex } from '@strapi/design-system';
|
||||
import { PageSizeURLQuery, PaginationURLQuery } from '@strapi/helper-plugin';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const PaginationFooter = ({ pagination }) => {
|
||||
return (
|
||||
<Box paddingTop={4}>
|
||||
<Flex alignItems="flex-end" justifyContent="space-between">
|
||||
<PageSizeURLQuery />
|
||||
<PaginationURLQuery pagination={pagination} />
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
PaginationFooter.defaultProps = {
|
||||
pagination: {
|
||||
pageCount: 0,
|
||||
pageSize: 50,
|
||||
total: 0,
|
||||
},
|
||||
};
|
||||
|
||||
PaginationFooter.propTypes = {
|
||||
pagination: PropTypes.shape({
|
||||
page: PropTypes.number,
|
||||
pageCount: PropTypes.number,
|
||||
pageSize: PropTypes.number,
|
||||
total: PropTypes.number,
|
||||
}),
|
||||
};
|
||||
|
||||
export default PaginationFooter;
|
||||
@ -1,110 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
ActionLayout,
|
||||
Box,
|
||||
ContentLayout,
|
||||
HeaderLayout,
|
||||
Layout,
|
||||
Main,
|
||||
} from '@strapi/design-system';
|
||||
import {
|
||||
AnErrorOccurred,
|
||||
DynamicTable,
|
||||
SettingsPageTitle,
|
||||
useFocusWhenNavigate,
|
||||
useQueryParams,
|
||||
useRBAC,
|
||||
} from '@strapi/helper-plugin';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { Filters } from '../../../../../../../../admin/src/pages/Settings/components/Filters';
|
||||
import { selectAdminPermissions } from '../../../../../../../../admin/src/selectors';
|
||||
|
||||
import useAuditLogsData from './hooks/useAuditLogsData';
|
||||
import Modal from './Modal';
|
||||
import PaginationFooter from './PaginationFooter';
|
||||
import TableRows from './TableRows';
|
||||
import getDisplayedFilters from './utils/getDisplayedFilters';
|
||||
import tableHeaders from './utils/tableHeaders';
|
||||
|
||||
const ListView = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const permissions = useSelector(selectAdminPermissions);
|
||||
|
||||
const {
|
||||
allowedActions: { canRead: canReadAuditLogs, canReadUsers },
|
||||
} = useRBAC({
|
||||
...permissions.settings.auditLogs,
|
||||
readUsers: permissions.settings.users.read,
|
||||
});
|
||||
|
||||
const [{ query }, setQuery] = useQueryParams();
|
||||
const { auditLogs, users, isLoading, hasError } = useAuditLogsData({
|
||||
canReadAuditLogs,
|
||||
canReadUsers,
|
||||
});
|
||||
|
||||
useFocusWhenNavigate();
|
||||
|
||||
const displayedFilters = getDisplayedFilters({ formatMessage, users, canReadUsers });
|
||||
|
||||
const title = formatMessage({
|
||||
id: 'global.auditLogs',
|
||||
defaultMessage: 'Audit Logs',
|
||||
});
|
||||
|
||||
const headers = tableHeaders.map((header) => ({
|
||||
...header,
|
||||
metadatas: {
|
||||
...header.metadatas,
|
||||
label: formatMessage(header.metadatas.label),
|
||||
},
|
||||
}));
|
||||
|
||||
if (hasError) {
|
||||
return (
|
||||
<Layout>
|
||||
<ContentLayout>
|
||||
<Box paddingTop={8}>
|
||||
<AnErrorOccurred />
|
||||
</Box>
|
||||
</ContentLayout>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Main aria-busy={isLoading}>
|
||||
<SettingsPageTitle name={title} />
|
||||
<HeaderLayout
|
||||
title={title}
|
||||
subtitle={formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.listview.header.subtitle',
|
||||
defaultMessage: 'Logs of all the activities that happened in your environment',
|
||||
})}
|
||||
/>
|
||||
<ActionLayout startActions={<Filters displayedFilters={displayedFilters} />} />
|
||||
<ContentLayout canRead={canReadAuditLogs}>
|
||||
<DynamicTable
|
||||
contentType="Audit logs"
|
||||
headers={headers}
|
||||
rows={auditLogs?.results || []}
|
||||
withBulkActions
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<TableRows
|
||||
headers={headers}
|
||||
rows={auditLogs?.results || []}
|
||||
onOpenModal={(id) => setQuery({ id })}
|
||||
/>
|
||||
</DynamicTable>
|
||||
<PaginationFooter pagination={auditLogs?.pagination} />
|
||||
</ContentLayout>
|
||||
{query?.id && <Modal handleClose={() => setQuery({ id: null }, 'remove')} logId={query.id} />}
|
||||
</Main>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListView;
|
||||
@ -1,19 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { CheckPagePermissions } from '@strapi/helper-plugin';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { selectAdminPermissions } from '../../../../../../../../admin/src/selectors';
|
||||
import ListView from '../ListView';
|
||||
|
||||
const ProtectedListPage = () => {
|
||||
const permissions = useSelector(selectAdminPermissions);
|
||||
|
||||
return (
|
||||
<CheckPagePermissions permissions={permissions.settings.auditLogs.main}>
|
||||
<ListView />
|
||||
</CheckPagePermissions>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProtectedListPage;
|
||||
@ -0,0 +1,32 @@
|
||||
import { Combobox, ComboboxOption, ComboboxProps } from '@strapi/design-system';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
type ComboboxFilterProps = {
|
||||
value?: string;
|
||||
options?: { label: string; customValue: string }[];
|
||||
onChange?: ComboboxProps['onChange'];
|
||||
};
|
||||
|
||||
export const ComboboxFilter = (
|
||||
{ value, options, onChange }: ComboboxFilterProps = {
|
||||
value: undefined,
|
||||
}
|
||||
) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const ariaLabel = formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.filter.aria-label',
|
||||
defaultMessage: 'Search and select an option to filter',
|
||||
});
|
||||
|
||||
return (
|
||||
<Combobox aria-label={ariaLabel} value={value} onChange={onChange}>
|
||||
{options?.map(({ label, customValue }) => {
|
||||
return (
|
||||
<ComboboxOption key={customValue} value={customValue}>
|
||||
{label}
|
||||
</ComboboxOption>
|
||||
);
|
||||
})}
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,175 @@
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Grid,
|
||||
JSONInput,
|
||||
Loader,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalLayout,
|
||||
Typography,
|
||||
} from '@strapi/design-system';
|
||||
import { Breadcrumbs, Crumb } from '@strapi/design-system/v2';
|
||||
import { useFetchClient, useNotification } from '@strapi/helper-plugin';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { QueryStatus, useQuery } from 'react-query';
|
||||
|
||||
import { AuditLog, Get } from '../../../../../../../../shared/contracts/audit-logs';
|
||||
import { useFormatTimeStamp } from '../hooks/useFormatTimeStamp';
|
||||
import { actionTypes, getDefaultMessage } from '../utils/getActionTypesDefaultMessages';
|
||||
|
||||
type ActionBodyProps = {
|
||||
status: QueryStatus;
|
||||
data: AuditLog;
|
||||
formattedDate: string;
|
||||
};
|
||||
|
||||
type ModalProps = {
|
||||
handleClose: () => void;
|
||||
logId: string;
|
||||
};
|
||||
|
||||
type ActionItemProps = {
|
||||
actionLabel: string;
|
||||
actionName: string;
|
||||
};
|
||||
|
||||
export const Modal = ({ handleClose, logId }: ModalProps) => {
|
||||
const { get } = useFetchClient();
|
||||
const toggleNotification = useNotification();
|
||||
|
||||
const fetchAuditLog = async (id: string) => {
|
||||
const { data } = await get<Get.Response>(`/admin/audit-logs/${id}`);
|
||||
|
||||
if (!data) {
|
||||
throw new Error('Audit log not found');
|
||||
}
|
||||
|
||||
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 && 'date' in data ? formatTimeStamp(data.date) : '';
|
||||
|
||||
return (
|
||||
<ModalLayout onClose={handleClose} labelledBy="title">
|
||||
<ModalHeader>
|
||||
{/**
|
||||
* TODO: this is not semantically correct and should be amended.
|
||||
*/}
|
||||
<Breadcrumbs label={formattedDate} id="title">
|
||||
<Crumb isCurrent>{formattedDate}</Crumb>
|
||||
</Breadcrumbs>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<ActionBody status={status} data={data as AuditLog} formattedDate={formattedDate} />
|
||||
</ModalBody>
|
||||
</ModalLayout>
|
||||
);
|
||||
};
|
||||
|
||||
const ActionBody = ({ status, data, formattedDate }: ActionBodyProps) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<Flex padding={7} justifyContent="center" alignItems="center">
|
||||
{/**
|
||||
* TODO: this will need to be translated.
|
||||
*/}
|
||||
<Loader>Loading content...</Loader>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
const { action, user, payload } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box marginBottom={3}>
|
||||
<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}
|
||||
marginBottom={4}
|
||||
background="neutral100"
|
||||
hasRadius
|
||||
>
|
||||
<ActionItem
|
||||
actionLabel={formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.action',
|
||||
defaultMessage: 'Action',
|
||||
})}
|
||||
actionName={formatMessage(
|
||||
{
|
||||
id: `Settings.permissions.auditLogs.${action}`,
|
||||
defaultMessage: getDefaultMessage(action as keyof typeof actionTypes),
|
||||
},
|
||||
// @ts-expect-error - any
|
||||
{ model: payload?.model }
|
||||
)}
|
||||
/>
|
||||
<ActionItem
|
||||
actionLabel={formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.date',
|
||||
defaultMessage: 'Date',
|
||||
})}
|
||||
actionName={formattedDate}
|
||||
/>
|
||||
<ActionItem
|
||||
actionLabel={formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.user',
|
||||
defaultMessage: 'User',
|
||||
})}
|
||||
actionName={user?.displayName || '-'}
|
||||
/>
|
||||
<ActionItem
|
||||
actionLabel={formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.userId',
|
||||
defaultMessage: 'User ID',
|
||||
})}
|
||||
actionName={user?.id.toString() || '-'}
|
||||
/>
|
||||
</Grid>
|
||||
<JSONInput
|
||||
value={JSON.stringify(payload, null, 2)}
|
||||
disabled
|
||||
label={formatMessage({
|
||||
id: 'Settings.permissions.auditLogs.payload',
|
||||
defaultMessage: 'Payload',
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ActionItem = ({ actionLabel, actionName }: ActionItemProps) => {
|
||||
return (
|
||||
<Flex direction="column" alignItems="baseline" gap={1}>
|
||||
<Typography textColor="neutral600" variant="sigma">
|
||||
{actionLabel}
|
||||
</Typography>
|
||||
<Typography textColor="neutral600">{actionName}</Typography>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Box, Flex } from '@strapi/design-system';
|
||||
import { PageSizeURLQuery, PaginationURLQuery } from '@strapi/helper-plugin';
|
||||
|
||||
import { Pagination } from '../../../../../../../../shared/contracts/shared';
|
||||
|
||||
type PaginationFooterProps = {
|
||||
pagination: Pagination;
|
||||
};
|
||||
|
||||
export const PaginationFooter = (
|
||||
{ pagination }: PaginationFooterProps = {
|
||||
pagination: {
|
||||
page: 1,
|
||||
pageCount: 0,
|
||||
pageSize: 50,
|
||||
total: 0,
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return (
|
||||
<Box paddingTop={4}>
|
||||
<Flex alignItems="flex-end" justifyContent="space-between">
|
||||
<PageSizeURLQuery />
|
||||
<PaginationURLQuery pagination={pagination} />
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@ -3,17 +3,37 @@ import React from 'react';
|
||||
import { Flex, IconButton, Tbody, Td, Tr, Typography } from '@strapi/design-system';
|
||||
import { onRowClick, stopPropagation } from '@strapi/helper-plugin';
|
||||
import { Eye } from '@strapi/icons';
|
||||
import { Attribute, Entity } from '@strapi/types';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import useFormatTimeStamp from '../hooks/useFormatTimeStamp';
|
||||
import { ListLayoutRow } from '../../../../../../../../admin/src/content-manager/utils/layouts';
|
||||
import { AuditLog } from '../../../../../../../../shared/contracts/audit-logs';
|
||||
import { useFormatTimeStamp } from '../hooks/useFormatTimeStamp';
|
||||
import { getDefaultMessage } from '../utils/getActionTypesDefaultMessages';
|
||||
|
||||
const TableRows = ({ headers, rows, onOpenModal }) => {
|
||||
export interface TableHeader extends Omit<ListLayoutRow, 'metadatas' | 'fieldSchema' | 'name'> {
|
||||
metadatas: Omit<ListLayoutRow['metadatas'], 'label'> & {
|
||||
label: string;
|
||||
};
|
||||
name: keyof AuditLog;
|
||||
fieldSchema?: Attribute.Any | { type: 'custom' };
|
||||
cellFormatter?: (data?: AuditLog[keyof AuditLog]) => React.ReactNode;
|
||||
}
|
||||
|
||||
type TableRowsProps = {
|
||||
headers: TableHeader[];
|
||||
rows: AuditLog[];
|
||||
onOpenModal: (id: Entity.ID) => void;
|
||||
};
|
||||
|
||||
export const TableRows = ({ headers, rows, onOpenModal }: TableRowsProps) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const formatTimeStamp = useFormatTimeStamp();
|
||||
|
||||
const getCellValue = ({ type, value, model }) => {
|
||||
// Not sure that 'value' can be typed properly
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const getCellValue = ({ type, value, model }: { type: string; value: any; model: unknown }) => {
|
||||
if (type === 'date') {
|
||||
return formatTimeStamp(value);
|
||||
}
|
||||
@ -24,6 +44,7 @@ const TableRows = ({ headers, rows, onOpenModal }) => {
|
||||
id: `Settings.permissions.auditLogs.${value}`,
|
||||
defaultMessage: getDefaultMessage(value),
|
||||
},
|
||||
// @ts-expect-error - Model
|
||||
{ model }
|
||||
);
|
||||
}
|
||||
@ -41,13 +62,15 @@ const TableRows = ({ headers, rows, onOpenModal }) => {
|
||||
fn: () => onOpenModal(data.id),
|
||||
})}
|
||||
>
|
||||
{headers.map(({ key, name, cellFormatter }) => {
|
||||
{headers?.map(({ key, name, cellFormatter }) => {
|
||||
const rowValue = data[name];
|
||||
|
||||
return (
|
||||
<Td key={key}>
|
||||
<Typography textColor="neutral800">
|
||||
{getCellValue({
|
||||
type: key,
|
||||
value: cellFormatter ? cellFormatter(data[name]) : data[name],
|
||||
value: cellFormatter ? cellFormatter(rowValue) : rowValue,
|
||||
model: data.payload?.model,
|
||||
})}
|
||||
</Typography>
|
||||
@ -83,5 +106,3 @@ TableRows.propTypes = {
|
||||
rows: PropTypes.array,
|
||||
onOpenModal: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default TableRows;
|
||||
@ -1,16 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import { lightTheme, ThemeProvider } from '@strapi/design-system';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
import TableRows from '..';
|
||||
import { TableHeader, TableRows } from '../TableRows';
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
const headers = [
|
||||
const headers: TableHeader[] = [
|
||||
{
|
||||
name: 'action',
|
||||
key: 'action',
|
||||
@ -25,7 +23,8 @@ const headers = [
|
||||
key: 'user',
|
||||
name: 'user',
|
||||
metadatas: { label: 'User', sortable: false },
|
||||
cellFormatter: (value) => (value ? value.fullname : null),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
cellFormatter: (value: any) => (value ? value.fullname : null),
|
||||
},
|
||||
];
|
||||
|
||||
@ -34,37 +33,57 @@ const rows = [
|
||||
id: 1,
|
||||
action: 'role.update',
|
||||
date: '2022-11-14T23:04:00.000Z',
|
||||
payload: {},
|
||||
user: {
|
||||
id: 1,
|
||||
fullname: 'John Doe',
|
||||
email: 'test@email.com',
|
||||
displayName: 'John Doe',
|
||||
isActive: true,
|
||||
blocked: false,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
roles: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
action: 'permission.create',
|
||||
date: '2022-11-04T18:24:00.000Z',
|
||||
payload: {},
|
||||
user: {
|
||||
id: 2,
|
||||
fullname: 'Kai Doe',
|
||||
email: 'test2@email.com',
|
||||
displayName: 'Kai Doe',
|
||||
isActive: true,
|
||||
blocked: false,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
roles: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
action: 'custom.action',
|
||||
date: '2022-11-04T18:23:00.000Z',
|
||||
payload: {},
|
||||
user: {
|
||||
id: 2,
|
||||
fullname: 'Kai Doe',
|
||||
email: 'test2@email.com',
|
||||
displayName: 'Kai Doe',
|
||||
isActive: true,
|
||||
blocked: false,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
roles: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const onModalOpen = jest.fn();
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const App = (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<IntlProvider locale="en" messages={{}} defaultLocale="en" textComponent="span">
|
||||
@ -96,14 +115,15 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView | Dynamic Table | Table Rows', (
|
||||
it('should open a modal when clicked on a view details icon button', () => {
|
||||
render(App);
|
||||
const label = screen.getByText(/update action details/i);
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
const viewDetailsButton = label.closest('button');
|
||||
fireEvent.click(viewDetailsButton);
|
||||
if (viewDetailsButton) fireEvent.click(viewDetailsButton);
|
||||
expect(onModalOpen).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should open a modal when clicked on a row', () => {
|
||||
it('should open a modal when clicked on a row', async () => {
|
||||
render(App);
|
||||
const rows = document.querySelectorAll('tr');
|
||||
const rows = await screen.findAllByRole('row');
|
||||
fireEvent.click(rows[0]);
|
||||
expect(onModalOpen).toHaveBeenCalled();
|
||||
});
|
||||
@ -2,9 +2,16 @@ import { useFetchClient, useNotification, useQueryParams } from '@strapi/helper-
|
||||
import { useQuery } from 'react-query';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { useAdminUsers } from '../../../../../../../../../admin/src/hooks/useAdminUsers';
|
||||
import { useAdminUsers } from '../../../../../../../../admin/src/hooks/useAdminUsers';
|
||||
import { GetAll } from '../../../../../../../../shared/contracts/audit-logs';
|
||||
|
||||
const useAuditLogsData = ({ canReadAuditLogs, canReadUsers }) => {
|
||||
export const useAuditLogsData = ({
|
||||
canReadAuditLogs,
|
||||
canReadUsers,
|
||||
}: {
|
||||
canReadAuditLogs: boolean;
|
||||
canReadUsers: boolean;
|
||||
}) => {
|
||||
const { get } = useFetchClient();
|
||||
const { search } = useLocation();
|
||||
const toggleNotification = useNotification();
|
||||
@ -14,7 +21,7 @@ const useAuditLogsData = ({ canReadAuditLogs, canReadUsers }) => {
|
||||
keepPreviousData: true,
|
||||
retry: false,
|
||||
staleTime: 1000 * 20, // 20 seconds
|
||||
onError: (error) => toggleNotification({ type: 'warning', message: error.message }),
|
||||
onError: (error: Error) => toggleNotification({ type: 'warning', message: error.message }),
|
||||
};
|
||||
|
||||
const {
|
||||
@ -37,7 +44,7 @@ const useAuditLogsData = ({ canReadAuditLogs, canReadUsers }) => {
|
||||
} = useQuery(
|
||||
['auditLogs', search],
|
||||
async () => {
|
||||
const { data } = await get(`/admin/audit-logs`, {
|
||||
const { data } = await get<GetAll.Response['data']>(`/admin/audit-logs`, {
|
||||
params: query,
|
||||
});
|
||||
|
||||
@ -56,5 +63,3 @@ const useAuditLogsData = ({ canReadAuditLogs, canReadUsers }) => {
|
||||
hasError: isAuditLogsError || isUsersError,
|
||||
};
|
||||
};
|
||||
|
||||
export default useAuditLogsData;
|
||||
@ -1,10 +1,10 @@
|
||||
import parseISO from 'date-fns/parseISO';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const useFormatTimeStamp = () => {
|
||||
export const useFormatTimeStamp = () => {
|
||||
const { formatDate } = useIntl();
|
||||
|
||||
const formatTimeStamp = (value) => {
|
||||
const formatTimeStamp = (value: string) => {
|
||||
const date = parseISO(value);
|
||||
|
||||
const formattedDate = formatDate(date, {
|
||||
@ -20,5 +20,3 @@ const useFormatTimeStamp = () => {
|
||||
|
||||
return formatTimeStamp;
|
||||
};
|
||||
|
||||
export default useFormatTimeStamp;
|
||||
@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { fixtures } from '@strapi/admin-test-utils';
|
||||
import { lightTheme, ThemeProvider } from '@strapi/design-system';
|
||||
@ -11,12 +9,14 @@ import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
import useAuditLogsData from '../hooks/useAuditLogsData';
|
||||
import ListView from '../index';
|
||||
import { useAuditLogsData } from '../hooks/useAuditLogsData';
|
||||
import { ListView } from '../ListPage';
|
||||
|
||||
import { getBigTestPageData, TEST_PAGE_DATA, TEST_SINGLE_DATA } from './utils/data';
|
||||
|
||||
jest.mock('../hooks/useAuditLogsData', () => jest.fn());
|
||||
jest.mock('../hooks/useAuditLogsData', () => ({
|
||||
useAuditLogsData: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockUseQuery = jest.fn();
|
||||
|
||||
@ -41,8 +41,8 @@ jest.mock('@strapi/helper-plugin', () => ({
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
const setup = (props) => ({
|
||||
...render(<ListView {...props} />, {
|
||||
const setup = () => ({
|
||||
...render(<ListView />, {
|
||||
wrapper({ children }) {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: {
|
||||
@ -105,6 +105,7 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView', () => {
|
||||
});
|
||||
|
||||
it('should render page with right header details', () => {
|
||||
// @ts-expect-error - mock
|
||||
useAuditLogsData.mockReturnValue({
|
||||
auditLogs: {
|
||||
results: [],
|
||||
@ -121,6 +122,7 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView', () => {
|
||||
});
|
||||
|
||||
it('should show a list of audit logs with all actions', async () => {
|
||||
// @ts-expect-error - mock
|
||||
useAuditLogsData.mockReturnValue({
|
||||
auditLogs: {
|
||||
results: TEST_PAGE_DATA,
|
||||
@ -128,12 +130,19 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView', () => {
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const { getByText } = setup();
|
||||
setup();
|
||||
|
||||
await waitFor(() => expect(getByText('Create role')).toBeInTheDocument());
|
||||
await waitFor(() => expect(getByText('Delete role')).toBeInTheDocument());
|
||||
await waitFor(() => expect(getByText('Create entry (article)')).toBeInTheDocument());
|
||||
await waitFor(() => expect(getByText('Admin logout')).toBeInTheDocument());
|
||||
const createRoleEl = await screen.findByText('Create role');
|
||||
expect(createRoleEl).toBeInTheDocument();
|
||||
|
||||
const deleteRoleEl = await screen.findByText('Delete role');
|
||||
expect(deleteRoleEl).toBeInTheDocument();
|
||||
|
||||
const createEntryEl = await screen.findByText('Create entry (article)');
|
||||
expect(createEntryEl).toBeInTheDocument();
|
||||
|
||||
const adminLogoutEl = await screen.findByText('Admin logout');
|
||||
expect(adminLogoutEl).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open a modal when clicked on a table row and close modal when clicked', async () => {
|
||||
@ -152,8 +161,10 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView', () => {
|
||||
status: 'success',
|
||||
});
|
||||
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
const auditLogRow = getByText('Create role').closest('tr');
|
||||
await user.click(auditLogRow);
|
||||
|
||||
if (auditLogRow) await user.click(auditLogRow);
|
||||
|
||||
const modal = screen.getByRole('dialog');
|
||||
expect(modal).toBeInTheDocument();
|
||||
@ -163,12 +174,16 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView', () => {
|
||||
expect(modalContainer.getByText('test user')).toBeInTheDocument();
|
||||
expect(modalContainer.getAllByText('December 22, 2022, 16:11:03')).toHaveLength(2);
|
||||
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
const closeButton = modalContainer.getByText(/close the modal/i).closest('button');
|
||||
await user.click(closeButton);
|
||||
|
||||
if (closeButton) await user.click(closeButton);
|
||||
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show pagination and be on page 1 on first render', async () => {
|
||||
// @ts-expect-error - mock
|
||||
useAuditLogsData.mockReturnValue({
|
||||
auditLogs: {
|
||||
results: getBigTestPageData(15),
|
||||
@ -182,12 +197,17 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView', () => {
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const { getByText } = setup();
|
||||
setup();
|
||||
|
||||
await waitFor(() => expect(getByText(/go to page 1/i).closest('a')).toHaveClass('active'));
|
||||
const goToPageEl = await screen.findByText(/go to page 1/i);
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
const goToPageLink = goToPageEl.closest('a');
|
||||
|
||||
expect(goToPageLink).toHaveClass('active');
|
||||
});
|
||||
|
||||
it('should paginate the results', async () => {
|
||||
// @ts-expect-error - mock
|
||||
useAuditLogsData.mockReturnValue({
|
||||
auditLogs: {
|
||||
results: getBigTestPageData(35),
|
||||
@ -207,28 +227,37 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView', () => {
|
||||
// Should have pagination section with 4 pages
|
||||
const pagination = getByLabelText(/pagination/i);
|
||||
expect(pagination).toBeVisible();
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
const pageButtons = getAllByText(/go to page \d+/i).map((el) => el.closest('a'));
|
||||
expect(pageButtons.length).toBe(4);
|
||||
|
||||
// Can't go to previous page since there isn't one
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
expect(getByText(/go to previous page/i).closest('a')).toHaveAttribute('aria-disabled', 'true');
|
||||
|
||||
// Can go to next page
|
||||
await user.click(getByText(/go to next page/i).closest('a'));
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
const nextPageLinkEl = getByText(/go to next page/i).closest('a');
|
||||
if (nextPageLinkEl) await user.click(nextPageLinkEl);
|
||||
expect(history.location.search).toBe('?page=2');
|
||||
|
||||
// Can go to previous page
|
||||
await user.click(getByText(/go to previous page/i).closest('a'));
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
const previousPageLinkEl = getByText(/go to previous page/i).closest('a');
|
||||
if (previousPageLinkEl) await user.click(previousPageLinkEl);
|
||||
expect(history.location.search).toBe('?page=1');
|
||||
|
||||
// Can go to specific page
|
||||
await user.click(getByText(/go to page 3/i).closest('a'));
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
const thirdPageLinkEl = getByText(/go to page 3/i).closest('a');
|
||||
if (thirdPageLinkEl) await user.click(thirdPageLinkEl);
|
||||
expect(history.location.search).toBe('?page=3');
|
||||
});
|
||||
|
||||
it('should show 20 elements if pageSize is 20', async () => {
|
||||
history.location.search = '?pageSize=20';
|
||||
|
||||
// @ts-expect-error - mock
|
||||
useAuditLogsData.mockReturnValue({
|
||||
auditLogs: {
|
||||
results: getBigTestPageData(20),
|
||||
@ -244,11 +273,17 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView', () => {
|
||||
|
||||
const { container } = setup();
|
||||
|
||||
const rows = await waitFor(() => container.querySelector('tbody').querySelectorAll('tr'));
|
||||
expect(rows.length).toEqual(20);
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
const tableRows = container.querySelector('tbody')?.querySelectorAll('tr');
|
||||
|
||||
if (tableRows) {
|
||||
const rows = await waitFor(() => tableRows);
|
||||
expect(rows.length).toEqual(20);
|
||||
}
|
||||
});
|
||||
|
||||
it('should show the correct inputs for filtering', async () => {
|
||||
// @ts-expect-error - mock
|
||||
useAuditLogsData.mockReturnValue({
|
||||
auditLogs: {
|
||||
results: TEST_PAGE_DATA,
|
||||
@ -260,8 +295,8 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView', () => {
|
||||
const filtersButton = getByRole('button', { name: /filters/i });
|
||||
await user.click(filtersButton);
|
||||
|
||||
const filterButton = getByLabelText(/select field/i, { name: 'action' });
|
||||
const operatorButton = getByLabelText(/select filter/i, { name: 'is' });
|
||||
const filterButton = getByLabelText(/select field/i);
|
||||
const operatorButton = getByLabelText(/select filter/i);
|
||||
const comboBoxInput = getByPlaceholderText(/select or enter a value/i);
|
||||
const addFilterButton = getByRole('button', { name: /add filter/i });
|
||||
|
||||
@ -272,6 +307,7 @@ describe('ADMIN | Pages | AUDIT LOGS | ListView', () => {
|
||||
});
|
||||
|
||||
it('should add filters to the query params', async () => {
|
||||
// @ts-expect-error - mock
|
||||
useAuditLogsData.mockReturnValue({
|
||||
auditLogs: {
|
||||
results: TEST_PAGE_DATA,
|
||||
@ -57,7 +57,7 @@ const TEST_SINGLE_DATA = {
|
||||
},
|
||||
};
|
||||
|
||||
const getBigTestPageData = (quantity) => {
|
||||
const getBigTestPageData = (quantity: number) => {
|
||||
const data = [];
|
||||
|
||||
for (let i = 0; i < quantity; i++) {
|
||||
@ -29,6 +29,6 @@ export const actionTypes = {
|
||||
'permission.delete': 'Delete permission',
|
||||
};
|
||||
|
||||
export const getDefaultMessage = (value) => {
|
||||
export const getDefaultMessage = (value: keyof typeof actionTypes) => {
|
||||
return actionTypes[value] || value;
|
||||
};
|
||||
@ -1,4 +1,7 @@
|
||||
import ComboboxFilter from '../ComboboxFilter';
|
||||
import { IntlShape } from 'react-intl';
|
||||
|
||||
import { SanitizedAdminUser } from '../../../../../../../../shared/contracts/shared';
|
||||
import { ComboboxFilter } from '../components/ComboboxFilter';
|
||||
|
||||
import { actionTypes, getDefaultMessage } from './getActionTypesDefaultMessages';
|
||||
|
||||
@ -13,8 +16,17 @@ const customOperators = [
|
||||
},
|
||||
];
|
||||
|
||||
const getDisplayedFilters = ({ formatMessage, users, canReadUsers }) => {
|
||||
const actionOptions = Object.keys(actionTypes).map((action) => {
|
||||
export const getDisplayedFilters = ({
|
||||
formatMessage,
|
||||
users,
|
||||
canReadUsers,
|
||||
}: {
|
||||
formatMessage: IntlShape['formatMessage'];
|
||||
users: SanitizedAdminUser[];
|
||||
canReadUsers: boolean;
|
||||
}) => {
|
||||
// Default return of Object.keys function is string
|
||||
const actionOptions = (Object.keys(actionTypes) as (keyof typeof actionTypes)[]).map((action) => {
|
||||
return {
|
||||
label: formatMessage(
|
||||
{
|
||||
@ -54,7 +66,7 @@ const getDisplayedFilters = ({ formatMessage, users, canReadUsers }) => {
|
||||
];
|
||||
|
||||
if (canReadUsers && users) {
|
||||
const getDisplayNameFromUser = (user) => {
|
||||
const getDisplayNameFromUser = (user: SanitizedAdminUser) => {
|
||||
if (user.username) {
|
||||
return user.username;
|
||||
}
|
||||
@ -103,5 +115,3 @@ const getDisplayedFilters = ({ formatMessage, users, canReadUsers }) => {
|
||||
|
||||
return filters;
|
||||
};
|
||||
|
||||
export default getDisplayedFilters;
|
||||
@ -1,4 +1,7 @@
|
||||
const tableHeaders = [
|
||||
import { SanitizedAdminUserForAuditLogs } from '../../../../../../../../shared/contracts/audit-logs';
|
||||
import { TableHeader } from '../components/TableRows';
|
||||
|
||||
export const tableHeaders = [
|
||||
{
|
||||
name: 'action',
|
||||
key: 'action',
|
||||
@ -31,8 +34,7 @@ const tableHeaders = [
|
||||
},
|
||||
sortable: false,
|
||||
},
|
||||
cellFormatter: (user) => (user ? user.displayName : ''),
|
||||
cellFormatter: (user: any) =>
|
||||
user ? (user as SanitizedAdminUserForAuditLogs).displayName : '',
|
||||
},
|
||||
];
|
||||
|
||||
export default tableHeaders;
|
||||
] as const;
|
||||
@ -1,19 +1,27 @@
|
||||
import getDisplayedFilters from '../utils/getDisplayedFilters';
|
||||
import { getDisplayedFilters } from '../getDisplayedFilters';
|
||||
|
||||
const mockUsers = [
|
||||
{
|
||||
id: 1,
|
||||
firstname: 'test',
|
||||
lastname: 'tester',
|
||||
username: null,
|
||||
email: 'test@test.com',
|
||||
isActive: true,
|
||||
blocked: false,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
roles: [],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
firstname: 'test2',
|
||||
lastname: 'tester2',
|
||||
username: null,
|
||||
email: 'test2@test.com',
|
||||
isActive: true,
|
||||
blocked: false,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
roles: [],
|
||||
},
|
||||
];
|
||||
|
||||
@ -21,6 +29,7 @@ describe('Audit Logs getDisplayedFilters', () => {
|
||||
it('should return all filters when canReadUsers is true', () => {
|
||||
const filters = getDisplayedFilters({
|
||||
users: mockUsers,
|
||||
// @ts-expect-error - mock
|
||||
formatMessage: jest.fn(({ defaultMessage }) => defaultMessage),
|
||||
canReadUsers: true,
|
||||
});
|
||||
@ -31,6 +40,7 @@ describe('Audit Logs getDisplayedFilters', () => {
|
||||
it('should not return user filter when canReadUsers is false', () => {
|
||||
const filters = getDisplayedFilters({
|
||||
users: mockUsers,
|
||||
// @ts-expect-error - mock
|
||||
formatMessage: jest.fn(({ defaultMessage }) => defaultMessage),
|
||||
canReadUsers: false,
|
||||
});
|
||||
@ -1,6 +1,11 @@
|
||||
import { errors } from '@strapi/utils';
|
||||
import { Entity, Pagination, SanitizedAdminUser } from './shared';
|
||||
|
||||
// displayName seems to be used only for audit logs
|
||||
export interface SanitizedAdminUserForAuditLogs extends SanitizedAdminUser {
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
interface AuditLog extends Pick<Entity, 'id'> {
|
||||
date: string;
|
||||
action: string;
|
||||
@ -9,7 +14,7 @@ interface AuditLog extends Pick<Entity, 'id'> {
|
||||
* However, we know it's JSON.
|
||||
*/
|
||||
payload: Record<string, unknown>;
|
||||
user?: SanitizedAdminUser;
|
||||
user?: SanitizedAdminUserForAuditLogs;
|
||||
}
|
||||
|
||||
namespace GetAll {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user