View Source ReflectOS.Kernel.LayoutManager behaviour (reflect_os_kernel v0.10.1)
Layout Managers are responsible for determining which layout should be rendered to the dashboard.
Overview
Layout Managers are specialized GenServers which control which layout should be shown on the dashboard at any given time. Layout Managers are a unique feature of ReflectOS, allowing developers to build managers which can change the layout in response to just about anything (e.g. a fixed schedule, weather conditions, etc). Using Layout Managers, users can ensure their ReflectOS dashhboard shows just what they need, when they need it.
Layouts Managers follow the same pattern of Scenic.Scene
, and utilize
a dedicated struct (see ReflectOS.Kernel.LayoutManager.State
) with an
assigns
property as their state. You can use the assigns
property to
hold any data in state that you need. Layout Managers are passed their
configuration as an init argument.
There are few requirements for LayoutManagers, which allows a high degree
of flexibility for building custom logic around which layout is displayed.
LayoutManagers
simple use the push_layout/2
function to notify the
ReflectOS system that a new layout should be displayed.
Like all extensions to ReflectOS, developers are also required to implement a set of callbacks which are used to allow run time configuration. If you plan on publishing your extensions for use by others in the community, you can use these callbacks to create a thoughtful and intuitive configuration experience.
Implementing a Layout Manager
Layouts are just modules which use ReflectOS.Kernel.LayoutManager
and implement it's behavior. The callbacks and other requirements for a Layout fall into two major categories:
- Configuration experience via the ReflectOS Console web ui.
- Managing the logic around which layout should be displayed
Configuration Experience
In order to drive the ReflectOS console UI, layout managers are required to contain an Ecto.Schema
which represents the available configuration options. This is typically done using the embedded_schema/1
macro, as they are not persisted via Ecto.Repo
.
Note that embedding schemas in your root schema (e.g. Ecto.Schema.embeds_many/4
) is not currently supported.
Additionally, layouts must implement the following callbacks which are used by the console:
Layout Display Logic
In order to determine which layout renders to the ReflectOS dashboard, modules are on required to implement a single callback:
Sections can also optionally implement the following callbacks:
See the documentation for each callback below for more details.
Example
Here is the Static
layout manager which ships with the pre-built
ReflectOS firmware. It simply allows the user the select a single
layout via the console UI and sets it as the active layout.
defmodule ReflectOS.Core.LayoutManagers.Static do
use ReflectOS.Kernel.LayoutManager
import Phoenix.Component, only: [sigil_H: 2]
alias ReflectOS.Kernel.Option
alias ReflectOS.Kernel.LayoutManager
alias ReflectOS.Kernel.LayoutManager.Definition
alias ReflectOS.Kernel.LayoutManager.State
alias ReflectOS.Kernel.Settings.LayoutStore
@impl true
def layout_manager_definition() do
%Definition{
name: "Static Layout",
icon: """
<svg class="text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7.757 12h8.486M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
</svg>
""",
description: fn assigns ->
~H"""
Allows selecting a single, static layout.
"""
end
}
end
embedded_schema do
field(:layout, :string)
end
@impl true
def changeset(%__MODULE__{} = layout_manager, params \ %{}) do
layout_manager
|> cast(params, [:layout])
|> validate_required([:layout])
end
@impl true
def layout_manager_options(),
do: [
%Option{
key: :layout,
label: "Layout",
config: %{
type: "select",
prompt: "--Select Layout--",
options:
LayoutStore.list()
|> Enum.map(fn %{name: name, id: id} ->
{name, id}
end)
}
}
]
@impl LayoutManager
def init_layout_manager(%State{} = state, %{layout: layout_id}) do
state =
state
|> push_layout(layout_id)
{:ok, state}
end
end
Summary
Callbacks
Used to cast and validate input from the user via the ReflectOS Console UI.
Optional callback for when users update a layout manager's configuration.
Callback invoked during initialization of the layout manager.
Provides the ReflectOS.Kernel.LayoutManager.Definition
struct for the layout.
Provides the list of options which can be configured through the ReflectOS console.
Functions
Convenience function to assign a list or map of values into a ReflectOS.Kernel.LayoutManager.State
struct.
Convenience function to assign a value into a ReflectOS.Kernel.LayoutManager.State
struct.
Convenience function to assign a list of new values into a ReflectOS.Kernel.LayoutManager.State
struct.
Convenience function to assign a new values into a ReflectOS.Kernel.LayoutManager.State
struct.
Convenience function to fetch an assigned value out of a ReflectOS.Kernel.LayoutManager.State
struct.
Convenience function to get an assigned value out of a ReflectOS.Kernel.LayoutManager.State
struct.
Pushes the layout with the provided id to the dashboard.
Types
Callbacks
@callback changeset(layout_manager :: any(), params :: %{required(binary()) => any()}) :: Ecto.Changeset.t()
Used to cast and validate input from the user via the ReflectOS Console UI.
Can be used like a standard Ecto.Changeset
. Here is the callback from the example given above:
@impl true
def changeset(%__MODULE__{} = layout_manager, params \ %{}) do
layout_manager
|> cast(params, [:layout])
|> validate_required([:layout])
end
@callback handle_config_update( scene :: ReflectOS.Kernel.LayoutManager.State.t(), config :: struct() ) :: ReflectOS.Kernel.LayoutManager.State.t()
Optional callback for when users update a layout manager's configuration.
The default behavior when the layout manager's configuration changes is to restart the layout manager process with the new configuration. This will likely work in most circumstances, but you can override this behavior if it is desirable (e.g. your layout manager has a lengthy start up time).
@callback init_layout_manager( state :: ReflectOS.Kernel.LayoutManager.State.t(), config :: term() ) :: {:ok, state} | {:ok, state, timeout() | :hibernate | {:continue, continue_arg :: term()}} | :ignore | {:stop, reason :: any()} when state: ReflectOS.Kernel.LayoutManager.State.t()
Callback invoked during initialization of the layout manager.
Wraps the GenServer.init/1
callback, and allows the same return values. The config
argument will be the populated Ecto.Schema
defined in your section module.
From the Static
example above:
@impl LayoutManager
def init_layout_manager(%State{} = state, %{layout: layout_id}) do
state =
state
|> push_layout(layout_id)
{:ok, state}
end
@callback layout_manager_definition() :: ReflectOS.Kernel.LayoutManager.Definition.t()
Provides the ReflectOS.Kernel.LayoutManager.Definition
struct for the layout.
This is used to show your layout manager in the Console UI. Here is the callback from the example above:
@impl true
def layout_manager_definition() do
%Definition{
name: "Static Layout",
icon: """
<svg class="text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7.757 12h8.486M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
</svg>
""",
description: fn assigns ->
~H"""
Allows selecting a single, static layout.
"""
end
}
end
Note that the icon
property is passed in as simple string, while the description
property takes a function which can be passed to Phoenix.LiveView
. This allows you to use html tags in your description. Additionally, note that the argument must be called assigns
.
See ReflectOS.Kernel.LayoutManager.Definition
for more details.
@callback layout_manager_options() :: [ ReflectOS.Kernel.Option.t() | ReflectOS.Kernel.OptionGroup.t() ]
Provides the list of options which can be configured through the ReflectOS console.
From the example above:
@impl true
def layout_manager_options(),
do: [
%Option{
key: :layout,
label: "Layout",
config: %{
type: "select",
prompt: "--Select Layout--",
options:
LayoutStore.list()
|> Enum.map(fn %{name: name, id: id} ->
{name, id}
end)
}
}
]
Note that any properties in the config
map will be passed directly to the HTML input in the Console UI configuration form.
See ReflectOS.Kernel.Option
and ReflectOS.Kernel.OptionGroup
for more information.
Functions
@spec assign( state :: ReflectOS.Kernel.LayoutManager.State.t(), assigns :: Keyword.t() | map() ) :: ReflectOS.Kernel.LayoutManager.State.t()
Convenience function to assign a list or map of values into a ReflectOS.Kernel.LayoutManager.State
struct.
iex> state = %State{assigns: %{}}
iex> assign(state, some_field: "Some Field", another_field: "Another Field")
%State{assigns: %{some_field: "Some Field", another_field: "Another Field"}}
iex> state = %State{assigns: %{}}
iex> assign(state, %{some_field: "Some Field"})
%State{assigns: %{some_field: "Some Field"}}
@spec assign( state :: ReflectOS.Kernel.LayoutManager.State.t(), key :: any(), value :: any() ) :: ReflectOS.Kernel.LayoutManager.State.t()
Convenience function to assign a value into a ReflectOS.Kernel.LayoutManager.State
struct.
iex> state = %State{assigns: %{}}
iex> assign(state, :some_field, "Some Field")
%State{assigns: %{some_field: "Some Field"}}
@spec assign_new( state :: ReflectOS.Kernel.LayoutManager.State.t(), key_list :: Keyword.t() | map() ) :: ReflectOS.Kernel.LayoutManager.State.t()
Convenience function to assign a list of new values into a ReflectOS.Kernel.LayoutManager.State
struct.
Only values that do not already exist will be assigned.
iex> state = %State{assigns: %{existing_field: "Existing Field"}}
iex> assign_new(state, existing_field: "New Value", new_field: "New Field")
%State{assigns: %{existing_field: "Existing Field", new_field: "New Field"}}
iex> state = %State{assigns: %{existing_field: "Existing Field"}}
iex> assign_new(state, %{existing_field: "New Value", new_field: "New Field"})
%State{assigns: %{existing_field: "Existing Field", new_field: "New Field"}}
@spec assign_new( state :: ReflectOS.Kernel.LayoutManager.State.t(), key :: any(), value :: any() ) :: ReflectOS.Kernel.LayoutManager.State.t()
Convenience function to assign a new values into a ReflectOS.Kernel.LayoutManager.State
struct.
The value will only be assigned if it does not already exist in the struct.
iex> state = %State{assigns: %{existing_field: "Existing Field"}}
iex> assign_new(state, :existing_field, "New Value")
%State{assigns: %{existing_field: "Existing Field"}}
iex> state = %State{assigns: %{}}
iex> assign_new(state, :new_field, "New Value")
%State{assigns: %{new_field: "New Value"}}
@spec fetch(state :: ReflectOS.Kernel.LayoutManager.State.t(), key :: any()) :: {:ok, any()} | :error
Convenience function to fetch an assigned value out of a ReflectOS.Kernel.LayoutManager.State
struct.
iex> state = %State{assigns: %{some_field: "some value"}}
iex> fetch(state, :some_field)
{:ok, "some value"}
iex> state = %State{assigns: %{some_field: "some value"}}
iex> fetch(state, :invalid_key)
:error
@spec get( state :: ReflectOS.Kernel.LayoutManager.State.t(), key :: any(), default :: any() ) :: any()
Convenience function to get an assigned value out of a ReflectOS.Kernel.LayoutManager.State
struct.
iex> state = %State{assigns: %{some_field: "some value"}}
iex> get(state, :some_field)
"some value"
iex> state = %State{assigns: %{some_field: "some value"}}
iex> get(state, :invalid_key, "default value")
"default value"
@spec push_layout( layout_manager :: ReflectOS.Kernel.LayoutManager.State.t(), new_layout_id :: binary() ) :: ReflectOS.Kernel.LayoutManager.State.t()
Pushes the layout with the provided id to the dashboard.