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:

  1. AdminSuite.config.custom_renderers[key] — a legacy proc, if registered (deprecated, see below)
  2. AdminSuite::RendererRegistry.lookup(key) — an explicitly registered Renderer class (this is how the four built-ins register themselves)
  3. Admin::Renderers::<Key>Renderer in the host app, autoloaded by convention
  4. 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 shown
  • view — 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 the panel DSL 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 to view
  • h(text) — HTML-escapes text (delegates to the view context’s ERB::Util#h)
  • source_value(default = nil) — resolves options[: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 to default
  • json_block(value, title: nil) — pretty-printed, copyable JSON block; renders title as a small heading above it when given
  • code_block(text, language: nil) — syntax-highlighted <pre><code> block
  • key_value_list(pairs) — pairs is Array<[label, value]>; renders a label/value list
  • data_table(rows, columns:, empty: nil) — rows is Array<Hash>, columns is Array<Symbol>; renders empty (or "None found.") when rows is blank. Row hashes may be String- or Symbol-keyed (JSONB columns deserialize to String keys) — both work.
  • badge(text, color: :slate) — a small label badge
  • empty_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 attributes
  • empty: — 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 a Hash or an Array of [key, value] pairs (defaults to {}). Anything else that responds to #to_a (an ActiveRecord::Relation, a Set, …) is coerced the same way. A bare Hash argument to source:, or an Array containing non-pair elements, raises ArgumentError.
  • 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 an Array of Hash rows. Other Enumerables (an ActiveRecord::Relation, a Set, …) are coerced via #to_a. A bare Hash raises ArgumentError (it is not an enumeration of rows).
  • columns: — Array of column names; defaults to the first row’s keys
  • empty: — message shown when source_value is 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 to record.code
  • language: — 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.