Skip to content

Commit fe1d3b5

Browse files
authored
Enhancement: Add pulse on assign change (#822)
* Add pulse animation * Clear pulse when rerendering due to handle_event * Nitpicks * Fix tests * Add tests * Add comment for hook
1 parent 56489c9 commit fe1d3b5

File tree

13 files changed

+293
-46
lines changed

13 files changed

+293
-46
lines changed

assets/app/app.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import CopyButton from './hooks/copy_button';
1313
import TraceBodySearchHighlight from './hooks/trace_body_search_highlight';
1414
import TraceLabelSearchHighlight from './hooks/trace_label_search_highlight';
1515
import AssignsBodySearchHighlight from './hooks/assigns_body_search_highlight';
16+
import DiffPulse from './hooks/diff_pulse';
1617
import ChartHook from './hooks/chart_hook';
1718

1819
import topbar from './vendor/topbar';
@@ -39,6 +40,7 @@ function createHooks() {
3940
TraceBodySearchHighlight,
4041
TraceLabelSearchHighlight,
4142
AssignsBodySearchHighlight,
43+
DiffPulse,
4244
ChartHook,
4345
};
4446
}

assets/app/hooks/diff_pulse.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// This hook is resonsible for adding a pulse animation to an element
2+
// whenever its updated and it has the data-pulse attribute set.
3+
// Somewhat convoluted logic aims to reset the animation if update is triggered
4+
// before previous animation is complete.
5+
const DiffPulse = {
6+
mounted() {
7+
if (this.el.hasAttribute('data-pulse')) {
8+
this.el.removeAttribute('data-pulse');
9+
10+
this.el.classList.add('animate-diff-pulse');
11+
12+
this.timeout = setTimeout(() => {
13+
this.el.classList.remove('animate-diff-pulse');
14+
}, 500);
15+
}
16+
},
17+
updated() {
18+
if (this.el.hasAttribute('data-pulse')) {
19+
clearTimeout(this.timeout);
20+
this.el.removeAttribute('data-pulse');
21+
this.el.classList.remove('animate-diff-pulse');
22+
23+
setTimeout(() => this.el.classList.add('animate-diff-pulse'));
24+
25+
this.timeout = setTimeout(() => {
26+
this.el.classList.remove('animate-diff-pulse');
27+
}, 500);
28+
}
29+
},
30+
destroyed() {
31+
clearTimeout(this.timeout);
32+
},
33+
};
34+
35+
export default DiffPulse;

assets/app/styles/themes/dark.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,5 +79,8 @@
7979
--search-highlight-text: var(--gray-900);
8080

8181
--diff-border: var(--swm-brand-additional);
82+
83+
--diff-pulse-bg: var(--swm-sea-blue-60);
84+
--diff-pulse-text: var(--gray-900);
8285
}
8386
}

assets/app/styles/themes/light.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,7 @@
7878
--search-highlight-text: var(--slate-900);
7979

8080
--diff-border: var(--swm-brand-additional);
81+
82+
--diff-pulse-bg: var(--swm-sea-blue-60);
83+
--diff-pulse-text: var(--slate-900);
8184
}

assets/app/tailwind.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,20 @@ module.exports = {
9595
'0%': { opacity: '0', transform: 'translateY(1rem)' },
9696
'100%': { opacity: '1', transform: 'translateY(0)' },
9797
},
98+
diffPulse: {
99+
'0%': {
100+
backgroundColor: 'var(--diff-pulse-bg)',
101+
color: 'var(--diff-pulse-text)',
102+
},
103+
'100%': { backgroundColor: '', color: '' },
104+
},
98105
},
99106
animation: {
100107
'fade-out': 'fadeOut 200ms ease-out forwards',
101108
'fade-out-mobile': 'fadeOutMobile 200ms ease-out forwards',
102109
'fade-in': 'fadeIn 100ms ease-in forwards',
103110
'fade-in-mobile': 'fadeInMobile 100ms ease-in forwards',
111+
'diff-pulse': 'diffPulse 500ms ease-out',
104112
},
105113
},
106114
},

lib/live_debugger/app/debugger/node_state/web/components.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ defmodule LiveDebugger.App.Debugger.NodeState.Web.Components do
7474
data-search_phrase={@assigns_search_phrase}
7575
>
7676
<.assigns_sizes_section assigns_sizes={@assigns_sizes} id="display-fullscreen-size-label" />
77-
<ElixirDisplay.static_term node={@term_node} />
77+
<ElixirDisplay.static_term id="fullscreen-" node={@term_node} />
7878
</div>
7979
</.fullscreen>
8080
</div>

