mirror of
https://github.com/strapi/strapi.git
synced 2025-11-03 11:25:17 +00:00
Add audit log filters
This commit is contained in:
parent
73be2f243e
commit
377f5e8e8a
@ -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;
|
||||
@ -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';
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -25,7 +25,7 @@ const TableRows = ({ headers, rows, onOpenModal }) => {
|
||||
id: `Settings.permissions.auditLogs.${value}`,
|
||||
defaultMessage: getDefaultMessage(value),
|
||||
},
|
||||
{ model }
|
||||
{ model: `(${model})` }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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>
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user