sinaptia waves

Introducing Audited-UI

Patricio Mac Adden
Aug 30th, 2023

Some applications need to allow their users to review their models’ changes through time. For example, in the context of finances, one would like to know when an account was created when a transaction was created, or, if the application allows it, when an amount was updated. In certain cases, it’s also helpful to know when a record was deleted and even to recreate it. Audited is a Ruby on Rails extension providing such functionality that we’ve been using for a while.

Audited tracks all changes to your models. It can also record who made those changes, save comments, and associate models related to the changes. It’s a complete solution to the problem of auditing records, but it lacks one thing: a nice view to see and review those changes.

More than once we needed to build such UI so we could reuse it across different models, like this:

class AuditsController < ApplicationController
  before_action :set_resource

  # GET /audits
  def index
    @audits = @resource.audits.order(:version)
  end

  private

  def set_resource
    resource_param = params.permit!.to_hash.select { |k, _v| k.to_s.end_with? "_id" }.to_a.last

    @resource = if resource_param.present?
      klass = resource_param.first.to_s.gsub("_id", "").camelize.constantize
      id = resource_param.last

      klass.find id
    end
  end
end

A simplified view for that controller looked like this:

<h1><%=t ".title", resource: @resource.to_s %></h1>
<table>
  <thead>
    <tr>
      <th><%=t "activerecord.attributes.audit.created_at" %></th>
      <th><%=t "activerecord.attributes.audit.action" %></th>
      <th><%=t "activerecord.attributes.audit.user" %></th>
      <th colspan="3"><%=t ".changes" %></th>
      <th><%=t "activerecord.attributes.audit.comment" %></th>
    </tr>
    <tr>
      <th colspan="3"></th>
      <th><%=t ".attribute" %></th>
      <th><%=t ".before" %></th>
      <th><%=t ".after" %></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <% if @audits.any? %>
      <% @audits.each do |audit| %>
        <% audit.audited_changes.each_with_index do |(attribute, change), i| %>
          <tr>
            <% if i.zero? %>
              <td rowspan="<%= audit.audited_changes.size %>"><%= audit.created_at %></td>
              <td rowspan="<%= audit.audited_changes.size %>"><%=t ".actions.#{audit.action}" %></td>
              <td rowspan="<%= audit.audited_changes.size %>"><%= audit.user.try(:email) || t(".system") %></td>
            <% end %>
            <td><%=t "activerecord.attributes.#{@resource.class.name.underscore}.#{attribute}" %></td>
            <% if change.is_a? Array %>
              <td><%= change.first %></td>
              <td><%= change.last %></td>
            <% else %>
              <td></td>
              <td><%= change %></td>
            <% end %>
            <td><%= audit.comment %></td>
          </tr>
        <% end %>
      <% end %>
    <% else %>
      <tr>
        <td class="text-center" colspan="7"><%=t ".empty" %></td>
      </tr>
    <% end %>
  </tbody>
</table>

Then, each of the routes of the app would look like this:

resources :accounts do
  resources :audits, only: :index
end
resources :transactions do
  resources :audits, only: :index
end

And a link to that controller would be as easy as:

<%= link_to t(".audits"), account_audits_path(@account) %>

Having done this a couple of times, we decided this approach was useful for users and decided to create a Rails engine like the solution above, but with a twist:

  • A smart controller that can display all audit records for all models, all audit records for a specific model, and all audit records for a single record
  • It provides the simplest possible views, so you can customize them to match your app’s by running a generator and editing the partials
  • You can filter the changes by user, dates, and audited changes
  • Audit records are paginated
  • Everything’s i18n ready

View all, by model and by record

One of the features we like the most is the flexibility to view all audit records, all audit records by model, and all audit records by object. The idea is that viewing all records is useful when a series of changes are triggered when updating a record. Think of any ActiveRecord model that updates associated models after an update. In the example above, imagine we create a transaction and when a transaction is created a balance is updated in the associated account. By showing all Audited::Audit records in a single page we can see a transaction has been created, and that the associated account has been updated with a new balance.

Barebone views

Following devise’s approach, we don’t want to impose any kind of views or designs. Imagine building an application with tailwindcss and having to include an engine whose views are implemented with bootstrap. It doesn’t make any sense!

Audited-UI comes with just the markup to display the audited records in a table, with records spanning multiple rows if it has more than 1 change.

Conclusion

We’ve been using audited for a couple of projects and coupled with audited-ui, it has finally become our default audit solution. We thought it might be useful to someone else in the same problem space, so we released it as an open-source gem.

The code lives in our GitHub at sinaptia/audited-ui. All contributions are welcome!

We hope you find it as useful as we do! and if you use it, drop us a line to let us know what you think.