Skip to content

Audit Trail History

AuditHistoryPlugin records accountable data-change transactions for spreadsheet-like business workflows. Use it when the grid edits important records and your application needs to answer who changed what, when it changed, and how to recover it.

Source code
TypeScript ts
import { defineCustomElements } from '@revolist/revogrid/loader';
defineCustomElements();

import {
  AuditHistoryPlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  RowOddPlugin,
  defineAuditHistoryPanel,
  type AuditHistoryConfig,
} from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';

const { isDark } = currentTheme();

const rows = [
  { id: 'invoice-1001', customer: 'Northwind', status: 'Draft', amount: 1280 },
  { id: 'invoice-1002', customer: 'Acme Finance', status: 'Approved', amount: 2460 },
  { id: 'invoice-1003', customer: 'Globex', status: 'Pending', amount: 980 },
  { id: 'invoice-1004', customer: 'Initech', status: 'Draft', amount: 1750 },
];

const auditHistory: AuditHistoryConfig = {
  getCurrentUser: () => ({
    id: 'avery-stone',
    name: 'Avery Stone',
    email: '[email protected]',
  }),
  rowIdProp: 'id',
  storage: 'memory',
};

export function load(parentSelector: string) {
  const parent = document.querySelector(parentSelector);
  if (!parent) return;

  const container = document.createElement('div');
  container.className = 'grow flex flex-col h-full gap-3';

  const grid = document.createElement('revo-grid') as HTMLRevoGridElement;
  grid.className = 'cell-border';
  grid.style.flexGrow = '0';
  grid.style.minHeight = '250px';
  grid.range = true;
  grid.hideAttribution = true;
  grid.theme = isDark() ? 'darkMaterial' : 'material';
  grid.stretch = 'all';
  grid.columns = [
    { name: 'Invoice', prop: 'id', readonly: true, size: 140 },
    { name: 'Customer', prop: 'customer' },
    { name: 'Status', prop: 'status' },
    { name: 'Amount', prop: 'amount' },
  ];
  grid.auditHistory = auditHistory;
  grid.plugins = [
    EventManagerPlugin,
    AuditHistoryPlugin,
    ColumnStretchPlugin,
    RowOddPlugin,
  ];

  const panel = document.createElement('div');
  panel.className = 'min-h-56';

  container.appendChild(grid);
  container.appendChild(panel);
  parent.appendChild(container);
  defineAuditHistoryPanel(panel, grid);
  grid.source = rows.map(row => ({ ...row }));

  return () => {
    grid.remove();
    panel.remove();
    container.remove();
  };
}
React tsx
import { useEffect, useMemo, useRef, useState } from 'react';
import { RevoGrid, type DataType } from '@revolist/react-datagrid';
import {
  AuditHistoryPlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  RowOddPlugin,
  defineAuditHistoryPanel,
  type AuditHistoryConfig,
} from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';

const initialRows = [
  { id: 'invoice-1001', customer: 'Northwind', status: 'Draft', amount: 1280 },
  { id: 'invoice-1002', customer: 'Acme Finance', status: 'Approved', amount: 2460 },
  { id: 'invoice-1003', customer: 'Globex', status: 'Pending', amount: 980 },
  { id: 'invoice-1004', customer: 'Initech', status: 'Draft', amount: 1750 },
];

function AuditHistory() {
  const { isDark } = currentTheme();
  const gridRef = useRef<HTMLRevoGridElement>(null);
  const panelRef = useRef<HTMLDivElement>(null);
  const [source] = useState<DataType[]>(() => initialRows.map(row => ({ ...row })));

  const columns = useMemo(() => [
    { name: 'Invoice', prop: 'id', readonly: true, size: 140 },
    { name: 'Customer', prop: 'customer' },
    { name: 'Status', prop: 'status' },
    { name: 'Amount', prop: 'amount' },
  ], []);

  const auditHistory = useMemo<AuditHistoryConfig>(() => ({
    getCurrentUser: () => ({
      id: 'avery-stone',
      name: 'Avery Stone',
      email: '[email protected]',
    }),
    rowIdProp: 'id',
    storage: 'memory',
  }), []);

  const additionalData = useMemo(() => ({
    auditHistory,
  }), [auditHistory]);

  const plugins = useMemo(() => [
    EventManagerPlugin,
    AuditHistoryPlugin,
    ColumnStretchPlugin,
    RowOddPlugin,
  ], []);

  useEffect(() => {
    if (panelRef.current && gridRef.current) {
      defineAuditHistoryPanel(panelRef.current, gridRef.current);
    }
  }, []);

  return (
    <div className="grow flex flex-col h-full gap-3">
      <RevoGrid
        ref={gridRef}
        className="cell-border"
        style={{ flexGrow: 0, minHeight: 250 }}
        theme={isDark() ? 'darkMaterial' : 'material'}
        columns={columns}
        source={source}
        plugins={plugins}
        additionalData={additionalData}
        range
        stretch="all"
        hideAttribution
      />
      <div ref={panelRef} className="min-h-56" />
    </div>
  );
}

