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 adminPermissions from '../../../../../permissions';
import TableRows from './DynamicTable/TableRows';
import Filters from './Filters';
import Filters from '../../../components/Filters';
import ModalForm from './ModalForm';
import PaginationFooter from './PaginationFooter';
import { deleteData, fetchData } from './utils/api';

View File

@ -185,11 +185,11 @@
"Settings.permissions.auditLogs.details": "Log Details",
"Settings.permissions.auditLogs.payload": "Payload",
"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.update": "Update entry ({model})",
"Settings.permissions.auditLogs.entry.delete": "Delete entry ({model})",
"Settings.permissions.auditLogs.entry.publish": "Publish entry ({model})",
"Settings.permissions.auditLogs.entry.unpublish": "Unpublish entry ({model})",
"Settings.permissions.auditLogs.entry.create": "Create entry {model}",
"Settings.permissions.auditLogs.entry.update": "Update entry {model}",
"Settings.permissions.auditLogs.entry.delete": "Delete entry {model}",
"Settings.permissions.auditLogs.entry.publish": "Publish entry {model}",
"Settings.permissions.auditLogs.entry.unpublish": "Unpublish entry {model}",
"Settings.permissions.auditLogs.media.create": "Create media",
"Settings.permissions.auditLogs.media.update": "Update media",
"Settings.permissions.auditLogs.media.delete": "Delete media",

View File

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

View File

@ -1,9 +1,9 @@
const actionTypes = {
'entry.create': 'Create entry ({model})',
'entry.update': 'Update entry ({model})',
'entry.delete': 'Delete entry ({model})',
'entry.publish': 'Publish entry ({model})',
'entry.unpublish': 'Unpublish entry ({model})',
export const actionTypes = {
'entry.create': 'Create entry {model}',
'entry.update': 'Update entry {model}',
'entry.delete': 'Delete entry {model}',
'entry.publish': 'Publish entry {model}',
'entry.unpublish': 'Unpublish entry {model}',
'media.create': 'Create media',
'media.update': 'Update 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);
}
// 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({
id: `components.FilterOptions.FILTER_TYPES.${operator}`,
defaultMessage: operator,
@ -60,7 +72,11 @@ AttributeTag.propTypes = {
attribute: PropTypes.shape({
name: PropTypes.string.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,
filter: PropTypes.object.isRequired,
onClick: PropTypes.func.isRequired,

View File

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