Source: https://monarchui.dev/components/audit-log-viewer/

Patterns · Technical infrastructure

# AuditLogViewer

Security and audit events in a table: actor, action, target, IP and device, a details drawer, filters, export and paging.

@rookcom/monarch-react/collaboration

Pattern

## Workspace audit log

## Usage examples

The example at the top of the page is the only one for this component.

## Purpose

Shows who did what, to what, from where and when, for workspace admins and security reviews. Each row opens a drawer with the full record: field-level before and after, reason and any payload.

### Use when

- Workspace or product settings › Audit log.
- Security events: sign-ins, key use, permission changes.
- Any immutable change history that needs export.

### Avoid when

- A record’s own history in context: use ActivityTimeline.
- Editable data: audit entries are read-only.

## Design principles

- Absolute times in the table (audits are compared across days); relative time in the drawer for context.
- People, automations, systems and API keys are told apart in text, never colour alone.
- Changes are a structured before and after, not raw JSON; raw payloads go in `details` when needed.
- Filtering, fetching and the export file are the product’s: the viewer offers slots and callbacks.

## API reference

```tsx
import { AuditLogViewer, auditLogToCsv } from '@rookcom/monarch-react/collaboration';
```

| Export | Kind | Rendering | Root class |
| --- | --- | --- | --- |
| `AuditLogViewer` | component | client | `.mn-audit-log-viewer` |
| `auditLogToCsv` | utility | client | – |

Built on Monarch Table, Sheet, DescriptionList and LoadMore.

`auditLogToCsv(rows)` builds an RFC 4180 CSV and defuses spreadsheet formulas; use it for client-side exports of what is loaded, or export on the server for everything.

`selectedId` / `defaultSelectedId` / `onSelectedChange` control the open drawer, e.g. to deep-link an event.

The ref is forwarded to the root `<div>`.

Generated from the TypeScript declarations of `packages/react/src/patterns/technical/audit-log-viewer`.

## Properties

### `AuditLogViewer`

Security and audit events in a table: time, actor, action, target, IP and device, with a details drawer (field changes, reason, payload), a filters slot, an export action and cursor pagination.

| Prop | Type | Description |
| --- | --- | --- |
| `events` required | `readonly AuditLogEvent[]` | Loaded events, newest first. |
| `className` | `string` |  |
| `columns` | `readonly AuditLogColumn[]` | Columns to show, in order. Default all five. Hidden columns stay in the details drawer. |
| `defaultSelectedId` | `string \| null` |  |
| `density` | `"compact" \| "comfortable"` | `compact` table rows. Default `comfortable`. |
| `empty` | `ReactNode` | Replaces the default empty text, e.g. an EmptyState with a clear-filters action. |
| `error` | `ReactNode` | Failure, e.g. an InlineErrorRetry or a permission note. Replaces the table. |
| `exporting` | `boolean` | The export is running: the button shows a spinner. |
| `filters` | `ReactNode` | Filter controls (FilterBar, Select, SearchInput). The product filters or refetches `events`. |
| `hasMore` | `boolean` | Cursor pagination: another page exists. |
| `id` | `string` |  |
| `loading` | `boolean` | First load: shows a skeleton. |
| `loadingMore` | `boolean` |  |
| `loadMoreError` | `boolean` |  |
| `locale` | `string` |  |
| `messages` | `Partial<AuditLogViewerMessages>` |  |
| `now` | `DateInput` |  |
| `onExport` | `(() => void)` | Shows an Export button. Build the file with `auditLogToCsv` or on the server. |
| `onLoadMore` | `(() => void)` |  |
| `onSelectedChange` | `((id: string \| null) => void)` |  |
| `renderDetails` | `((event: AuditLogEvent) => ReactNode)` | Extra drawer content for an event, after the built-in fields. |
| `selectedId` | `string \| null` | The event whose details drawer is open, controlled. |
| `timeZone` | `string` |  |

## Variants

| Prop | Values | Description |
| --- | --- | --- |
| `columns` | `'['time', 'actor', 'action', 'target', 'origin']'``'any subset'` | Columns shown, in order. Hidden fields still appear in the drawer. |
| `density` | `'comfortable'``'compact'` | Table row height. |

## States

| State | Exposed as | Behaviour |
| --- | --- | --- |
| Loading | – | Skeleton rows in place of the table. |
| Error | – | The `error` node (for example a permission note) replaces the table. |
| Empty | – | “No events match”, or the `empty` node (an EmptyState with Clear filters). |
| Selected | `aria-selected="true"` | The row whose drawer is open. |
| Exporting | – | The export button shows a spinner and stays focusable. |

## Interaction behaviour

- Details opens a side drawer with every field, the changes table, `details` and `renderDetails(event)`.
- Closing the drawer (Escape, Close or outside click) returns focus to the row’s Details button, also when the product opened it.
- Export calls `onExport`; LoadMore calls `onLoadMore` for the next cursor page.

## Keyboard support

| Keys | Action |
| --- | --- |
| Tab | Moves through filters, Export, each row’s Details button and Load more. |
| Enter / Space | Opens the details drawer for the row. |
| Escape | Closes the drawer. |

## Accessibility

Role

table, with a dialog for details

Accessible name

The table is labelled “Audit log” (

messages.caption

); the drawer is named by the event’s action.

- Each Details button is named with its row’s action (“Details Changed a member’s role”).
- The changes table uses row headers for field names.
- Times are `<time datetime>` elements.
- Actor kinds are text tags (“Automation”, “System”, “API”).

## Responsive behaviour

- A container query hides IP and device below 44rem; below 32rem the target is hidden and the time moves under the action. Everything stays in the drawer.
- The toolbar wraps; the table scrolls horizontally inside its wrapper if it still does not fit.
- The drawer becomes full width on phones (Sheet).

