Skip to content

Audit Trail

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.

Try the compact audit feed with sample operational data, then inspect the framework source below the grid.

Source code
TypeScriptts
import { defineCustomElements } from '@revolist/revogrid/loader';
import {
  AuditHistoryPlugin,
  CellFlashPlugin,
  EventManagerPlugin,
  defineAuditHistoryPanel,
} from '@revolist/revogrid-pro';
import { currentTheme, observeCurrentTheme } from '../../composables/useRandomData';
import {
  createAuditColumns,
  createAuditHistoryConfig,
  createCellFlashConfig,
  createInvoiceRows,
  createPanelOptions,
  type InvoiceRow,
} from './audit-history.shared';
import './audit-history.scss';

defineCustomElements();

const plugins = [EventManagerPlugin, AuditHistoryPlugin, CellFlashPlugin];

export function load(parentSelector: string, rows?: InvoiceRow[]) {
  const parent = document.querySelector(parentSelector);
  if (!parent) return () => undefined;

  const source = rows?.length ? rows : createInvoiceRows();
  const showcase = document.createElement('section');
  showcase.className = 'audit-showcase';
  showcase.setAttribute('aria-label', 'Invoice audit history workspace');

  const workspace = document.createElement('div');
  workspace.className = 'audit-workspace';
  const grid = document.createElement('revo-grid');
  grid.className = 'audit-grid';
  grid.theme = currentTheme().isDark() ? 'darkMaterial' : 'material';
  grid.columns = createAuditColumns();
  grid.plugins = plugins;
  grid.auditHistory = createAuditHistoryConfig();
  grid.cellFlash = createCellFlashConfig();
  grid.range = true;
  grid.stretch = 'last';
  grid.hideAttribution = true;

  const panel = document.createElement('aside');
  panel.className = 'audit-panel-host';
  workspace.append(grid, panel);
  showcase.appendChild(workspace);
  parent.appendChild(showcase);
  const panelHandle = defineAuditHistoryPanel(panel, grid, createPanelOptions());
  grid.source = source;
  const disconnectTheme = observeCurrentTheme((isDark) => {
    grid.theme = isDark ? 'darkMaterial' : 'material';
  });

  return () => {
    disconnectTheme();
    panelHandle.destroy();
    grid.remove();
    panel.remove();
    showcase.remove();
  };
}
Reacttsx
import { useEffect, useMemo, useRef, useState } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import {
  AuditHistoryPlugin,
  CellFlashPlugin,
  EventManagerPlugin,
  defineAuditHistoryPanel,
} from '@revolist/revogrid-pro';
import { currentTheme, observeCurrentTheme } from '../../composables/useRandomData';
import {
  createAuditColumns,
  createAuditHistoryConfig,
  createCellFlashConfig,
  createInvoiceRows,
  createPanelOptions,
  type InvoiceRow,
} from './audit-history.shared';
import './audit-history.scss';

export default function AuditHistory({ rows }: { rows?: InvoiceRow[] }) {
  const gridRef = useRef<HTMLRevoGridElement>(null);
  const panelRef = useRef<HTMLElement>(null);
  const source = useMemo(() => rows?.length ? rows : createInvoiceRows(), [rows]);
  const columns = useMemo(() => createAuditColumns(), []);
  const plugins = useMemo(() => [EventManagerPlugin, AuditHistoryPlugin, CellFlashPlugin], []);
  const auditHistory = useMemo(() => createAuditHistoryConfig(), []);
  const cellFlash = useMemo(() => createCellFlashConfig(), []);
  const [darkTheme, setDarkTheme] = useState(() => currentTheme().isDark());

  useEffect(() => observeCurrentTheme(setDarkTheme), []);

  useEffect(() => {
    const grid = gridRef.current;
    const panel = panelRef.current;
    if (!grid || !panel) return;
    const handle = defineAuditHistoryPanel(panel, grid, createPanelOptions());
    return () => handle.destroy();
  }, []);

  return (
    <section className="audit-showcase" aria-label="Invoice audit history workspace">
      <div className="audit-workspace">
        <RevoGrid
          ref={gridRef}
          className="audit-grid"
          theme={darkTheme ? 'darkMaterial' : 'material'}
          source={source}
          columns={columns}
          plugins={plugins}
          auditHistory={auditHistory}
          cellFlash={cellFlash}
          range={true}
          stretch="last"
          hideAttribution={true}
        />
        <aside ref={panelRef} className="audit-panel-host" />
      </div>
    </section>
  );
}
Vuevue
<template>
  <section class="audit-showcase" aria-label="Invoice audit history workspace">
    <div class="audit-workspace">
      <RevoGrid
        class="audit-grid"
        :theme="darkTheme ? 'darkMaterial' : 'material'"
        :source="rows"
        :columns="columns"
        :plugins="plugins"
        :audit-history.prop="auditHistory"
        :cell-flash.prop="cellFlash"
        :range="true"
        stretch="last"
        hide-attribution
        @aftergridinit="mountAuditPanel"
      />
      <aside ref="panelRef" class="audit-panel-host"></aside>
    </div>
  </section>
