Back to Kibana

@kbn/change-history-ui

x-pack/platform/packages/shared/kbn-change-history-ui/README.md

9.5.05.9 KB
Original Source

@kbn/change-history-ui

Shared browser package for change history UI in Kibana. Domains integrate via ChangeHistoryAdapter; the package ships a fullscreen modal shell and reusable timeline components.

The host app must provide QueryClientProvider (@kbn/react-query). This package does not create its own QueryClient.

Integration

  1. Implement ChangeHistoryAdapterlistChanges, getChange, optional restoreChange, optional getPendingChange.
  2. Wrap with ChangeHistoryProvider — adapter, renderPreview, labels.previewTitle, scope, optional renderBadge, optional renderChangesSummary, features, permissions, optional listPageSize (defaults to DEFAULT_CHANGE_HISTORY_PAGE_SIZE, currently 20), and optional analytics. Enable restore with both features={{ restore: true }} and permissions={{ canRestore: true }}. Enable unsaved in-editor state with features={{ unsavedChanges: true }} when the adapter implements getPendingChange. Disable compare with features={{ compare: false }} (enabled by default).
  3. Render ChangeHistoryTrigger and ChangeHistoryModal.

Minimal (domain-neutral)

tsx
import { QueryClientProvider } from '@kbn/react-query';
import {
  ChangeHistoryProvider,
  ChangeHistoryModal,
  ChangeHistoryTrigger,
} from '@kbn/change-history-ui';

<QueryClientProvider client={queryClient}>
  <ChangeHistoryProvider
    objectId={documentId}
    adapter={documentChangeHistoryAdapter}
    renderPreview={({ change, compareSpec, diffTelemetry }) => (
      <pre>{JSON.stringify(compareSpec?.target.snapshot ?? change.snapshot, null, 2)}</pre>
    )}
    labels={{ previewTitle: documentTitle }}
    scope={{
      module: 'stack',
      dataset: 'documents',
      objectType: 'document',
    }}
    analytics={{ reportEvent: core.analytics.reportEvent }}
  >
    <ChangeHistoryTrigger />
    <ChangeHistoryModal />
  </ChangeHistoryProvider>
</QueryClientProvider>

Implement ChangeHistoryAdapter.listChanges / getChange against your domain API. Snapshots are opaque (unknown); map your entity shape in the adapter. Call diffTelemetry?.reportDiffViewed() from renderPreview when your diff UI shows a non-empty comparison.

Compare: When compare is enabled, getChange supplies baseline/target snapshot detail as needed. Implementations should resolve any requested changeId. List rows must stay newest-first.

scope{ module, dataset, objectType }, aligned with @kbn/change-history server clients and telemetry payloads. objectId must be unique within that scope when multiple domains share one QueryClient.

In renderPreview, call diffTelemetry?.reportDiffViewed() when your consumer shows a non-empty diff. Use diffTelemetry.reportDiffChangeNavigated(source) for in-diff navigation (e.g. hunk prev/next).

HTTP adapter

createChangeHistoryHttpAdapter uses 0-based page query params. Domains with 1-based list APIs or detail embedded in list rows should implement a custom ChangeHistoryAdapter.

Telemetry

Register EBT event types once in the consuming plugin's setup():

tsx
import { registerChangeHistoryTelemetryEvents } from '@kbn/change-history-ui';

registerChangeHistoryTelemetryEvents(analytics);

For bundle size, lazy-load registration:

tsx
void import('@kbn/change-history-ui/src/telemetry/register_change_history_telemetry_events').then(
  ({ registerChangeHistoryTelemetryEvents }) => registerChangeHistoryTelemetryEvents(analytics)
);

Pass analytics={{ reportEvent: core.analytics.reportEvent }} and scope into ChangeHistoryProvider. Set features={{ telemetry: false }} to disable reporting.

Registration is idempotent: call from each consumer's setup(); Core rejects duplicate event types and other errors are rethrown. First successful registration wins for schema ownership.

Every payload includes eventName, module, dataset, and objectType (from scope). Use useChangeHistoryConfig().telemetry to emit additional events from custom UI.

Events

Event typeeventNameWhen emittedNotable properties
change_history_openedChange history openedEach time the modal is opened (closed → open)
change_history_change_selectedChange history change selectedUser selects a timeline row or auto-selects latestselectionSource (user_click | auto_latest), hasSequence, optional eventAction
change_history_filter_appliedChange history filter appliedFilter UI applies a change (not wired yet)filterType (timeRange | actor), optional hasActiveTimeRange, activeActorCount
change_history_diff_viewedChange history diff viewedPreview consumer calls diffTelemetry.reportDiffViewed() when a non-empty diff is showncomparisonType (vs_previous | vs_row), optional versionDistance (from metadata.version when both rows include it), compareMode, hasChangesSummaryTooltip
change_history_diff_change_navigatedChange history diff change navigatedPreview consumer reports diff navigation (e.g. hunk prev/next)navigationSource (consumer-defined keyword)
change_history_restore_confirmedChange history restore confirmedUser confirms restore in the dialogoptional restoredFromSequence, currentSequence, rollbackDistance, hadUnsavedLocalEdits
change_history_restore_completedChange history restore completedRestore API succeedssame sequence fields + optional hadUnsavedLocalEdits + optional durationMs (confirm → API success)
change_history_restore_failedChange history restore failedRestore API failsoptional sequence fields + optional hadUnsavedLocalEdits + optional errorCode (e.g. RESTORE_CONFLICT)

rollbackDistance is currentSequence - restoredFromSequence when both are present. Sequence fields are omitted when list rows lack object.sequence.