Portals & dashboards

AdminSuite navigation is organized by portal and section:

  • A portal is the top-level grouping (e.g. :ops, :ai)
  • A section is a grouping within a portal (e.g. :billing, :users)
  • A resource belongs to exactly one portal + section via the resource DSL

You can configure portals in two complementary ways:

  1. Portal metadata via AdminSuite.config.portals (label/icon/color/order)
  2. Portal DSL via AdminSuite.portal(:key) { ... } (metadata + dashboard layout)

Portal metadata (config.portals)

AdminSuite.configure do |config|
  config.portals = {
    ops: { label: "Ops", icon: "settings", color: :amber, order: 10 },
    ai: { label: "AI", icon: "cpu", color: :cyan, order: 20 }
  }
end

Portal fields

  • label (String): display label
  • icon (String/Symbol): lucide icon name (e.g. "settings")
  • color (Symbol/String): used for accents (:amber, :emerald, :cyan, :violet, :slate)
  • order (Integer): sort order in the root dashboard and sidebar
  • description (String, optional): shown on the root dashboard cards when present

Portal DSL (AdminSuite.portal)

Portal DSL files are loaded from AdminSuite.config.portal_globs (defaults include config/admin_suite/portals/*.rb, app/admin/portals/*.rb, app/admin_suite/portals/*.rb).

Example file:

# config/admin_suite/portals/ops.rb
AdminSuite.portal :ops do
  label "Ops Portal"
  icon "settings"
  color :amber
  order 10
  description "Operational tools and internal resources."

  dashboard do
    row do
      stat_panel "New users (24h)", -> { User.where("created_at > ?", 24.hours.ago).count }, color: :emerald, span: 3
      stat_panel "Failed jobs", -> { SolidQueue::FailedExecution.count }, color: :red, span: 3
    end

    row do
      recent_panel "Recent signups", scope: -> { User.order(created_at: :desc).limit(5) }, span: 6
      table_panel "Queue summary",
        rows: -> { SolidQueue::Job.order(created_at: :desc).limit(10) },
        columns: %i[id class_name created_at],
        span: 6
    end
  end
end

Section DSL (section inside AdminSuite.portal)

Sections are first-class as of 0.4.0: declare one inside a portal block to control its sidebar label, icon, order, and description instead of accepting the humanized key and alphabetical placement.

AdminSuite.portal :ops do
  section(:runs) do
    label "Runs"
    icon "play"
    order 10
    description "Background job runs and their status."
  end
end
  • label(value) (String): sidebar label; falls back to the humanized section key when not set
  • icon(value) (String/Symbol): lucide icon name
  • order(value) (Integer): sort order among sections within the portal
  • description(value) (String): shown when the section renders with no resources yet

Calling section(:key) again in the same or another block reopens the same definition (e.g. to set fields incrementally).

Undeclared sections are unchanged: a resource’s section :billing still works with no section(:billing) { ... } block anywhere — it renders with a humanized label ("Billing") and alphabetical placement, exactly as before 0.4.0. Declaring a section is opt-in polish, not a requirement.

Dashboard DSL

The dashboard DSL is available inside portal.dashboard do ... end.

  • dashboard contains row { ... }
  • each row contains one or more panel(...) calls

Panel helpers

These are convenience helpers that all create a panel under the hood:

  • stat_panel(title, value=nil, span: nil, **options, &block)
  • health_panel(title, status: nil, metrics: nil, span: nil, **options, &block)
  • chart_panel(title, data: nil, span: nil, **options, &block) — **options includes type: (:bar/:line/:area/:doughnut), height: (pixels, default 192), and color:. See Charts for the full DSL, the data contract, and the no-JS degraded behavior.
  • cards_panel(title, resources: nil, span: nil, **options, &block)
  • recent_panel(title, scope: nil, link: nil, span: nil, **options, &block)
  • table_panel(title, rows: nil, columns: nil, span: nil, **options, &block)

span

span controls width in a 12-column grid. Typical values: 3, 4, 6, 12.

Portal pages

Once mounted, portal pages are served at:

  • /:portal (relative to the mount path)

Example:

  • /internal/admin/ops
  • /internal/admin/ai

Root dashboard (AdminSuite.root_dashboard)

The engine root (/, relative to the mount path) renders a default dashboard (portal cards + basic stats).

To customize it, create a dashboard definition file (loaded from AdminSuite.config.dashboard_globs, which defaults to paths like config/admin_suite/dashboard.rb and app/admin_suite/dashboard.rb):

# config/admin_suite/dashboard.rb
AdminSuite.configure do |config|
  config.root_dashboard_title = "Developer Portal"
  config.root_dashboard_description = "Internal tools for managing application resources."
end

AdminSuite.root_dashboard do
  row do
    cards_panel "Portals",
      span: 12,
      variant: :portals,
      resources: ->(view) do
        view.navigation_items
          .sort_by { |(_k, meta)| (meta[:order] || 100).to_i }
          .map do |portal_key, portal|
            {
              key: portal_key,
              label: portal[:label] || portal_key.to_s.humanize,
              description: portal[:description],
              color: view.portal_color(portal_key),
              icon: portal[:icon],
              path: view.portal_path(portal: portal_key),
              count: (portal[:sections] || {}).values.sum { |s| Array(s[:items]).size }
            }
          end
      end
  end

  row do
    stat_panel "Portals", ->(view) { view.navigation_items.keys.count }, span: 6, variant: :mini, color: :slate
    stat_panel "Resources", -> { Admin::Base::Resource.registered_resources.count }, span: 6, variant: :mini, color: :slate
  end
end