Lotus.Dashboards (Lotus v1.1.0)

Copy Markdown View Source

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.

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.

Runs all query cards in a dashboard and returns their results.

Runs a single dashboard card and returns its result.

Types

attrs()

@type attrs() :: map()

id()

@type id() :: integer() | binary()

Functions

card_variables(mappings, filters, filter_values, opts \\ [])

@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

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

  1. For each filter, get the value from filter_values, or else the filter's default_value. A filter with no value gives no variable
  2. Resolve a relative date token in the value for the filter_type of the filter. See Lotus.Dashboards.DateToken for the rules and Lotus.list_relative_date_tokens/0 for the tokens
  3. For each mapping, apply its transform to the value. The keys of the transform are strings, as Lotus.Storage.DashboardCardFilterMapping stores 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 change
    • nil or any other transform - the value does not change
  4. Put the value under the variable_name of 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"}

create_dashboard(attrs, opts \\ [])

@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{}}

create_dashboard_card(dashboard_or_id, attrs, opts \\ [])

@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{}}

create_dashboard_filter(dashboard_or_id, attrs, opts \\ [])

@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{}}

create_filter_mapping(card, filter, variable_name, opts \\ [])

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{}}

delete_dashboard(dashboard, opts \\ [])

@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{}}

delete_dashboard_card(card_or_id, opts \\ [])

@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.

delete_dashboard_filter(filter_or_id, opts \\ [])

@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.

delete_filter_mapping(mapping_or_id, opts \\ [])

@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.

disable_public_sharing(dashboard, opts \\ [])

@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}}

enable_public_sharing(dashboard, opts \\ [])

@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..."}}

get_dashboard(id)

@spec get_dashboard(id()) :: Lotus.Storage.Dashboard.t() | nil

Gets a single dashboard by ID.

Returns nil if the dashboard does not exist.

get_dashboard!(id)

@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.

get_dashboard_by_token(token)

@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.

get_dashboard_card(id, opts \\ [])

@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

get_dashboard_card!(id, opts \\ [])

@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

get_dashboard_filter(id)

@spec get_dashboard_filter(id()) :: Lotus.Storage.DashboardFilter.t() | nil

Gets a single filter by ID.

Returns nil if the filter does not exist.

get_dashboard_filter!(id)

@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.

list_card_filter_mappings(card_id)

@spec list_card_filter_mappings(Lotus.Storage.DashboardCard.t() | id()) :: [
  Lotus.Storage.DashboardCardFilterMapping.t()
]

Lists all filter mappings for a card.

list_dashboard_cards(dashboard_or_id, opts \\ [])

@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: [...]}, ...]

list_dashboard_filter_options(filter_or_id, opts \\ [])

@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, :cache and :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}

list_dashboard_filters(dashboard_id)

@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.

list_dashboards(opts \\ [])

@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{}, ...]}, ...]

list_dashboards_by(opts \\ [])

@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: [...]}, ...]

reorder_dashboard_cards(dashboard_or_id, card_ids, opts \\ [])

@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

run_dashboard(dashboard, opts \\ [])

@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"}
}

run_dashboard_card(card, opts \\ [])

@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{}}

update_dashboard(dashboard, attrs, opts \\ [])

@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{}}

update_dashboard_card(card, attrs, opts \\ [])

@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{}}

update_dashboard_filter(filter, attrs, opts \\ [])

@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{}}