Back to Datahub

Document Change History Architecture

datahub-web-react/src/app/entityV2/document/changeHistory/ARCHITECTURE.md

1.5.0.39.3 KB
Original Source

Document Change History Architecture

Overview

The Document Change History feature provides a timeline view of all changes made to a document, including creation, title changes, content modifications, moves, state changes, and deletions. Users can view previous versions and restore them if needed.

Architecture Principles

This implementation follows best practices for:

  • Testability: Complex logic extracted into pure utility functions and custom hooks
  • Extensibility: Easy to add new change types without touching existing code
  • Type Safety: Full TypeScript support with proper type definitions
  • Error Handling: Graceful degradation with loading and error states
  • User Experience: Relative timestamps with full date on hover, smooth interactions

Directory Structure

changeHistory/
├── ARCHITECTURE.md                      # This file
├── hooks/
│   └── useParentDocumentTitle.ts       # Custom hook for fetching parent titles
├── utils/
│   └── changeUtils.ts                  # Pure utility functions for data extraction
├── changeMessages/
│   ├── ChangeMessageComponents.tsx     # Individual message components + router
│   └── README.md                       # Guide for adding new change types
├── DocumentChangeHistoryDrawer.tsx     # Main drawer container
├── DocumentHistoryTimeline.tsx         # Timeline list component
├── DocumentChangeTimelineContent.tsx   # Individual timeline entry
├── DocumentChangeTimelineDot.tsx       # User avatar for each change
└── PreviousVersionModal.tsx            # View/restore previous content

Component Hierarchy

DocumentChangeHistoryDrawer
└── DocumentHistoryTimeline
    └── Timeline (from alchemy-components)
        ├── DocumentChangeTimelineDot (for each item)
        └── DocumentChangeTimelineContent (for each item)
            ├── ChangeMessage (router component)
            │   ├── CreatedMessage
            │   ├── TitleChangedMessage
            │   ├── TextChangedMessage
            │   ├── StateChangedMessage
            │   ├── ParentChangedMessage (uses useParentDocumentTitle hook)
            │   ├── DeletedMessage
            │   └── DefaultMessage
            └── PreviousVersionModal (conditional, for text changes)

Key Features

1. Testable Utilities

extractChangeDetails(details)

Converts GraphQL StringMapEntry[] to Record<string, string>

  • Input: Array of {key, value} objects
  • Output: Simple key-value object
  • Testable: Pure function, no dependencies
  • Location: utils/changeUtils.ts
typescript
// Easy to unit test
const details = [
    { key: 'oldTitle', value: 'Old' },
    { key: 'newTitle', value: 'New' },
];
const result = extractChangeDetails(details);
// { oldTitle: 'Old', newTitle: 'New' }

getActorDisplayName(actor, getDisplayName)

Extracts actor display name with fallback

  • Input: Actor object + display name function
  • Output: String name or 'System'
  • Testable: Can mock the getDisplayName function
  • Location: utils/changeUtils.ts

2. Custom Hooks

useParentDocumentTitle(parentUrn)

Fetches parent document title with proper state handling

  • Returns: { title, loading, error }
  • States:
    • Loading: returns '...'
    • Error: returns 'Unknown Document' + logs error
    • Success: returns actual title
    • No URN: returns '...'
  • Testable: Can be tested with mocked GraphQL queries
  • Location: hooks/useParentDocumentTitle.ts

3. Error & Loading States

ComponentLoading HandledError HandledNotes
useParentDocumentTitleShows '...' while loading, 'Unknown Document' on error
PreviousVersionModalDisables buttons during restore, shows error message
DocumentHistoryTimelineShows loading skeleton, empty state message

4. User Experience

Timestamps

  • Relative: "2 minutes ago", "3 days ago" (using dayjs.fromNow())
  • Absolute: Hover shows full timestamp "March 15, 2024 2:30:45 PM"
  • Implementation: Popover with formatted timestamp

Change Messages

All messages follow the pattern: {ActorName} {action} {target}

  • ✅ Actor names in bold
  • ✅ Important values (titles, parent names) in bold
  • ✅ Interactive elements (links) styled appropriately

