How to Add Custom Filters to Administrate Dashboards
Tags: ruby, rails
Published: 2021-11-06 01:46:40

Administrate is one of my favorites Rails gems. 

It gives you a CRUD interface for your entire database in seconds.

This means any admin user can log in and get access to our database records from the browser. They can create, read, update, or delete data without having to write any code, scripts, or SQL. (Don't worry you can limit which tables can be updated or deleted too.)

One problem is Administrate doesn't come with any kind of filtering and it can be hard to find specific records or records that meet certain conditions. You can sort columns alphabetically but when you have large sets of data this really doesn't help much.

There are a couple steps needed to add our own custom filters on top of Administrate, but its just a few new lines of code in a few places and I'll dive deep into why and how it works.

If you want to skip the explanation and just want the code, <a href="#solution">it's right here</a>.

### What We Will Cover
1. <a href="#filters">How to apply custom filters to our Administrate data</a>
2. <a href="#session">Storing filters in the user's session</a>
3. <a href="#query-params">Passing query parameters to update the user's session</a>
4. <a href="#applying-filters">Display the Administrate view using the current filters</a>


<a id="filters" name="filters"></a>

---

## How to Filter Records in Administrate

When we land on an Administrate page, the request is routed to an admin controller. 

Just like any other Rails controller, our admin controller is responsible for querying the database and passing this data to the view to render it on the page.

Administrate gives us a method called `scoped_resource`. This method pulls our data from the database and we can add additional conditions to filter out data or organize it in a different way. This method allows us to filter our query any way we like.

Inside this method we see a variable called `resource_class`. This is inferred from the controller you are looking at. For example, the Admin::UsersController references your User model so the `resource_class` would evaluate to `Users.all`.

Our solution will add a few lines of logic to this method so that we can filter our Administrate views based on variables that we set in the current user's session. 

If you aren't familiar with how session storage works within Rails, fear not. We'll jump into this next.

Plus you can copy the code below and change it for your specific use case and it should work out of the box.

Here's an example of the `scoped_resource` method you will override in the controller.

This example returns a query result that applies different filters based on the user.

```ruby
def scoped_resource
  if current_user.super_admin?
    resource_class
  else
    resource_class.where(private: false)
  end
end
```

In the above code we are saying if you are logged in as a super admin, Administrate should return all records. 

BUT if you aren't, only return records where the `private: false` condition is met. 

In this example we're assuming the resource has a field called private and any records marked private are only viewable to super admin users.

This is how you can apply filters to Administrate views. Add your desired conditions to this method and we will exclude any records that don't match.

<a id="session" name="session"></a>

---

## Storing Filters in the User's Session

The previous example above demonstrates how to filter records in Administrate, but it's not dynamic. Meaning we can't click a button to turn this filter on or off. It's just tied to whether or not we are a super admin. 

In order to let users toggle certain filters we need to store some kind of on/off state somewhere. 

This is what Rails session storage is really good at.

The Rails session is kind of like a dictionary or hash that every visitor gets. We can store and modify variables specific to each user.

### How to Use Session Variables
We can update the data stored in a user's session by accessing the session hash and updating the entry for a certain key or attribute.

```ruby
session[:filter_published] = true
```

This would update the `filter_published` entry and set it to true only for this user.

Since session storage uses a hash, we read from this hash as we would any other normal hash. It should look like this.

```ruby
session[:filter_published]
=> true
```

The last point I'll make here is that Rails session storage is a backend mechanism. In other words we have to hit the backend again or make an API call to update the user's session data.

Let's wire up the frontend to set filters on the user's session on the backend.

<a id="query-params" name="query-params"></a>

---

## Passing Query Parameters to Update the User's Session

For a quick recap, we can currently customize our Administrate views by modifying the query in the `scoped_resource` method of our admin controller.

We can also update the user's session to store and persist any custom filters that are enabled or disabled.

Now we need to tell the backend which of our filters we want to toggle so we can make the expected change in the user's session.

### Query Parameter Basics
Query Parameters are the extra attributes added at the end of a URL. You have probably seen a link resembling something like `https://testsuite.io/send-emails-from-rails?ref=testsuite&view=focused`.

That link will route you to `https://testsuite.io/send-emails-from-rails` but it also adds `?ref=testsuite&filter=true`. The latter portion contains the query parameters.

This tells the server to render the expected page but to take into account the `ref=testsuite` and `filter=true`.

We can follow the same approach to pass our desired filters to Administrate so that we can customize our Administrate views.

### How to Pass Query Parameters

First, we need a link that appends our desired query parameters. 

If we use a Rails `link_to` helper method it could look something like this:

```erb
<%= link_to('Published Filter', admin_blog_posts_path(published: true), class: 'admin-filter') %>
```

This will generate a link with the text "Published Filter".
The link will point to the `admin_blog_posts_path` which evaluates to `/admin/blog_posts`.
Our route helper, the `admin_blog_posts_path(published: true)` also passes the `published: true` key/value pair - those are the query parameters!

The resulting HTML would look something like this:

```erb
<a href="/admin/blog_posts?published=true" class="admin--filter">
  Published Filter
</a>
```

And now we have a link that we can click to view our blog posts (or whatever model you desire to add custom filters to in Administrate).

If we want to allow multiple types of filters or add buttons to apply custom ordering, we can add more buttons that pass different query parameters.


<a id="applying-filters" name="applying-filters"></a>

---

## Display the Administrate view using the current filters

Now that we are passing our query parameters to the backend, our controller can save our filters in the user's session.

In the admin controller that we are filtering, we need to save our query parameters in the user's session storage. It is as easy as updating the session hash with the desired key and value.

```ruby
session[:published] = params[:published] unless params[:published].blank?
# Save the published filter to the user's session if it was passed as a query parameter
```

We can use the same strategy to clear and erase our current filters too:

```ruby
session[:published] = nil if params[:clear]
# Erase and reset our filter if we pass the clear=true query parameter
```

Now everything should be wired correctly and we can get back to the `scoped_resource` method. This is were it all started and now we have the buttons and session storage setup to persist any filters that we want to set.

To apply a custom filter on our Administrate view, we need to check if we saved a value in the user's session storage.

The below example shows how to check the session storage, and modify the `resource_class`.

```ruby
def scoped_resource
  if session[:published]
    resource_class.where(published: true)
  else
    resource_class
  end
end
```

We look up the published attribute in the user's session storage and if `session[:published]` evaluates to true, then we apply the `.where(published: true)` condition to our database query. Otherwise, we pull the data normally and nothing is filtered.

<a id="solution" name="solution"></a>

---

## Final Solution

Here's all the code without diving into explanations.

### 1. Add buttons to your Administrate view that append the desired filters as query parameters

```erb
<%= link_to('Published', admin_blog_posts_path(published: true), class: 'admin--filter') %>
<%= link_to('Clear Filters', admin_blog_posts_path(clear: true), class: 'admin--filter') %>
```

### 2. Save the query parameters to the user's session

This code can live in the admin controller that you are calling. Be sure to call it before `scoped_resource` so that we update the session before we apply any filters.

```ruby
session[:published] = nil if params[:clear]
# Erase and reset our filter if we pass the clear=true query parameter

session[:published] = params[:published] unless params[:published].blank?
# Save the published filter to the user's session if it was passed as a query parameter
```

###3. Apply the filters to the `scoped_resource` method

Inside of our `scoped_resource` method, we can check for any filters we saved in our session and apply them to our query.

This will apply a custom filter on our Administrate view and only show us resources that have their published field set to true.

```ruby
def scoped_resource
  if session[:published]
    resource_class.where(published: true)
  else
    resource_class
  end
end
```


## Optional: Style buttons

Typically Administrate interfaces are only used by your internal team. 

It's intended for administrative users so the design might not be a high priority, but you can make your filter buttons look good with just a few lines of CSS. Here's a class you can paste into your project.

```html
<style>
  .admin--filter {
    text-decoration: none !important;
    margin: 5px;
    padding: 10px 20px;
    background-color: #f3f6f9;
  }
</style>
```

Your Administrate controller isn't going to import your stylesheet by default since it doesn't extend your normal page layout in `views/layouts/application.html.erb`. 

This layout is typically imported by all your other views and sets up basic things like your meta tags, font imports, and your CSS stylesheets. But since Administrate doesn't extend your `application_controller.html.erb` it also doesn't inherit this layout. So your normal fonts and styles won't be accessible on any of your Administrate views.

A couple of ways around this are to either explicitly add this layout to your Administrate controllers, or to add these styles directly to your Administrate views.

I find the approach with views faster and easier (granted perhaps not the best long-term solution).

You have to override the default Administrate view anyway to add your filter buttons so sprinkling in a little bit of CSS in the same view isn't a big deal.