Статья про Ruby в блоге компании ДомКлик! Как так получилось, что в молодую компанию завезли мертвый язык? Секрет в том, что на Ruby можно быстро написать и протестировать бизнес-идею. И делается это не без помощи Rails и Active Admin — библиотеки, которая позволяет быстро создать админку с минимальными затратами сил и времени.



Часто можно встретить мнение, что Active Admin хорош только для 15-минутного блога. Мы в ДомКлик считаем (и доказываем на практике), что из этой библиотеки можно выжать намного больше.


Я расскажу про некоторые подходы, которые мы применяем при работе с Active Admin.


Active Admin базируется на нескольких библиотеках, среди которых я бы выделил arbre, formtastic, inherited_resources и ransack. Каждая из них отвечает за свою часть и заслуживает отдельного рассмотрения. Начнем по алфавиту — с библиотеки, которая отпочковалась от самого Active Admin.


Arbre: кастомизация компонентов


Одна из проблем Active Admin — на глазах распухающие файлы ресурсов: фильтры, дополнительные action'ы, верстка страниц, формы, и всё это в одном файле. Где-то вдали слышен протяжный стон одинокого пуриста «где же single responsibility?» Не завезли. Но давайте разберемся, как можно изолировать часть верстки в отдельных классах.


Arbre — библиотека для описания шаблонов с помощью Ruby. Вот пример простейшей страницы, написанной с помощью DSL Arbre:


html do
  head do
    title('Welcome page')
  end
  body do
    para('Hello, world')
  end
end

DSL расширяется с помощью компонентов. Например, в Active Admin это tabs, table_for, paginated_collection и даже сами страницы. Продолжим знакомство с библиотекой рассмотрением структуры простейшего Arbre компонента.


Arbre: hello world компонент


Как и все компоненты Arbre, наш Admin::Components::HelloWorld наследован от класса Arbre::Component:


# app/admin/components/hello_world.rb
module Admin
  module Components
    class HelloWorld < Arbre::Component
      builder_method :hello_world

      def build(attributes = {})
        super(attributes)
        text_node('Hello world!')
        add_class('hello-world')
      end

      def tag_name
        'h1'
      end
    end
  end
end

Начнем сверху вниз: builder_method определяет метод, с помощью которого мы сможем создать компонент при использовании DSL. Аргументы, переданные в компонент, попадут в метод #build.


В Arbre каждый компонент — это отдельный DOM-элемент (напоминает механизм работы современных frontend-фреймворков, только датируется 2012 годом). По умолчанию все компоненты представляют из себя div, чтобы изменить это поведение, можно переопределить метод #tag_name. Метод #add_class, как не сложно догадаться, добавляет атрибут class к корневому DOM-элементу.


Осталось вызвать наш новый компонент. Для примера, сделаем это в app/admin/dashboard.rb


# app/admin/dashboard.rb
ActiveAdmin.register_page 'Dashboard' do
  menu priority: 1, label: proc { I18n.t('active_admin.dashboard') }

  content do
    hello_world
  end
end


Теперь рассмотрим пример небольшого рефакторинга админки с использованием собственного компонента.


Arbre: пример из реальной жизни (почти)


Для того, чтобы понять, как использовать Arbre в условиях, приближенных к боевым, возьмем синтетический пример. Предположим, что у нас есть блог с записями (Article) и комментариями (Comment) со связью 1:M. Нам необходимо вывести 10 последних комментариев на странице конкретной записи (блок show).


# app/admin/articles.rb
ActiveAdmin.register Article do
  permit_params :title, :body

  show do
    attributes_table(:body, :created_at)

    panel I18n.t('active_admin.articles.new_comments') do
      table_for resource.comments.order(created_at: :desc).first(10) do
        column(:author)
        column(:text)
        column(:created_at)
      end
    end
  end
end


А теперь вынесем таблицу с комментариями в отдельный компонент. Создадим новый класс и унаследуем его от ActiveAdmin::Views::Panel. Если создать новый компонент с нуля (как в hello_world выше) и в нем вызвать panel, то panel окажется внутри еще одного div, а это наверняка поломает верстку.


Мы в нашей команде разместили бы этот класс в app/admin/components/articles/new_comments.rb, но это вкусовщина. Просто знайте, что Active Admin автоматически загрузит всё, что находится внутри app/admin/**/*:


