Add audit log filters

This commit is contained in:
Mark Kaylor 2023-01-12 17:28:54 +01:00
parent 73be2f243e
commit 377f5e8e8a
11 changed files with 227 additions and 51 deletions

View File

@ -0,0 +1,38 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Combobox, ComboboxOption } from '@strapi/design-system/Combobox';
const ComboboxFilter = ({ value, options, setModifiedData }) => {
return (
<Combobox
aria-label="filter"
value={value}
onChange={(value) => setModifiedData((prev) => ({ ...prev, value }))}
>
{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,
setModifiedData: PropTypes.func.isRequired,
};
export default ComboboxFilter;

View File

@ -18,7 +18,7 @@ import { useIntl } from 'react-intl';
import { useMutation, useQuery, useQueryClient } from 'react-query'; import { useMutation, useQuery, useQueryClient } from 'react-query';
import adminPermissions from '../../../../../permissions'; import adminPermissions from '../../../../../permissions';
import TableRows from './DynamicTable/TableRows'; import TableRows from './DynamicTable/TableRows';
import Filters from './Filters'; import Filters from '../../../components/Filters';
import ModalForm from './ModalForm'; import ModalForm from './ModalForm';
import PaginationFooter from './PaginationFooter'; import PaginationFooter from './PaginationFooter';
import { deleteData, fetchData } from './utils/api'; import { deleteData, fetchData } from './utils/api';

View File

@ -185,11 +185,11 @@
"Settings.permissions.auditLogs.details": "Log Details", "Settings.permissions.auditLogs.details": "Log Details",
"Settings.permissions.auditLogs.payload": "Payload", "Settings.permissions.auditLogs.payload": "Payload",
"Settings.permissions.auditLogs.listview.header.subtitle": "Logs of all the activities that happened in your environment", "Settings.permissions.auditLogs.listview.header.subtitle": "Logs of all the activities that happened in your environment",
"Settings.permissions.auditLogs.entry.create": "Create entry ({model})", "Settings.permissions.auditLogs.entry.create": "Create entry {model}",
"Settings.permissions.auditLogs.entry.update": "Update entry ({model})", "Settings.permissions.auditLogs.entry.update": "Update entry {model}",
"Settings.permissions.auditLogs.entry.delete": "Delete entry ({model})", "Settings.permissions.auditLogs.entry.delete": "Delete entry {model}",
"Settings.permissions.auditLogs.entry.publish": "Publish entry ({model})", "Settings.permissions.auditLogs.entry.publish": "Publish entry {model}",
"Settings.permissions.auditLogs.entry.unpublish": "Unpublish entry ({model})", "Settings.permissions.auditLogs.entry.unpublish": "Unpublish entry {model}",
"Settings.permissions.auditLogs.media.create": "Create media", "Settings.permissions.auditLogs.media.create": "Create media",
"Settings.permissions.auditLogs.media.update": "Update media", "Settings.permissions.auditLogs.media.update": "Update media",
"Settings.permissions.auditLogs.media.delete": "Delete media", "Settings.permissions.auditLogs.media.delete": "Delete media",

View File

@ -25,7 +25,7 @@ const TableRows = ({ headers, rows, onOpenModal }) => {
id: `Settings.permissions.auditLogs.${value}`, id: `Settings.permissions.auditLogs.${value}`,
defaultMessage: getDefaultMessage(value), defaultMessage: getDefaultMessage(value),
}, },
{ model } { model: `(${model})` }
); );
} }

View File

