Resources
Resources are defined using Admin::Base::Resource.
AdminSuite loads resource definition files from AdminSuite.config.resource_globs
(defaults include config/admin_suite/resources/*.rb and app/admin/resources/*.rb).
Create a resource
A resource is a Ruby class under Admin::Resources ending in Resource.
Example:
# config/admin_suite/resources/user.rb
module Admin
module Resources
class UserResource < Admin::Base::Resource
model ::User
portal :ops
section :accounts
nav label: "Users", icon: "users", order: 10
index do
searchable :email, :name
sortable :created_at, :email, default: :created_at, direction: :desc
paginate 50
columns do
column :id
column :email
column :created_at
column :admin, type: :toggle, toggle_field: :admin, header: "Admin?"
column :status, type: :label, label_color: ->(u) { u.active? ? :emerald : :slate }, label_size: :sm
end
filters do
filter :search, type: :text, placeholder: "Search users..."
filter :status, type: :select, options: [["Active", "active"], ["Inactive", "inactive"]]
end
stats do
stat :total, -> { User.count }, color: :slate
stat :new_24h, -> { User.where("created_at > ?", 24.hours.ago).count }, color: :emerald
end
end
form do
section "Basics", description: "Core account fields" do
field :email, type: :email, required: true
field :name, required: true
end
row cols: 2 do
field :admin, type: :toggle, help: "Grants access to internal tools."
field :status, type: :select, collection: [["Active", "active"], ["Inactive", "inactive"]]
end
end
show do
main do
panel :details, title: "User details", fields: %i[email name status created_at]
panel :activity, title: "Activity", render: :custom_activity_timeline
end
sidebar do
panel :summary, title: "Summary", fields: %i[id admin]
end
end
actions do
action :reset_password, label: "Reset password", icon: "key", confirm: "Send reset email?"
end
end
end
end
Core DSL
Model
model ::User
Navigation placement
portal :ops
section :accounts
These determine:
- URL:
/:portal/:resource_name - Sidebar placement: portal group → section group → resource link
Navigation metadata
nav label: "Users", icon: "users", order: 10
Also available as convenience setters:
label "Users"
icon "users"
order 10
Index DSL (index do ... end)
Search
searchable :name, :email
Search uses ILIKE across the configured fields.
Sort
sortable :created_at, :email, default: :created_at, direction: :desc
Pagination
paginate 25
paginate(n) sets the resource’s index page size — Pagy respects it as of
0.5.0. In every earlier release this value was silently ignored (see
Fixed: paginate(n) was ignored
below); every index page previously served Pagy’s own default of 20 rows
regardless of what was declared here. Upgrading to 0.5.0 may change the row
counts and pagination boundaries a resource’s index page shows.
Eager loading (includes:)
index do
includes :company, :line_items
# ...
end
Applies scope.includes(*args) to the index’s filtered/sorted/searched
scope, before pagination, so eager loading covers exactly the rows actually
rendered on the current page. Accepts one or more association names
(Symbols or Strings); array arguments are flattened and nil entries are
dropped.
Degrades safely rather than raising:
- If the scope doesn’t respond to
#includesat all (a non-ActiveRecord scope), the option is skipped silently — no warning, no error. - If
#includesitself raises (e.g. a typo’d/renamed association name), the error is logged viaRails.logger.warnand the index renders normally, just without the eager loading — a badincludes:value degrades performance, not availability.
Columns
columns do
column :email
column :job_listings, ->(u) { u.job_listings.count }
column :status, align: :center, class: "font-mono"
end
column options:
header:string (defaults to a humanized name)class:css class(es) applied to the rendered<td>align::left,:center, or:right— emits the matching Tailwind text-alignment class on the<td>. Any other value (a typo, an unsupported Symbol) is silently ignored — no alignment class is added, and nothing is raised or interpolated into a fabricated class name.render:custom render key (advanced)type::toggleor:label(special rendering)toggle_field:field to flip whentype: :togglelabel_color:color fortype: :label(Symbol or Proc)label_size::sm/:md(or Proc)sortable:boolean (reserved for future per-column sorting UI)
A column value that is nil — whether a plain scalar or a belongs_to
association — renders as —, not a blank cell.
A column value that is an ActiveRecord::Base instance (e.g. a belongs_to
association returned from a column proc) renders as a link to that record’s
own admin show page when one is registered, falling back to plain text
(never a raw #<Company:0x...> inspect string) when no resource is
registered for that class, the record is unpersisted, or its display title
raises.
Fixed: paginate(n) was ignored before 0.5.0
The controller called Pagy with items: n, but Pagy’s vars key is limit:
— items: is an unknown key that Pagy silently ignores, so every
resource’s paginate(n) value has been ignored since the gem’s first
commit. This was a long-standing latent bug, not a 0.5.0 regression; it’s
now fixed, and declared page sizes take effect.
Filters
filters do
filter :status, type: :select, options: [["Active", "active"], ["Inactive", "inactive"]]
end
Filter options:
type:(default:text)label:placeholder:options:/collection:(for select-like UI)field:which model field to filter on (defaults to the filter name)apply:Proc that receives the scope (advanced)
Stats
stats do
stat :total, -> { User.count }, color: :slate
end
Form DSL (form do ... end)
form do
field :name, required: true
field :website, type: :url
end
See Fields for supported types and options.
AdminSuite also supports basic layout helpers in forms:
section "Title" do ... endrow cols: 2 do ... end
Show DSL (show do ... end)
Show is section-based. Sections can live in the main column or sidebar.
show do
main do
panel :details, title: "Details", fields: %i[name email]
panel :related, title: "Projects", association: :projects, display: :table, columns: %i[id name status], paginate: true
end
sidebar do
panel :meta, title: "Meta", fields: %i[id created_at updated_at]
end
end
Panel options:
title:(defaults to a humanized name)fields:array of field names to displayrender:custom renderer key (seecustom_renderersin Configuration)association:association name to render (has_many,belongs_to, etc.)display::list(default),:table, or:cardsfor associationscolumns:columns for association:tabledisplaylink_to:helper method name to build links for association items (optional)paginate:boolean, enables pagination within the association sectionper_page:items per page for association paginationlimit:max items (if not paginating)collapsible:/collapsed:(reserved for future UI toggles)hide_blank:boolean, defaultfalse— hides afields:row entirely instead of rendering a label with an empty value. Works for both sidebar and main-columnfields:panels. Only affectsfields:panels — it has no effect onassociation:orrender:-driven panels.
sidebar do
panel :meta, title: "Meta", fields: %i[id internal_notes], hide_blank: true
end
The blank contract is stricter than .blank? — read this before relying
on it. A value is hidden only when it is nil, or it responds to
#empty? and #empty? is true ("", [], {}). It is deliberately
NOT Rails’ #blank?:
| Value | Hidden under hide_blank: true? |
|---|---|
nil |
Yes |
"" |
Yes |
[] / {} |
Yes |
" " (whitespace-only) |
No — kept, because String#empty? is a length check (" ".empty? is false), not a content check |
false |
No — kept, because FalseClass has no #empty? — and false is a real, meaningful rendered value (a “No” toggle), not an absence |
0 / 0.0 |
No — kept, same reasoning — Integer/Float have no #empty? |
If a field’s accessor raises, the row is currently rescued to nil and
therefore also hidden under hide_blank: true — indistinguishable from a
value that was never populated. (Without hide_blank, a raising accessor
still shows its label with a bare —.) This is a known sharp edge, not a
bug fix target for 0.5.0 — flagging it here since it’s non-obvious.
Actions DSL (actions do ... end)
actions do
action :reindex, label: "Reindex", method: :post, confirm: "Reindex this record?"
end
See Actions for how actions execute and how to define handlers.
MCP exposure (0.6.0)
Use mcp false to exclude a resource from discovery and calls. MCP serializes
only declared fields/association columns; see Admin MCP.
The deprecated no-op exportable DSL method is removed; delete its calls before upgrading.