Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Статья про 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. Обратите внимание на то, как она устроена, если захотите сделать свою тему для админки и использовать ее во всех проектах (именно так и сделано у нас в Домклике).