@ -0,0 +1,49 @@
import { useQueries } from 'react-query';
import { useNotification, useFetchClient } from '@strapi/helper-plugin';
import { useLocation } from 'react-router-dom';
const useAuditLogsData = ({ canRead }) => {
const { get } = useFetchClient();
const { search } = useLocation();
const toggleNotification = useNotification();
const fetchAuditLogsPage = async ({ queryKey }) => {
const search = queryKey[1];
const { data } = await get(`/admin/audit-logs${search}`);
return data;
};
const fetchAllUsers = async () => {
const { data } = await get(`/admin/users`);
return data;
};
const queryOptions = {
enabled: canRead,
keepPreviousData: true,
retry: false,
staleTime: 1000 * 20,
onError() {
toggleNotification({
type: 'warning',
message: { id: 'notification.error', defaultMessage: 'An error occured' },
});
},
};
const [auditLogsData, userData] = useQueries([
{ queryKey: ['auditLogs', search], queryFn: fetchAuditLogsPage, ...queryOptions },
{ queryKey: ['auditLogsUsers'], queryFn: fetchAllUsers, ...queryOptions },
]);
const { data: users, isLoading: isLoadingUsers } = userData;
const { data: auditLogs, isLoadingAuditLogs } = auditLogsData;
const isLoading = isLoadingAuditLogs || isLoadingUsers;
return { auditLogs, users: users?.data, isLoading };
};
export default useAuditLogsData;

View File

@ -4,53 +4,54 @@ import {
SettingsPageTitle, SettingsPageTitle,
DynamicTable, DynamicTable,
useRBAC, useRBAC,
useNotification,
useFocusWhenNavigate, useFocusWhenNavigate,
useFetchClient,
useQueryParams, useQueryParams,
} from '@strapi/helper-plugin'; } from '@strapi/helper-plugin';
import { HeaderLayout, ContentLayout } from '@strapi/design-system/Layout'; import { HeaderLayout, ContentLayout, ActionLayout } from '@strapi/design-system/Layout';
import { Main } from '@strapi/design-system/Main'; import { Main } from '@strapi/design-system/Main';
import { useLocation } from 'react-router-dom';
import { useQuery } from 'react-query';
import adminPermissions from '../../../../../../../admin/src/permissions'; import adminPermissions from '../../../../../../../admin/src/permissions';
import TableRows from './TableRows'; import TableRows from './TableRows';
import tableHeaders from './utils/tableHeaders'; import tableHeaders from './utils/tableHeaders';
import PaginationFooter from './PaginationFooter'; import PaginationFooter from './PaginationFooter';
import Modal from './Modal'; import Modal from './Modal';
import Filters from '../../../../../../../admin/src/pages/SettingsPage/components/Filters';
import getDisplayedFilters from './utils/getDisplayedFilters';
import getDefaultMessage, { actionTypes } from './utils/getActionTypesDefaultMessages';
import useAuditLogsData from './hooks/useAuditLogsData';
const ListView = () => { const ListView = () => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const toggleNotification = useNotification();
const { const {
allowedActions: { canRead }, allowedActions: { canRead },
} = useRBAC(adminPermissions.settings.auditLogs); } = useRBAC(adminPermissions.settings.auditLogs);
const { get } = useFetchClient();
const { search } = useLocation();
const [{ query }, setQuery] = useQueryParams(); const [{ query }, setQuery] = useQueryParams();
const { auditLogs, users, isLoading } = useAuditLogsData({ canRead });
useFocusWhenNavigate(); useFocusWhenNavigate();
const fetchAuditLogsPage = async ({ queryKey }) => { const actionOptions = Object.keys(actionTypes).map((action) => {
const search = queryKey[1]; return {
const { data } = await get(`/admin/audit-logs${search}`); label: formatMessage(
{
return data; id: `Settings.permissions.auditLogs.${action}`,
}; defaultMessage: getDefaultMessage(action),
},
const { data, isLoading } = useQuery(['auditLogs', search], fetchAuditLogsPage, { { model: '' }
enabled: canRead, ),
keepPreviousData: true, customValue: action,
retry: false, };
staleTime: 1000 * 10,
onError() {
toggleNotification({
type: 'warning',
message: { id: 'notification.error', defaultMessage: 'An error occured' },
});
},
}); });
const userOptions = users?.results.map((user) => {
return {
label: `${user.firstname} ${user.lastname}`,
// Combobox expects a string value
customValue: user.id.toString(),
};
});
const displayedFilters = getDisplayedFilters({ actionOptions, userOptions });
const title = formatMessage({ const title = formatMessage({
id: 'global.auditLogs', id: 'global.auditLogs',
defaultMessage: 'Audit Logs', defaultMessage: 'Audit Logs',
@ -74,21 +75,22 @@ const ListView = () => {
defaultMessage: 'Logs of all the activities that happened in your environment', defaultMessage: 'Logs of all the activities that happened in your environment',
})} })}
/> />
<ActionLayout startActions={<Filters displayedFilters={displayedFilters} />} />
<ContentLayout canRead={canRead}> <ContentLayout canRead={canRead}>
<DynamicTable <DynamicTable
contentType="Audit logs" contentType="Audit logs"
headers={headers} headers={headers}
rows={data?.results || []} rows={auditLogs?.results || []}
withBulkActions withBulkActions
isLoading={isLoading} isLoading={isLoading}
> >
<TableRows <TableRows
headers={headers} headers={headers}
rows={data?.results || []} rows={auditLogs?.results || []}
onOpenModal={(id) => setQuery({ id })} onOpenModal={(id) => setQuery({ id })}
/> />
</DynamicTable> </DynamicTable>
<PaginationFooter pagination={data?.pagination} /> <PaginationFooter pagination={auditLogs?.pagination} />
</ContentLayout> </ContentLayout>
{query?.id && <Modal handleClose={() => setQuery({ id: null }, 'remove')} logId={query.id} />} {query?.id && <Modal handleClose={() => setQuery({ id: null }, 'remove')} logId={query.id} />}
</Main> </Main>