# app/admin/components/articles/new_comments.rb
module Admin
  module Components
    module Articles
      class NewComments < ActiveAdmin::Views::Panel
        builder_method :articles_new_comments

        def build(article)
          super(I18n.t('active_admin.articles.new_comments'))

          table_for last_comments(article) do
            column(:author)
            column(:text)
            column(:created_at)
          end
        end

        private

        def last_comments(article)
          article.comments
                 .order(created_at: :desc)
                 .first(10)
        end
      end
    end
  end
end

Теперь заменим panel в app/admin/articles.rb на вызов нашего нового компонента и передадим в него resource:


# app/admin/articles.rb
ActiveAdmin.register Article do
  permit_params :title, :body

  show do
    attributes_table(:body, :created_at)

    articles_new_comments(resource)
  end
end

Красота! Отмечу, что resource можно было бы не передавать в компонент, а использовать через контекст. Однако, явно передав resource, мы ослабили связность компонента, что позволит переиспользовать его в будущем.


К слову о переиспользовании, всё содержимое блока show (как и других блоков с шаблонами) можно вынести в partial:


# app/admin/articles.rb
ActiveAdmin.register Article do
  show do
    render('show', article: resource)
  end
end

# app/views/admin/articles/_show.html.arb
panel(ActiveAdmin::Localizers.resource(active_admin_config).t(:details)) do
  attributes_table_for(article, :body, :created_at)
end

articles_new_comments(article)

Само собой, вы можете использовать знакомый .erb и другие шаблонизаторы, но, пожалуй, оставим это в качестве факультатива.


Arbre: что еще посмотреть


Прежде всего, я посоветовал бы ознакомиться с описанием компонентов Active Admin в официальной документации.


Для более глубокого изучения можно посмотреть код базовых компонентов из arbre и компонентов activeadmin, ведь часто именно на их основе будут строиться ваши собственные. Кроме того, обратите внимание на gem activeadmin_addons, в котором есть множество интересных компонентов.


Ну, а если вы вдруг до сих пор не пишете код без ошибок, то стоит обратить внимание на то, как можно тестировать компоненты.


Formtastic: кастомизация форм


Formtastic — библиотека для описания форм с помощью DSL. Простейшая форма выглядит вот так:


semantic_form_for object do |f|
  f.inputs
  f.actions
end

В этом примере Formtastic автоматически вытаскивает все атрибуты из переданного объекта object и подставляет их в форму с типами input'ов по-умолчанию. Список доступных типов input'ов можно найти в README. Как и Arbre, Formtastic можно расширить с помощью создания собственных классов-компонентов. Для того, чтобы разобраться в базовых вещах, давайте создадим hello world компонент.


Formtastic: hello world компонент


По аналогии с компонентами Arbre, разместим новый класс в app/admin/inputs:


  # app/admin/inputs/hello_world_input.rb
  class HelloWorldInput
    include Formtastic::Inputs::Base

    def to_html
      "Input for ##{object.public_send(method)}"
    end
  end

Чтобы вызвать новый input, достаточно указать его название в параметре :as, например, так:


# app/admin/articles.rb
ActiveAdmin.register Article do
  form do |f|
    f.inputs do
      f.input(:id, as: :hello_world)
      f.input(:title)
      f.input(:body)
    end
    f.actions
  end
end


Все необходимые для отрисовки формы параметры (в том числе object и символ method) попадают в #initialize, определенный в модуле Formtastic::Inputs::Base. За отображение input'а отвечает метод #to_html.


Может показаться что этот пример бесполезен, но на самом деле на его основе мы в компании рендерим read-only поля. Давайте добавим всего пару методов, доступных в Formtastic, и превратим наш hello world в полезный read-only input. Следите за руками:


  # app/admin/inputs/hello_world_input.rb
class HelloWorldInput
  include Formtastic::Inputs::Base

  def to_html
    input_wrapping do
      label_html <<
        object.public_send(method).to_s
    end
  end
end


Всё, что мы добавили — это два метода с говорящими названиями. input_wrapping пришел из модуля Formtastic::Inputs::Base::Wrapping и отвечает за обертку input'а. В том числе, он включает в себя элементы для вывода ошибок и подсказок. label_html из модуля Formtastic::Inputs::Base::Labelling рендерит лейбл для input'а. Эти два хелпера мгновенно превращают наш hello world в применимый в бою input (разве что нейминг класса бы еще поправить).


