ra-audit-log

react-admin ≥ 3.16.6

Display event lists and audit logs, keep track of user actions, and get an overview of the activity of your admin.

Test it live in the Enterprise Edition Storybook.

Installation

npm install --save @react-admin/ra-audit-log
# or
yarn add @react-admin/ra-audit-log

Tip: ra-audit-log is part of the React-Admin Enterprise Edition, and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package.

Overview

This module introduces 2 new components:

  • <Timeline> shows a list of all recent changes in the admin. It's a great component for dashboards.
  • <EventList> is a ready-to-use List component for navigating in your admin history, complete with filters and pagination.

Setup

Event Format

This package expects you to store and expose one "event" for each change triggered by a user action.

Event records must contain the following properties:

  • id
  • date
  • author
  • resource
  • action

Here is a typical event:

{
  date: '2020-09-08 15:01:00',
  author: 567,
  resource: 'posts',
  action: 'create',
  payload: { title: 'foo', body: 'bar' }
}

We recommend that you create event logs server-side, i.e. add a new event record each time a user performs a mutation (create, update, delete, updateMany, deleteMany). Typically, you can achieve this using a proxy server, a database trigger, or a middleware in your API server. Then, it's your responsibility to expose an API for these events and to update your dataProvider so that it can access events like any other resource.

Events Resource

To let react-admin components query the backend for events, you should add a <Resource> for them:

import { Admin, Resource } from 'react-admin';
import dataProvider from './dataProvider';

export const App = () => (
    <Admin dataProvider={dataProvider}>
        {/* your Resources go here */}
+       <Resource name="events" />
    </Admin>
)

Client-Side Tracking

You can choose to let ra-audit-log create events for you. In this case, each mutation performed by a user will trigger a call to dataProvider.create('events', [event details]). We call this feature client-side tracking.

To enable client-side tracking, build a new dataProvider by calling the addEventsForMutations helper function with your original dataProvider and your authProvider:

import { addEventsForMutations } from '@react-admin/ra-audit-log';
import simpleRestProvider from 'ra-data-simple-rest';
import authProvider from './authProvider';

const dataProvider = addEventsForMutations(
    simpleRestProvider('http://path.to.my.api/'),
    authProvider
);

Your authProvider must have a getIdentity method which returns an object containing the id, the fullName and optionally the avatar (an image URL) for the current logged user:

const authProvider = {
    login: ...,
    logout: ...,
    ...,
    getIdentity: () => Promise.resolve({ id: '1', fullName: 'test', avatar: 'https://myavatar.com' }),
}

When enabled, after each successful mutation, the client-side tracking wrapper adds a new event. For instance:

// when your app calls
dataProvider.update('orders', {
    id: 123,
    data: { status: 'delivered' },
    previousData: { id: 123, status: 'received' },
});
// the wrapper calls
dataProvider.create('events', {
    date: '2020-09-08 15:01:00',
    author: 567,
    resource: 'orders',
    action: 'update',
    payload: {
        id: 123,
        data: { status: 'delivered' },
        previousData: { id: 123, status: 'received' },
    },
});

In addition to the dataProvider to extend, the addEventsForMutations function accepts the following options:

  • name: the name of the event logs resource (defaults to events)
import { addEventsForMutations } from '@react-admin/ra-audit-log';
import simpleRestProvider from 'ra-data-simple-rest';
import authProvider from './authProvider';

const dataProvider = addEventsForMutations(
    simpleRestProvider('http://path.to.my.api/'),
    authProvider,
    { name: 'events' }
);
  • resources: the resources and mutations you want to track with events. It can be an array of resource names or an array of arrays defining both the resource and the dataProvider calls to track
import { addEventsForMutations } from '@react-admin/ra-audit-log';
import simpleRestProvider from 'ra-data-simple-rest';
import authProvider from './authProvider';

const dataProvider = addEventsForMutations(
    simpleRestProvider('http://path.to.my.api/'),
    authProvider,
    {
        resources: [
            // create an event for all known mutations on posts
            'posts',
            // create an event only for create and update on comments, but not for delete, deleteMany or updateMany
            ['comments', ['create', 'update']],
        ],
    }
);
  • shouldAudit: as an alternative to resources, you can specify a function which, given a dataProvider method name (create, update, etc.) and the arguments it was called with, should return true to create an event. This allows to target custom dataProvider methods:
import { addEventsForMutations } from '@react-admin/ra-audit-log';
import simpleRestProvider from 'ra-data-simple-rest';
import authProvider from './authProvider';

