Patterns · Technical infrastructure
AuditLogViewer
Security and audit events in a table: actor, action, target, IP and device, a details drawer, filters, export and paging.
Workspace audit log
| Time | Actor | Action | Target | IP and device | Details |
|---|---|---|---|---|---|
| Ana Lopez | Changed a member’s role | Ben Okafor | 203.0.113.24Firefox on macOS | ||
| Chloé Martin | Signed in | — | 198.51.100.8Safari on iPhone | ||
| Retention policyAutomation | Deleted 14 expired exportsScheduled | Exports | — | ||
| Deploy keyAPI | Updated a webhook | Billing events | 192.0.2.44API client |
Showing 4 events
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: '[email protected]' },
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: '[email protected]' },
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 />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
detailswhen needed. - Filtering, fetching and the export file are the product’s: the viewer offers slots and callbacks.
API reference
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,
detailsandrenderDetails(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 callsonLoadMorefor 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 indanger-strong, after values insuccess-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: History in the context of one record.
- DataTable: The table primitives it uses.
- Sheet: The details drawer.
- FilterBar: Filters for the
filtersslot. - LoadMore: Cursor pagination.
Dependencies
| Kind | Depends on |
|---|---|
| npm packages | react |
| Monarch components | Avatar, Button, RelativeTime, DescriptionList, DataTable, LoadMore, Skeleton, 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.
auditLogToCsvquoting, 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