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:
- Portal metadata via
AdminSuite.config.portals(label/icon/color/order) - 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 labelicon(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 sidebardescription(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 seticon(value)(String/Symbol): lucide icon nameorder(value)(Integer): sort order among sections within the portaldescription(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.
dashboardcontainsrow { ... }- each
rowcontains one or morepanel(...)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)—**optionsincludestype:(:bar/:line/:area/:doughnut),height:(pixels, default 192), andcolor:. 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