Restore Flow

  1. Click "See previous version" → Opens modal
  2. Review old content
  3. Click "Restore" → Shows confirmation
  4. Confirm → Restores content + refetches + closes modals
  5. Error handling → Shows error message, keeps modals open

Adding a New Change Type

See changeMessages/README.md for detailed instructions. Quick summary:

  1. Update GraphQL schema (backend)
  2. Update event generator (backend, if needed)
  3. Create message component:
    typescript
    export const MyNewMessage: React.FC<ActorWithDetailsProps> = ({ actorName, details }) => (
        <ActionText>
            <ActorName>{actorName}</ActorName> did something
        </ActionText>
    );
    
  4. Add to router:
    typescript
    case DocumentChangeType.MyNewType:
        return <MyNewMessage actorName={actorName} details={details} />;
    
  5. Run yarn generate to update types

GraphQL Integration

Query

graphql
query getDocumentChangeHistory($urn: String!, $limit: Int) {
    document(urn: $urn) {
        changeHistory(limit: $limit) {
            changeType
            description
            actor {
                urn
                type
                username
                info
                editableProperties
            }
            timestamp
            details {
                key
                value
            }
        }
    }
}

Mutation (for restore)

graphql
mutation updateDocumentContents($input: UpdateDocumentContentsInput!) {
    updateDocumentContents(input: $input)
}

Testing Strategy

  • changeUtils.ts functions (pure functions, easy to test)
  • Individual message components (can pass mock data)
  • useParentDocumentTitle hook (mock GraphQL responses)

Integration Tests

  • Full timeline rendering with mock data
  • Restore flow (mock mutations)
  • Error handling scenarios

E2E Tests

  • Create document → view history → see creation event
  • Edit title → view history → see title change with new title
  • Edit content → view history → see content change → restore previous version
  • Move document → view history → see move with parent name

Performance Considerations

Optimizations

  1. Conditional Query: Parent title only fetched when needed
  2. Skip Flag: GraphQL queries skipped when URN is empty
  3. Memoization: details object memoized in component
  4. Code Splitting: Large modal only loaded when needed

Data Loading

  • Timeline loads up to 100 most recent changes
  • Each change with a parent triggers a separate query (could be optimized with batching if needed)
  • Modal content is part of change details (no additional fetch needed)

Future Enhancements

Potential Improvements

  1. Diff View: Show inline diffs for content changes (not just full previous version)
  2. Batch Parent Queries: Load all parent titles in one query
  3. Infinite Scroll: Load more changes on demand
  4. Filtering: Filter by change type, date range, or actor
  5. Comparison: Compare any two versions side-by-side
  6. Annotations: Add comments to specific changes

Extensibility Points

  • Add new change types in ChangeMessageComponents.tsx
  • Add new data fetching hooks in hooks/
  • Add new utilities in utils/
  • Customize message rendering per change type

Code Quality Metrics

  • Lines of Code: ~600 total
  • Number of Components: 11
  • Number of Hooks: 1 custom
  • Number of Utilities: 2
  • TypeScript Coverage: 100%
  • Linting Errors: 0
  • Type Errors: 0

Dependencies

Internal

  • @app/entityV2/document - Document queries and mutations
  • @app/useEntityRegistry - Entity display names
  • @app/sharedV2/modals/ConfirmationModal - Confirmation dialogs
  • @src/alchemy-components - UI components (Timeline, Popover, Button, etc.)

External

  • dayjs - Date formatting and relative time
  • antd - Modal component
  • react - Component framework
  • styled-components - Styling

Maintenance Guide

Common Tasks

Update message text: Edit the appropriate component in changeMessages/ChangeMessageComponents.tsx

Change timestamp format: Modify dayjs.format() calls in DocumentChangeTimelineContent.tsx

Add new detail field: Update backend event generator → regenerate types → use in message component

Customize styling: Update styled-components in respective files

Debugging

Timeline not showing: Check GraphQL query response in DevTools Network tab

Parent name shows '...': Check if parent URN is valid and document exists

Restore not working: Check mutation response and refetch behavior

Wrong actor name: Verify actor data in change history response