Customizing Dashboards
Dashboards define which fields appear on each page of your admin panel. The DSL is identical to Administrate.
Basic Structure
# app/dashboards/product_dashboard.rb
class ProductDashboard < Terrazzo::BaseDashboard
ATTRIBUTE_TYPES = {
id: Field::String,
name: Field::String,
price: Field::Number.with_options(prefix: "$", decimals: 2),
description: Field::Text,
category: Field::Select.with_options(
collection: %w[Electronics Books Clothing]
),
customer: Field::BelongsTo,
tags: Field::HasMany,
created_at: Field::DateTime,
updated_at: Field::DateTime,
}.freeze
COLLECTION_ATTRIBUTES = %i[id name price category].freeze
SHOW_PAGE_ATTRIBUTES = %i[id name price description category customer tags created_at updated_at].freeze
FORM_ATTRIBUTES = %i[name price description category customer tags].freeze
endATTRIBUTE_TYPES
A hash mapping attribute names to field types. Every attribute you want to display anywhere must be listed here.
See Customizing Fields for the full list of field types and their options.
COLLECTION_ATTRIBUTES
An array of attributes shown on the index (list) page. Keep this short — typically 3-5 columns.
SHOW_PAGE_ATTRIBUTES
An array of attributes shown on the detail page.
FORM_ATTRIBUTES
An array of attributes shown on the new/edit form. You can also define separate lists for create and update:
FORM_ATTRIBUTES_NEW = %i[name email password].freeze
FORM_ATTRIBUTES_EDIT = %i[name email].freezeWhen these are defined, they take precedence over FORM_ATTRIBUTES for their respective actions.
COLLECTION_FILTERS
Named filters for the index page:
COLLECTION_FILTERS = {
active: ->(resources) { resources.where(active: true) },
recent: ->(resources) { resources.where("created_at > ?", 30.days.ago) },
}.freezeFilters that only need the resource relation are rendered as index facets automatically. Filters that require a value, such as ->(resources, value) { ... }, are supported through the filter and filter_value params but are not rendered as one-click facets by default.
Override collection_filter_label to customize facet text:
def collection_filter_label(filter_name)
filter_name == :recent ? "Recently added" : super
endOverride collection_filter_options when you need full control over which filters appear as facets:
def collection_filter_options(_view)
[
{ label: "Recently added", value: "recent" },
{ label: "VIP", value: "vip" },
]
endDisplay Name
Override display_resource to customize how a record is shown in links, titles, and association dropdowns:
def display_resource(resource)
resource.name
endThe default resource title is "ClassName #id". Association fields and form dropdowns use your display_resource override when you define one; otherwise they fall back to common model display methods such as display_name, name, or title so generated dashboards remain readable.
Attribute Labels and Hints
Override attribute_label to rename fields across index headers, show pages, forms, nested has_many tables, and CSV exports:
def attribute_label(attribute, context = nil)
return "SKU" if attribute == :sku
return "Customer email" if attribute == :email && context == :csv
super
endThe context argument is one of :index, :show, :form, or :csv.
Override attribute_hint to show supporting text on forms and show pages:
def attribute_hint(attribute, context = nil)
return "Used for receipts and account notifications." if attribute == :email && context == :form
super
endCollection Cell Options
Use collection_cell_options when a table cell needs presentation metadata without ejecting the index page:
def collection_cell_options(attribute, resource)
return {} unless attribute == :status
{
class_name: resource.overdue? ? "text-destructive font-medium" : "text-muted-foreground",
tone: resource.overdue? ? "danger" : "neutral",
}
endThe default table applies class_name / className to the <td>. Other keys are serialized under cellOptions.meta so custom index field components can use them.
Collection Header Options
Use collection_header_options when a table header needs presentation metadata:
def collection_header_options(attribute)
return {} unless attribute == :total
{
class_name: "w-32 text-right",
align: "right",
}
endThe default sortable header applies class_name / className to the <th>. Other keys are serialized under headerOptions.meta so an ejected or registered SortableHeader can use them.
Collection Row Options
Use collection_row_options when an entire table row needs presentation metadata:
def collection_row_options(resource)
return {} unless resource.overdue?
{
class_name: "bg-destructive/10",
tone: "danger",
}
endThe default table applies class_name / className to the <tr>. Other keys are serialized under rowOptions.meta so an ejected or registered ResourceTable can use them.
Custom Row Actions
By default, each row on the index page shows Show, Edit, and Destroy buttons. Override collection_item_actions in your dashboard to customize these per resource type:
class OrderDashboard < Terrazzo::BaseDashboard
# ...
def collection_item_actions(resource, view)
[
{ label: "View Order", url: view.admin_order_path(resource) },
{ label: "Edit", url: view.edit_admin_order_path(resource) },
{ label: "Invoice", url: view.invoice_admin_order_path(resource) },
]
end
endUse super when you want to keep the default Show/Edit/Destroy actions and add one more action:
def collection_item_actions(resource, view)
super + [
{ label: "Invoice", url: view.invoice_admin_order_path(resource), sg_visit: false },
]
endThe view parameter provides access to route helpers. Each action hash supports:
| Key | Required | Description |
|---|---|---|
label | Yes | Button text |
url | Yes | Link URL |
method | No | HTTP method (e.g. "delete" or :delete; normalized case-insensitively) — renders as a form instead of a link |
confirm | No | Confirmation message shown before submitting non-GET form actions |
sg_visit | No | Set to false to bypass SPA navigation and perform a standard browser request (useful for actions that redirect outside the admin) |
variant | No | Button variant for toolbar and page header actions (for example "default", "outline", or "destructive"). Row dropdown actions ignore this key; delete row actions are styled as destructive automatically. |
Custom actions also appear in has_many tables on show pages — for example, if a Customer has_many Orders, the orders table on the customer show page will use the OrderDashboard's custom actions.
To add a custom action endpoint, define the route and controller action:
# config/routes.rb
resources :orders do
member { get :invoice }
end
# app/controllers/admin/orders_controller.rb
def invoice
redirect_to request.referer || admin_orders_path,
notice: "Printing invoice"
endResources without a collection_item_actions override use the default Show/Edit/Destroy buttons.
Custom Toolbar Actions
The index toolbar includes an Export CSV action by default. Add or replace toolbar buttons with collection_toolbar_actions:
class OrderDashboard < Terrazzo::BaseDashboard
# ...
def collection_toolbar_actions(view)
super + [
{ label: "Sync Orders", url: view.sync_admin_orders_path, method: "post" },
]
end
endToolbar actions use the same action hash shape as row actions and also render the optional variant key as a button variant. Non-GET toolbar actions default to "outline", except delete actions default to "destructive".
Bulk Collection Actions
Use collection_bulk_actions to add actions for selected rows on an index page. When at least one bulk action is present, the default table renders selection checkboxes and submits the selected row IDs as params[:ids].
class OrderDashboard < Terrazzo::BaseDashboard
# ...
def collection_bulk_actions(view)
[
{
label: "Mark shipped",
url: view.bulk_ship_admin_orders_path,
method: "post",
confirm: "Mark selected orders as shipped?",
},
]
end
endDefine the matching collection route and controller action in your Rails app:
# config/routes.rb
resources :orders do
collection { post :bulk_ship }
end
# app/controllers/admin/orders_controller.rb
def bulk_ship
scoped_resource.where(id: params[:ids]).update_all(shipped_at: Time.current)
redirect_to admin_orders_path, notice: "Marked selected orders shipped"
endBulk actions use the same action hash shape as row and toolbar actions. They default to the ids[] parameter name; pass param_name: "order_ids[]" when a custom controller action expects a different parameter.
Page Header Actions
Use layout_actions to add buttons to the page header slot on index, show, new, and edit pages:
class OrderDashboard < Terrazzo::BaseDashboard
# ...
def layout_actions(page, view, resource: nil)
return [] unless page == :show && resource
[
{
label: "Print Invoice",
url: view.invoice_admin_order_path(resource),
variant: "default",
sg_visit: false,
},
]
end
endLayout actions use the same action hash shape as row and toolbar actions and render the optional variant key as a button variant. Use this for page-level actions before ejecting the full layout or page.
CSV Exports
The default CSV export uses the filtered, searched, and sorted index relation, exports all matching rows, and uses COLLECTION_ATTRIBUTES as columns. Pagination params such as _page and per_page are omitted from the export URL.
Customize the export with dashboard methods:
class OrderDashboard < Terrazzo::BaseDashboard
# ...
def csv_attributes
%i[id customer created_at total]
end
def csv_filename
"orders-export.csv"
end
def csv_value(attribute, value, resource)
return resource.total_cents / 100.0 if attribute == :total
super
end
endDisable CSV export with:
def csv_export_enabled?
false
endEmpty State
Override empty_collection_message to customize the message shown when the index table has no rows:
def empty_collection_message
"No orders match your search or filters."
endSidebar Navigation
Dashboards can control how their resource appears in the sidebar without ejecting the navigation partial:
class OrderDashboard < Terrazzo::BaseDashboard
NAVIGATION_LABEL = "Orders"
NAVIGATION_GROUP = "Commerce"
NAVIGATION_GROUP_ORDER = 10
NAVIGATION_ORDER = 20
endUse SHOW_IN_NAVIGATION = false to hide a resource from the generated sidebar.
Resources for namespaced models are grouped by namespace by default, so Blog::PostDashboard appears under Blog. Non-namespaced resources appear under Resources.
Generating Dashboards
rails g terrazzo:dashboard ProductThe generator inspects your model's columns and associations to produce a reasonable starting dashboard. You can then customize it as needed.
All Instance Methods
| Method | Description |
|---|---|
#attribute_types | Returns the ATTRIBUTE_TYPES hash |
#attribute_type_for(attr) | Returns the field class for a given attribute |
#form_attributes(action) | Returns form attributes — nil returns FORM_ATTRIBUTES, "create" returns FORM_ATTRIBUTES_NEW if defined, "update" returns FORM_ATTRIBUTES_EDIT if defined |
#collection_attributes | Returns COLLECTION_ATTRIBUTES |
#show_page_attributes | Returns SHOW_PAGE_ATTRIBUTES |
#search_attributes | Returns attributes where .searchable? is true |
#collection_includes | Returns eager-loadable attributes visible in collection |
#permitted_attributes | Maps form attributes through .permitted_attribute for strong params |
#display_resource(resource) | Display string for the resource (default: "ClassName #id") |
#attribute_label(attribute, context) | Display label for an attribute on index, show, form, nested table, and CSV contexts |
#attribute_hint(attribute, context) | Supporting text for attributes on form and show contexts |
#collection_cell_options(attribute, resource) | Per-cell metadata for index and nested has_many tables |
#collection_header_options(attribute) | Per-header metadata for index and nested has_many tables |
#collection_row_options(resource) | Per-row metadata for index and nested has_many tables |
#collection_filter_options(view) | Index filter facets generated from COLLECTION_FILTERS |
#collection_filter_label(filter_name) | Label for an index filter facet |
#collection_item_actions(resource, view) | Per-row action buttons (default: Show/Edit/Destroy) |
#collection_toolbar_actions(view) | Index toolbar actions (default: Export CSV) |
#layout_actions(page, view, resource:) | Page header action slot for index, show, new, and edit pages |
#csv_export_enabled? | Whether to show the default CSV export action |
#csv_attributes | Attributes exported to CSV (default: COLLECTION_ATTRIBUTES) |
#csv_filename | Download filename for CSV exports |
#csv_value(attribute, value, resource) | Converts a serialized field value for CSV output |
#empty_collection_message | Index empty-state description |
#navigation_label | Sidebar link label (default: plural resource name) |
#navigation_group | Sidebar group label (default: model namespace or Resources) |
#navigation_group_order | Sort key for sidebar groups |
#navigation_order | Sort key within a sidebar group |
#show_in_navigation? | Whether the resource appears in the generated sidebar |