Skip to content

Dashboard DSL Reference ​

Experimental

Dashboards are experimental: the DSL and behavior may change in a future release.

A dashboard is a subclass of Plutonium::Dashboard::Base. Everything is declared at class level; an instance is built per request with the view context and is the receiver of every card block.

Presentation ​

ruby
presents label: "Sales", description: "Orders and revenue", icon: Phlex::TablerIcons::ChartBar
KeyDefault
labelplutonium.dashboards.<key>.label, then the class name without Dashboard
descriptionplutonium.dashboards.<key>.description, then nothing
iconPhlex::TablerIcons::LayoutDashboard (used in the sidebar)

<key> is Class.i18n_key: the class name underscored without the suffix (AdminPortal::SalesDashboard → admin_portal/sales). label and description accept the class-level lazy t("...").

Board-level options ​

refresh(seconds) ​

Default reload interval for every lazy card. nil (the default) disables it.

width(token) ​

Page width, one of :sm, :md, :lg, :xl, :full. Default :full.

filter(key, choices: nil, scopes: nil, default: nil, label: nil) ​

A page-level control every card reads, such as a period or a region.

ruby
filter :period, choices: {"7" => "Last 7 days", "30" => "Last 30 days", "90" => "Last 90 days"}, default: "30"

metric(:orders) { orders.where(created_at: filter_value(:period).to_i.days.ago..).count }
  • choices: maps each value to its label (a string or a lazy t proc), in display order. default: must be one of the values and is the first when omitted.
  • The key cannot be one the card route or url_for uses (card, format, anchor, host, use_route, ...), which raises when declared, or a path segment of the dashboard's route (the entity param on a path-scoped portal, a (:locale) scope), which raises on the request.
  • The value comes from the query string under the filter's key. A value that is not one of the choices reads as the default.
  • Every lazy card's frame URL carries every filter's value, so a card's own request reads what the page read.
  • The page renders each filter as a segmented row of links above the cards, so a choice is a URL that survives a reload and can be shared. Other query parameters on the page are kept.
  • A card's href: is not rewritten with the filter values. Use a scope filter (below) to link a card to the matching list.

Scope filters ​

scopes: takes a resource instead of choices:. The choices are "All" plus the scopes the resource's definition declares, labelled and gated (condition:) the way the index's scope pills are. The default is default:, else the definition's default_scope, else "All". A default whose condition: hides it from the current user reads as "All" for that user.

ruby
filter :status, scopes: Post

metric(:posts, href: -> { filtered_url(Post) }) { filtered(Post).count }
chart(:by_author) { filtered(Post).group(:author_name).count }
  • filtered(Post) is authorized_resource_scope(Post) with the chosen scope applied through the definition's own scope, the same code the index runs, so the card's number matches the list.
  • filtered_url(Post) is the Post index on that scope (q[scope]=...). With "All" chosen and a default_scope on the definition, it sends an explicit blank scope so the index shows everything.
  • Without a scope filter for the resource, filtered is the authorized scope as is and filtered_url the plain index.
  • A scope's block runs against the dashboard's controller. current_user, current_scoped_entity and the portal's helpers work there; resource-controller helpers such as current_parent do not.
  • One scope filter per resource; a second raises when declared. A default: that is not one of the definition's scopes, or a definition scope named :all (the "All" choice's value), raises on the request.

A button in the toolbar above the cards, such as the dashboard's settings or a full report. href: and condition: take the same forms as a card's and are evaluated on the dashboard instance. A link whose href: resolves to nil is left out.

ruby
link :settings, icon: Phlex::TablerIcons::Settings, href: -> { resource_url_for(Setting) },
  condition: -> { allowed_to?(:update?, Setting) }

Cards ​

ruby
metric(key, **options) { ... }
chart(key, **options) { ... }
card(key, **options) { ... }

Keys are unique per dashboard; a duplicate raises at class load. Cards render in declaration order.

Common options ​

