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
4 changes: 4 additions & 0 deletions assets/css/themes/dark.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
--button-secondary-content: var(--gray-200);
--button-secondary-content-hover: var(--gray-200);

/* Tooltip */
--tooltip-text: var(--gray-900);
--tooltip-bg: var(--swm-sea-blue-60);

/* Icons */
--accent-icon: var(--swm-sea-blue-60);

Expand Down
4 changes: 4 additions & 0 deletions assets/css/themes/light.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
--button-secondary-content: var(--swm-brand);
--button-secondary-content-hover: var(--swm-brand);

/* Tooltip */
--tooltip-text: var(--neutrals-white);
--tooltip-bg: var(--swm-brand);

/* Icons */
--accent-icon: var(--swm-brand);

Expand Down
50 changes: 39 additions & 11 deletions assets/js/hooks/tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,53 @@ const Tooltip = {
mounted() {
this.handleMouseEnter = () => {
tooltipEl.style.display = 'block';
tooltipEl.style.fontWeight =
this.el.dataset.variant === 'primary' ? '600' : '400';
tooltipEl.innerHTML = this.el.dataset.tooltip;

const tooltipRect = tooltipEl.getBoundingClientRect();
const rect = this.el.getBoundingClientRect();

const topOffset =
this.el.dataset.position == 'top'
? rect.top - tooltipRect.height
: rect.bottom;
// Reset any previous positioning
tooltipEl.style.top = '';
tooltipEl.style.left = '';
tooltipEl.style.right = '';
tooltipEl.style.bottom = '';

if (rect.left + tooltipRect.width > window.innerWidth) {
tooltipEl.style.right = `${window.innerWidth - rect.right}px`;
tooltipEl.style.left = 'auto';
} else {
tooltipEl.style.left = `${rect.left}px`;
tooltipEl.style.right = 'auto';
switch (this.el.dataset.position) {
case 'top':
tooltipEl.style.top = `${rect.top - tooltipRect.height}px`;
tooltipEl.style.left = `${rect.left}px`;
break;
case 'bottom':
tooltipEl.style.top = `${rect.bottom}px`;
tooltipEl.style.left = `${rect.left}px`;
break;
case 'left':
tooltipEl.style.left = `${rect.left - tooltipRect.width}px`;
tooltipEl.style.top = `${rect.top + (rect.height - tooltipRect.height) / 2}px`;
break;
case 'right':
tooltipEl.style.left = `${rect.right}px`;
tooltipEl.style.top = `${rect.top + (rect.height - tooltipRect.height) / 2}px`;
break;
}

// Handle horizontal overflow for top/bottom positions
if (['top', 'bottom'].includes(this.el.dataset.position)) {
if (rect.left + tooltipRect.width > window.innerWidth) {
tooltipEl.style.right = `${window.innerWidth - rect.right}px`;
tooltipEl.style.left = 'auto';
}
}

// Handle vertical overflow for left/right positions
if (['left', 'right'].includes(this.el.dataset.position)) {
if (rect.top + tooltipRect.height > window.innerHeight) {
tooltipEl.style.top = `${window.innerHeight - tooltipRect.height}px`;
}
}

tooltipEl.style.top = `${topOffset}px`;
tooltipEl.style.zIndex = 100;
};
this.handleMouseLeave = () => {
Expand Down
2 changes: 2 additions & 0 deletions assets/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ module.exports = {
'button-secondary-content': 'var(--button-secondary-content)',
'button-secondary-content-hover':
'var(--button-secondary-content-hover)',
'tooltip-text': 'var(--tooltip-text)',
'tooltip-bg': 'var(--tooltip-bg)',
'accent-icon': 'var(--accent-icon)',
'sidebar-bg': 'var(--sidebar-bg)',
'code-1': 'var(--code-1)',
Expand Down
4 changes: 2 additions & 2 deletions lib/live_debugger/gen_servers/callback_tracing_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ defmodule LiveDebugger.GenServers.CallbackTracingServer do
fun = trace.function

pid
|> PubSubUtils.trace_topic(node_id, fun, :call)
|> PubSubUtils.trace_topic_per_node(node_id, fun, :call)
|> PubSubUtils.broadcast({:new_trace, trace})
end

Expand All @@ -227,7 +227,7 @@ defmodule LiveDebugger.GenServers.CallbackTracingServer do
end

pid
|> PubSubUtils.trace_topic(node_id, fun, :return)
|> PubSubUtils.trace_topic_per_node(node_id, fun, :return)
|> PubSubUtils.broadcast({:updated_trace, trace})
end
end
13 changes: 11 additions & 2 deletions lib/live_debugger/utils/pubsub.ex
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,25 @@ defmodule LiveDebugger.Utils.PubSub do

Use `{:new_trace, trace}` or `{:updated_trace, trace}` for broadcasting.
"""
@spec trace_topic(
@spec trace_topic_per_node(
pid :: pid(),
node_id :: TreeNode.id(),
fun :: atom(),
type :: :call | :return
) :: String.t()
def trace_topic(pid, node_id, fun, type \\ :call) do
def trace_topic_per_node(pid, node_id, fun, type \\ :call) do
"#{inspect(pid)}/#{inspect(node_id)}/#{inspect(fun)}/#{inspect(type)}"
end

@spec trace_topic_per_pid(
pid :: pid(),
fun :: atom(),
type :: :call | :return
) :: String.t()
def trace_topic_per_pid(pid, fun, type \\ :call) do
"#{inspect(pid)}/#{inspect(fun)}/#{inspect(type)}"
end

@spec impl() :: module()
defp impl() do
Application.get_env(
Expand Down
14 changes: 14 additions & 0 deletions lib/live_debugger/utils/url.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ defmodule LiveDebugger.Utils.URL do
modify_query_params(url, &Map.drop(&1, keys))
end

@spec remove_query_params(url :: String.t()) :: String.t()
def remove_query_params(url) when is_binary(url) do
modify_query_params(url, fn _ -> %{} end)
end

@spec take_nth_segment(url :: String.t(), n :: integer()) :: String.t() | nil
def take_nth_segment(url, n) when is_binary(url) and is_integer(n) do
url
|> to_relative()
|> remove_query_params()
|> String.split("/")
|> Enum.at(n)
end
Comment thread
kraleppa marked this conversation as resolved.

@spec modify_query_params(url :: String.t(), fun :: (map() -> map())) :: String.t()
def modify_query_params(url, fun) when is_binary(url) and is_function(fun) do
uri = URI.parse(url)
Expand Down
16 changes: 14 additions & 2 deletions lib/live_debugger_web/components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,8 @@ defmodule LiveDebuggerWeb.Components do
"""
attr(:id, :string, required: true, doc: "ID of the tooltip. Prefix is added automatically.")
attr(:content, :string, default: nil)
attr(:position, :string, default: "top", values: ["top", "bottom"])
attr(:position, :string, default: "top", values: ["top", "bottom", "left", "right"])
attr(:variant, :string, default: "secondary", values: ["primary", "secondary"])
attr(:rest, :global)
slot(:inner_block, required: true)

Expand All @@ -559,6 +560,7 @@ defmodule LiveDebuggerWeb.Components do
phx-hook="Tooltip"
data-tooltip={@content}
data-position={@position}
data-variant={@variant}
{@rest}
>
<%= render_slot(@inner_block) %>
Expand Down Expand Up @@ -622,15 +624,25 @@ defmodule LiveDebuggerWeb.Components do
"""
attr(:icon, :string, required: true, doc: "Icon to be displayed.")
attr(:class, :any, default: nil, doc: "Additional classes to add to the nav icon.")
attr(:selected?, :boolean, default: false, doc: "Whether the icon is selected.")

attr(:rest, :global, include: ~w(id))

def nav_icon(assigns) do
selected_class =
if assigns.selected? do
"text-navbar-icon-hover bg-navbar-icon-bg-hover"
else
"text-navbar-icon hover:text-navbar-icon-hover hover:bg-navbar-icon-bg-hover"
end

assigns = assign(assigns, :selected_class, selected_class)

~H"""
<button
aria-label={Parsers.kebab_to_text(@icon)}
class={[
"w-8! h-8! px-[0.25rem] py-[0.25rem] w-max h-max rounded text-xs font-semibold text-navbar-icon hover:text-navbar-icon-hover hover:bg-navbar-icon-bg-hover"
"w-8! h-8! px-[0.25rem] py-[0.25rem] w-max h-max rounded text-xs font-semibold #{@selected_class}"
| List.wrap(@class)
]}
{@rest}
Expand Down
67 changes: 43 additions & 24 deletions lib/live_debugger_web/components/navbar.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ defmodule LiveDebuggerWeb.Components.Navbar do
use LiveDebuggerWeb, :component

alias LiveDebuggerWeb.Helpers.RoutesHelper
alias LiveDebugger.Utils.Parsers

@doc """
Renders base navbar component.
Expand Down Expand Up @@ -83,27 +84,22 @@ defmodule LiveDebuggerWeb.Components.Navbar do
When button is clicked, it will trigger a `find-successor` event with the PID of the LiveView.
"""
attr(:id, :string, required: true)
attr(:connected?, :boolean, required: true, doc: "Whether LiveView is connected.")
attr(:pid, :string, required: true, doc: "The PID of the LiveView.")
attr(:lv_process, :map, required: true, doc: "The LiveView process.")
attr(:rest, :global)

def connected(assigns) do
def connected(%{lv_process: %{ok?: true}} = assigns) do
connected? = assigns.lv_process.result.alive?
status = if(connected?, do: :connected, else: :disconnected)

assigns = assign(assigns, status: status, connected?: connected?)

~H"""
<.tooltip
id={@id}
position="bottom"
content={
if(@connected?,
do: "LiveView process is alive.",
else: "LiveView process is dead. You can still debug the last state."
)
}
>
<.tooltip id={@id} position="bottom" content={tooltip_content(@connected?)}>
<div id={@id} class="flex items-center gap-1 text-xs text-primary ml-1">
<.status_icon connected?={@connected?} />
<.status_icon status={@status} />
<%= if @connected? do %>
<span class="font-medium">Monitored PID </span>
<%= @pid %>
<%= Parsers.pid_to_string(@lv_process.result.pid) %>
<% else %>
<span class="font-medium">Disconnected</span>
<.button phx-click="find-successor" variant="secondary" size="sm">Continue</.button>
Expand All @@ -113,19 +109,42 @@ defmodule LiveDebuggerWeb.Components.Navbar do
"""
end

attr(:connected?, :boolean, required: true)
def connected(assigns) do
~H"""
<div id={@id} class="flex items-center gap-1 text-xs text-primary ml-1">
<.status_icon status={:loading} />
<span class="font-medium">Loading LiveView process...</span>
</div>
"""
end

attr(:status, :atom, required: true, values: [:connected, :disconnected, :loading])

defp status_icon(assigns) do
assigns =
case(assigns.status) do
:connected ->
assign(assigns, icon: "icon-check-small", class: "bg-[--swm-green-100]")

:disconnected ->
assign(assigns, icon: "icon-cross-small", class: "bg-[--swm-pink-100]")

:loading ->
assign(assigns, icon: nil, class: "bg-[--swm-yellow-100] animate-pulse")
end

~H"""
<div class={[
"w-4 h-4 rounded-full flex items-center justify-center",
if(@connected?, do: "bg-[--swm-green-100]", else: "bg-[--swm-pink-100]")
]}>
<.icon
name={if(@connected?, do: "icon-check-small", else: "icon-cross-small")}
class="bg-white w-4 h-4"
/>
<div class={["w-4 h-4 rounded-full flex items-center justify-center", @class]}>
<.icon :if={@icon} name={@icon} class="bg-white w-4 h-4" />
</div>
"""
end

defp tooltip_content(true) do
"LiveView process is alive"
end

defp tooltip_content(false) do
"LiveView process is dead - you can still debug the last state"
end
end
Loading