Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
defmodule LiveDebuggerRefactor.App.Debugger.ComponentsTree.Web.Components do
@moduledoc """
UI components used in the Components Tree
"""

use LiveDebuggerRefactor.App.Web, :component

alias LiveDebuggerRefactor.App.Debugger.TreeNode
alias LiveDebuggerRefactor.App.Utils.Parsers

@doc """
Renders a TreeNode component with its children recursively.

## Examples

<.ComponentsTree.Web.Components.tree_node
id="tree-id"
tree_node={@tree}
selected_node_id={@selected_node_id}
max_opened_node_level={2}
/>

"""
attr(:id, :string, required: true)
attr(:tree_node, TreeNode, required: true, doc: "TreeNode struct")
attr(:selected_node_id, :string, required: true)

attr(:max_opened_node_level, :integer,
required: true,
doc: "Maximum level of nesting to be opened by default."
)

# These attributes are used only for the recursive calls
attr(:root?, :boolean, default: true, doc: "Indicates if the node is a root node.")

attr(:level, :integer,
default: 0,
doc: "The level of the node in the tree. Used for indentation and recursive rendering."
)

def tree_node(assigns) do
parsed_node_id = TreeNode.parse_id(assigns.tree_node)

assigns =
assigns
|> assign(:parsed_node_id, parsed_node_id)
|> assign(:label_id, "tree-node-#{parsed_node_id}-#{assigns.id}")
|> assign(:collapsible?, length(assigns.tree_node.children) > 0)
|> assign(:selected?, assigns.tree_node.id == assigns.selected_node_id)
|> assign(:open, assigns.level < assigns.max_opened_node_level)

~H"""
<.collapsible
:if={@collapsible?}
id={"collapsible-#{@id}-#{@parsed_node_id}"}
chevron_class="text-accent-icon h-5 w-5"
open={@open}
label_class="rounded-md py-1 hover:bg-surface-1-bg-hover"
style={style_for_padding(@level, @collapsible?)}
>
<:label>
<.label
id={@label_id}
tree_node={@tree_node}
parsed_node_id={@parsed_node_id}
selected?={@selected?}
level={@level}
collapsible?={true}
/>
</:label>
<div class="flex flex-col">
<.tree_node
:for={child <- @tree_node.children}
id={@id}
tree_node={child}
selected_node_id={@selected_node_id}
root?={false}
max_opened_node_level={@max_opened_node_level}
level={@level + 1}
/>
</div>
</.collapsible>
<.label
:if={not @collapsible?}
id={@label_id}
tree_node={@tree_node}
parsed_node_id={@parsed_node_id}
selected?={@selected?}
level={@level}
collapsible?={false}
/>
"""
end

attr(:id, :string, required: true)
attr(:tree_node, TreeNode, required: true)
attr(:parsed_node_id, :string, required: true)
attr(:level, :integer, required: true)
attr(:collapsible?, :boolean, required: true)
attr(:selected?, :boolean, required: true)

defp label(assigns) do
assigns =
assigns
|> assign(:padding_style, style_for_padding(assigns.level, assigns.collapsible?))
|> assign(:button_id, "button-#{assigns.id}")
|> assign(:icon, node_icon(assigns.tree_node))
|> assign(:tooltip_content, node_tooltip(assigns.tree_node))
|> assign(:label, node_label(assigns.tree_node))

~H"""
<span
id={@id}
phx-hook="Highlight"
phx-value-search-attribute={@tree_node.dom_id.attribute}
phx-value-search-value={@tree_node.dom_id.value}
class={[
"flex shrink grow items-center rounded-md hover:bg-surface-1-bg-hover",
if(!@collapsible?, do: "p-1")
]}
style={if(!@collapsible?, do: @padding_style)}
>
<button
id={@button_id}
phx-click="select_node"
phx-value-node-id={@parsed_node_id}
phx-value-search-attribute={@tree_node.dom_id.attribute}
phx-value-search-value={@tree_node.dom_id.value}
class="flex min-w-0 gap-1 items-center"
>
<.icon name={@icon} class="text-accent-icon w-4 h-4 shrink-0" />
<.tooltip id={"tooltip-#{@id}"} content={@tooltip_content} class="truncate">
<span class={["hover:underline", if(@selected?, do: "font-semibold")]}>
<%= @label %>
</span>
</.tooltip>
</button>
</span>
"""
end

