# `Lotus.Dashboards`
[🔗](https://github.com/elixir-lotus/lotus/blob/v1.1.0/lib/lotus/dashboards.ex#L1)

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

# `attrs`

```elixir
@type attrs() :: map()
```

# `id`

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

# `card_variables`

```elixir
@spec card_variables(
  [Lotus.Storage.DashboardCardFilterMapping.t() | map()],
  [Lotus.Storage.DashboardFilter.t() | map()],
  %{required(String.t()) =&gt; term()},
  keyword()
) :: %{required(String.t()) =&gt; 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 from
    `list_card_filter_mappings/1`. Each mapping is a
    `Lotus.Storage.DashboardCardFilterMapping` struct or a map with the keys
    `:filter_id`, `:variable_name` and `:transform`. The `:filter`
    association does not have to be loaded
  * `filters` - The filters that the mappings refer to, for example from
    `list_dashboard_filters/1`. Each filter is a
    `Lotus.Storage.DashboardFilter` struct or a map with the keys `:id`,
    `:name`, `:filter_type` and `:default_value`
  * `filter_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

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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

# `delete_dashboard`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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!`

```elixir
@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`

```elixir
@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`

```elixir
@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!`

```elixir
@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`

```elixir
@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!`

```elixir
@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`

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

Lists all filter mappings for a card.

# `list_dashboard_cards`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@spec run_dashboard(Lotus.Storage.Dashboard.t() | id(), keyword()) :: %{
  required(id()) =&gt; {: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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
