defmodule LiveDebuggerWeb.Live.Nested.StateLive do @moduledoc """ This nested live view displays the state of a LiveView. """ use LiveDebuggerWeb, :live_view import LiveDebuggerWeb.Helpers.NestedLiveViewHelper alias Phoenix.LiveView.AsyncResult alias LiveDebuggerWeb.Components.ElixirDisplay alias LiveDebugger.Structs.TreeNode alias LiveDebugger.Services.ChannelService alias LiveDebugger.Utils.TermParser alias LiveDebugger.Utils.PubSub, as: PubSubUtils alias LiveDebuggerWeb.Helpers.StateHelper attr(:socket, :map, required: true) attr(:id, :string, required: true) attr(:lv_process, :map, required: true) attr(:params, :map, required: true) attr(:class, :string, default: "", doc: "CSS class for the container") def live_render(assigns) do session = %{ "lv_process" => assigns.lv_process, "params" => assigns.params, "parent_pid" => self() } assigns = assign(assigns, session: session) ~H""" <%= live_render(@socket, __MODULE__, id: @id, session: @session, container: {:div, class: @class} ) %> """ end @impl true def mount(_params, session, socket) do lv_process = session["lv_process"] parent_pid = session["parent_pid"] if connected?(socket) do parent_pid |> PubSubUtils.params_changed_topic() |> PubSubUtils.subscribe!() end socket |> assign(:lv_process, lv_process) |> assign_node_id(session) |> subscribe_to_state_changed() |> assign_async_node_with_type() |> ok() end @impl true def render(assigns) do ~H"""
<.async_result :let={node} assign={@node}> <:loading>
<.spinner size="sm" />
<:failed> <.alert class="w-full" variant="danger" with_icon heading="Error while fetching node state"> Check logs for more <.assigns_section assigns={node.assigns} /> <.fullscreen id="assigns-display-fullscreen" title="Assigns">
""" end @impl true def handle_info({:state_changed, channel_state, _trace}, socket) do socket |> assign_async_node_with_type(channel_state) |> noreply() end @impl true def handle_info({:params_changed, new_params}, socket) do socket |> unsubscribe_from_state_changed() |> assign_node_id(new_params) |> subscribe_to_state_changed() |> assign_async_node_with_type() |> noreply() end attr(:assigns, :list, required: true) defp assigns_section(assigns) do ~H""" <.section id="assigns" class="h-max overflow-y-hidden" title="Assigns"> <:right_panel>
<.copy_button id="assigns" variant="icon-button" value={TermParser.term_to_copy_string(@assigns)} /> <.fullscreen_button id="assigns-display-fullscreen" />
""" end defp assign_async_node_with_type(socket, channel_state \\ nil) defp assign_async_node_with_type( %{assigns: %{node_id: node_id, lv_process: %{pid: pid}}} = socket, channel_state ) when not is_nil(node_id) do assign_async(socket, [:node, :node_type], fn -> with {:ok, channel_state} <- StateHelper.maybe_get_state(pid, channel_state), {:ok, node} <- ChannelService.get_node(channel_state, node_id), true <- not is_nil(node) do {:ok, %{node: node, node_type: TreeNode.type(node)}} else false -> {:error, :node_deleted} err -> err end end) end defp assign_async_node_with_type(socket, _channel_state) do socket |> assign(:node, AsyncResult.failed(%AsyncResult{}, :no_node_id)) |> assign(:node_type, AsyncResult.failed(%AsyncResult{}, :no_node_id)) end defp subscribe_to_state_changed(socket) do if connected?(socket) do socket.assigns.lv_process.pid |> PubSubUtils.state_changed_topic(socket.assigns.node_id) |> PubSubUtils.subscribe!() end socket end defp unsubscribe_from_state_changed(socket) do socket.assigns.lv_process.pid |> PubSubUtils.state_changed_topic(socket.assigns.node_id) |> PubSubUtils.unsubscribe() socket end end