@e-llm-studio/audit-log
v2.0.5
Published
A flexible, composable **React** library for rendering audit / change-log surfaces with search, sorting, advanced filtering, infinite scroll, expandable row detail, and version restore.
Maintainers
Keywords
Readme
@e-llm-studio/audit-log
A flexible, composable React library for rendering audit / change-log surfaces with search, sorting, advanced filtering, infinite scroll, expandable row detail, and version restore.
It ships one drop-in component — <AuditLog /> — that wires everything together from a few API endpoints, plus every sub-component is exported individually so you can compose your own UI.
Package:
@e-llm-studio/audit-logPart of: the e-LLM Studio component library
Table of contents
- What you get
- Installation
- Setup (styles + router)
- Quick start
<AuditLog />— full prop reference- The
LogRowshape - Behavior details
<AuditLog />examples- API contracts
- Using the individual components
- Exports reference
- Gotchas
1. What you get
<AuditLog /> renders a complete audit-log surface:
- A toolbar — keyword search (debounced), a date-range picker, a sort menu (Latest / Oldest / Impact % High↔Low) and a Filter button.
- A right-side sidebar — multi-select "Modified By", "Change Title", a date-range filter, and an "Impact percentage" range slider. Uses a draft model (changes apply only on Apply Filter).
- A virtualized data table — infinite scroll, per-row Restore this version action, optional expandable row detail, and an inline restore toast.
- An internal data layer — calls your cursor-paginated audit API, auto-populates sidebar dropdowns, and handles restore. Or you fully control data externally.
URL state for search, date range, sort, analyst filter, change-title filter, and impact range is handled automatically via react-router-dom's useSearchParams, so links are shareable and the back button works.
Every sub-piece — table, toolbar, sidebar, date picker, multi-select, impact slider — is also exported on its own for fully custom layouts (see §10).
2. Installation
npm install @e-llm-studio/audit-logPeer dependencies
Your host app must provide these (they are not bundled):
{
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^7.6.2",
"lucide-react": "^0.476.0",
"primeicons": "^7.0.0",
"primereact": "^10.9.7"
}npm install react react-dom react-router-dom lucide-react primeicons primereact3. Setup (styles + router)
1. Import the stylesheet once (e.g. in your app entry point):
import "@e-llm-studio/audit-log/dist/styles.css";2. Render inside a router. Filter state lives in the URL, so the component must be mounted inside a <BrowserRouter> (or any router exposing useSearchParams):
import { BrowserRouter } from "react-router-dom";
<BrowserRouter>
{/* ...your routes, including the page that renders <AuditLog /> */}
</BrowserRouter>4. Quick start
import { AuditLog } from "@e-llm-studio/audit-log";
import { useParams } from "react-router-dom";
export function AuditLogPage() {
const { id: externalResourceId } = useParams();
const API = import.meta.env.VITE_BASE_API_URL;
return (
<AuditLog
configId="6a0ab685346f096a6934b358"
getData={{ baseUrl: API, endpoint: "/claims-management-mode/audits/" }}
distinctValuesApi={{ baseUrl: API, endpoint: "/audit-log/distinct-values" }}
restoreApi={{ baseUrl: API, endpoint: "/audit-log/restore", method: "POST" }}
query={{
externalResourceId: [externalResourceId!],
paginationLimit: 20,
}}
table={{ tableProps: { scrollHeight: "60vh" } }}
/>
);
}That's a fully functional audit-log page: data fetching, filtering, sorting, infinite scroll, sidebar dropdowns, and restore.
5. <AuditLog /> — full prop reference
| Prop | Type | Required | Description |
|---|---|:---:|---|
| configId | string | ✅ | Tenant / configuration identifier sent to every audit API call. |
| getData | { baseUrl: string; endpoint: string } | optional¹ | Audit-logs API for internal fetching (POST). Required unless you use external data. |
| distinctValuesApi | { baseUrl: string; endpoint?: string } | optional | If provided, the sidebar's "Modified By" and "Change Title" dropdowns are auto-populated. endpoint defaults to /audit-log/distinct-values. |
| restoreApi | { baseUrl: string; endpoint: string; method?: "POST" \| "PUT" } | optional | Enables the Restore this version button. Without it, restore only logs a warning. |
| query | { externalResourceId?: string[]; paginationLimit?: number } | optional | The resource(s) to scope the log to, and the page size (pageSize) for pagination. |
| filters | Partial<LogFiltersProps> | optional | Pass-through overrides for the toolbar. See §10.2. |
| sidebar | Partial<LogFiltersSidebarProps> | optional | Pass-through overrides for the filter sidebar. See §10.3. |
| table | Partial<LogTableProps> | optional | Pass-through overrides for the data table. See §10.1. |
| config | { showFilters?: boolean; showSidebar?: boolean } | optional | Toggle whole sub-sections off. Both default to true. |
| data | LogRow[] | optional | External data mode. Skips the internal fetcher and renders these rows. |
| loading | boolean | optional | Initial-load flag (external mode). |
| loadingMore | boolean | optional | "Loading more" flag (external mode). |
| hasMore | boolean | optional | Whether more rows exist (external mode). |
| onLoadMore | () => void | optional | Infinite-scroll callback (external mode). |
| refetchKey | string \| number | optional | Change this value to force an internal refetch (e.g. after a mutation elsewhere). |
¹ If both
dataandgetDataare passed, external data wins — internal fetching is disabled.
6. The LogRow shape
type LogRow = {
id?: string; // table key; auto-derived from change_log_id
historyId?: string; // used as the key for restore
modifiedBy?: string;
actionType?: string; // shown as the chip label in "Change Summary"
actionText?: string; // long text (truncated at 95 chars w/ "See More")
actionDate?: string; // ISO date string
updatedByICE?: boolean; // when true, "Modified By" shows "ICE"
isLive?: boolean; // when true, shows a red "Live" pill instead of Restore
[key: string]: any;
};When the internal fetcher is used, the raw API response is normalized into this shape automatically (see §9.1). In external mode you must provide rows matching this shape.
7. Behavior details
URL state
The component stores filter state in the URL via react-router-dom:
| URL param | Meaning |
|---|---|
| keyword | Search input (debounced 350 ms) |
| createdAtStart / createdAtEnd | Date range (ISO strings) |
| orderBy | latest (default), oldest, impactHighToLow, or impactLowToHigh |
| userEmails | Comma-separated list (Modified By) |
| changeTitles | Comma-separated list (Change Title) |
| impactMin / impactMax | Impact-percentage range (set on Apply) |
| cursor | Cleared on every filter change |
Sidebar draft state
The sidebar uses a draft model — selections are local until the user clicks Apply Filter, which writes them to the URL. Reset Filter clears both the draft and the applied URL state.
Infinite scroll
The internal fetcher is cursor-based. When the user scrolls within 100 px of the bottom (or when content isn't tall enough to scroll), fetchMore() fires. Skeletons render during the initial load and at the bottom during "load more".
Restore flow
- User clicks Restore this version on a row → button shows "Restoring…".
POST/PUT{baseUrl}{endpoint}is called with{ change_log_id }.- On success, the internal data
refetch()s and a success toast shows (customizable viatable.restoreToast). - Rows with
isLive === truedisplay a red Live pill instead of the restore button.
Expandable row detail
Pass table.rowDetail.render to render your own content inline beneath a clicked row (AI summaries, diffs, request/response payloads, etc.). Accordion by default; pass multiple: true to allow several open at once. See §10.1.
8. <AuditLog /> examples
8.1 Minimal — API-driven, all defaults
<AuditLog
configId={CONFIG_ID}
getData={{ baseUrl: API, endpoint: "/audits" }}
query={{ externalResourceId: [resourceId] }}
/>8.2 With sidebar dropdowns + restore
<AuditLog
configId={CONFIG_ID}
getData={{ baseUrl: API, endpoint: "/audits" }}
distinctValuesApi={{ baseUrl: API }} // endpoint defaults
restoreApi={{ baseUrl: API, endpoint: "/restore" }}
query={{ externalResourceId: [resourceId], paginationLimit: 25 }}
/>8.3 External data mode (you control fetching)
const { rows, isLoading, hasMore, loadMore } = useMyAuditQuery(resourceId);
<AuditLog
configId={CONFIG_ID}
data={rows}
loading={isLoading}
hasMore={hasMore}
onLoadMore={loadMore}
restoreApi={{ baseUrl: API, endpoint: "/restore" }}
/>8.4 Hide the sidebar, keep just toolbar + table
<AuditLog
configId={CONFIG_ID}
getData={{ baseUrl: API, endpoint: "/audits" }}
query={{ externalResourceId: [resourceId] }}
config={{ showSidebar: false }}
filters={{ config: { showFilterButton: false } }}
/>8.5 Limit the sort options + impact range
<AuditLog
configId={CONFIG_ID}
getData={{ baseUrl: API, endpoint: "/audits" }}
query={{ externalResourceId: [resourceId] }}
filters={{
config: {
sortOptions: { latest: true, oldest: true, impactHighToLow: false, impactLowToHigh: false },
},
}}
sidebar={{
impactRangeLimits: { min: 0, max: 50 }, // clamp the slider
config: { showImpactPercentage: true },
}}
/>8.6 Expandable row detail
<AuditLog
configId={CONFIG_ID}
getData={{ baseUrl: API, endpoint: "/audits" }}
query={{ externalResourceId: [resourceId] }}
table={{
rowDetail: {
expandOnRowClick: true, // default
multiple: false, // accordion (default)
render: (row, { close }) => (
<div style={{ padding: 16 }}>
<h4>Change detail — {row.actionType}</h4>
<pre>{row.actionText}</pre>
<button onClick={close}>Close</button>
</div>
),
},
}}
/>8.7 Add a custom column + tweak chip colors
import { ShieldCheck } from "lucide-react";
<AuditLog
configId={CONFIG_ID}
getData={{ baseUrl: API, endpoint: "/audits" }}
query={{ externalResourceId: [resourceId] }}
table={{
hiddenColumns: ["logId"],
extraColumns: [
{
key: "severity",
header: "Severity",
body: (row) => <span>{row.severity ?? "—"}</span>,
style: { width: "120px" },
},
],
chipConfig: {
getTone: (row) => (row.modifiedBy === "ICE" ? "orange" : "purple"),
getIcon: () => <ShieldCheck size={12} />,
},
tableProps: { scrollHeight: "70vh" },
emptyState: {
title: "No changes yet",
description: "Edit the record to see audit entries here.",
},
}}
/>8.8 Customize the restore toast
<AuditLog
configId={CONFIG_ID}
getData={{ baseUrl: API, endpoint: "/audits" }}
restoreApi={{ baseUrl: API, endpoint: "/restore" }}
query={{ externalResourceId: [resourceId] }}
table={{
restoreToast: {
enabled: true, // set false to suppress and handle toasts yourself
position: "top-right",
life: 4000,
success: { summary: "Restored", detail: (id) => `Reverted to ${id}` },
error: { summary: "Oops", detail: () => "Restore failed, try again." },
},
}}
/>8.9 Localized labels
<AuditLog
configId={CONFIG_ID}
getData={{ baseUrl: API, endpoint: "/audits" }}
query={{ externalResourceId: [resourceId] }}
sidebar={{
labels: {
title: "Filtrer par",
multiSelectLabel: "Modifié par",
dateLabel: "Sélectionner une date",
changeTitleLabel: "Titre du changement",
impactPercentageTitle: "Pourcentage d'impact",
},
}}
/>8.10 Analytics — tap into every interaction
<AuditLog
configId={CONFIG_ID}
getData={{ baseUrl: API, endpoint: "/audits" }}
query={{ externalResourceId: [resourceId] }}
filters={{
onChange: {
onSearch: (v) => analytics.track("audit_search", { v }),
onSort: (v) => analytics.track("audit_sort", { v }),
},
}}
sidebar={{
onApply: () => analytics.track("audit_filter_apply"),
onReset: () => analytics.track("audit_filter_reset"),
}}
/>9. API contracts
9.1 getData — audit-log fetch
Request — POST {baseUrl}{endpoint}:
{
"configId": "string",
"filters": {
"externalResourceId": ["string"],
"userEmails": ["string?"],
"changeTitle": ["string?"],
"content": "string?",
"modifiedDateStart": "ISO date?",
"modifiedDateEnd": "ISO date?",
"orderBy": "latestFirst", // | oldestFirst | impactPercentageHighToLow | impactPercentageLowToHigh
"impactPercentageStart": 0, // only sent once the impact filter is applied
"impactPercentageEnd": 100
},
"pageSize": 20,
"cursor": "opaque-cursor-from-previous-page?"
}Response — the library normalizes items into LogRows:
{
"data": {
"items": [
{
"change_log_id": "string", // → id + historyId
"external_resource_id": "string",
"modifiedbyname": "string", // → modifiedBy
"modifiedbyemail": "string",
"changeTitle": "string", // → actionType
"change": "string", // → actionText
"created_at": "ISO date" // → actionDate
}
],
"nextCursor": "string?",
"hasNext": true
}
}9.2 distinctValuesApi
Request — GET {baseUrl}{endpoint}/{configId}?external_resource_id=... (one query param per id). endpoint defaults to /audit-log/distinct-values.
Response:
{
"success": true,
"data": {
"change_made_title": ["string"], // → Change Title options
"change_made_by_user": { "User Name": "user@x" }, // → Modified By options (label: name, value: email)
"truncated": false
}
}9.3 restoreApi
Request — POST (or PUT) {baseUrl}{endpoint}:
{ "change_log_id": "string" }A non-2xx response throws Failed to restore audit version.
10. Using the individual components
Every building block is exported so you can compose a bespoke layout instead of using <AuditLog />. Import the stylesheet once (§3) regardless of which components you use.
import {
LogTable,
LogFilters,
LogFiltersSidebar,
DateRangeSelector,
ImpactRangeSelector,
CustomMultiSelect,
ActionTakenBody,
} from "@e-llm-studio/audit-log";10.1 LogTable — the data table
Renders rows, infinite-scroll skeletons, the empty state, the chip + restore cells, and optional expandable detail. You own the data and pagination.
| Prop | Type | Description |
|---|---|---|
| rows | LogRow[] | Rows to render. |
| loading / loadingMore | boolean | Initial-load / load-more flags. |
| hasMore | boolean | Whether more rows exist. |
| onLoadMore | () => void | Called on scroll-to-bottom. |
| emptyState | { title?; description? } | Empty-state copy. |
| hiddenColumns | string[] | Hide built-ins by key: logId, modifiedBy, changeSummary, changeDateTime, actions. |
| extraColumns | { key; header; body; style? }[] | Append columns, or replace a built-in by reusing its key. |
| chipConfig | { getTone?; getIcon?; getStyle? } | Per-row chip customization. getTone returns a suffix used as audit-chip--{tone}. |
| tableProps | Record<string, any> | Forwarded verbatim to the PrimeReact <DataTable> (scrollHeight, className, …). |
| onRowClick | (row, helpers) => void | Fires on row click. helpers: { isExpanded, expand, collapse, toggle }. Ignores clicks on buttons/links/inputs. |
| rowDetail | { render; expandOnRowClick?; multiple?; className?; style? } | Consumer-rendered inline detail under a clicked row. render(row, { close }). |
| restoringId | string \| null | Marks the row currently restoring. |
| onRestore | (historyId) => void \| Promise | Called when the restore button is clicked. |
| restoreToast | { enabled?; position?; life?; success?; error? } | Inline PrimeReact toast config. |
import { LogTable } from "@e-llm-studio/audit-log";
function MyTable({ rows, hasMore, loadMore, loading }) {
return (
<LogTable
rows={rows}
loading={loading}
hasMore={hasMore}
onLoadMore={loadMore}
chipConfig={{ getTone: (r) => (r.updatedByICE ? "orange" : "purple") }}
onRestore={async (id) => myRestoreApi(id)}
rowDetail={{
render: (row, { close }) => (
<div style={{ padding: 12 }}>
{row.actionText}
<button onClick={close}>Close</button>
</div>
),
}}
tableProps={{ scrollHeight: "65vh" }}
/>
);
}10.2 LogFilters — the toolbar
Controlled search / date / sort toolbar. value and onChange are required.
| Prop | Type | Description |
|---|---|---|
| value | { search?; dateRange?; sort? } | Current toolbar state. |
| onChange | { onSearch?; onDate?; onSort? } | Change handlers. |
| config | { showSearch?; showDate?; showSort?; showFilterButton?; sortOptions? } | Toggle pieces. sortOptions toggles each of latest/oldest/impactHighToLow/impactLowToHigh. |
| onOpenFilter | () => void | Fired when the Filter button is clicked. |
import { LogFilters, SortOrder, DateRange } from "@e-llm-studio/audit-log";
import { useState } from "react";
function Toolbar() {
const [search, setSearch] = useState("");
const [dateRange, setDateRange] = useState<DateRange>({ start: null, end: null });
const [sort, setSort] = useState<SortOrder>("latest");
return (
<LogFilters
value={{ search, dateRange, sort }}
onChange={{ onSearch: setSearch, onDate: setDateRange, onSort: setSort }}
config={{ showFilterButton: true }}
onOpenFilter={() => console.log("open sidebar")}
/>
);
}10.3 LogFiltersSidebar — the filter panel
A right-side PrimeReact Sidebar with multi-selects, a date range, and an impact slider. Fully controlled.
| Prop | Type | Description |
|---|---|---|
| visible / onHide | boolean / () => void | Open state. |
| options / selectedValues / onChange | — | "Modified By" multi-select. |
| changeTitleOptions / selectedChangeTitles / onChangeTitleChange | — | "Change Title" multi-select. |
| dateRange / onDateRangeChange | — | Date-range filter. |
| impactRange / impactRangeLimits / onImpactRangeChange | ImpactRange | Impact-percentage slider ({ min, max }). |
| onApply / onReset | () => void | Footer actions. |
| config | { showAnalystFilter?; showDateFilter?; showChangeTitleFilter?; showImpactPercentage? } | Toggle blocks. |
| labels | object | Localize every label/hint/title. |
import { LogFiltersSidebar } from "@e-llm-studio/audit-log";
<LogFiltersSidebar
visible={open}
onHide={() => setOpen(false)}
options={[{ label: "Renee Ramirez", value: "[email protected]" }]}
selectedValues={analysts}
onChange={setAnalysts}
dateRange={dateRange}
onDateRangeChange={setDateRange}
impactRange={impact}
impactRangeLimits={{ min: 0, max: 100 }}
onImpactRangeChange={setImpact}
onApply={applyFilters}
onReset={resetFilters}
/>10.4 DateRangeSelector — range date picker
Self-contained day/month/year range picker with draft + Apply/Reset. Note: the committed end is stored as exclusive (one day after the visually selected end).
import { DateRangeSelector, DateRange } from "@e-llm-studio/audit-log";
import { useState } from "react";
const [range, setRange] = useState<DateRange>({ start: null, end: null });
<DateRangeSelector
value={range}
onChange={setRange}
maxDate={new Date()}
endIcon // render the calendar icon on the right (sidebar style)
/>10.5 ImpactRangeSelector — min/max slider
A PrimeReact range slider with numeric Min/Max inputs.
import { ImpactRangeSelector, ImpactRange } from "@e-llm-studio/audit-log";
import { useState } from "react";
const [impact, setImpact] = useState<ImpactRange>({ min: 0, max: 100 });
<ImpactRangeSelector value={impact} onChange={setImpact} min={0} max={100} />10.6 CustomMultiSelect — searchable multi-select
A lightweight checkbox dropdown with search and "All".
import { CustomMultiSelect, SelectOption } from "@e-llm-studio/audit-log";
import { useState } from "react";
const options: SelectOption[] = [
{ label: "Renee Ramirez", value: "[email protected]" },
{ label: "ICE", value: "[email protected]" },
];
const [selected, setSelected] = useState<string[]>([]);
<CustomMultiSelect
options={options}
value={selected}
onChange={setSelected}
placeholder="Select analysts"
/>10.7 ActionTakenBody — chip + truncated text cell
The chip-plus-"See More" cell renderer used inside the table. Useful as a custom column body.
import { ActionTakenBody } from "@e-llm-studio/audit-log";
import { FileText } from "lucide-react";
<ActionTakenBody
title="Field updated"
desciption={row.actionText} // note: prop spelling is `desciption`
tone="purple"
icon={<FileText size={12} />}
/>11. Exports reference
// Components
import {
AuditLog, // the all-in-one component
LogTable,
LogFilters,
LogFiltersSidebar,
ActionTakenBody,
DateRangeSelector,
ImpactRangeSelector,
CustomMultiSelect,
} from "@e-llm-studio/audit-log";
// Types
import type {
LogRow,
RowClickHelpers,
RowDetailHelpers,
LogTableProps,
LogFiltersProps,
SortOrder,
LogFiltersSidebarProps,
LogFiltersSidebarOption,
DateRange,
ImpactRange,
SelectOption,
} from "@e-llm-studio/audit-log";
// Styles (import once)
import "@e-llm-studio/audit-log/dist/styles.css";12. Gotchas
<AuditLog />must be rendered inside a router — filter state lives in the URL.- Don't forget to import the stylesheet (
dist/styles.css) or the UI will be unstyled. - If
query.externalResourceIdis empty/undefined, the internal fetcher will not fire. distinctValuesApiis fetched lazily — only when the sidebar is visible (config.showSidebar !== false).restoreApiis opt-in; without it, the restore button onlyconsole.warns.- In external data mode, you are responsible for refetching after a restore — the internal
refetch()is skipped. UserefetchKey(internal mode) to force a refresh from a parent. paginationLimitis sent aspageSize; the backend decides the page size if omitted.- The impact filter is only sent to the API once applied (committed to
impactMin/impactMaxin the URL). ActionTakenBody's description prop is spelleddesciption(kept for backward compatibility).
