Active Admin Gem Tips and Performance Tuning for Rails Apps Updated Jun 28, 2019

6 minute read

ActiveAdmin gem is a popular tool for building admin interfaces in Ruby on Rails apps. In this tutorial, I will describe a couple of less obvious tips and performance optimization techniques.

Active Admin should probably never be used for client-facing parts of the interface because it’s a bit clunky. But it can hardly be matched for an internal admin user interface development speed and simplicity.

For a great intro about how to start using ActiveAdmin with modern Rails check out this article and the official docs.

Here comes the first tip:

Add query persistence to filters

Filters are one of my favorite features of Active Admin. They let you mix various search conditions with a simple UI. One issue with default implementation is that the search query is not persistent. Every time you click away to a different page, you need to do a new search from scratch.

Filters UX can be improved by adding the following files:

app/config/initializers/aa_filters_persistance.rb

"aa" prefix is required for initializer file containing new module definition to be loaded before "active_admin.rb" file.

module ActiveAdmin module FiltersPersistance extend ActiveSupport :: Concern CLEAR_FILTERS = "clear_filters" FILTER = "Filter" included do before_action :resolve_filters end private def resolve_filters session_key = " #{ controller_name } _q" . to_sym if params [ :commit ] == CLEAR_FILTERS session . delete ( session_key ) elsif ( params [ :q ] || params [ :commit ] == FILTER ) && action_name . inquiry . index? session [ session_key ] = params [ :q ] elsif session [ session_key ] && action_name . inquiry . index? params [ :q ] = session [ session_key ] end end end end

Also, add the following code to app/assets/javascripts/active_admin.js

//= require active_admin/base $ ( function () { $ ( '.clear_filters_btn' ). attr ( 'href' , '?commit=clear_filters' ); })

By default Active Admin uses "js.coffee" extension but since CoffeeScript is no longer the thing you can just rename it to "js".

Next just add this code at the bottom of config/initializers/active_admin.rb

ActiveAdmin :: BaseController . send ( :include , ActiveAdmin :: FiltersPersistance )

With that in place, your filter queries will be persisted in session on a per page basis, making navigating the admin panel more pleasant.

Normalize blank attributes

Active Admin uses Formtastic under the hood. There is a known issue with blank values populating your models after submitting a form with empty fields.

If your model does not validate a presence of data, you might end up with several attributes set to an empty string "" because that’s what is sent from an empty form field.

There is a simple way to define declarative API for keeping your attributes in a correct state. It uses Active Record callbacks under the hood, but the despite all the hate they get, I think normalizing model attributes can be a valid use case for them. I can recommend this blog post for an interesting write up on potential callback use cases.

To avoid blank ghost attributes you need to add the following module:

config/initializers/normalize_blank_values.rb

module NormalizeBlankValues extend ActiveSupport :: Concern included do before_save :normalize_blank_values end def normalize_blank_values begin self . class . const_get ( "NORMALIZABLE_ATTRIBUTES" ) rescue NameError [] end . each do | column | self [ column ]. present? || self [ column ] = nil end end end

and then include and setup it in your model:

app/models/user.rb

class User < ApplicationRecord include NormalizeBlankValues NORMALIZABLE_ATTRIBUTES = %i(email) ... end

Unless you explicitly bypass callbacks, you should not see empty string instead of nil again.

Watch out for slow filters

Talking about filters, their default implementation in Active Admin can slow your Rails app to a crawl. The problem is that Active Admin creates a select filter for all the has_many relation on a model. So if a user has_many posts, /admin/users view will display select box, rendering data of ALL the posts present in the database.

This issue can easily be overlooked when starting to work on an app, and your dataset is still small. Only after a while, you might begin to notice delays, memory issues or even server timeouts.

A simple way to significantly reduce memory usage is to pluck the necessary attributes from the collection, to avoid instantiating the full-blown Active Record objects:

in app/admin/users.rb

ActiveAdmin . register User do preserve_default_filters! filter :posts , as: :select , collection: -> { Post . pluck ( :title , :id ) } ... end

This tip should be applied to all the collection values in your Active Admin Formtastic forms and views.

In case your collections have more than couple thousand objects you should consider adding autocomplete powered by JSON endpoint, but that’s outside the scope of this tutorial.

Use custom form views

Rendering custom form views is a powerful way to customize Active Admin interface. Declaring collection, member or batch actions, that render a custom view is quite simple but not mentioned in the official docs. I will cover a batch action example because its most complex.

You need to start by declaring a batch action that renders a view and another one that receives params submitted from the form:

app/admin/users.rb

batch_action :bulk_set_email do | ids | render "admin/users/bulk_set_email" , locals: { form: Admin :: Users :: BulkSetEmailForm . new ( ids ) } end collection_action :bulk_set_email , method: :put do form = Admin :: Users :: BulkSetEmailForm . new ( params . fetch ( :users ). fetch ( :users_ids ) ) if form . submit ( params . fetch ( :users )) redirect_to admin_users_path , notice: "Users emails updated" else flash [ :notice ] = "There was a problem updating users emails" render "admin/users/bulk_set_email" , locals: { form: form } end end

and adding the view file itself:

app/views/admin/users/bulk_set_email.html.erb

< %= semantic_form_for [ :admin , form ], html: { method: :put , :class = > "form-horizontal" }, url: bulk_set_email_admin_users_path do | f | % > < %= f . semantic_errors :base % > < %= f . actions do % > < %= f . action :submit , as: :button , label: " Update emails " % > <br> <br> < % form . users . each do | user | % > <label> <input type= "text" name= "users[users_emails][]" id= "users_users_emails_user_email" value= "<%= user.email %>" /> <input type= "hidden" name= "users[users_ids][]" id= "users_users_emails_user_id" value= "<%= user.id %>" /> </label> < % end % > <br> <br> < %= f . action :submit , as: :button , label: " Update emails " % > < % end % > < % end % >

Finally, you need to add a form object that contains the code required to perform the action. Form objects are a good way to avoid cluttering controller DSL with business logic, check out this blog post for more in depth info about them. I’ve recently started following a convention where form object accepts arguments needed to render a form in an initializer. It also has a submit method doing the actual work done and returning true or false based on whether action succeed.

app/forms/admin/users/bulk_set_email_form.rb

class Admin :: Users :: BulkSetEmailForm include ActiveModel :: Conversion include ActiveModel :: Validations VALID_EMAIL_REGEX = /\A[\w+\-.] [email protected] [a-z\d\-.]+\.[a-z]+\z/i attr_reader :users def initialize ( users_ids = []) @users = User . where ( id: users_ids ) end def submit ( params ) @params = params return false unless valid? @params . fetch ( :users_ids ). zip ( @params . fetch ( :users_emails )). each do | pair | User . find ( pair . fetch ( 0 )). update! ( email: pair . fetch ( 1 )) end true rescue => e Rails . logger . error e false end def valid? emails_valid = @params . fetch ( :users_emails ). all? do | email | email . match ( VALID_EMAIL_REGEX ) end errors . add :base , "All emails must be valid" unless emails_valid emails_valid end def self . model_name ActiveModel :: Name . new ( self , nil , "Users" ) end end

Check out this GIF to see a custom Active Admin form in action.

You could even embed a dynamic React component inside Active Admin using this technique, but that’s a story for another blogpost.

Summary

I’ve worked on a couple of commercial projects where using Active Admin made a lot of sense business-wise. I am aware that many serious developers would frown upon seeing it in their Gemfiles, but I have a feeling this library is not going away any time soon. You can check out this repository to see all the described tips applied to a barebones Rails app.