Теперь мы можем перейти к чуть более сложному примеру, который продемонстрирует, как можно интегрировать в форму JS-библиотеку.


Formtastic: пример из реальной жизни (почти)


Возьмем за основу очередной выдуманный пример, который продемонстрирует, как работать с HTML, CSS и JS. То есть покроет все шаги написания нового input'а.


Предположим, что к нам пришел запрос от редактора блога: при написании статьи он хотел бы прямо в форме ввода видеть количество слов. Как известно, в мире JavaScript'ов для всего существуют библиотеки, для нашей задачи такая тоже нашлась: Countable.js. Давайте возьмем стандартный input для текста и расширим его, добавив подсчет слов.


Прикинем, что нам потребуется для реализации нового input'а:


  • взять существующий текстовый input и добавить к нему div для вывода количества слов;
  • добавить CSS-стили для нового div;
  • вызвать Countable.js на нужном нам поле и записать с его помощью информацию о количестве слов в новый div.

Начнем с создания нового класса и наследуем его от Formtastic::Inputs::TextInput. Добавим дополнительный атрибут class="countable-input" к элементу textarea, и рядом с ним создадим новый пустой div с атрибутом class="countable-content":


# app/admin/inputs/countable_input.rb
class CountableInput < Formtastic::Inputs::TextInput
  def to_html
    input_wrapping do
      label_html <<
        builder.text_area(method, input_html_options.merge(class: 'countable-input')) <<
        template.content_tag(:div, '', class: 'countable-content')
    end
  end
end

