Creating Rails Admin Pages from Scratch (Part 1: Index)

Previously I wrote about some problems I’ve been having using the common Rails admin gems. This is the second article I’ll be writing as part of a series where I’ll be examining how easy it is to create most of the functionality of these gems from scratch with the help of a few smaller, single-purpose gems.

Last time I wrote about how we could simply use the Rails scaffold generator to create our admin pages. Eventually we’ll find that approach to be somewhat limited. Instead what we’ll do is create our own base admin controller and views and use those to build pages for an existing model (the Admin user account model from the previous post).

There’s a fair amount of work needed to implement the different request types so this article will only cover the index action.

It’s Time to Namespace

Up to now we’ve been content using the default scaffolding and its url routes for our application’s admin interface. However, we’re about to run into some problems with that. The first is the unfortunate naming of our dashboard page. Our route for that page is currently:

get 'admin' => 'admin#home'

If we look at the routes and the path/url helper methods for this route (using rake routes) we see the line:

admin GET        /admin(.:format)                admin#home

We would like to add our Admin model as a resource to our routes so this is what we might put in our config/routes.rb file (for now let’s assume it’s after the line for our dashboard page):

resources :admins

Looking at our defined routes and helpers again we’ll see the previous route for our admin dashboard page but we’ll also see a resource route for our show action on our admins controller:

      GET        /admins/:id(.:format)           admins#show

If we didn’t have our dashboard route this is the route that would have admin_path and admin_url helper methods defined for it. The first column of the rake routes output, if it exists, indicates the base name of the url/path helper methods and as you can see here our new resource route doesn’t indicate that it has the admin helper.

The reason this becomes a problem is that several other things in Rails (most notably the form_for view helper methods) rely on those helper methods pointing to the resource controller.

Using a Generator to Build a Namespaced Controller for an Existing Model

Our first thought might be to turn to generators to build us a namespaced scaffold. By namespaced we mean that all the resource routes will be prefixed with the same starting path, in our case we’ll use /admin.

Rails also provides a generator that will build the controller and associated pieces for an existing model. We’ll try using that to create the namespaced scaffold for our Admin model:

$ rails generate scaffold_controller admin/admin email:string password:string

This will create a namespaced controller and put it in app/controllers/admin with a name of admins_controller.rb. It will not create a route for this new controller so we need to add one to config/routes.rb:

namespace :admin do
  resources :admins
end

However, there are a couple of problems with this scheme. One is that we would need to change all of the references to our Admin model in the controller from the namespaced Admin::Admin to just Admin.

The other is that the form_for(@admin_admin) statement in the view’s form partial bases its <form> action URL on the model’s name. Because of that it won’t properly follow our routing namespace scheme. While this can be fixed by modifying the form partial it means we would have to do this for each model we build an admin page for.

Building Our Own Admin Controller

It turns out that the generator isn’t doing much for us. We can do better if we build our own base admin controller class and simply derive our namespaced admin controller classes from that. In the process we’ll do a bit of metaprogramming to abstract the actions in our controller to work with our models.

To keep things manageable for this article we’ll only create the index action. Future articles will cover the rest of the actions.

The Datagrid Gem

There is a lot of functionality on the index page of the admin gems. To avoid developing this all ourselves we’ll look to the Datagrid gem. Datagrid works well with a couple of the pagination gems so we’ll choose Kaminari for our implementation and add them to our Gemfile.

gem 'datagrid'
gem 'kaminari'

With these two gems we’ll get a way to create paged tables that can be sorted on selected fields. We can also define our own filters to decide which records from our models we wish to have displayed.

Building a Base Controller

To figure out what we need for our base admin controller let’s first look at what the Datagrid scaffold would build for us by running rails g datagrid:scaffold admin. Note that for our solution we will not be using the Datagrid scaffold generator but because setup for this gem is fairly complex we’ll use a trial run as reference output.

This is the index action we’ll see in the controller class that Datagrid creates for us (it happens to be the only action this generator creates):

def index
  @grid = AdminsGrid.new(params[:admins_grid]) do |scope|
    scope.page(params[:page])
  end
