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
presents label: "Sales", description: "Orders and revenue", icon: Phlex::TablerIcons::ChartBar| Key | Default |
|---|---|
label | plutonium.dashboards.<key>.label, then the class name without Dashboard |
description | plutonium.dashboards.<key>.description, then nothing |
icon | Phlex::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.
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 lazytproc), 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_foruses (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.
filter :status, scopes: Post
metric(:posts, href: -> { filtered_url(Post) }) { filtered(Post).count }
chart(:by_author) { filtered(Post).group(:author_name).count }filtered(Post)isauthorized_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 adefault_scopeon the definition, it sends an explicit blank scope so the index shows everything.- Without a scope filter for the resource,
filteredis the authorized scope as is andfiltered_urlthe plain index. - A scope's block runs against the dashboard's controller.
current_user,current_scoped_entityand the portal's helpers work there; resource-controller helpers such ascurrent_parentdo 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.
link(key, href:, label: nil, icon: nil, condition: nil)
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.
link :settings, icon: Phlex::TablerIcons::Settings, href: -> { resource_url_for(Setting) },
condition: -> { allowed_to?(:update?, Setting) }Cards
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
| Option | Type | Default | Meaning |
|---|---|---|---|
label: | String, lazy t | convention, then key titleized | Title |
description: | String, lazy t | convention | Caption |
icon: | Phlex icon class | none | Shown beside the title |
span: | 1..12, :full | metric 3, chart 6, card 6 | Columns 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: | Boolean | true | true: 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 false | dashboard refresh | Reload 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, Symbol | none | Hides the card and 404s its endpoint when false |
href: | String, Proc | none | Links 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 key | Meaning |
|---|---|
value | The number or string. nil renders plutonium.dashboard.metric.empty |
previous | Computes 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 |
change | A 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_label | Caption after the change |
| Option | Values | Meaning |
|---|---|---|
format: | :number (default), :currency, :percentage, :human, Proc | How value is rendered. A proc receives the value and runs on the dashboard |
precision: | Integer | Decimal places; defaults per format (2 for currency, 1 for percentage, 3 significant for human) |
unit: | String | Currency symbol for :currency |
prefix: / suffix: | String | Wrapped around the formatted value |
positive: | :up (default), :down | Which direction is good; drives the change colour |
change_label: | String | Same as the hash key |
change_unit: | :percent (default), :points | How 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.
| Option | Values | Meaning |
|---|---|---|
type: | :line (default), :area, :column, :bar, :pie, :donut, :scatter | Chartkick chart class |
height: | CSS length | Drawing area height, default "240px" |
| anything else | Passed 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
| Method | Meaning |
|---|---|
authorize? | Override to gate the page and every card. Default true |
visible_cards | Cards 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_links | Toolbar 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 / helpers | The 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_path | Delegated to the view context |
Class API
| Method | Meaning |
|---|---|
cards | All declared cards, in order |
find_card(key) / find_card!(key) | Lookup by key |
filters / find_filter(key) | Declared filters |
links | Declared toolbar links |
label, description, icon | Resolved presentation |
i18n_key, route_name | admin_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.