defp style_for_padding(level, collapsible?) do
padding = (level + 1) * 0.5 + if(collapsible?, do: 0, else: 1.5)

"padding-left: #{padding}rem;"
end

defp node_icon(%TreeNode{type: :live_view}), do: "icon-liveview"
defp node_icon(%TreeNode{type: :live_component}), do: "icon-component"

defp node_tooltip(%TreeNode{type: :live_view} = node) do
Parsers.module_to_string(node.module)
end

defp node_tooltip(%TreeNode{type: :live_component} = node) do
"#{Parsers.module_to_string(node.module)} (#{Parsers.cid_to_string(node.id)})"
end

defp node_label(%TreeNode{type: :live_view} = node) do
Parsers.module_to_string(node.module)
end

defp node_label(%TreeNode{type: :live_component} = node) do
"#{short_name(node.module)} (#{Parsers.cid_to_string(node.id)})"
end

defp short_name(module) when is_atom(module) do
module
|> Module.split()
|> List.last()
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
defmodule LiveDebuggerRefactor.App.Debugger.ComponentsTree.Web.ComponentsTreeLive do
@moduledoc """
Nested LiveView component that displays a tree of LiveView and LiveComponent nodes.
"""

use LiveDebuggerRefactor.App.Web, :live_view

alias Phoenix.LiveView.AsyncResult
alias LiveDebuggerRefactor.App.Debugger.ComponentsTree.Web.Components, as: TreeComponents

@doc """
Renders the ComponentsTreeLive as nested LiveView component.
"""
attr(:id, :string, required: true)
attr(:socket, Phoenix.LiveView.Socket, required: true)

def live_render(assigns) do
assigns = assign(assigns, :session, %{})

~H"""
<%= live_render(@socket, __MODULE__, id: @id, session: @session) %>
"""
end

@impl true
def mount(_params, _session, socket) do
# Dummy data
dummy_node = %LiveDebuggerRefactor.App.Debugger.TreeNode{
id: :c.pid(0, 123, 0),
type: :live_view,
module: LiveDebuggerRefactor.App.Settings.Web.SettingsLive,
dom_id: %{
attribute: "id",
value: "phx-somevalue"
},
assigns: %{
dummy_assign: :value
},
children: []
}

# Assigns with dummy data
socket =
socket
|> assign(:tree, AsyncResult.ok(dummy_node))
|> assign(:selected_node_id, dummy_node.id)
|> assign(:max_opened_node_level, 1)

socket
|> assign(:highlight?, false)
|> ok()
end

@impl true
def render(assigns) do
~H"""
<.async_result :let={tree} assign={@tree}>
<:loading>
<div class="w-full flex justify-center mt-5"><.spinner size="sm" /></div>
</:loading>
<:failed :let={_error}>
<.alert>Couldn't load a tree</.alert>
</:failed>
<div class="min-h-20 px-1 overflow-y-auto overflow-x-hidden flex flex-col">
<div class="flex items-center justify-between">
<div class="shrink-0 font-medium text-secondary-text px-6 py-3">Components Tree</div>
<.toggle_switch
:if={LiveDebuggerRefactor.Feature.enabled?(:highlighting)}
id="highlight-switch"
label="Highlight"
checked={@highlight?}
phx-click="toggle-highlight"
/>
</div>
<div class="flex-1">
<TreeComponents.tree_node
id="components-tree"
tree_node={tree}
selected_node_id={@selected_node_id}
max_opened_node_level={@max_opened_node_level}
/>
</div>
</div>
</.async_result>
"""
end