export default AuditHistory;
Vue vue
<template>
  <div class="grow flex flex-col h-full gap-3">
    <VGrid
      ref="gridRef"
      class="cell-border"
      :style="{ flexGrow: 0, minHeight: '250px' }"
      :theme="isDark ? 'darkMaterial' : 'material'"
      :columns="columns"
      :source="rows"
      :plugins="plugins"
      :additional-data="additionalData"
      :range="true"
      @aftergridinit="initPanel"
      stretch="all"
      hide-attribution
    />
    <div ref="panelRef" class="min-h-56"></div>
  </div>
</template>

<script setup lang="ts">
import { computed, nextTick, onMounted, ref } from 'vue';
import { VGrid } from '@revolist/vue3-datagrid';
import {
  AuditHistoryPlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  RowOddPlugin,
  defineAuditHistoryPanel,
  type AuditHistoryConfig,
} from '@revolist/revogrid-pro';
import { currentThemeVue } from '../composables/useRandomData';

const { isDark } = currentThemeVue();

const plugins = [
  EventManagerPlugin,
  AuditHistoryPlugin,
  ColumnStretchPlugin,
  RowOddPlugin,
];

const columns = [
  { name: 'Invoice', prop: 'id', readonly: true, size: 140 },
  { name: 'Customer', prop: 'customer' },
  { name: 'Status', prop: 'status' },
  { name: 'Amount', prop: 'amount' },
];

const rows = ref([
  { id: 'invoice-1001', customer: 'Northwind', status: 'Draft', amount: 1280 },
  { id: 'invoice-1002', customer: 'Acme Finance', status: 'Approved', amount: 2460 },
  { id: 'invoice-1003', customer: 'Globex', status: 'Pending', amount: 980 },
  { id: 'invoice-1004', customer: 'Initech', status: 'Draft', amount: 1750 },
]);

const auditHistory: AuditHistoryConfig = {
  getCurrentUser: () => ({
    id: 'avery-stone',
    name: 'Avery Stone',
    email: '[email protected]',
  }),
  rowIdProp: 'id',
  storage: 'memory',
};

const additionalData = computed(() => ({
  auditHistory,
}));

const gridRef = ref<HTMLRevoGridElement | null>(null);
const panelRef = ref<HTMLElement | null>(null);
let panelInitialized = false;

onMounted(async () => {
  await nextTick();

  const grid = ((gridRef.value as any)?.$el ?? gridRef.value) as HTMLRevoGridElement | null;
  if (grid && panelRef.value) {
    defineAuditHistoryPanel(panelRef.value, grid);
    panelInitialized = true;
  } else {
    console.error('Grid or panel reference is not available');
  }
});

const initPanel = () => {
  if (panelInitialized) return;

  const grid = ((gridRef.value as any)?.$el ?? gridRef.value) as HTMLRevoGridElement | null;
  if (grid && panelRef.value) {
    defineAuditHistoryPanel(panelRef.value, grid);
    panelInitialized = true;
  } else {
    console.error('Grid or panel reference is not available');
  }
};
</script>
Angular ts
import { AfterViewInit, Component, ElementRef, NO_ERRORS_SCHEMA, ViewChild, ViewEncapsulation } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import {
  AuditHistoryPlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  RowOddPlugin,
  defineAuditHistoryPanel,
  type AuditHistoryConfig,
} from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';

@Component({
  selector: 'audit-history-grid',
  standalone: true,
  imports: [RevoGrid],
  template: `
    <div class="grow flex flex-col h-full gap-3">
      <revo-grid
        #grid
        class="cell-border"
        style="flex-grow: 0; min-height: 250px;"
        [theme]="theme"
        [columns]="columns"
        [source]="rows"
        [plugins]="plugins"
        [additionalData]="additionalData"
        [range]="true"
        stretch="all"
        [hideAttribution]="true"
      ></revo-grid>
      <div #panel class="min-h-56"></div>
    </div>
  `,
  encapsulation: ViewEncapsulation.None,
  schemas: [NO_ERRORS_SCHEMA],
})
export class AuditHistoryGridComponent implements AfterViewInit {
  @ViewChild('grid', { read: ElementRef }) gridElement!: ElementRef<HTMLRevoGridElement>;
  @ViewChild('panel', { read: ElementRef }) panelElement!: ElementRef<HTMLElement>;