## Theme support

- Table tokens from DataTable; IP addresses in `font-mono`; before values in `danger-strong`, after values in `success-strong`.

## Do and don't

### Do

- Write actions as short sentences in the past tense.
- Record and show the reason when the product asks for one.
- Show approximate locations honestly (“approximate”).

### Don't

- Do not let admins edit or delete entries from this view.
- Do not show secrets or full tokens in `details`.
- Do not export only the loaded page without saying so; export on the server for the full range.

## Related components

- [ActivityTimeline](https://monarchui.dev/components/activity-timeline/): History in the context of one record.
- [DataTable](https://monarchui.dev/components/data-table/): The table primitives it uses.
- [Sheet](https://monarchui.dev/components/sheet/): The details drawer.
- [FilterBar](https://monarchui.dev/components/filter-bar/): Filters for the `filters` slot.
- [LoadMore](https://monarchui.dev/components/load-more/): Cursor pagination.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [Avatar](https://monarchui.dev/components/avatar/), [Button](https://monarchui.dev/components/button/), [RelativeTime](https://monarchui.dev/components/relative-time/), [DescriptionList](https://monarchui.dev/components/description-list/), [DataTable](https://monarchui.dev/components/data-table/), [LoadMore](https://monarchui.dev/components/load-more/), [Skeleton](https://monarchui.dev/components/skeleton/), [Sheet](https://monarchui.dev/components/sheet/) |

## Testing coverage

- Labelled table with all columns, actor kinds and unknown actors.
- Details buttons named by their row.
- Drawer with fields, the changes table and payload; Escape closes and returns focus.
- Controlled selection and `renderDetails`.
- Filters slot, export (and exporting), cursor pagination.
- Column choice keeps hidden fields in the drawer; loading, error and empty.
- `auditLogToCsv` quoting, escaping and formula defusing.

| Check | Covered |
| --- | --- |
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/audit-log-viewer (table and open drawer); Cross-browser axe: /components/audit-log-viewer |

Tests: `packages/react/src/patterns/technical/audit-log-viewer`

## All example code

### Workspace audit log

```tsx
import { useState } from 'react';
import { Badge, Select, SelectContent, SelectItem, SelectTrigger } from '@rookcom/monarch-react';
import { AuditLogViewer, auditLogToCsv, type AuditLogEvent } from '@rookcom/monarch-react/collaboration';

const NOW = '2026-03-02T12:00:00Z';

/** Built lazily: docs files cannot create JSX at module scope. */
const sampleEvents = (): (AuditLogEvent & { area: string })[] => [
  {
    id: 'a1',
    area: 'members',
    time: '2026-03-02T11:04:12Z',
    actor: { name: 'Ana Lopez', detail: 'ana@example.com' },
    action: 'Changed a member’s role',
    target: 'Ben Okafor',
    ip: '203.0.113.24',
    device: 'Firefox on macOS',
    reason: 'Moved to the platform team',
    changes: [
      { field: 'Role', before: 'Member', after: 'Admin' },
      { field: 'Team', before: 'Web', after: 'Platform' },
    ],
  },
  {
    id: 'a2',
    area: 'security',
    time: '2026-03-02T09:47:51Z',
    actor: { name: 'Chloé Martin', detail: 'chloe@example.com' },
    action: 'Signed in',
    ip: '198.51.100.8',
    device: 'Safari on iPhone',
    location: 'Lisbon, Portugal (approximate)',
  },
  {
    id: 'a3',
    area: 'settings',
    time: '2026-03-02T03:00:00Z',
    actor: { name: 'Retention policy', kind: 'automation' },
    action: 'Deleted 14 expired exports',
    target: 'Exports',
    source: <Badge size="sm">Scheduled</Badge>,
    details: <pre>{JSON.stringify({ policy: 'exports-30d', deleted: 14 }, null, 2)}</pre>,
  },
  {
    id: 'a4',
    area: 'settings',
    time: '2026-03-01T15:22:09Z',
    actor: { name: 'Deploy key', kind: 'api', detail: 'Key ending 7f3a' },
    action: 'Updated a webhook',
    target: 'Billing events',
    ip: '192.0.2.44',
    device: 'API client',
    changes: [
      {
        field: 'URL',
        before: 'https://hooks.example.com/v1',
        after: 'https://hooks.example.com/v2',
      },
    ],
  },
];

function download(name: string, text: string) {
  const url = URL.createObjectURL(new Blob([text], { type: 'text/csv' }));
  const a = document.createElement('a');
  a.href = url;
  a.download = name;
  a.click();
  URL.revokeObjectURL(url);
}

function Demo() {
  const [area, setArea] = useState('all');
  const events = area === 'all' ? sampleEvents() : sampleEvents().filter((e) => e.area === area);
  return (
    <div style={{ width: '100%' }}>
      <AuditLogViewer
        events={events}
        filters={
          <Select value={area} onValueChange={setArea}>
            <SelectTrigger aria-label="Area" style={{ width: '12rem' }} />
            <SelectContent>
              <SelectItem value="all">All areas</SelectItem>
              <SelectItem value="members">Members</SelectItem>
              <SelectItem value="security">Security</SelectItem>
              <SelectItem value="settings">Settings</SelectItem>
            </SelectContent>
          </Select>
        }
        onExport={() =>
          download(
            'audit-log.csv',
            auditLogToCsv([
              ['time', 'actor', 'action', 'ip'],
              ...events.map((e) => [new Date(e.time), e.actor?.name, String(e.action), e.ip]),
            ]),
          )
        }
        hasMore
        onLoadMore={() => {}}
        now={NOW}
        timeZone="UTC"
        locale="en-GB"
      />
    </div>
  );
}

<Demo />
```