</template>

<script setup lang="ts">
import { nextTick, onMounted, onUnmounted, ref } from 'vue';
import RevoGrid from '@revolist/vue3-datagrid';
import {
  AuditHistoryPlugin,
  CellFlashPlugin,
  EventManagerPlugin,
  defineAuditHistoryPanel,
  type AuditHistoryPanelOptions,
} from '@revolist/revogrid-pro';
import { currentTheme, observeCurrentTheme } from '../../composables/useRandomData';
import {
  createAuditColumns,
  createAuditHistoryConfig,
  createCellFlashConfig,
  createInvoiceRows,
  createPanelOptions,
  type InvoiceRow,
} from './audit-history.shared';
import './audit-history.scss';

const props = defineProps<{ rows?: InvoiceRow[]; example?: 'records' | 'panel' | 'restore' | 'storage' | 'policies' | 'localization' | 'export' }>();
const rows = ref(props.rows?.length ? props.rows : createInvoiceRows());
const columns = createAuditColumns();
const plugins = [EventManagerPlugin, AuditHistoryPlugin, CellFlashPlugin];
const auditHistory = createAuditHistoryConfig();
const panelOptions: AuditHistoryPanelOptions = createPanelOptions();
if (props.example) panelOptions.allowClose = false;
if (props.example === 'storage') {
  const seededRecords = auditHistory.records;
  auditHistory.records = undefined;
  auditHistory.storage = 'custom';
  auditHistory.storageAdapter = { load: async () => seededRecords ?? [] };
}
if (props.example === 'policies') {
  auditHistory.immutable = true;
  panelOptions.restoreActions = false;
}
if (props.example === 'localization') {
  panelOptions.locale = 'es-ES';
  panelOptions.dateFormat = 'eu';
  panelOptions.timeFormat = '24h';
  panelOptions.labels = {
    title: 'Registro de cambios', allTab: 'Toda actividad', rowTab: 'Esta fila', cellTab: 'Esta celda',
    addFilter: 'Filtros', closeFilters: 'Listo', searchPlaceholder: 'Buscar…', searchAriaLabel: 'Buscar actividad',
    userPlaceholder: 'Persona', allColumns: 'Todos los campos', allActions: 'Todos los eventos',
    clearFilters: 'Quitar filtros', export: 'Exportar', dayToday: 'Hoy', dayYesterday: 'Ayer',
  };
}
if (props.example === 'records' || props.example === 'storage' || props.example === 'localization' || props.example === 'export') {
  panelOptions.restoreActions = false;
}
const cellFlash = createCellFlashConfig();
const darkTheme = ref(currentTheme().isDark());
const panelRef = ref<HTMLElement | null>(null);
let panelHandle: ReturnType<typeof defineAuditHistoryPanel> | undefined;
let disposed = false;
let disconnectTheme: (() => void) | undefined;

async function mountAuditPanel(event: Event) {
  // The functional wrapper has no component ref; use the native readiness event.
  await nextTick();
  if (disposed || panelHandle || !panelRef.value) return;
  panelHandle = defineAuditHistoryPanel(panelRef.value, event.target as HTMLRevoGridElement, panelOptions);
}

onMounted(() => {
  disconnectTheme = observeCurrentTheme((isDark) => {
    darkTheme.value = isDark;
  });
});

onUnmounted(() => {
  disposed = true;
  disconnectTheme?.();
  panelHandle?.destroy();
});
</script>
Angularts
import {
  AfterViewInit,
  Component,
  ElementRef,
  Input,
  NO_ERRORS_SCHEMA,
  OnDestroy,
  ViewChild,
  ViewEncapsulation,
} from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import {
  AuditHistoryPlugin,
  CellFlashPlugin,
  EventManagerPlugin,
  defineAuditHistoryPanel,
} from '@revolist/revogrid-pro';
import { currentTheme, observeCurrentTheme } from '../../composables/useRandomData';
import {
  createAuditColumns,
  createAuditHistoryConfig,
  createCellFlashConfig,
  createInvoiceRows,
  createPanelOptions,
  type InvoiceRow,
} from './audit-history.shared';