  theme = currentTheme().isDark() ? 'darkMaterial' : 'material';

  rows = [
    { id: 'invoice-1001', customer: 'Northwind', status: 'Draft', amount: 1280 },
    { id: 'invoice-1002', customer: 'Acme Finance', status: 'Approved', amount: 2460 },
    { id: 'invoice-1003', customer: 'Globex', status: 'Pending', amount: 980 },
    { id: 'invoice-1004', customer: 'Initech', status: 'Draft', amount: 1750 },
  ];

  columns = [
    { name: 'Invoice', prop: 'id', readonly: true, size: 140 },
    { name: 'Customer', prop: 'customer' },
    { name: 'Status', prop: 'status' },
    { name: 'Amount', prop: 'amount' },
  ];

  auditHistory: AuditHistoryConfig = {
    getCurrentUser: () => ({
      id: 'avery-stone',
      name: 'Avery Stone',
      email: '[email protected]',
    }),
    rowIdProp: 'id',
    storage: 'memory',
  };

  additionalData = {
    auditHistory: this.auditHistory,
  };

  plugins = [
    EventManagerPlugin,
    AuditHistoryPlugin,
    ColumnStretchPlugin,
    RowOddPlugin,
  ];

  ngAfterViewInit() {
    defineAuditHistoryPanel(this.panelElement.nativeElement, this.gridElement.nativeElement);
  }
}

When users edit operational data, the final cell value is not enough. Audit history keeps the context around each change so business users, admins, and support teams can review the full lifecycle of a value.

Accountability

Record the current user, timestamp, action type, row identity, changed column, previous value, and new value.

Bulk Awareness

Group paste and range-edit operations into one transaction instead of treating every changed cell as an unrelated record.

Recovery

Restore a single cell, a row snapshot, or a complete transaction after accidental edits.

Persistence

Keep records in memory, browser localStorage, or a custom backend adapter.

Audit history is useful when RevoGrid edits real business data, not temporary UI state.

ERP And Operations

Track changes to orders, invoices, stock levels, delivery statuses, prices, and approvals.

Financial Planning

Review manual adjustments to budgets, forecasts, risk values, and reporting tables.

Admin Panels

Log edits to permissions, limits, subscriptions, account status, and configuration records.

Support Workflows

Explain why a value changed without guessing from the current dataset.

Use AuditHistoryPlugin with EventManagerPlugin. The event manager normalizes cell edits, range edits, paste operations, undo, and redo into edit events the audit plugin can record.

  1. Import the plugins, panel helper, and config type.

    import {
    AuditHistoryPlugin,
    EventManagerPlugin,
    defineAuditHistoryPanel,
    type AuditHistoryConfig,
    } from '@revolist/revogrid-pro';
  2. Provide the audit config.

    const auditHistory: AuditHistoryConfig = {
    getCurrentUser: () => ({
    id: 'avery-stone',
    name: 'Avery Stone',
    }),
    rowIdProp: 'id',
    storage: 'memory',
    onAuditRecord(record) {
    // Persist to your API, Supabase, Firebase, PostgreSQL, etc.
    console.log(record);
    },
    };
  3. Register plugins and bind the config directly on the grid.

    grid.plugins = [EventManagerPlugin, AuditHistoryPlugin];
    grid.auditHistory = auditHistory;
  4. Mount the optional review panel.

    defineAuditHistoryPanel(panelElement, grid, {
    pageSize: 25,
    allowExport: true,
    });

Audit records should point to business records, not visual row positions. Row indexes can change after sorting, filtering, grouping, or lazy loading, so every row should expose a stable identity.

const rows = [
{ id: 'invoice-123', status: 'Draft', amount: 1200 },
{ id: 'invoice-124', status: 'Paid', amount: 800 },
];
const auditHistory: AuditHistoryConfig = {
rowIdProp: 'id',
};

If your row identity is derived, use getRowId instead:

const auditHistory: AuditHistoryConfig = {
getRowId: (row) => `${row.tenantId}:${row.invoiceNumber}`,
};

Each top-level audit item is an AuditRecord. A record contains one transaction ID and one or more AuditChange entries.

{
id: 'audit-001',
transactionId: 'tx-001',
type: 'cell-change',
changedAt: '2026-05-08T12:00:00Z',
changedBy: {
id: 'avery-stone',
},
changes: [
{
id: 'change-001',
rowId: 'invoice-123',
rowType: 'rgRow',
column: 'status',
oldValue: 'Draft',
newValue: 'Approved',
},
],
}

