Skip to content

Commit

Permalink
Merge pull request #15735 from strapi/feature/audit-logs-add-filters
Browse files Browse the repository at this point in the history
Add audit log filters
  • Loading branch information
markkaylor committed Feb 8, 2023
2 parents 0072ce9 + b4188fc commit 9c2c9e6
Show file tree
Hide file tree
Showing 17 changed files with 378 additions and 75 deletions.
1 change: 1 addition & 0 deletions jest.base-config.front.js
Expand Up @@ -47,6 +47,7 @@ module.exports = {
'<rootDir>/packages/admin-test-utils/lib/mocks/IntersectionObserver.js',
'<rootDir>/packages/admin-test-utils/lib/mocks/ResizeObserver.js',
'<rootDir>/packages/admin-test-utils/lib/mocks/windowMatchMedia.js',
'<rootDir>/packages/admin-test-utils/lib/mocks/mockRangeApi.js',
],
setupFilesAfterEnv: [
'<rootDir>/packages/admin-test-utils/lib/setup/styled-components.js',
Expand Down
16 changes: 16 additions & 0 deletions packages/admin-test-utils/lib/mocks/mockRangeApi.js
@@ -0,0 +1,16 @@
/* eslint-disable no-undef */

'use strict';

// Codemirror inner dependency, reference: https://github.com/jsdom/jsdom/issues/3002
// Otherwise it throws: TypeError: range(...).getBoundingClientRect is not a function

document.createRange = () => {
const range = new Range();
range.getClientRects = jest.fn(() => ({
item: () => null,
length: 0,
}));

return range;
};
Expand Up @@ -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';
Expand Down
12 changes: 7 additions & 5 deletions packages/core/admin/admin/src/translations/en.json
Expand Up @@ -181,15 +181,16 @@
"Settings.permissions.auditLogs.action": "Action",
"Settings.permissions.auditLogs.date": "Date",
"Settings.permissions.auditLogs.user": "User",
"Settings.permissions.auditLogs.user.fullname": "{firstname} {lastname}",
"Settings.permissions.auditLogs.userId": "User ID",
"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, select, undefined {} other { ({model})}}",
"Settings.permissions.auditLogs.entry.update": "Update entry{model, select, undefined {} other { ({model})}}",
"Settings.permissions.auditLogs.entry.delete": "Delete entry{model, select, undefined {} other { ({model})}}",
"Settings.permissions.auditLogs.entry.publish": "Publish entry {model, select, undefined {} other {({model})}}",
"Settings.permissions.auditLogs.entry.unpublish": "Unpublish entry{model, select, undefined {} other { ({model})}}",
"Settings.permissions.auditLogs.media.create": "Create media",
"Settings.permissions.auditLogs.media.update": "Update media",
"Settings.permissions.auditLogs.media.delete": "Delete media",
Expand All @@ -210,6 +211,7 @@
"Settings.permissions.auditLogs.permission.create": "Create permission",
"Settings.permissions.auditLogs.permission.update": "Update permission",
"Settings.permissions.auditLogs.permission.delete": "Delete permission",
"Settings.permissions.auditLogs.filters.combobox.aria-label": "Search and select an option to filter",
"Settings.profile.form.notify.data.loaded": "Your profile data has been loaded",
"Settings.profile.form.section.experience.clear.select": "Clear the interface language selected",
"Settings.profile.form.section.experience.here": "here",
Expand Down
@@ -0,0 +1,41 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { Combobox, ComboboxOption } from '@strapi/design-system';

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;
Expand Up @@ -7,7 +7,7 @@ import { Box } from '@strapi/design-system/Box';
import { Flex } from '@strapi/design-system/Flex';
import { Typography } from '@strapi/design-system/Typography';
import { JSONInput } from '@strapi/design-system/JSONInput';
import getDefaultMessage from '../utils/getActionTypesDefaultMessages';
import { getDefaultMessage } from '../utils/getActionTypesDefaultMessages';
import ActionItem from './ActionItem';

const ActionBody = ({ status, data, formattedDate }) => {
Expand Down
Expand Up @@ -8,7 +8,7 @@ import { Tbody, Td, Tr } from '@strapi/design-system/Table';
import Eye from '@strapi/icons/Eye';
import { onRowClick, stopPropagation } from '@strapi/helper-plugin';
import useFormatTimeStamp from '../hooks/useFormatTimeStamp';
import getDefaultMessage from '../utils/getActionTypesDefaultMessages';
import { getDefaultMessage } from '../utils/getActionTypesDefaultMessages';

const TableRows = ({ headers, rows, onOpenModal }) => {
const { formatMessage } = useIntl();
Expand Down
@@ -0,0 +1,47 @@
import { useQuery } 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, // 20 seconds
onError: (error) => toggleNotification({ type: 'warning', message: error.message }),
};

const {
data: auditLogs,
isLoading,
isError: isAuditLogsError,
} = useQuery(['auditLogs', search], fetchAuditLogsPage, queryOptions);

const { data: users, isError: isUsersError } = useQuery(['auditLogsUsers'], fetchAllUsers, {
...queryOptions,
staleTime: 2 * (1000 * 60), // 2 minutes
});

const hasError = isAuditLogsError || isUsersError;

return { auditLogs, users: users?.data, isLoading, hasError };
};

export default useAuditLogsData;
Expand Up @@ -4,52 +4,38 @@ import {
SettingsPageTitle,
DynamicTable,
useRBAC,
useNotification,
useFocusWhenNavigate,
useFetchClient,
useQueryParams,
AnErrorOccurred,
} from '@strapi/helper-plugin';
import { HeaderLayout, ContentLayout } from '@strapi/design-system/Layout';
import { Main } from '@strapi/design-system/Main';
import { useLocation } from 'react-router-dom';
import { useQuery } from 'react-query';
import {
Box,
HeaderLayout,
ContentLayout,
ActionLayout,
Layout,
Main,
} from '@strapi/design-system';
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 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, hasError } = 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 displayedFilters = getDisplayedFilters({ formatMessage, users });

const title = formatMessage({
id: 'global.auditLogs',
Expand All @@ -64,6 +50,18 @@ const ListView = () => {
},
}));

if (hasError) {
return (
<Layout>
<ContentLayout>
<Box paddingTop={8}>
<AnErrorOccurred />
</Box>
</ContentLayout>
</Layout>
);
}

return (
<Main aria-busy={isLoading}>
<SettingsPageTitle name={title} />
Expand All @@ -74,21 +72,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>
Expand Down

0 comments on commit 9c2c9e6

Please sign in to comment.