OptionTypeDefaultMeaning
label:String, lazy tconvention, then key titleizedTitle
description:String, lazy tconventionCaption
icon:Phlex icon classnoneShown beside the title
span:1..12, :fullmetric 3, chart 6, card 6Columns of the 12-column grid. :full is 12. On tablets (2 columns) a span of 6 or more takes the row; phones are one column
lazy:Booleantruetrue: own lazy turbo frame, block runs in a separate request. false: inline, block runs in the page request (adding its query time to every page load); no frame, never refreshes. When config.consider_all_requests_local is true (development and test), an inline card's exception fails the whole page. See the guide
refresh:Integer seconds, or falsedashboard refreshReload interval; requires lazy: true (a number on a lazy: false card raises ArgumentError). false opts the card out of the dashboard's refresh
condition:Proc, SymbolnoneHides the card and 404s its endpoint when false
href:String, ProcnoneLinks the title

Procs (condition:, href:, the block) run on the dashboard instance with instance_exec; a one-argument proc receives the instance instead.

Locale conventions ​

plutonium.dashboards.<key>.cards.<card>.label
plutonium.dashboards.<key>.cards.<card>.description
plutonium.portals.<portal>.dashboards.<key>...

metric ​

The block returns a value or a hash.

Hash keyMeaning
valueThe number or string. nil renders plutonium.dashboard.metric.empty
previousComputes change from this value: a percentage of it, or the difference with change_unit: :points. A zero previous yields no percentage (a neutral dash when there is a caption); with :points the difference still shows. The change is rounded to one decimal before its trend is read
changeA number (2.5 → +2.5%, or +2.5 pts with change_unit: :points) or a string shown as-is
trend:up, :down, :flat; inferred from the sign of change
change_labelCaption after the change
OptionValuesMeaning
format::number (default), :currency, :percentage, :human, ProcHow value is rendered. A proc receives the value and runs on the dashboard
precision:IntegerDecimal places; defaults per format (2 for currency, 1 for percentage, 3 significant for human)
unit:StringCurrency symbol for :currency
prefix: / suffix:StringWrapped around the formatted value
positive::up (default), :downWhich direction is good; drives the change colour
change_label:StringSame as the hash key
change_unit::percent (default), :pointsHow the change reads. Use :points for a value that is itself a percentage: 50% to 55.6% reads +5.6 pts, not +11.2%

chart ​

The block returns Chartkick data: {label => value}, [[label, value], ...] or [{name:, data:}, ...]. Date and Time keys serialise as ISO strings and draw a time axis.

OptionValuesMeaning
type::line (default), :area, :column, :bar, :pie, :donut, :scatterChartkick chart class
height:CSS lengthDrawing area height, default "240px"
anything elsePassed to Chartkick unchanged: colors, stacked, min, max, prefix, suffix, thousands, decimal, xtitle, ytitle, legend, curve, points, discrete, download, library, ...

The chart Stimulus controller merges these over Plutonium's defaults: series colours from --pu-chart-1 to --pu-chart-8, axis text from --pu-text-muted, grid lines from --pu-border, and the empty-data message from plutonium.js.charts.empty. A colour-mode switch redraws every chart.

card ​

No kind-specific options. The block is instance_execed in Plutonium::UI::Dashboard::Custom, a Phlex component: it emits markup directly, and any method the component does not define is forwarded to the dashboard instance.

Instance API ​

MethodMeaning
authorize?Override to gate the page and every card. Default true
visible_cardsCards whose condition: passes
filter_value(key)The filter's value for this request, or its default
filter_values{key => value} for every filter; what each card's frame URL carries
filtered(resource_class)The authorized scope with the resource's scope filter applied
filtered_url(resource_class)The resource's index on the scope that filter has chosen
visible_linksToolbar links whose condition: passes
visible_card!(key)The card, or Plutonium::Dashboard::UnknownCardError (404)
refresh_for(card)The card's interval, else the dashboard's; nil for a card declared refresh: false
view_context / helpersThe Rails view context
current_user, current_scoped_entity, scoped_to_entity?, params, request, controller, current_engine, resource_url_for, authorized_resource_scope, allowed_to?, policy_for, registered_resources, root_pathDelegated to the view context

Class API ​

MethodMeaning
cardsAll declared cards, in order
find_card(key) / find_card!(key)Lookup by key
filters / find_filter(key)Declared filters
linksDeclared toolbar links
label, description, iconResolved presentation
i18n_key, route_nameadmin_portal/sales, sales

Subclasses inherit the parent's cards, filters, links and options by copy, so a portal-specific subclass can add cards without touching the parent.

Released under the MIT License.