const dataProvider = addEventsForMutations(
    simpleRestProvider('http://path.to.my.api/'),
    authProvider,
    {
        shouldAudit: (action, ...args) => {
            if (action === 'myCustomMethod') {
                return true;
            }
        },
    }
);

Translations

In order to have correct labels on the <Timeline> and <EventList> components, you have to set up the internationalization with the provided translations or your own. Check out Internationalization for detailed steps.

<Timeline>

Timeline component

The <Timeline> component displays a list of events, the most recent first, grouped by day. It's the ideal component for checking the recent activity of an admin.

It accepts the following props:

  • records: An array of events
  • loaded: Optional. Boolean, indicating whether the data has been loaded at least once.
  • children: Optional. Component rendered for each group of audit logs.
  • groupLogs: Optional. A function called with the audit logs and the locale. Must return groups.
  • skeleton: Optional. The element to display while loaded is true.

Basic Usage

It's your responsibility to get the events from the dataProvider and pass them to <Timeline>.

import { useGetList } from 'react-admin';
import { Timeline } from '@react-admin/ra-audit-log';

const Dashboard = () => {
    const { data, ids, loaded } = useGetList(
        'events',
        { page: 1, perPage: 25 },
        { field: 'date', order: 'desc' },
    });
    const records = ids.map(id => data[id]);

    return (
        <Timeline loaded={loaded} records={records} />
    );
}

Custom Group Component

By default, <Timeline> uses the <TimelineGroup> component to render each group. You can use your own component instead by passing it as the <Timeline> child. In this component, use the useTimelineGroup hook to grab the group information.

import { useGetList } from 'react-admin';
import { Timeline, useTimelineGroup } from '@react-admin/ra-audit-log';

const Dashboard = () => {
    const { data, ids, loaded } = useGetList(
        'audit-logs',
        { page: 1, perPage: 25 },
        { field: 'date', order: 'desc' },
    });
    const records = ids.map(id => data[id]);

    return (
        <Timeline loaded={loaded} records={records}>
            <MyTimelineGroup />
        </Timeline>
    );
}

const MyTimelineGroup = () => {
    const { label, records } = useTimelineGroup();
    return (
        <article>
            <h1>{label}</h1>
            <ul>
                {records.map(record => (
                    <li>{JSON.stringify(record)}</li>
                ))}
            </ul>
        </article>
    );
};

Custom Item Component

<TimelineGroup> renders each event using the <TimelineItem> component. You can replace it with your own component instead, by passing it as a child to <TimelineGroup>. Use the useRecordContext hook to grab the event record.

import { useGetList, useRecordContext } from 'react-admin';
import { AuditLogLabel, Timeline, TimelineItem, TimelineGroup, useTimeline } from '@react-admin/ra-audit-log';

const Dashboard = () => {
    const { data, ids, loaded } = useGetList(
        'audit-logs',
        { page: 1, perPage: 25 },
        { field: 'date', order: 'desc' },
    });
    const records = ids.map(id => data[id]);

    return (
        <Timeline loaded={loaded} records={records}>
            <MyTimelineGroup />
        </Timeline>
    );
}

const MyTimelineGroup = () => {
    return (
        <TimelineGroup>
            <MyTimelineItem />
        </TimelineGroup>
    );
};

const MyTimelineItem = (props) => {
    const record = useRecordContext(props.record);

    return (
        <ListItem>
            <ListItemText
                primary={record.author.fullName}
                secondary={<AuditLogLabel />}
            />
        </ListItem>
    );
};

Custom skeleton

You may want to display more items in the skeleton while data is loading.

import { useGetList } from 'react-admin';
import { Timeline, TimelineSkeleton } from '@react-admin/ra-audit-log';

const Dashboard = () => {
    const { data, ids, loaded } = useGetList(
        'events',
        { page: 1, perPage: 25 },
        { field: 'date', order: 'desc' },
    });
    const records = ids.map(id => data[id]);

    return (
        <Timeline
            loaded={loaded}
            records={records}
            skeleton={<TimelineSkeleton length={50} />}
        />
    );
}

<RecordTimeline>

RecordTimeline component

The <RecordTimeline> component is a specialized version of the <Timeline>. It accepts the same props except records and loaded. It will fetch the event related to the current record. This record can be either the one from the current RecordContext (set by react-admin components) or the one specified using the record prop.

It accepts the following props:

  • record: Optional. The record for which to fetch the events
  • resource: Optional. The resource.
  • eventResource: Optional. The resource for the events.
  • children: Optional. Component rendered for each group of audit logs.
  • groupLogs: Optional. A function called with the audit logs and the locale. Must return groups.
  • skeleton: Optional. The element to display while loaded is true.

Usage

You can use the <RecordTimeline> as an aside for an <Edit> view:

