Building a React.js event log in a Rails admin panel

Building a React.js event log in a Rails admin panel

Recently I talked with some awesome Rails developers about the Event Sourcing. We talked about introducing ES concept in a legacy Rails applications. That conversation inspired me to write a post about our experiences with the Event Sourcing. The most important thing to remember is that we don’t have to implement all blocks related to ES at the beginning (Aggregates, Read models, Denormalizers and so on). You can implement only one pattern and improve it slowly to full an Event Sourcing implementation. This strategy will involve small steps down a long road. This is how we work in the Arkency.

Example

We have experimented with the Event Sourcing in couple client’s projects. Some time ago we launched our vision of an Event Store (we call it RES) which we use in customer’s applications. It help as a lot to start Event-think during implementation. This example will show you how to simply introduce an ES in a Rails app. We will create a simple events browser. We will collect events describing user’s registration. Events will be saved to streams, each stream per user. This way we will create a simple log.

The idea is to display events to the admin of the Rails app. We treat it as a “monitoring” tool and it is also first step to use events in a Rails application.

Backend part

We start by adding a rails_event_store gem to our Gemfile (installation instructions). Next thing is that we need some events to collect. We have to create an event class representing a user creation. To do this we will use the class provided by our gem.

class UserCreated < RailsEventStore::Event; end 

Now we need to find place to track this event. I thing that UsersController will be the best place. In the create method we build new User’s model. As event_data we save information about user and some additional data like controller name or IP address.

class UsersController < ActionController::Base   after_filter :user_created_event, only: :create    def create     #user registration   end    def event_store     @rails_event_store_client ||= RailsEventStore::Client.new   end    private    def user_created_event     stream_name = "user_#{current_user.id}"     event_data = {       data: {         user: {           login: current_user.login         },         remote_ip: request.remote_ip,         controller: controller_name,       }     }     event_store.publish_event(UserCreated.new(event_data), stream_name)   end end 

The last thing is to implement a simple API to get information about streams and events.

class StreamBrowsersController < ApplicationController   def index   end    def get_streams     render json: RailsEventStore::EventEntity.select(:stream)   end    def get_events     render json: event_store.read_all_events(params[:stream_name])   end end 

Frontend part

Instead of using Rails views we will use React’s components. I created four components. The view structure you can see on following schema.

Building a React.js event log in a Rails admin panel

I use coffeescript to build components. As you can see on following example I use requirejs to manage them. Recently we launched a great book about React where you can read more about our experiences with React and coffeescript. Of course you could go with JSX as well.

define (require) ->   React = require('react')   {div, a, li, ul, nav} = React.DOM    Pagination = React.createClass     displayName: 'Paginator'      previousHandler: ->       event.preventDefault()       @props.onPrevious()      nextHandler: ->       event.preventDefault()       @props.onNext()      render: ->       nav null,         ul           className: 'pager'           li null,             a({onClick: @previousHandler, href: "#"}, 'Previous')           li null,             a({onClick: @nextHandler, href: "#"}, 'Next')    Streams = React.createClass     displayName: 'Stream'      clickHandler: ->       event.preventDefault()       @props.onClick(@props.stream)      render: ->       div null,         a({onClick: @clickHandler, href: "#"}, @props.stream)    Event = React.createClass     displayName: 'Event'      render: ->       ul         className: 'list-group'         li           className: 'list-group-item'           JSON.stringify(@props.event)    Events = React.createClass     displayName: 'Events'      render: ->       div null,         for event in @props.events           React.createElement Event,             key: event.table.event_id             event: event.table    ShowStreams = React.createClass     displayName: 'ShowStreams'      getInitialState: ->       events: []       selectedStream: null       streamsPage: 0       eventsPage: 0      onStreamsClicked: (stream_key) ->       callback = (data) =>         @setState selectedStream: stream_key, events: data, eventsPage: 0       @props.storage.get_events(stream_key, callback)      onNextStreamPage: ->       if @props.streams[@state.streamsPage + 1]         @setState streamsPage: @state.streamsPage + 1      onPreviousStreamPage: ->       if @props.streams[@state.streamsPage - 1]         @setState streamsPage: @state.streamsPage - 1      onNextEventsPage: ->       if @state.events[@state.eventsPage + 1]         @setState eventsPage: @state.eventsPage + 1      onPreviousEventsPage: ->       if @state.events[@state.eventsPage - 1]         @setState eventsPage: @state.eventsPage - 1      render: ->       div         className: 'container'         div           className: 'row'           div             className: 'col-md-4'             React.createElement Pagination,               key: 'stream_paginator'               onNext: @onNextStreamPage               onPrevious: @onPreviousStreamPage           div             className: 'col-md-8'             React.createElement Pagination,               key: 'event_paginator'               onNext: @onNextEventsPage               onPrevious: @onPreviousEventsPage         div           className: 'row'           div             className: 'col-md-4'             for val in @props.streams[@state.streamsPage]               React.createElement Streams,                 key: val.stream                 stream: val.stream                 onClick: @onStreamsClicked           div             className: 'col-md-8'             if @state.selectedStream != null               React.createElement Events,                 key: 'events'                 events: @state.events[@state.eventsPage] 

Last thing is to render above components on the view. I created an additional class to build the ShowStreams component and render it on the page. I implemented it this way because we use the react-rails gem in version 0.12. In newer version you can use react_component helper to render component on server side. This makes using easier to start with React with Rails views.

define (require) ->   React = require('react')    {ShowStreams} = require('./components')   Storage = require('./storage')    class App     run: =>       @storage = new Storage()       callback = (data) =>         mountNode = document.querySelector '.streams'         ShowStreams = React.createFactory ShowStreams         React.render(ShowStreams({streams: data, storage: @storage}), mountNode)       @storage.get_streams(callback) 
= content_for :bottom_js do   :javascript     $(function() {       require(['admin/streams/app'], function(App) {         window.app = new App();         window.app.run();       })     }); .streams 

The last piece of the puzzle is the Storage class. This simple class is responsible for calling the API using Ajax.

define (require) ->   class Storage      constructor: ->      get_events: (stream_key, callback) =>       $.getJSON('/admin/stream_browsers/get_events', stream_name: stream_key).done (data) =>         callback(@paginateData(data, 20)._wrapped)      get_streams: (callback) =>       $.getJSON '/admin/stream_browsers/get_streams', (data) =>         callback(@paginateData(data, 20)._wrapped)      paginateData: (data, count) ->       //this method split streams and events data into chunks. It is needed to pagination 

What next?

The above example shows how simple is to introduce events in you app. For now it is only simple events log. We started to collect events related to User model. We don’t build state from this events. Although you can use them in some Read models. In next step you can collect all events related to User. Then you will be able to treat User as a Aggregate and build state from events.

Our other posts related to ES topic:

Leave a Reply

Your email address will not be published. Required fields are marked *