end

To create an index method for our base admin controller we’ll need to do a bit of metaprogramming wherever we see ‘admins’ in this code. Specifically we’ll need:

We will also structure our base controller a bit differently but we’ll get to that in a bit. First let’s take a look at how we want the controller for our model to look:

class Admin::AdminsController < Admin::BaseController
  class Grid
    include Datagrid
    scope { Admin }
    #...
  end
end

That will be it for now. We’re going to define the Grid class for each model type in the corresponding controller rather than having a separate directory of these classes.

Going back to our base controller we can see it will be fairly straightforward to get the grid class and maybe just a little more work to get the correct parameters from the params hash.

We’ve added a default of 10 for the number of items to show per page by adding .per(10) to our scope. We will also need to provide a path for the filter form so we’ll set the instance variable @index_path for this purpose. Finally, we’ll want to render a base form template and we’ll need to explicitly call that out in our render method call.

class Admin::BaseController < ApplicationController
  def index
    @grid = grid_class.new(grid_params) do |scope|
      scope.page(params[:page]).per(10)
    end
    @index_path = request.path
    render 'admin/base/index'
  end
end

Some Fancy Metaprogramming

We now need to write methods for grid_class and grid_params:

class Admin::BaseController < ApplicationController
  # ...

private
  def grid_class
    @@grid_class ||= self.class::Grid
  end

  def grid_params
    params[grid_class.name.underscore.gsub('/', '_')]
  end
end

Note here that we’ve memoized a couple of things using class variables. This speeds up our controller modestly and we can use class variables because these values will remain constant for all instantiations of objects from the child class. We did not memoize grid_params because we anticipate that it could be different for every request.

The Base View

The default view for the base controller index can be pretty much the same as what we were given by Datagrid’s scaffold generator. Our file wil reside at app/views/admin/base/index.html.erb and in it we see:

<%= datagrid_form_for @grid, :method => :get, :url => @index_path %>

<%= paginate(@grid.assets) %>
<%= datagrid_table @grid %>
<%= paginate(@grid.assets) %>

We saw earlier how @grid and @index_path were set in our index action.

What Goes in the Grid Class?

It’s now time to fill in the Grid class we created in our Admin::AdminsController earlier. This class is a DSL for the Datagrid gem and it primarily defines two things about our grid display: the fields to use in a form to filter which items we want displayed and the columns to display. For our Admin model we have several fields defined by the Devise gem that we might want to display. For example, we might define our Grid like:

class Admin::AdminsController < Admin::BaseController
  class Grid
    include Datagrid

    scope { Admin }

    filter(:id, :integer)
    filter(:email, :string) { |value| where('email like ?', "%#{value}%") }
    filter(:sign_in_count, :integer)
    filter(:last_sign_in_at, :date, range: true)
    filter(:created_at, :date, :range => true)

    column(:id)
    column(:email)
    column(:sign_in_count)
    column(:current_sign_in_at) do |model|
      model.current_sign_in_at.to_time if model.last_sign_in_at
    end
    column(:last_sign_in_at) do |model|
      model.last_sign_in_at.to_time if model.last_sign_in_at
    end
    column(:created_at) do |model|
      model.created_at.to_time
    end
  end
end

One thing to note here is that I didn’t use a full string match on the email field but instead I’m using like pattern matching. This way our users can enter partial email addresses in and have the filter act more like a search function.

You can read more on the Datagrid DSL on github.

Linking to our New Page

Finally we need to add a link to our new admin users index to our admin home page in app/views/admin/home.html.erb:

<ul>
  <li><%= link_to 'Admin Users', admin_admins_path %></li>
  ...
</ul>

When we navigate to our new index page we’ll see something like this:

Example index page

Conclusion

While this looks like quite a bit of code for just the index action, we’ll have a lot more control over where our admin pages go from here. We can override the index action for individual controllers to set things up slightly differently if we wish to.

Index is also likely one of the most difficult actions to create/display. We should see in future articles that the other actions like show, new/create, and edit/update are much simpler to implement.

comments powered by Disqus