defmodule CodeNameRaven.Display do @moduledoc """ The display behaviour for a Raven integration. Handles the dashboard panel: rendering, layout hints, data preparation before the render cycle, and optional user interaction handling. ## Relationship to Monitor `CodeNameRaven.Monitor` defines the collection side — `collect/2`, `healthy?/1`, `metrics/1`, and the collection lifecycle. `CodeNameRaven.Display` defines the display side — what the panel looks like, how large it is, and how it responds to user interaction. The two can live in the same module or in separate modules: **Single-module (common case):** defmodule Integrations.Http do use CodeNameRaven.Monitor use CodeNameRaven.Display, size_hint: :small @impl CodeNameRaven.Monitor def collect(params, state), do: ... @impl CodeNameRaven.Monitor def healthy?(result), do: ... @impl CodeNameRaven.Display def render(assigns) do ~H"..." end end **Two-module (collection/display separation):** # Collection side — may run on any node, including headless collectors defmodule Integrations.Database do use CodeNameRaven.Monitor @impl true def collect(params, state), do: ... @impl true def healthy?(result), do: ... end # Display side — runs only on nodes with raven_web defmodule Integrations.Database.Panel do use CodeNameRaven.Display, size_hint: :large @impl true def render(assigns) do ~H"..." end end `CodeNameRaven.Monitor` never provides a display default of any kind — no `render/1`, no `category/0`, nothing. A module gets a working panel only by adding `use CodeNameRaven.Display` itself, whether bundled alongside Monitor in the same module or as a companion module. There is no implicit shim and no backwards-compatibility path between the two behaviours. ## Callbacks """ @current_display_api_version 2 @min_supported_display_api_version 1 @type size_hint :: :small | :medium | :large | :full @type category :: atom() # --------------------------------------------------------------------------- # Callbacks # --------------------------------------------------------------------------- @doc """ Returns the API version this display module was written against. Defaults to `2`. Used by the host application to verify compatibility with the currently running version of `raven_sdk_display`. """ @callback display_api_version() :: pos_integer() @doc """ Renders the integration's dashboard panel. Called by the LiveView dashboard. Receives the assigns map built by Raven and enriched by `prepare_assigns/1`. Compose from `CodeNameRaven.MonitorComponents` or write raw HEEx. The default (from `use CodeNameRaven.Display`) is `CodeNameRaven.MonitorComponents.default_render/1` — a generic status/ last-checked/error/result-table panel. Override this to give the integration a purpose-built one. """ @callback render(assigns :: map()) :: Phoenix.LiveView.Rendered.t() @doc """ Enriches the assigns map before `render/1` is called. Called once per dashboard tick, outside the render cycle. Use this to fetch data (DB queries, sample reads) that should not happen on every render pass. The returned map is passed directly to `render/1`. The default is the identity function. """ @callback prepare_assigns(assigns :: map()) :: map() @doc """ Returns the human-readable name for this integration. Shown in the monitor catalogue and as the default panel title. The default derives a name from the module name (e.g. `Integrations.Http` → `"Http"`). """ @callback display_name() :: String.t() @doc """ Returns the category this integration belongs to. Used to group integrations in the catalogue UI. Use a built-in category atom or define your own. The default is `:custom`. """ @callback category() :: category() @doc """ Declares the preferred panel size for the dashboard layout engine. The engine uses this as a hint, not a hard constraint. * `:small` — compact status indicator; one or two values * `:medium` — a handful of metrics; default * `:large` — multiple metric groups or a chart * `:full` — full dashboard width; complex or multi-section displays """ @callback size_hint() :: size_hint() @doc """ Returns a structured layout specification for the panel. Used by the layout engine for placement control beyond the coarse size hint. Return `nil` (the default) to let the engine derive placement from `size_hint/0` alone. Reserved for future layout engine extensions — the shape of this map is not yet stable. """ @callback layout() :: map() | nil @doc """ Handles a LiveView event routed from the dashboard to this panel. Called when the dashboard LiveView receives a `phx-click` or `phx-change` event scoped to this panel. Receives the event name, the event params, and the current socket. Return `{:noreply, socket}` to update state and re-render. The default is a no-op that leaves the socket unchanged. """ @callback handle_ui_event( event :: String.t(), params :: map(), socket :: Phoenix.LiveView.Socket.t() ) :: {:noreply, Phoenix.LiveView.Socket.t()} @doc """ Builds the display's initial subscriber state. Called once when a configured Display instance starts, before it begins receiving metric/status messages from the DataBus. The returned state is threaded through `display_on_metric/3`/`display_on_status/3` and is what those callbacks accumulate into over the instance's lifetime. The default returns an empty map. """ @callback display_init(params :: map()) :: {:ok, term()} | {:error, String.t()} @doc """ Handles a metric event from a watched monitor. Called only for monitors on this Display instance's watch-list — the runtime filters everything else before this callback is reached. `metric` is a `CodeNameRaven.DataBus.MonitorMetric` struct at runtime — not typed as such here since `raven_sdk` does not (yet) publish `CodeNameRaven.DataBus`. Return the updated subscriber state. The default leaves state unchanged. """ @callback display_on_metric(metric :: term(), params :: map(), state :: term()) :: {:ok, term()} @doc """ Handles a status event from a watched monitor. Same watch-list filtering as `display_on_metric/3`. `status` is a `CodeNameRaven.DataBus.MonitorStatus` struct at runtime, same caveat as `display_on_metric/3`. Return the updated subscriber state. The default leaves state unchanged. """ @callback display_on_status(status :: term(), params :: map(), state :: term()) :: {:ok, term()} @doc """ Declares which Monitor module(s) this display knows how to render. Used by the instance-configuration UI to filter which monitors can be added to a watch-list for this display — not consulted for any automatic, type-wide enablement. The default is an empty list. """ @callback compatible_monitors() :: [module()] @doc """ Returns a template map of params this display expects. Mirrors `CodeNameRaven.Monitor.params_template/0` — same purpose, one level up: a Monitor's params configure *what's collected*; a Display's params configure *how an instance presents it* (e.g. a chart's y-axis range), and are independent of the monitor's own params even when both live in the same 1:1 bundled instance. Named `display_params_*` rather than plain `params_*` because a module doing `use CodeNameRaven.Monitor` alongside `use CodeNameRaven.Display` (the common single-module pattern) would otherwise get a "conflicting behaviours" callback collision between the two — the same reason `display_init/1`/`display_on_metric/3` are prefixed instead of plain `init/1`/`on_metric/3`. Used by the admin UI to decide whether to show a params form for a Display instance. The default is `%{}` (no params). Override in any display that needs per-instance presentation configuration: def display_params_template, do: %{y_min: 0.0} """ @callback display_params_template() :: map() @doc """ Returns the NimbleOptions-style schema for validating/rendering this display's params. Mirrors `CodeNameRaven.Monitor.params_schema/0`. Defaults to `[]` (no params, fully backward compatible with every display predating this callback). """ @callback display_params_schema() :: keyword() @doc """ Returns true if the given module implements the Display behaviour. Mirrors `CodeNameRaven.Monitor.monitor_module?/1` — checks for `render/1`, the one truly-required Display callback, the same way Monitor checks for `collect/2`/`healthy?/1`. """ @spec display_module?(module()) :: boolean() def display_module?(module) do Code.ensure_loaded?(module) and function_exported?(module, :render, 1) end @doc """ Returns true if the given module has actually opted into the Display.Server subscriber runtime (`display_init/1`, `on_metric/3`, `on_status/3`) — narrower than `display_module?/1`. The two checks usually agree — `use CodeNameRaven.Display` provides both `render/1` and `display_init/1` together. They can diverge for a module that hand-writes `render/1` itself without the macro (implementing the behaviour directly rather than `use`-ing it) — `display_module?/1` would see it as renderable, but it never got `display_init/1`. Use this check wherever a real `Display.Server` instance is about to be started for the module (e.g. bundled-default auto-fill, which needs `display_init/1` to exist or `Display.Server.init/1` crashes) — not the dashboard-card-rendering check `display_module?/1` is for. """ @spec display_server_module?(module()) :: boolean() def display_server_module?(module) do Code.ensure_loaded?(module) and function_exported?(module, :display_init, 1) end @doc """ Returns the current Display API version of the SDK. """ @spec current_display_api_version() :: pos_integer() def current_display_api_version, do: @current_display_api_version @doc """ Returns the minimum Display API version supported by the SDK. """ @spec min_supported_display_api_version() :: pos_integer() def min_supported_display_api_version, do: @min_supported_display_api_version @doc """ Returns true if the display module's declared `display_api_version/0` falls within the supported range of this SDK. """ @spec display_api_version_supported?(module()) :: boolean() def display_api_version_supported?(module) do version = if function_exported?(module, :display_api_version, 0) do module.display_api_version() else # Legacy displays predating version tracking 1 end version in @min_supported_display_api_version..@current_display_api_version end # --------------------------------------------------------------------------- # __using__ — sets up the behaviour and provides default implementations # --------------------------------------------------------------------------- @doc false defmacro __using__(opts \\ []) do size_hint = Keyword.get(opts, :size_hint, :medium) category = Keyword.get(opts, :category, :custom) quote do @behaviour CodeNameRaven.Display use Phoenix.Component @impl CodeNameRaven.Display def display_api_version, do: 2 @impl CodeNameRaven.Display def render(assigns) do CodeNameRaven.MonitorComponents.default_render(assigns) end @impl CodeNameRaven.Display def prepare_assigns(assigns), do: assigns @impl CodeNameRaven.Display def display_name do CodeNameRaven.Monitor.derive_display_name(__MODULE__) end @impl CodeNameRaven.Display def category, do: unquote(category) @impl CodeNameRaven.Display def size_hint, do: unquote(size_hint) @impl CodeNameRaven.Display def layout, do: nil @impl CodeNameRaven.Display def handle_ui_event(_event, _params, socket), do: {:noreply, socket} @impl CodeNameRaven.Display def display_init(_params), do: {:ok, %{}} @impl CodeNameRaven.Display def display_on_metric(_metric, _params, state), do: {:ok, state} @impl CodeNameRaven.Display def display_on_status(_status, _params, state), do: {:ok, state} @impl CodeNameRaven.Display def compatible_monitors, do: [] @impl CodeNameRaven.Display def display_params_template, do: %{} @impl CodeNameRaven.Display def display_params_schema, do: [] defoverridable display_api_version: 0, render: 1, prepare_assigns: 1, display_name: 0, category: 0, size_hint: 0, layout: 0, handle_ui_event: 3, display_init: 1, display_on_metric: 3, display_params_template: 0, display_params_schema: 0, display_on_status: 3, compatible_monitors: 0 end end end