npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

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-log Part of: the e-LLM Studio component library


Table of contents

  1. What you get
  2. Installation
  3. Setup (styles + router)
  4. Quick start
  5. <AuditLog /> — full prop reference
  6. The LogRow shape
  7. Behavior details
  8. <AuditLog /> examples
  9. API contracts
  10. Using the individual components
  11. Exports reference
  12. 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-log

Peer 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 primereact

3. 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 data and getData are 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

  1. User clicks Restore this version on a row → button shows "Restoring…".
  2. POST/PUT {baseUrl}{endpoint} is called with { change_log_id }.
  3. On success, the internal data refetch()s and a success toast shows (customizable via table.restoreToast).
  4. Rows with isLive === true display 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

RequestPOST {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

RequestGET {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

RequestPOST (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.externalResourceId is empty/undefined, the internal fetcher will not fire.
  • distinctValuesApi is fetched lazily — only when the sidebar is visible (config.showSidebar !== false).
  • restoreApi is opt-in; without it, the restore button only console.warns.
  • In external data mode, you are responsible for refetching after a restore — the internal refetch() is skipped. Use refetchKey (internal mode) to force a refresh from a parent.
  • paginationLimit is sent as pageSize; the backend decides the page size if omitted.
  • The impact filter is only sent to the API once applied (committed to impactMin/impactMax in the URL).
  • ActionTakenBody's description prop is spelled desciption (kept for backward compatibility).