@impl true
def handle_event("toggle-highlight", _params, socket) do
socket
|> update(:highlight?, &(not &1))
|> noreply()
end

def handle_event("select_node", _, socket) do
socket
|> noreply()
end
end
43 changes: 43 additions & 0 deletions lib/live_debugger_refactor/app/debugger/tree_node.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
defmodule LiveDebuggerRefactor.App.Debugger.TreeNode do
@moduledoc """
Structs and functions to build and manipulate the tree of LiveView and LiveComponent nodes.
"""

alias LiveDebuggerRefactor.App.Utils.Parsers
alias LiveDebuggerRefactor.CommonTypes

defstruct [
:id,
:dom_id,
:type,
:module,
:assigns,
:children
]

@type id() :: pid() | CommonTypes.cid()
@type type() :: :live_view | :live_component
@type t() :: %__MODULE__{
id: id(),
dom_id: %{
attribute: String.t(),
value: String.t()
},
type: type(),
module: module(),
assigns: map(),
children: [t()]
}

@doc """
Uses Parsers to convert the `id` field to the appropriate string representation.
"""
@spec parse_id(t()) :: String.t()
def parse_id(%__MODULE__{type: :live_view, id: pid}) when is_pid(pid) do
Parsers.pid_to_string(pid)
end

def parse_id(%__MODULE__{type: :live_component, id: cid}) do
Parsers.cid_to_string(cid)
end
end
8 changes: 1 addition & 7 deletions lib/live_debugger_refactor/app/web/components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -552,13 +552,7 @@ defmodule LiveDebuggerRefactor.App.Web.Components do

def tooltip(assigns) do
~H"""
<div
id={"tooltip_" <> @id}
phx-hook="Tooltip"
data-tooltip={@content}
data-position={@position}
{@rest}
>
<div id={@id} phx-hook="Tooltip" data-tooltip={@content} data-position={@position} {@rest}>
<%= render_slot(@inner_block) %>
</div>
"""
Expand Down
1 change: 1 addition & 0 deletions lib/live_debugger_refactor/app/web/helpers/routes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule LiveDebuggerRefactor.App.Web.Helpers.Routes do
router: LiveDebuggerRefactor.App.Web.Router

alias LiveDebuggerRefactor.App.Utils.Parsers
alias LiveDebuggerRefactor.CommonTypes

@spec discovery() :: String.t()
def discovery() do
Expand Down
5 changes: 5 additions & 0 deletions lib/live_debugger_refactor/feature.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ defmodule LiveDebuggerRefactor.Feature do
Application.get_env(:live_debugger, :garbage_collection?, true)
end

def enabled?(:highlighting) do
Application.get_env(:live_debugger, :browser_features?, true) and
Application.get_env(:live_debugger, :highlighting?, true)
end

def enabled?(feature_name) do
raise "Feature #{feature_name} is not allowed"
end
Expand Down
24 changes: 24 additions & 0 deletions test/live_debugger_refactor/app/debugger/tree_node_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule LiveDebuggerRefactor.App.Debugger.TreeNodeTest do
use ExUnit.Case, async: true

alias LiveDebuggerRefactor.Fakes
alias LiveDebuggerRefactor.App.Debugger.TreeNode

describe "parse_id/1" do
test "parses id for LiveView" do
pid = :c.pid(0, 1, 0)

tree_node = Fakes.tree_node_live_view(id: pid)

assert "0.1.0" == TreeNode.parse_id(tree_node)
end

test "parses id for LiveComponent" do
cid = %Phoenix.LiveComponent.CID{cid: 2}

tree_node = Fakes.tree_node_live_component(id: cid)

assert "2" == TreeNode.parse_id(tree_node)
end
end
end
Loading