@Component({
  selector: 'audit-history-grid',
  standalone: true,
  imports: [RevoGrid],
  encapsulation: ViewEncapsulation.None,
  schemas: [NO_ERRORS_SCHEMA],
  styleUrls: ['./audit-history.scss'],
  template: `
    <section class="audit-showcase" aria-label="Invoice audit history workspace">
      <div class="audit-workspace">
        <revo-grid
          #grid
          class="audit-grid"
          [theme]="theme"
          [source]="source"
          [columns]="columns"
          [plugins]="plugins"
          [auditHistory]="auditHistory"
          [cellFlash]="cellFlash"
          [range]="true"
          [stretch]="'last'"
          [hideAttribution]="true"
        ></revo-grid>
        <aside #panel class="audit-panel-host"></aside>
      </div>
    </section>
  `,
})
export class AuditHistoryGridComponent implements AfterViewInit, OnDestroy {
  @ViewChild('grid', { read: ElementRef }) private gridElement!: ElementRef<HTMLRevoGridElement>;
  @ViewChild('panel', { read: ElementRef }) private panelElement!: ElementRef<HTMLElement>;
  private panelHandle?: ReturnType<typeof defineAuditHistoryPanel>;

  @Input()
  set rows(value: InvoiceRow[] | undefined) {
    this.source = value?.length ? value : createInvoiceRows();
  }

  theme: HTMLRevoGridElement['theme'] = currentTheme().isDark() ? 'darkMaterial' : 'material';
  private readonly disconnectTheme = observeCurrentTheme((isDark) => {
    this.theme = isDark ? 'darkMaterial' : 'material';
  });
  readonly columns = createAuditColumns();
  readonly plugins = [EventManagerPlugin, AuditHistoryPlugin, CellFlashPlugin];
  readonly auditHistory = createAuditHistoryConfig();
  readonly cellFlash = createCellFlashConfig();
  source = createInvoiceRows();

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

  ngOnDestroy() {
    this.disconnectTheme();
    this.panelHandle?.destroy();
  }
}

The demo is wired like a production audit surface: edit cells, select a changed cell, filter the history, export records, and restore previous values from the panel.

Edit And Review

Change a cell and watch the panel add a timestamped record with user and before/after value details.

Inspect Markers

Hover a marked cell to see the latest change summary while keeping the full audit trail in the panel.

Scope The Trail

Select a cell or row, then choose All activity, This row, or This cell in the panel scope menu. Grid selection updates the available context without switching the chosen scope.

Recover Fast

Use restore actions to roll back a cell, row snapshot, or transaction while recording the restore itself.

Register AuditHistoryPlugin, provide the current user and a stable row ID, then mount the optional panel.

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

    import {
    AuditHistoryPlugin,
    CellFlashPlugin,
    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 = [AuditHistoryPlugin, CellFlashPlugin];
    grid.auditHistory = auditHistory;

    HistoryPlugin does not need to be listed for normal Audit History restore behavior. Add it explicitly only when the page also has direct History UI controls or you want to control plugin order yourself.

  4. Mount the optional review panel.

    const auditPanel = defineAuditHistoryPanel(panelElement, grid, {
    pageSize: 25,
    allowExport: true,
    placement: 'right',
    restoreActions: ['cell', 'row'],
    dateFormat: 'eu',
    timeFormat: '24h',
    events: {
    beforeExport: ({ format }) => format !== 'json',
    beforeRestore: ({ type }) => type !== 'transaction',
    },
    });
    // Later:
    auditPanel.setPlacement('bottom');
    auditPanel.refresh();

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}`,
};
GoalGuide
Enable audit history and mount the panelGetting started
Try editing, filtering and recoveryLive demo
Understand transactions and custom eventsRecords and events
Configure the compact feed and scopesPanel and filters
Compare and restore changesReview and restore
Persist and load historyStorage and providers
Apply redaction and restore policiesPolicies and privacy
Translate labels and format timestampsLocalization and dates
Export filtered recordsExport

For signatures, see Audit History API and Audit History Panel API.

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.

User edits a grid cell or range
-> one audit transaction captures the user and before/after values
-> records are retained locally or sent to your backend
-> users filter and compare changes in the panel
-> permitted restores recover values and create their own audit record