import { Edit, SimpleForm, TextField } from 'react-admin';
import { RecordTimeline } from '@react-admin/ra-audit-log';

const ProductEdit = props => {
    return (
        <Edit {...props} aside={<RecordTimeline />}>
            <SimpleForm>
                <TextField source="name" />
            </SimpleForm>
        </Edit>
    );
};

useRecordEvents

This hook cn be used to fetch events related to a record. It accept the following options:

  • record: Optional. The record for which to fetch the events. Will be inferred from the RecordContext if not provided
  • resource: Optional. The resource of the record. Will be inferred from the ResourceContext if not provided
  • eventResource: Optional. The resource for the events. Defaults to events
  • page: Optional. The page of events to fetch. Defaults to 1
  • perPage: Optional. The number of events to fetch. Defaults to 25
  • sort: Optional. The field used to sort the events. Defaults to date
  • order: Optional. The order into which to sort the events. Defaults to DESC

It returns the same object as the useGetListhook, containing the following properties:

  • data: An object where keys are records identifiers and values the records
  • ids: An array of the records identifiers
  • loaded: A boolean indicating whether the list has been fetched at least once
  • loading: A boolean indicating whether the list is currently loading
  • error: An object containing an error when one occurs

<EventList>

The <EventList> component is a full-featured List component for events, pre-configured with a Datagrid and a filter sidebar.

Basic Usage

Use the <EventList> component as the list prop of the events <Resource>:

// In App.js
import { Admin, Resource } from 'react-admin';
import { EventList } from '@react-admin/ra-audit-log';

export const App = () => (
    <Admin>
        <Resource name="events" list={EventList} />
    </Admin>
);

In addition to the usual List props, <EventList> accepts the following props:

  • eventResource: The name of the events resource. Defaults to events.
  • authorResource: You can optionally provide the resource name for the authors of the events. ra-audit-log will use it to create links to authors.
  • dateFilters: A map of date filters to replace the default ones.

Events Resource

<EventList> allows users to filter events by resource, exluding the events resource itself. By default, it excludes the events resource. If you used a different resource name for events, use the eventResource prop to change it:

// In App.js
import { Admin, Resource } from 'react-admin';
import { MyEventList } from './evenList';

export const App = () => (
    <Admin>
        <Resource name="audit-logs" list={MyEventList} />
    </Admin>
);

// in ./eventList.js
import { EventList } from '@react-admin/ra-audit-log';

export const MyEventList = props => <EventList eventResource="audit-logs" />;

Authors Resource

You can provide a resource name for the authors of the events. <EventList> uses it to render links to each author in the list, and an AutocompleteArrayInput to filter them.

Without authors resource: event list without authors resource

With authors resource: event list with authors resource

Use the authorResource prop to set the author resource in the <EventList>:

// In App.js
import { Admin, Resource } from 'react-admin';
import { MyEventList } from './evenList';

export const App = () => (
    <Admin>
        <Resource name="events" list={MyEventList} />
    </Admin>
);

// In ./eventList.js
import { EventList } from '@react-admin/ra-audit-log';

export const MyEventList = props => <EventList authorResource="users" />;

Date Filters

By default, @react-admin/ra-audit-log provides filters for the following date ranges:

  • Today
  • Last week
  • Last month
  • Last quarter

You can provide your own set of date filters by using the dateFilters prop:

// In App.js
import { Admin, Resource } from 'react-admin';
import { MyEventList } from './evenList';

export const App = () => (
    <Admin>
        <Resource name="events" list={MyEventList} />
    </Admin>
);

// In ./eventList.js
import { DefaultDateFilters, EventList } from '@react-admin/ra-audit-log';

const dateFilters = {
    ...DefaultDateFilters,
    'This Year': () => startOfYear(new Date()).toISOString(),
};

export const MyEventList = props => <EventList dateFilters={dateFilters} />;

Internationalization

ra-audit-log provides translations for English (raAuditLogLanguageEnglish) and French (raAuditLogLanguageFrench).

The interface in English looks like this: english interface

The interface in French looks like this: french interface

You should merge these translations with the other interface messages before passing them to your i18nProvider:

import { mergeTranslations } from 'react-admin';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';
import {
    raAuditLogLanguageEnglish,
    raAuditLogLanguageFrench,
} from '@react-admin/ra-audit-log';

const i18nProvider = polyglotI18nProvider(locale =>
    locale === 'en'
        ? mergeTranslations(englishMessages, raAuditLogLanguageEnglish)
        : mergeTranslations(frenchMessages, raAuditLogLanguageFrench)
);

The event field names are translatable. ra-audit-log uses the react-admin resource and field name translation system. This is an example of an English translation file:

// in i18n/en.js

export default {
    resources: {
        events: {
            name: 'Log |||| Log',
            fields: {
                date: 'Date',
                author: 'Author',
                resource: 'Resource',
                action: 'Action',
            },
        },
    },
};

ra-audit-log also supports internationalization for the textual representation of the event action.

If available, ra-audit-log uses the translation with the key ra-audit-log.event.[resource].[action], where resource and action are replaced with the actual resource and action names. For example: ra-audit-log.event.posts.create.

If not available, it will fall back to the following translation key: ra-audit-log.event.[action], where action is replaced with the actual action name. For example: ra-audit-log.event.create.

In all cases, the following variables are available for interpolation in the translated string:

  • name: the name of the record targeted by the action if available. It will be the first available property on the record in this list: name, label, title, reference, or id.
  • resource: the name of the event resource
  • ids: the list of the record identifiers if the action was targeting many (deleteMany, updateMany for example).
  • all_fields: the complete list of the fields which were modified in an update action.
  • fields: the list of the fields which were modified in an update action. Limited to a maximum of two. To be used with the smart_count variable.
  • smart_count: The number of updated fields in addition of the two available in the field variable. This allows to display messages like: Update field Name, Category and 5 others.

Building Blocks

Would you want to create your own <EventList> component, we provide some building blocks you might reuse:

  • <EventListFilter>: A component to pass to the list filter prop. It includes the author, date, and resource filters.

  • <AuthorInput>: An input used to filter events by authors. It's a basic TextInput unless the authorResource prop is provided. In this case, it's a <ReferenceArrayInput> with an <AutocompleteArrayInput>.

  • <EventDateInput>: An input used to filter events by date. It provides the default date filters presented before. It accepts a dateFilters prop similar to the <EventList>.

  • <ResourceInput>: An input used to filter events by resource. It's an <AutocompleteArrayInput> which lists all the resources declared in the <Admin> component but the events.

  • <EventFilterList>: A component to pass to the list aside prop. It includes the author, date, and resource filters presented as a sidebar.

  • <AuthorFilterList>: A filter sidebar part used to filter events by authors. It's a basic TextInput unless the authorResource prop is provided. In this case, it's a <ReferenceArrayInput> with an <AutocompleteArrayInput>.

  • <EventFilterList>: A filter sidebar part used to filter events by date. It provides the default date filters presented before and displayed as filter list items. It accepts a dateFilters prop similar to the <EventList>.

  • <ResourceFilterList>: A filter sidebar part used to filter events by resource. It's an <AutocompleteArrayInput> which lists all the resources declared in the Admin component but the events.

  • <ActionField>: A react-admin field that displays a formated text for the event action.

  • <AuthorField>: A react-admin field that displays the event authors. If the authorResource prop is provided, it uses a <ReferenceField> and will show a link to the author.

  • <AvatarField>: A react-admin field that displays the author avatar with a fallback to a material-ui icon.

  • <ResourceField>: A react-admin field that displays the event resource. The resource name is translated like in react-admin. It will use the resource label option if available.

CHANGELOG

v2.4.1

2021-07-08

  • (fix) Fix TimelineItem might trigger an error on logout

v2.4.0

2021-07-07

  • (feat) updateMany and deleteMany now create an event log for each targeted record

v2.3.1

2021-06-29

  • (fix) Update peer dependencies ranges (support react 17)

v2.3.0

2021-06-28

  • (feat) <Timeline> now has a skeleton prop which accept an element, allowing you to customize the skeleton UI displayed while loading.
  • (feat) <RecordTimeline> now takes less space and draw less attention.
  • (feat) <RecordTimeline> now displays the names of the fields which were updated for any update event record.

v2.0.2

2021-04-20

  • (fix) Add missing date-fns dependency

v2.0.1

2021-04-12

  • (fix) Make Resource and Event Inputs Resettable

v2.0.0

2021-03-22

  • (feat) Introduce <RecordTimeline> component
  • (feat) Introduce useRecordEvents hook

Breaking Change:

  • (feat) Refactor useEventLabel to accept a variant prop instead of the inline prop
-useEventLabel({ inline: true })
+useEventLabel({ variant: 'inline' })

v1.0.3

2021-03-22

  • (fix) Fix ra-audit-log published without built files

v1.0.2

2021-03-19

  • (fix) Add a better screencast to showcase the package

v1.0.1

2021-03-18

  • (fix) Fix Filters in Sidebar erase existing filters
  • (fix) Fix Filters in Sidebar initial values
  • (fix) Fix Filters in Sidebar display

v1.0.0

2021-03-17

  • First release