Renderers
Show-panel render: keys (panel :costs, render: :provider_costs, ...) resolve
to a small, versioned rendering API in 0.4.0: AdminSuite::Renderer. This page
covers the base class, the primitives available to subclasses, the four
built-in renderers, how to register your own, and how to migrate off the
older config.custom_renderers proc form.
Resolution order
When a show panel is rendered, render_custom_section resolves render: in
this order:
AdminSuite.config.custom_renderers[key]— a legacy proc, if registered (deprecated, see below)AdminSuite::RendererRegistry.lookup(key)— an explicitly registeredRendererclass (this is how the four built-ins register themselves)Admin::Renderers::<Key>Rendererin the host app, autoloaded by convention- Otherwise, a plain “Unknown render type” placeholder
A legacy proc for a key always wins over a renderer class registered (or
autoloaded) under the same key, so migrating a host one renderer at a time is
safe — remove the proc from config.custom_renderers when its renderer class
is ready.
AdminSuite::Renderer
class AdminSuite::Renderer
def initialize(record, view, options = {})
def render # must be implemented by subclasses; returns HTML-safe markup
end
record— the resource being shownview— the calling view/helper context (ActionView::Base); use it for anything not covered by a primitive (view.link_to,view.render, …)options— the panel DSL’s leftover keyword options (source:,columns:,empty:,language:, and any other option not reserved by thepanelDSL itself), forwarded verbatim
Naming convention
Host renderers live in app/admin/renderers/*.rb, one class per file, named
Admin::Renderers::<Key>Renderer (camelized) for a panel declared with
render: :<key>:
# app/admin/renderers/provider_costs_renderer.rb
module Admin
module Renderers
class ProviderCostsRenderer < AdminSuite::Renderer
def render
rows = record.provider_costs.map { |c| { provider: c.provider, amount: c.amount } }
data_table(rows, columns: %i[provider amount], empty: "No costs recorded.")
end
end
end
end
# in a resource's show block
panel :costs, title: "Provider Costs", render: :provider_costs
AdminSuite maps app/admin to the Admin namespace (and app/admin/renderers
to Admin::Renderers specifically, even for hosts that only use the portal/
resource DSL and define no other Admin::* constants), so this file is
autoloaded with no extra setup.
Primitives
These are private instance methods available inside #render:
content_tag(...),safe_join(...)— delegate straight toviewh(text)— HTML-escapestext(delegates to the view context’sERB::Util#h)source_value(default = nil)— resolvesoptions[:source]:Proc— called with the record (or with no args, if the proc takes none)Symbol/String— sent to the record (record.public_send(source))- anything else — used as a literal value
nil— falls back todefault
json_block(value, title: nil)— pretty-printed, copyable JSON block; renderstitleas a small heading above it when givencode_block(text, language: nil)— syntax-highlighted<pre><code>blockkey_value_list(pairs)—pairsisArray<[label, value]>; renders a label/value listdata_table(rows, columns:, empty: nil)—rowsisArray<Hash>,columnsisArray<Symbol>; rendersempty(or"None found.") whenrowsis blank. Row hashes may be String- or Symbol-keyed (JSONB columns deserialize to String keys) — both work.badge(text, color: :slate)— a small label badgeempty_state(message)— a muted “nothing here” paragraph
Built-in renderers
Four renderers are registered out of the box — no host code required:
:json
panel :attributes, render: :json
Renders source_value (default: record.attributes, or record itself if
it doesn’t respond to attributes) as a json_block.
source:— Proc/Symbol/String/literal (see above); defaults to the record’s attributesempty:— message shown when the resolved value is blank (default:"Nothing to display.")
:key_value
panel :summary, render: :key_value, source: ->(record) { { plan: record.plan, seats: record.seat_count } }
Renders source_value as a label/value list via key_value_list. Labels are
humanized (:seat_count → "Seat count").
source:— resolves to aHashor anArrayof[key, value]pairs (defaults to{}). Anything else that responds to#to_a(anActiveRecord::Relation, aSet, …) is coerced the same way. A bareHashargument tosource:, or anArraycontaining non-pair elements, raisesArgumentError.empty:— message shown when there are no pairs (default:"Nothing to display.")
:table_from
panel :costs, render: :table_from, source: :provider_costs, columns: %i[provider amount charged_on]
Renders source_value (default: []) as a data_table.
source:— resolves to anArrayofHashrows. Other Enumerables (anActiveRecord::Relation, aSet, …) are coerced via#to_a. A bareHashraisesArgumentError(it is not an enumeration of rows).columns:—Arrayof column names; defaults to the first row’s keysempty:— message shown whensource_valueis blank (default:"None found.")
:code
panel :sql, render: :code, source: :generated_sql, language: "sql"
Renders source_value (default: record.code, or record.to_s if it
doesn’t respond to code) as a code_block.
source:— Proc/Symbol/String/literal; defaults torecord.codelanguage:— passed straight to the syntax highlighter (e.g."ruby","sql","json")empty:— message shown when the resolved text is blank (default:"Nothing to display.")
Registering your own renderer
Host renderers under app/admin/renderers/*.rb are autoloaded by naming
convention alone — no registration call needed. If you want a renderer to
answer to a different key than its class name would imply (or you’re
registering from a gem, an engine, or anywhere outside the autoloaded host
tree), register it explicitly:
# config/initializers/admin_suite.rb, or anywhere loaded at boot
AdminSuite::RendererRegistry.register(:provider_costs, Admin::Renderers::BillingSnapshotRenderer)
panel :costs, render: :provider_costs
Migrating from config.custom_renderers
The pre-0.4.0 way to write a custom panel renderer was a proc assigned to
config.custom_renderers. It still works in 0.4.x (see Configuration),
but is deprecated — migrate to a Renderer subclass when convenient.
Before:
# config/initializers/admin_suite.rb
AdminSuite.configure do |config|
config.custom_renderers[:billing_snapshot] = ->(record, view) do
rows = record.provider_costs.map { |c| { provider: c.provider, amount: c.amount } }
view.render(partial: "admin/billing_snapshot", locals: { rows: rows })
end
end
After:
# app/admin/renderers/billing_snapshot_renderer.rb
module Admin
module Renderers
class BillingSnapshotRenderer < AdminSuite::Renderer
def render
rows = record.provider_costs.map { |c| { provider: c.provider, amount: c.amount } }
data_table(rows, columns: %i[provider amount], empty: "No costs recorded.")
end
end
end
end
# no config.custom_renderers entry needed — remove it once the class ships
panel :billing, title: "Billing Snapshot", render: :billing_snapshot
The renderer class version gets the shared primitives (data_table,
json_block, key_value_list, code_block, badge, empty_state) instead
of hand-building a partial, so panels stay visually consistent with the rest
of the admin UI as it evolves.
Removed in 0.6.0: Gleania renderer implementations
The gem implementations for :prompt_template_preview, :messages_preview,
:tool_args_preview and :turn_messages_preview are removed. The keys remain
usable when a host registers its own AdminSuite::Renderer subclass. Migrate host
implementations before upgrading; there is no generic replacement.