Посмотрим, что нового у нас добавилось. input_html_options— метод родительского класса с говорящим именем. builder — инстанс класса ActiveAdmin::FormBuilder, наследник ActionView::Helpers::FormBuilder. template — это контекст, в котором исполняются темплейты (то есть огромный набор view-helper'ов). Таким образом, если нам нужно создать кусочек формы, то обращаемся к builder. А если хотим использовать что-то типа link_to, то нам поможет template.


Библиотеку Countable.js завендорим: положим в директорию app/assets/javascripts/inputs/countable_input и добавим простенький .js файл, который будет вызывать Countable.js и закидывать информацию в div.countable-content (прошу сильно не пинать ногами за JS-спагетти):


// app/assets/javascripts/inputs/countable_input.js
//= require ./countable_input/countable.min.js

const countable_initializer = function () {
  $('.countable-input').each(function (i, e) {
    Countable.on(e, function (counter) {
      $(e).parent().find('.countable-content').html('words: ' + counter['words']);
    });
  });
}

$(countable_initializer);
$(document).on('turbolinks:load', countable_initializer);

И теперь подтягиваем файл в app/assets/javascripts/active_admin.js:


// app/assets/javascripts/active_admin.js
// ...

//= require inputs/countable_input

Последний штрих — добавляем CSS-файл и подгружаем его в app/assets/stylesheets/active_admin.scss:


// app/assets/stylesheets/inputs/countable_input.scss
.countable-content {
  float: right;
  font-weight: bold;
}

// app/assets/stylesheets/active_admin.scss
// ...

@import "inputs/countable_input";

Вот и всё, наш input готов. Осталось только вызвать его в форме:


# app/admin/articles.rb
ActiveAdmin.register Article do
  form do |f|
    f.inputs do
      f.input(:id, as: :hello_world)
      f.input(:title)
      f.input(:body, as: :countable)
    end
    f.actions
  end
end


Таким образом мы создаем кастомные компоненты для форм в своих проектах. Например, файловые загрузчики или input'ы с хитрым автозаполнением. В подобных компонентах чуть больше кода, но подход остается неизменным.


Formtastic: пламенный привет пуристам


Как и в случае с компонентами Arbre, формы можно выносить в partial'ы, хотя синтаксис немного отличается:


# app/admin/articles.rb
ActiveAdmin.register Article do
  form(partial: 'form')
end

# app/views/admin/articles/_form.html.arb
active_admin_form_for resource do
  inputs(:title, :body)
  actions
end

Недостаток этого подхода в том, что формы лежат где-то глубоко в директории views. На мой взгляд, это немного усложняет навигацию по коду, но тут на вкус и цвет, как говорится.


Formtastic: что еще посмотреть


Formtastic — достаточно обширная библиотека, и я настоятельно рекомендую прочитать подробный README, чтобы ознакомиться со всеми возможностями кастомизации. Также будет полезно посмотреть уже упомянутый activeadmin_addons. В этом gem'е есть множество дополнительных input'ов, которые наверняка пригодятся в хозяйстве.


Отдельно замечу, что хотя в статье я разделил Formtastic и Arbre по разным блокам, они прекрасно работают вместе, ведь вы можете создавать формы или части форм в качестве Arbre-компонентов.


Inherited Resources — кастомизация контроллеров


Чтобы понять. откуда берется магический resource, как поменять поведение при сохранении. и многое другое, нам будет необходимо познакомиться с еще одним gem'ом.


Inherited Resources — библиотека, призванная избавить контроллеры от однообразной CRUD-рутины.


Библиотека, с одной стороны, простая, а с другой обширная. Поэтому галопом по Европам рассмотрим несколько полезных методов:


class ArticlesController < InheritedResources::Base
  respond_to :html
  respond_to :json, only: :index
  actions :index, :new, :create

  def update
    resource.updated_by = current_user
    update! { articles_path }
  end
end

Итак, .respond_to отвечает за доступные форматы. Все вызовы .respond_to «складываются», а не переопределяют друг друга. Чтобы сбросить форматы, понадобится метод .clear_respond_to.


.actions определяет доступные CRUD-методы (index, show, new, edit, create, update и destroy).


resource — один из доступных хелперов, среди которых:


resource        #=> @article
collection      #=> @articles
resource_class  #=> Article

И наконец, #update! — это просто alias для #update, который можно использовать при перегрузке методов вместо super.


Отдельно рассмотрим применение метода .has_scope. Предположим, что в классе Article определен scope :published:


class Article < ApplicationRecord
  scope :published, -> { where(published: true) }
end

Тогда мы можем использовать в контроллере метод .has_scope:


class ArticlesController < InheritedResources::Base
  has_scope :published, type: :boolean
end

.has_scope добавляет возможность фильтрации с помощью query-параметров. В примере выше мы сможем применить scope :published, если обратимся к коллекции по URL /articles?published=true.


Подробное описание этих и других возможностей библиотеки можно найти в обширном README. А мы, пожалуй, остановимся на этом и перейдем, наконец, к взаимодействию с Active Admin.


Inherited Resources: расширение контроллера


Все контроллеры Active Admin наследованы от InheritedResources::Base, а это значит, что у нас есть возможность модифицировать их поведение, используя методы библиотеки.


Например, список доступных action'ов контроллера определяется следующим образом:


# app/admin/articles.rb
ActiveAdmin.register Article do
  actions :all, :except => [:destroy]
end

Отлично, мы убрали action удаления статьи. Кажется, всё очевидно: используем ресурс Active Admin как контроллер. Но не будем спешить с выводами и попробуем добавить еще одну фичу.


По умолчанию Active Admin включает рендеринг всех страниц в качестве HTML, JSON и XML (а index доступен еще и в формате CSV). Попробуем избавимся от XML-рендеринга нашей страницы с помощью знакомых нам методов:


# app/admin/articles.rb
ActiveAdmin.register Article do
  clear_respond_to
  respond_to :html, :json
  respond_to :csv, only: :index
end


Ой, теперь мы получили ошибку undefined method 'clear_respond_to' for #<ActiveAdmin::ResourceDSL>.


Дело в том, что когда мы описываем класс-ресурс, мы находимся в контексте ActiveAdmin::ResourceDSL, а не в контексте контроллера. Код из предыдущего примера работает только потому, что ActiveAdmin::ResourceDSL делегирует контроллеру метод #actions.


Но не отчаивайтесь, чтобы добраться до контроллера и выполнить код в его контексте, необходимо всего-навсего вызвать метод #controller:


# app/admin/articles.rb
ActiveAdmin.register Article do
  controller do
    clear_respond_to
    respond_to :html, :json
    respond_to :csv, only: :index
  end
end

Вуаля, теперь localhost:3000/admin/articles.xml возвращает ошибку. А что на счет модификации поведения action'ов?


Inherited Resources: перегрузка методов


Предположим, что при сохранении нам необходимо задать атрибут Article#created_by_admin. Воспользуемся для этого возможностью перегрузки метода #create:


# app/admin/articles.rb
ActiveAdmin.register Article do
  controller do
    def create
      build_resource
      @article.created_by_admin = true
      create!
    end
  end
end

Итак, мы вызываем build_resource — метод, который инициализирует новый объект и присваивает его переменной @article. Далее задаем атрибут created_by_admin и вызываем create! (он же super), который продолжает оперировать созданным нами @article.


Хотелось бы отдельно отметить: будьте внимательны с хелперами. Inherited Resources активно использует instance-переменные для кеширования. В данном случае это помогло нам создать и модифицировать объект, но при неаккуратном использовании, результаты могут быть неожиданными (проверено на собственной шкуре).


А теперь вернемся на пару шагов назад, к моменту, когда мы отключали XML-рендеринг статей. Что, если мы хотим убрать рендеринг XML из всех ресурсов? Не будем же мы писать один и тот же код в каждом новом классе?


Расширение базового контроллера


Не будем! Давайте создадим модуль, который скорректирует поведение класса ActiveAdmin::ResourceController:


# lib/active_admin/remove_xml_rendering_extension.rb
module ActiveAdmin
  module RemoveXmlRenderingExtension
    def self.included(base)
      base.send(:clear_respond_to)
      base.send(:respond_to, :html, :json)
      base.send(:respond_to, :csv, only: :index)
    end
  end
end

В метод .included будет передан расширяемый класс, к которому будут применены нужные нам модификаторы. Воспользуемся инициализатором Active Admin и подключим новый модуль к ActiveAdmin::ResourceController:


# config/initializers/active_admin.rb
require 'lib/active_admin/remove_xml_rendering_extension'

ActiveAdmin::ResourceController.send(
  :include,
  ActiveAdmin::RemoveXmlRenderingExtension
)
# ...

Немного магии метапрограммирования с #include и #included, и готово! Теперь ни один ресурс не ответит на формат .xml.


К слову, если вы думали, что #prepend, #include и #extend — это методы из вопросов, которыми на собеседованиях валят неугодных, то боюсь вас разочаровать. Когда возникает необходимость модифицировать код внешней библиотеки, подобные подходы нередко становятся единственным доступным инструментом.


Inherited Resources: что еще посмотреть


Прежде всего обратите внимание на подробный README. Помимо этого посмотрите на то, как устроены контроллеры в Active Admin, обратите внимание на логику авторизации и другие мелочи, вроде дополнительных хелперов.


Ransack: кастомизация фильтров


По умолчанию Active Admin на каждой index-странице предоставляет развесистый блок с фильтрацией, из которого чаще приходится убирать лишнее, нежели добавлять что-то свое. Но на самом деле это лишь верхушка айсберга под названием Ransack.


Ransack — библиотека для создания поисковых форм, которая позволяет собирать сложные SQL-запросы, интерпретируя переданные имена параметров. Звучит сложно, но я уверен, пример позволит быстро разобраться. о чем идет речь.


Предположим, что нам необходимо фильтровать записи блога (Article) по вхождению строки в название (title). С помощью Ransack мы можем это сделать следующим образом:


Article.ransack(title_cont: 'Домклик').result

Постфикс _cont — это один из множества предикатов, доступных в Ransack. Предикаты определяют то, какой SQL-запрос будет сгенерирован для поиска. Подробно обо всех доступных предикатах можно прочитать в официальной wiki.


А теперь чуть усложним задачу: заказчик попросил нас добавить фильтр, который позволит искать вхождение строки одновременно и в заголовке, и в теле (body). С Ransack это проще некуда:


Article.ransack(title_or_body_cont: 'active admin').result

Помимо этого, Ransack позволяет искать записи, обращаясь к связанным моделям. Для демонстрации, добавим возможность искать статьи по тексту комментариев (Comment#text):


Article.ransack(comments_text_cont: 'I hate type annotations!').result

Как несложно догадаться, подобные конструкции могут быстро разрастись. Да и использование сложных параметров в нескольких местах может привести к проблемам. В качестве решения Ransack предлагает использовать #ransack_alias. Добавим к поиску по тексту комментария поиск по его автору и дадим короткий alias: comments, который в дальнейшем можно будет использовать с нужными нам предикатами:


# app/models/article.rb
class Article < ActiveRecord::Base
  has_many :comments

  ransack_alias :comments, :comments_text_or_comments_author
end

Article.ransack(comments_cont: 'Matz').result

Разобравшись с тем, как Ransack позволяет структурировать запросы, перейдем, наконец, к тому, как мы можем использовать это в Active Admin.


Ransack: использование составных фильтров


Возьмем за основу примеры выше и используем их в качестве фильтров для ресурса Active Admin:


# app/admin/articles.rb
ActiveAdmin.register Article do
  preserve_default_filters!
  filter :title_or_body_cont,
         as: :string, 
         label: I18n.t('active_admin.filters.title_or_body_cont')
  filter :comments, 
         as: :string
end


Вот и всё, весьма прямолинейно. Разве что отмечу метод #preserve_default_filters!, который оставляет на месте стандартные фильтры.


Ransack: использование scope-фильтров


По умолчанию Ransack позволяет фильтровать по всем атрибутам и связям модели. Это может быть опасно с точки зрения безопасности, поэтому обратите внимание на возможность ограничения доступа к определенным полям и связям с помощью методов ransackable_attributes, ransackable_associations и ransackable_scopes. Вопросы авторизации я хотел бы оставить за рамками данной статьи (тем более, что у Active Admin в документации есть подробный раздел), поэтому обратим внимание лишь на метод ransackable_scopes.


В отличие от других методов авторизации, ransackable_scopes по умолчанию запрещает использование любых scope'ов. Таким образом, чтобы иметь возможность фильтровать по scope (или по любому другому методу класса модели), необходимо вернуть его название из .ransackable_scopes.


Для примера, добавим фильтр по количеству комментариев с использованием scope:


# app/models/article.rb
class Article < ActiveRecord::Base
  has_many :comments

  scope :comments_count_gt, (lambda do |comments_count|
     joins(:comments)
       .group('articles.id')
       .having('count(comments.id) > ?', comments_count)
  end)

  def self.ransackable_scopes(auth_object = nil)
    [:comments_count_gt]
  end
end

Обратите внимание на auth_object: в теории, это объект по которому можно определить стратегию авторизации. Я бы ожидал, что сюда будет передаваться current_user, однако Active Admin этого не делает.


Мы добавили scope и вернули его название в .ransackable_scopes, осталось только добавить фильтр в ресурс Active Admin:


# app/admin/articles.rb
ActiveAdmin.register Article do
  filter :comments_count_gt, 
         as: :number,
         label: I18n.t('active_admin.filters.comments_count_gt')


Осталась одна мелочь: если мы попробуем отфильтровать все статьи с двумя и более комментариями — всё отлично, но если попробовать подать единицу, то мы получим ошибку:



Это нам «помогло» приведение типов, которое по историческим причинам делает Ransack. Чтобы отключить сомнительную фичу, мы добавим инициализатор с заданным параметром sanitize_custom_scope_booleans:


# /config/initializers/ransack.rb
Ransack.configure do |config|
  config.sanitize_custom_scope_booleans = false
end

Готово, теперь наш фильтр работает, даже если мы подадим 1 в качестве аргумента, и мы умеем использовать фильтры на основе scope'ов.


Ransack: что еще посмотреть


Прежде всего, стоит еще раз заглянуть в документацию Active Admin про фильтры. Продолжить обзор можно в официальных README и wiki, в которых, помимо всего прочего, вы сможете найти view-хелперы для создания своих поисковых форм.


Для особо запущенных случаев вы можете обратить внимание на то, как создавать собственные предикаты, и на Ransackers — расширения, которые преобразуют параметры напрямую в Arel (внутренняя библиотека ActiveRecord, используемая для конструирования SQL-запросов).


Итоги


Надеюсь, что после этой статьи вы взглянули на Active Admin с новой стороны и, возможно, захотели зарефакторить класс-другой в своих проектах. Ведь Active Admin позволяет быстро запустить рабочую систему и направить все силы frontend-разработчиков на полезный для конечного пользователя продукт.


Я старался не сильно пересекаться с официальной документацией Active Admin, в которой можно найти описание множества интересных возможностей библиотеки, например, авторизацию или использование декораторов.


Также в очередной раз упомяну activeadmin_addons, в котором, помимо множества компонентов, доступна симпатичная тема для Active Admin. Обратите внимание на то, как она устроена, если захотите сделать свою тему для админки и использовать ее во всех проектах (именно так и сделано у нас в Домклике).