For a paste operation, the same record can contain many changes:

{
transactionId: 'tx-002',
type: 'bulk-paste',
changedAt: '2026-05-08T12:05:00Z',
changedBy: { id: 'avery-stone' },
changes: [
{
id: 'change-002',
rowId: 'invoice-123',
rowType: 'rgRow',
column: 'status',
oldValue: 'Draft',
newValue: 'Approved',
},
{
id: 'change-003',
rowId: 'invoice-124',
rowType: 'rgRow',
column: 'status',
oldValue: 'Draft',
newValue: 'Rejected',
},
],
}

Memory

Use storage: 'memory' for demos, temporary sessions, and test fixtures.

Local Storage

Use storage: 'localStorage' when browser persistence is enough for the workflow.

Custom Adapter

Use storage: 'custom' with storageAdapter when your backend should load, append, save, or clear records.

Record Limit

Use maxRecords to bound retained records. The implementation defaults to 1000.

For production systems, persist records on the server:

const auditHistory: AuditHistoryConfig = {
getCurrentUser: () => currentUser,
rowIdProp: 'id',
storage: 'memory',
async onAuditRecord(record) {
await fetch('/api/audit-history', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(record),
});
},
};

Production audit logs often need more than raw before/after values. Use ignoredColumns, sanitizeValue, getMetadata, canRestore, and immutable to align the client behavior with your business rules.

const auditHistory: AuditHistoryConfig = {
getCurrentUser: () => currentUser,
rowIdProp: 'id',
ignoredColumns: ['internalNotes'],
sanitizeValue(value, context) {
if (context.column === 'amount') {
return '[redacted]';
}
return value;
},
getMetadata() {
return {
workspaceId: currentWorkspace.id,
requestId: crypto.randomUUID(),
};
},
canRestore({ type }) {
return currentUser.role === 'admin' && type !== 'transaction';
},
};

For compliance-style screens where the client must not mutate local audit records, set immutable: true. This disables clear, replaceRecords, and restore actions in the plugin. Server-side immutability should still be enforced by your backend.

const auditHistory: AuditHistoryConfig = {
storage: 'custom',
storageAdapter,
immutable: true,
};

The audit panel lets users inspect grid changes without leaving the page. Common review flows include full-grid history, selected-row history, selected-cell history, user filtering, date filtering, action-type filtering, and old/new value comparison.

Use the plugin instance when you need audit data in custom UI:

const plugins = await grid.getPlugins();
const auditHistoryPlugin = plugins.find(
(plugin) => plugin instanceof AuditHistoryPlugin,
);
const records = auditHistoryPlugin?.getRecords({ userId: 'avery-stone' });
const cellHistory = auditHistoryPlugin?.getCellHistory('invoice-123', 'status');
const rowHistory = auditHistoryPlugin?.getRowHistory('invoice-123');
const lastStatusChange = auditHistoryPlugin?.getLastChange('invoice-123', 'status');
const stats = auditHistoryPlugin?.getStats();

Restore helpers return true when a restore was applied:

auditHistoryPlugin?.restoreCell(change);
auditHistoryPlugin?.restoreRow('invoice-123');
auditHistoryPlugin?.restoreTransaction(transactionId);

Query helpers return defensive copies, so application code cannot accidentally mutate the plugin’s internal audit log.

Use refreshRecords when your custom storage adapter can load existing records from a backend. By default, loaded records merge by ID with local records; pass { merge: false } to replace the local set.

await auditHistoryPlugin?.refreshRecords();
await auditHistoryPlugin?.replaceRecords(serverRecords, { merge: false });

Use exportRecords for custom export buttons or reporting workflows:

const csv = auditHistoryPlugin?.exportRecords({
format: 'csv',
filter: { actionType: 'bulk-paste' },
includeMetadata: true,
});
const json = auditHistoryPlugin?.exportRecords({ format: 'json' });

The built-in panel includes search, user/column/action/date filters, scoped cell and row views, pagination, and CSV/JSON export controls.

User edits grid
-> EventManagerPlugin normalizes the edit
-> AuditHistoryPlugin creates an audit transaction
-> the record is stored locally or sent to your backend
-> users review changes in the audit panel
-> admins restore cells, rows, or transactions when needed

AuditHistoryPlugin adds accountability to editable RevoGrid applications. Pair it with stable row IDs, a current-user provider, and a trusted persistence path when users edit important operational data.