View File

@ -1,9 +1,9 @@
const actionTypes = { export const actionTypes = {
'entry.create': 'Create entry ({model})', 'entry.create': 'Create entry {model}',
'entry.update': 'Update entry ({model})', 'entry.update': 'Update entry {model}',
'entry.delete': 'Delete entry ({model})', 'entry.delete': 'Delete entry {model}',
'entry.publish': 'Publish entry ({model})', 'entry.publish': 'Publish entry {model}',
'entry.unpublish': 'Unpublish entry ({model})', 'entry.unpublish': 'Unpublish entry {model}',
'media.create': 'Create media', 'media.create': 'Create media',
'media.update': 'Update media', 'media.update': 'Update media',
'media.delete': 'Delete media', 'media.delete': 'Delete media',

View File

@ -0,0 +1,60 @@
import ComboboxFilter from '../Filters/ComboboxFilter';
const customOperators = [
{
intlLabel: { id: 'components.FilterOptions.FILTER_TYPES.$eq', defaultMessage: 'is' },
value: '$eq',
},
{
intlLabel: { id: 'components.FilterOptions.FILTER_TYPES.$ne', defaultMessage: 'is not' },
value: '$ne',
},
];
const getDisplayedFilters = ({ actionOptions, userOptions }) => {
return [
{
name: 'action',
metadatas: {
label: 'Action',
options: actionOptions,
customOperators,
customInput: ComboboxFilter,
},
fieldSchema: { type: 'enumeration' },
},
{
name: 'user',
metadatas: {
label: 'User',
options: userOptions,
customOperators: [
...customOperators,
{
intlLabel: {
id: 'components.FilterOptions.FILTER_TYPES.$null',
defaultMessage: 'is null',
},
value: '$null',
},
{
intlLabel: {
id: 'components.FilterOptions.FILTER_TYPES.$notNull',
defaultMessage: 'is not null',
},
value: '$notNull',
},
],
customInput: ComboboxFilter,
},
fieldSchema: { type: 'relation', mainField: { name: 'id', schema: { type: 'integer' } } },
},
{
name: 'date',
metadatas: { label: 'Date' },
fieldSchema: { type: 'datetime' },
},
];
};
export default getDisplayedFilters;

View File

@ -42,6 +42,18 @@ const AttributeTag = ({ attribute, filter, onClick, operator, value }) => {
formattedValue = formatNumber(value); formattedValue = formatNumber(value);
} }
// Handle custom input
if (attribute.metadatas.customInput) {
// If options, get the option label
if (attribute.metadatas.options) {
const selectedOption = attribute.metadatas.options.find((option) => {
return option.customValue === value;
});
// Set the provided option label or fallback to the value from query
formattedValue = selectedOption?.label || value;
}
}
const content = `${attribute.metadatas.label || attribute.name} ${formatMessage({ const content = `${attribute.metadatas.label || attribute.name} ${formatMessage({
id: `components.FilterOptions.FILTER_TYPES.${operator}`, id: `components.FilterOptions.FILTER_TYPES.${operator}`,
defaultMessage: operator, defaultMessage: operator,
@ -60,7 +72,11 @@ AttributeTag.propTypes = {
attribute: PropTypes.shape({ attribute: PropTypes.shape({
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
fieldSchema: PropTypes.object.isRequired, fieldSchema: PropTypes.object.isRequired,
metadatas: PropTypes.shape({ label: PropTypes.string.isRequired }).isRequired, metadatas: PropTypes.shape({
label: PropTypes.string.isRequired,
options: PropTypes.array,
customInput: PropTypes.func,
}).isRequired,
}).isRequired, }).isRequired,
filter: PropTypes.object.isRequired, filter: PropTypes.object.isRequired,
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,

View File

@ -50,7 +50,7 @@ const FilterPopoverURLQuery = ({ displayedFilters, isVisible, onBlur, onToggle,
} }
if (type === 'enumeration') { if (type === 'enumeration') {
filterValue = options[0]; filterValue = options && options[0];
} }
const filter = getFilterList(nextField)[0].value; const filter = getFilterList(nextField)[0].value;
@ -113,6 +113,8 @@ const FilterPopoverURLQuery = ({ displayedFilters, isVisible, onBlur, onToggle,
const appliedFilter = displayedFilters.find((filter) => filter.name === modifiedData.name); const appliedFilter = displayedFilters.find((filter) => filter.name === modifiedData.name);
const operator = modifiedData.filter; const operator = modifiedData.filter;
const filterList = appliedFilter.metadatas.customOperators || getFilterList(appliedFilter);
const CustomInput = appliedFilter.metadatas.customInput;
return ( return (
<Popover source={source} padding={3} spacing={4} onBlur={onBlur}> <Popover source={source} padding={3} spacing={4} onBlur={onBlur}>
@ -150,7 +152,7 @@ const FilterPopoverURLQuery = ({ displayedFilters, isVisible, onBlur, onToggle,
value={modifiedData.filter} value={modifiedData.filter}
onChange={handleChangeOperator} onChange={handleChangeOperator}
> >
{getFilterList(appliedFilter).map((option) => { {filterList.map((option) => {
return ( return (
<Option key={option.value} value={option.value}> <Option key={option.value} value={option.value}>
{formatMessage(option.intlLabel)} {formatMessage(option.intlLabel)}
@ -161,12 +163,21 @@ const FilterPopoverURLQuery = ({ displayedFilters, isVisible, onBlur, onToggle,
</Box> </Box>
{operator !== '$null' && operator !== '$notNull' && ( {operator !== '$null' && operator !== '$notNull' && (
<Box> <Box>
<Inputs {CustomInput ? (
{...appliedFilter.metadatas} <CustomInput
{...appliedFilter.fieldSchema} {...appliedFilter.metadatas}
value={modifiedData.value} {...appliedFilter.fieldSchema}
onChange={(value) => setModifiedData((prev) => ({ ...prev, value }))} value={modifiedData.value}
/> setModifiedData={setModifiedData}
/>
) : (
<Inputs
{...appliedFilter.metadatas}
{...appliedFilter.fieldSchema}
value={modifiedData.value}
onChange={(value) => setModifiedData((prev) => ({ ...prev, value }))}
/>
)}
</Box> </Box>
)} )}
<Box> <Box>