Service functions for managing dashboards in Lotus.
Provides CRUD operations for dashboards, cards, filters, and filter mappings, as well as execution functions for running all cards in a dashboard.
Dashboard Structure
Dashboards contain:
- Cards - Query results, text, links, or headings arranged in a 12-column grid
- Filters - User inputs that control query variables across multiple cards
- Filter Mappings - Connections between dashboard filters and card query variables
Execution
Use run_dashboard/2 to execute all query cards in a dashboard simultaneously.
Filter values are resolved and passed to each card's query variables via the
configured mappings.
Content changes
Every function that creates, updates or deletes a dashboard, a card, a filter
or a filter mapping takes opts with a :context, opaque caller data such as
the current user, and fires the :before_content_change and
:after_content_change middleware. So does reorder_dashboard_cards/3,
which fires an :update for each card it moves, and so do
enable_public_sharing/2 and disable_public_sharing/2, which fire
:enable_sharing and :disable_sharing. A plug that halts makes the function
return {:error, {:halted, reason}} and nothing is written.
A delete fires an event only for the record it names. Deleting a dashboard
also deletes its cards, its filters and their filter mappings, and deleting a
card or a filter also deletes its filter mappings, with no event for those.
Deleting a filter also sets depends_on_filter_id to nil on the filters
that depend on it, with no event for those either. See Lotus.Middleware.
Cascading filters
A filter can get its select options from a saved query, and that query can
use the value of another filter. See list_dashboard_filter_options/2.
Summary
Functions
Returns the query variables of a card for the given filter values.
Creates a new dashboard.
Creates a new card for a dashboard.
Creates a new filter for a dashboard.
Creates a filter mapping connecting a dashboard filter to a card's query variable.
Deletes a dashboard.
Deletes a card.
Deletes a filter.
Deletes a filter mapping.
Disables public sharing for a dashboard by removing its token.
Enables public sharing for a dashboard by generating a unique token.
Gets a single dashboard by ID.
Gets a single dashboard by ID.
Gets a dashboard by its public sharing token.
Gets a single card by ID.
Gets a single card by ID.
Gets a single filter by ID.
Gets a single filter by ID.
Lists all filter mappings for a card.
Lists all cards for a dashboard.
Lists the select options of a filter.
Lists all filters for a dashboard.
Lists all dashboards.
Lists dashboards with optional filtering.
Reorders cards in a dashboard.
Runs all query cards in a dashboard and returns their results.
Runs a single dashboard card and returns its result.
Updates a dashboard.
Updates a card.
Updates a filter.
Types
Functions
@spec card_variables( [Lotus.Storage.DashboardCardFilterMapping.t() | map()], [Lotus.Storage.DashboardFilter.t() | map()], %{required(String.t()) => term()}, keyword() ) :: %{required(String.t()) => term()}
Returns the query variables of a card for the given filter values.
run_dashboard/2 and run_dashboard_card/2 get the :vars of each card
from this function. A caller that runs cards with its own executor can use it
to give a card the same variables.
Arguments
mappings- The filter mappings of the card, for example fromlist_card_filter_mappings/1. Each mapping is aLotus.Storage.DashboardCardFilterMappingstruct or a map with the keys:filter_id,:variable_nameand:transform. The:filterassociation does not have to be loadedfilters- The filters that the mappings refer to, for example fromlist_dashboard_filters/1. Each filter is aLotus.Storage.DashboardFilterstruct or a map with the keys:id,:name,:filter_typeand:default_valuefilter_values- Map of filter names to their current values
Options
:today- The date that relative date tokens resolve against (default:Date.utc_today/0). Give the same date for every card of one run
Resolution
- For each filter, get the value from
filter_values, or else the filter'sdefault_value. A filter with no value gives no variable - Resolve a relative date token in the value for the
filter_typeof the filter. SeeLotus.Dashboards.DateTokenfor the rules andLotus.list_relative_date_tokens/0for the tokens - For each mapping, apply its transform to the value. The keys of the
transform are strings, as
Lotus.Storage.DashboardCardFilterMappingstores them:%{"type" => "date_range_start"}- keeps the part before the comma%{"type" => "date_range_end"}- keeps the part after the comma. A value with no comma does not changenilor any other transform - the value does not change
- Put the value under the
variable_nameof the mapping
A mapping whose filter is not in filters, or whose filter has no value,
gives no variable.
Examples
iex> filter = %DashboardFilter{id: 1, name: "period", filter_type: :date_range, default_value: "last_7_days"}
iex> mappings = [
...> %DashboardCardFilterMapping{filter_id: 1, variable_name: "start_date", transform: %{"type" => "date_range_start"}},
...> %DashboardCardFilterMapping{filter_id: 1, variable_name: "end_date", transform: %{"type" => "date_range_end"}}
...> ]
iex> card_variables(mappings, [filter], %{}, today: ~D[2026-09-14])
%{"start_date" => "2026-09-08", "end_date" => "2026-09-14"}
@spec create_dashboard(attrs(), keyword()) :: {:ok, Lotus.Storage.Dashboard.t()} | {:error, Ecto.Changeset.t() | Lotus.Middleware.halted()}
Creates a new dashboard.
Examples
iex> create_dashboard(%{name: "Sales Dashboard"})
{:ok, %Dashboard{}}
iex> create_dashboard(%{})
{:error, %Ecto.Changeset{}}
@spec create_dashboard_card(Lotus.Storage.Dashboard.t() | id(), attrs(), keyword()) :: {:ok, Lotus.Storage.DashboardCard.t()} | {:error, Ecto.Changeset.t() | Lotus.Middleware.halted()}
Creates a new card for a dashboard.
Examples
iex> create_dashboard_card(dashboard, %{
...> card_type: :query,
...> query_id: 123,
...> position: 0,
...> layout: %{x: 0, y: 0, w: 6, h: 4}
...> })
{:ok, %DashboardCard{}}
@spec create_dashboard_filter(Lotus.Storage.Dashboard.t() | id(), attrs(), keyword()) :: {:ok, Lotus.Storage.DashboardFilter.t()} | {:error, Ecto.Changeset.t() | Lotus.Middleware.halted()}
Creates a new filter for a dashboard.
Examples
iex> create_dashboard_filter(dashboard, %{
...> name: "date_range",
...> label: "Date Range",
...> filter_type: :date_range,
...> widget: :date_range_picker,
...> position: 0
...> })
{:ok, %DashboardFilter{}}
@spec create_filter_mapping( Lotus.Storage.DashboardCard.t() | id(), Lotus.Storage.DashboardFilter.t() | id(), String.t(), keyword() ) :: {:ok, Lotus.Storage.DashboardCardFilterMapping.t()} | {:error, Ecto.Changeset.t() | Lotus.Middleware.halted()}
Creates a filter mapping connecting a dashboard filter to a card's query variable.
Options
:transform- Optional transformation config for the filter value:context- Opaque caller data passed to the content change middleware
Examples
iex> create_filter_mapping(card, filter, "start_date")
{:ok, %DashboardCardFilterMapping{}}
iex> create_filter_mapping(card, filter, "end_date", transform: %{type: "date_range_end"})
{:ok, %DashboardCardFilterMapping{}}
@spec delete_dashboard(Lotus.Storage.Dashboard.t(), keyword()) :: {:ok, Lotus.Storage.Dashboard.t()} | {:error, Ecto.Changeset.t() | Lotus.Middleware.halted()}
Deletes a dashboard.
Also deletes all associated cards, filters, and filter mappings.
Examples
iex> delete_dashboard(dashboard)
{:ok, %Dashboard{}}
@spec delete_dashboard_card(Lotus.Storage.DashboardCard.t() | id(), keyword()) :: {:ok, Lotus.Storage.DashboardCard.t()} | {:error, Ecto.Changeset.t() | :not_found | Lotus.Middleware.halted()}
Deletes a card.
Also deletes all associated filter mappings.
@spec delete_dashboard_filter(Lotus.Storage.DashboardFilter.t() | id(), keyword()) :: {:ok, Lotus.Storage.DashboardFilter.t()} | {:error, Ecto.Changeset.t() | :not_found | Lotus.Middleware.halted()}
Deletes a filter.
Also deletes all associated filter mappings. The filters that depend on it
get depends_on_filter_id set to nil by the database, with no content
change event for them.
@spec delete_filter_mapping( Lotus.Storage.DashboardCardFilterMapping.t() | id(), keyword() ) :: {:ok, Lotus.Storage.DashboardCardFilterMapping.t()} | {:error, Ecto.Changeset.t() | :not_found | Lotus.Middleware.halted()}
Deletes a filter mapping.
@spec disable_public_sharing(Lotus.Storage.Dashboard.t(), keyword()) :: {:ok, Lotus.Storage.Dashboard.t()} | {:error, Ecto.Changeset.t() | Lotus.Middleware.halted()}
Disables public sharing for a dashboard by removing its token.
Examples
iex> disable_public_sharing(dashboard)
{:ok, %Dashboard{public_token: nil}}
@spec enable_public_sharing(Lotus.Storage.Dashboard.t(), keyword()) :: {:ok, Lotus.Storage.Dashboard.t()} | {:error, Ecto.Changeset.t() | Lotus.Middleware.halted()}
Enables public sharing for a dashboard by generating a unique token.
The token can be used to access the dashboard without authentication
via get_dashboard_by_token/1.
Examples
iex> enable_public_sharing(dashboard)
{:ok, %Dashboard{public_token: "abc123..."}}
@spec get_dashboard(id()) :: Lotus.Storage.Dashboard.t() | nil
Gets a single dashboard by ID.
Returns nil if the dashboard does not exist.
@spec get_dashboard!(id()) :: Lotus.Storage.Dashboard.t() | no_return()
Gets a single dashboard by ID.
Raises Ecto.NoResultsError if the dashboard does not exist.
@spec get_dashboard_by_token(String.t()) :: Lotus.Storage.Dashboard.t() | nil
Gets a dashboard by its public sharing token.
Returns nil if no dashboard has the given token.
@spec get_dashboard_card(id(), keyword()) :: Lotus.Storage.DashboardCard.t() | nil
Gets a single card by ID.
Returns nil if the card does not exist.
Options
:preload- A list of associations to preload
@spec get_dashboard_card!(id(), keyword()) :: Lotus.Storage.DashboardCard.t() | no_return()
Gets a single card by ID.
Raises Ecto.NoResultsError if the card does not exist.
Options
:preload- A list of associations to preload
@spec get_dashboard_filter(id()) :: Lotus.Storage.DashboardFilter.t() | nil
Gets a single filter by ID.
Returns nil if the filter does not exist.
@spec get_dashboard_filter!(id()) :: Lotus.Storage.DashboardFilter.t() | no_return()
Gets a single filter by ID.
Raises Ecto.NoResultsError if the filter does not exist.
@spec list_card_filter_mappings(Lotus.Storage.DashboardCard.t() | id()) :: [ Lotus.Storage.DashboardCardFilterMapping.t() ]
Lists all filter mappings for a card.
@spec list_dashboard_cards(Lotus.Storage.Dashboard.t() | id(), keyword()) :: [ Lotus.Storage.DashboardCard.t() ]
Lists all cards for a dashboard.
Returns cards ordered by position, then by id.
Options
:preload- A list of associations to preload (e.g.,[:query, :filter_mappings])
Examples
iex> list_dashboard_cards(dashboard)
[%DashboardCard{}, ...]
iex> list_dashboard_cards(dashboard_id, preload: [:query, :filter_mappings])
[%DashboardCard{query: %Query{}, filter_mappings: [...]}, ...]
@spec list_dashboard_filter_options( Lotus.Storage.DashboardFilter.t() | id(), keyword() ) :: {:ok, [%{value: term(), label: term()}]} | {:error, term()}
Lists the select options of a filter.
A filter with no source_query_id returns the options under "options" in
its config: a map with "value" and "label", or a bare value that is both.
A filter with a source_query_id runs that query and returns one option for
each row. The first column is the value and the second column is the label. A
query with one column uses that column for both.
When the filter has a depends_on_filter_id, the value of that other filter
goes to the source query as the variable named after the other filter's
name. The value comes from :filter_values, or else from the
default_value of the other filter, and a relative date token resolves
first. When the other filter has no value, the result is {:ok, []} and the
query does not run.
Options
:filter_values- Map of filter names to their current values- Every other option goes to
Lotus.run_query/2, for example:context,:scope,:cacheand:timeout
Examples
iex> list_dashboard_filter_options(city_filter, filter_values: %{"country" => "PT"})
{:ok, [%{value: "Lisbon", label: "Lisbon"}, %{value: "Porto", label: "Porto"}]}
iex> list_dashboard_filter_options(999_999)
{:error, :not_found}
@spec list_dashboard_filters(Lotus.Storage.Dashboard.t() | id()) :: [ Lotus.Storage.DashboardFilter.t() ]
Lists all filters for a dashboard.
Returns filters ordered by position, then by id.
@spec list_dashboards(keyword()) :: [Lotus.Storage.Dashboard.t()]
Lists all dashboards.
Returns dashboards ordered by name.
Options
:preload- A list of associations to preload (e.g.,[:cards])
Examples
iex> list_dashboards()
[%Dashboard{}, ...]
iex> list_dashboards(preload: [:cards])
[%Dashboard{cards: [%DashboardCard{}, ...]}, ...]
@spec list_dashboards_by(keyword()) :: [Lotus.Storage.Dashboard.t()]
Lists dashboards with optional filtering.
Options
:search- Search term to match against dashboard names (case insensitive):preload- A list of associations to preload (e.g.,[:cards])
Examples
iex> list_dashboards_by(search: "sales")
[%Dashboard{name: "Sales Overview"}, ...]
iex> list_dashboards_by(search: "sales", preload: [:cards])
[%Dashboard{name: "Sales Overview", cards: [...]}, ...]
@spec reorder_dashboard_cards(Lotus.Storage.Dashboard.t() | id(), [id()], keyword()) :: :ok | {:error, Ecto.Changeset.t() | Lotus.Middleware.halted() | term()}
Reorders cards in a dashboard.
Accepts a list of card IDs in the desired order. Each card's position will be updated to match its index in the list. IDs of cards that belong to another dashboard are ignored.
Each card whose position changes is an :update of :dashboard_card. Every
:before_content_change runs before any position is written, so a halt on
one card leaves every position as it was. The positions are written in one
transaction.
Options
:context- Opaque caller data passed to the content change middleware
Examples
iex> reorder_dashboard_cards(dashboard, [card3_id, card1_id, card2_id])
:ok
@spec run_dashboard(Lotus.Storage.Dashboard.t() | id(), keyword()) :: %{ required(id()) => {:ok, Lotus.Result.t()} | {:error, term()} }
Runs all query cards in a dashboard and returns their results.
Returns a map of card IDs to their results. By default, cards are executed in parallel for better performance.
Options
:filter_values- Map of filter names to their current values:parallel- Whether to run cards in parallel (default: true):timeout- Timeout per card in milliseconds (default: 30000)
Filter Resolution
Filter values become the query variables of each card as card_variables/4
describes. All cards of one run resolve tokens against the same day.
Examples
iex> run_dashboard(dashboard, filter_values: %{"date_range" => "2024-01-01"})
%{
1 => {:ok, %Lotus.Result{}},
2 => {:ok, %Lotus.Result{}},
3 => {:error, "Missing required variable: status"}
}
@spec run_dashboard_card(Lotus.Storage.DashboardCard.t() | id(), keyword()) :: {:ok, Lotus.Result.t()} | {:error, term()}
Runs a single dashboard card and returns its result.
The query variables of the card come from card_variables/4.
Options
:filter_values- Map of filter names to their current values:timeout- Query timeout in milliseconds
Examples
iex> run_dashboard_card(card, filter_values: %{"user_id" => "123"})
{:ok, %Lotus.Result{}}
@spec update_dashboard(Lotus.Storage.Dashboard.t(), attrs(), keyword()) :: {:ok, Lotus.Storage.Dashboard.t()} | {:error, Ecto.Changeset.t() | Lotus.Middleware.halted()}
Updates a dashboard.
Examples
iex> update_dashboard(dashboard, %{name: "New Name"})
{:ok, %Dashboard{}}
@spec update_dashboard_card(Lotus.Storage.DashboardCard.t(), attrs(), keyword()) :: {:ok, Lotus.Storage.DashboardCard.t()} | {:error, Ecto.Changeset.t() | Lotus.Middleware.halted()}
Updates a card.
Examples
iex> update_dashboard_card(card, %{title: "Revenue Chart"})
{:ok, %DashboardCard{}}
@spec update_dashboard_filter(Lotus.Storage.DashboardFilter.t(), attrs(), keyword()) :: {:ok, Lotus.Storage.DashboardFilter.t()} | {:error, Ecto.Changeset.t() | Lotus.Middleware.halted()}
Updates a filter.
Examples
iex> update_dashboard_filter(filter, %{label: "Select Period"})
{:ok, %DashboardFilter{}}