lib/live_debugger/app/debugger/node_state/web/hooks/node_assigns.ex

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ defmodule LiveDebugger.App.Debugger.NodeState.Web.Hooks.NodeAssigns do
1010
alias LiveDebugger.App.Utils.TermDiffer
1111
alias LiveDebugger.App.Utils.TermDiffer.Diff
1212
alias LiveDebugger.App.Utils.TermParser
13+
alias LiveDebugger.App.Utils.TermNode
1314
alias LiveDebugger.Utils.Memory
1415

1516
@required_assigns [
@@ -27,9 +28,11 @@ defmodule LiveDebugger.App.Debugger.NodeState.Web.Hooks.NodeAssigns do
2728
socket
2829
|> check_assigns!(@required_assigns)
2930
|> attach_hook(:node_assigns, :handle_async, &handle_async/3)
31+
|> attach_hook(:node_assigns, :handle_event, &handle_event/3)
3032
|> register_hook(:node_assigns)
3133
|> assign(:node_assigns_info, AsyncResult.loading())
3234
|> assign(:assigns_sizes, AsyncResult.loading())
35+
|> put_private(:pulse_cleared?, true)
3336
|> assign_async_node_assigns()
3437
end
3538

@@ -67,6 +70,8 @@ defmodule LiveDebugger.App.Debugger.NodeState.Web.Hooks.NodeAssigns do
6770
|> assign(:node_assigns_info, node_assigns_info)
6871
|> assign(:assigns_sizes, assigns_sizes)
6972
|> start_async(:fetch_node_assigns, fn ->
73+
# Small sleep serves here as a debounce mechanism
74+
Process.sleep(100)
7075
NodeStateQueries.fetch_node_assigns(pid, node_id)
7176
end)
7277
end
@@ -76,16 +81,20 @@ defmodule LiveDebugger.App.Debugger.NodeState.Web.Hooks.NodeAssigns do
7681
|> assign(:node_assigns_info, AsyncResult.failed(%AsyncResult{}, :no_node_id))
7782
end
7883

84+
defp handle_event(_, _, socket) do
85+
socket
86+
|> maybe_clear_term_node_pulse()
87+
|> cont()
88+
end
89+
7990
defp handle_async(
8091
:fetch_node_assigns,
8192
{:ok, {:ok, node_assigns}},
82-
%{
83-
assigns: %{
84-
node_assigns_info: %AsyncResult{ok?: true, result: {old_assigns, old_term_node, _}}
85-
}
86-
} =
87-
socket
93+
%{assigns: %{node_assigns_info: %AsyncResult{ok?: true}}} = socket
8894
) do
95+
socket = maybe_clear_term_node_pulse(socket)
96+
%AsyncResult{result: {old_assigns, old_term_node, _}} = socket.assigns.node_assigns_info
97+
8998
node_assigns_info =
9099
case TermDiffer.diff(old_assigns, node_assigns) do
91100
%Diff{type: :equal} ->
@@ -95,15 +104,13 @@ defmodule LiveDebugger.App.Debugger.NodeState.Web.Hooks.NodeAssigns do
95104
copy_string = TermParser.term_to_copy_string(node_assigns)
96105

97106
case TermParser.update_by_diff(old_term_node, diff) do
98-
{:ok, term_node} ->
99-
AsyncResult.ok({node_assigns, term_node, copy_string})
100-
101-
{:error, reason} ->
102-
AsyncResult.failed(socket.assigns.node_assigns_info, reason)
107+
{:ok, term_node} -> AsyncResult.ok({node_assigns, term_node, copy_string})
108+
{:error, reason} -> AsyncResult.failed(socket.assigns.node_assigns_info, reason)
103109
end
104110
end
105111

106112
socket
113+
|> put_private(:pulse_cleared?, false)
107114
|> assign(:node_assigns_info, node_assigns_info)
108115
|> assign_size_async(node_assigns)
109116
|> halt()
@@ -168,4 +175,22 @@ defmodule LiveDebugger.App.Debugger.NodeState.Web.Hooks.NodeAssigns do
168175
defp assigns_serialized_size(assigns) do
169176
assigns |> Memory.serialized_term_size() |> Memory.bytes_to_pretty_string()
170177
end
178+
179+
defp maybe_clear_term_node_pulse(%{private: %{pulse_cleared?: true}} = socket) do
180+
socket
181+
end
182+
183+
defp maybe_clear_term_node_pulse(%{private: %{pulse_cleared?: false}} = socket) do
184+
case socket.assigns.node_assigns_info do
185+
%AsyncResult{ok?: true, result: {node_assigns, term_node, copy_string}} ->
186+
term_node = TermNode.set_pulse(term_node, false, recursive: true)
187+
188+
socket
189+
|> put_private(:pulse_cleared?, true)
190+
|> assign(:node_assigns_info, AsyncResult.ok({node_assigns, term_node, copy_string}))
191+
192+
_ ->
193+
socket
194+
end
195+
end
171196
end

lib/live_debugger/app/debugger/web/components/elixir_display.ex

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ defmodule LiveDebugger.App.Debugger.Web.Components.ElixirDisplay do
5959
"""
6060
end
6161

62+
attr(:id, :string, default: "")
6263
attr(:node, TermNode, required: true)
6364

6465
def static_term(assigns) do
@@ -78,36 +79,42 @@ defmodule LiveDebugger.App.Debugger.Web.Components.ElixirDisplay do
7879
>
7980
<:label :let={open}>
8081
<%= if open do %>
81-
<.text_items items={@node.expanded_before} />
82+
<.text_items id={@id <> @node.id <> "-expanded-before"} items={@node.expanded_before} />
8283
<% else %>
83-
<.text_items items={@node.content} />
84+
<.text_items id={@id <> @node.id <> "-content"} items={@node.content} />
8485
<% end %>
8586
</:label>
8687
<ol class="m-0 ml-[2ch] block list-none p-0">
8788
<li :for={{_, child} <- @node.children} class="flex flex-col">
88-
<.static_term node={child} />
89+
<.static_term id={@id} node={child} />
8990
</li>
9091
</ol>
9192
<div class="ml-[2ch]">
92-
<.text_items items={@node.expanded_after} />
93+
<.text_items id={@id <> @node.id <> "-expanded-after"} items={@node.expanded_after} />
9394
</div>
9495
</.static_collapsible>
9596
<% else %>
9697
<div class="ml-[2ch]">
97-
<.text_items items={@node.content} />
98+
<.text_items id={@id <> @node.id} items={@node.content} />
9899
</div>
99100
<% end %>
100101
</div>
101102
"""
102103
end
103104

105+
attr(:id, :string, default: nil)
104106
attr(:items, :list, required: true)
105107

106108
defp text_items(assigns) do
107109
~H"""
108110
<div class="flex">
109-
<%= for item <- @items do %>
110-
<span class={"#{text_item_color_class(item)}"}>
111+
<%= for {item, index} <- Enum.with_index(@items) do %>
112+
<span
113+
id={if(@id, do: @id <> "-#{index}")}
114+
phx-hook={if(@id, do: "DiffPulse")}
115+
data-pulse={item.pulse?}
116+
class={"#{text_item_color_class(item)}"}
117+
>
111118
<pre data-text_item="true"><%= item.text %></pre>
112119
</span>
113120
<% end %>

lib/live_debugger/app/utils/term_node.ex

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@ defmodule LiveDebugger.App.Utils.TermNode do
3232

3333
defmodule DisplayElement do
3434
@moduledoc false
35-
defstruct [:text, color: nil]
35+
defstruct [:text, color: nil, pulse?: false]
3636

3737
@type t :: %__MODULE__{
3838
text: String.t(),
39-
color: String.t() | nil
39+
color: String.t() | nil,
40+
pulse?: boolean()
4041
}
4142

4243
@spec blue(String.t()) :: t()
@@ -50,6 +51,11 @@ defmodule LiveDebugger.App.Utils.TermNode do
5051

5152
@spec green(String.t()) :: t()
5253
def green(text), do: %__MODULE__{text: text, color: "text-code-4"}
54+
55+
@spec set_pulse(t(), boolean()) :: t()
56+
def set_pulse(%__MODULE__{} = element, pulse?) do
57+
%__MODULE__{element | pulse?: pulse?}
58+
end
5359
end
5460

5561
@spec new(kind(), [DisplayElement.t()], Keyword.t()) :: t()
@@ -170,6 +176,28 @@ defmodule LiveDebugger.App.Utils.TermNode do
170176
end
171177
end
172178

179+
@spec set_pulse(t(), boolean(), recursive: boolean()) :: t()
180+
def set_pulse(%__MODULE__{} = term_node, pulse?, opts \\ []) do
181+
content = Enum.map(term_node.content, &DisplayElement.set_pulse(&1, pulse?))
182+
expanded_before = Enum.map(term_node.expanded_before, &DisplayElement.set_pulse(&1, pulse?))
183+
expanded_after = Enum.map(term_node.expanded_after, &DisplayElement.set_pulse(&1, pulse?))
184+
185+
children =
186+
if Keyword.get(opts, :recursive, true) do
187+
Enum.map(term_node.children, fn {key, child} -> {key, set_pulse(child, pulse?, opts)} end)
188+
else
189+
term_node.children
190+
end
191+
192+
%__MODULE__{
193+
term_node
194+
| content: content,
195+
expanded_before: expanded_before,
196+
expanded_after: expanded_after,
197+
children: children
198+
}
199+
end
200+
173201
defp open_first_element(%__MODULE__{} = term_node) do
174202
%__MODULE__{term_node | open?: true}
175203
end

0 commit comments

Comments
 (0)