mirror of
https://github.com/strapi/strapi.git
synced 2025-11-10 15:19:00 +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 { 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';
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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})` }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
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>
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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);
|
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,
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user