Skip to content

Commit 67e9df8

Browse files
authored
Enhancement: Add chevrons to sections in node inspector (#867)
* Change async_jobs and streams sections to collapsible sections * Save collapsible sections state in the browser * Add pulse animation on change inside collapsed section * Update e2e tests * update assets
1 parent 72d2e6f commit 67e9df8

File tree

13 files changed

+224
-22
lines changed

13 files changed

+224
-22
lines changed

assets/app/app.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import TraceLabelSearchHighlight from './hooks/trace_label_search_highlight';
1515
import AssignsBodySearchHighlight from './hooks/assigns_body_search_highlight';
1616
import DiffPulse from './hooks/diff_pulse';
1717
import ChartHook from './hooks/chart_hook';
18+
import CollapsedSectionPulse from './hooks/collapsed_section_pulse';
1819

1920
import topbar from './vendor/topbar';
2021

@@ -42,6 +43,7 @@ function createHooks() {
4243
AssignsBodySearchHighlight,
4344
DiffPulse,
4445
ChartHook,
46+
CollapsedSectionPulse,
4547
};
4648
}
4749

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const CollapsedSectionPulse = {
2+
mounted() {
3+
const collapsibleEl = this.el.closest('details');
4+
5+
this.handleEvent(`${this.el.id}-pulse`, () => {
6+
if (collapsibleEl.open) return;
7+
8+
collapsibleEl.classList.add('animate-section-pulse');
9+
10+
setTimeout(() => {
11+
collapsibleEl.classList.remove('animate-section-pulse');
12+
}, 500);
13+
});
14+
},
15+
};
16+
17+
export default CollapsedSectionPulse;

assets/app/hooks/collapsible.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,46 @@
11
function setOpen(el) {
2+
if (el.dataset.saveStateInBrowser === 'true') {
3+
setOpenFromLocalStorage(el);
4+
return;
5+
}
6+
27
if (el.dataset.open === 'true') {
38
el.open = true;
49
} else {
510
el.open = false;
611
}
712
}
813

14+
function setOpenFromLocalStorage(el) {
15+
const open_state = localStorage.getItem(`collapsible-open-${el.id}`);
16+
17+
if (open_state !== null) {
18+
el.open = open_state === 'true';
19+
return;
20+
}
21+
22+
if (el.dataset.open === 'true') {
23+
el.open = true;
24+
localStorage.setItem(`collapsible-open-${el.id}`, 'true');
25+
} else {
26+
el.open = false;
27+
localStorage.setItem(`collapsible-open-${el.id}`, 'false');
28+
}
29+
}
30+
31+
function maybeSaveStateOnChange(el) {
32+
if (el.dataset.saveStateInBrowser === 'true') {
33+
el.addEventListener('toggle', () => {
34+
localStorage.setItem(`collapsible-open-${el.id}`, el.open.toString());
35+
});
36+
37+
window.addEventListener('storage', ({ key }) => {
38+
if (key !== `collapsible-open-${el.id}`) return;
39+
setOpenFromLocalStorage(el);
40+
});
41+
}
42+
}
43+
944
function handleCollapsibleEvent(payload, el) {
1045
if (payload.action === 'toggle') {
1146
el.open = !el.open;
@@ -24,6 +59,8 @@ const Collapsible = {
2459
mounted() {
2560
setOpen(this.el);
2661

62+
maybeSaveStateOnChange(this.el);
63+
2764
this.handleEvent(`${this.el.id}-collapsible`, (payload) => {
2865
handleCollapsibleEvent(payload, this.el);
2966
});

assets/app/tailwind.config.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,22 @@ module.exports = {
114114
},
115115
'100%': { backgroundColor: '', color: '' },
116116
},
117+
sectionPulse: {
118+
'0%': {
119+
boxShadow: '0 0 10px var(--diff-pulse-bg)',
120+
},
121+
'100%': {
122+
boxShadow: '0 0 0px var(--diff-pulse-bg)',
123+
},
124+
},
117125
},
118126
animation: {
119127
'fade-out': 'fadeOut 200ms ease-out forwards',
120128
'fade-out-mobile': 'fadeOutMobile 200ms ease-out forwards',
121129
'fade-in': 'fadeIn 100ms ease-in forwards',
122130
'fade-in-mobile': 'fadeInMobile 100ms ease-in forwards',
123131
'diff-pulse': 'diffPulse 500ms ease-out',
132+
'section-pulse': 'sectionPulse 500ms ease-out',
124133
},
125134
},
126135
},

lib/live_debugger/app/debugger/async_jobs/web/async_jobs_live.ex

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ defmodule LiveDebugger.App.Debugger.AsyncJobs.Web.AsyncJobsLive do
1515

1616
alias LiveDebugger.App.Debugger.AsyncJobs.Queries, as: AsyncJobsQueries
1717

18+
@async_jobs_sectinon_id "async-jobs"
19+
1820
@doc """
1921
Renders the `AsyncJobsLive` as a nested LiveView component.
2022
@@ -72,9 +74,17 @@ defmodule LiveDebugger.App.Debugger.AsyncJobs.Web.AsyncJobsLive do
7274

7375
@impl true
7476
def render(assigns) do
77+
assigns = assign(assigns, id: @async_jobs_sectinon_id)
78+
7579
~H"""
7680
<div class="max-w-full flex flex-1">
77-
<.section title="Async jobs" id="async-jobs" inner_class="mx-0 p-4" class="flex-1">
81+
<.collapsible_section
82+
title="Async jobs"
83+
id={@id}
84+
inner_class="mx-0 p-4"
85+
class="flex-1"
86+
save_state_in_browser={true}
87+
>
7888
<div class="w-full h-full flex flex-col gap-2">
7989
<.async_result :let={async_jobs} assign={@async_jobs}>
8090
<:failed>
@@ -90,7 +100,7 @@ defmodule LiveDebugger.App.Debugger.AsyncJobs.Web.AsyncJobsLive do
90100
/>
91101
</.async_result>
92102
</div>
93-
</.section>
103+
</.collapsible_section>
94104
</div>
95105
"""
96106
end
@@ -113,6 +123,7 @@ defmodule LiveDebugger.App.Debugger.AsyncJobs.Web.AsyncJobsLive do
113123

114124
socket
115125
|> assign(:async_jobs, AsyncResult.ok(updated_async_jobs))
126+
|> push_event("#{@async_jobs_sectinon_id}-summary-pulse", %{})
116127
|> noreply()
117128
end
118129

@@ -137,6 +148,12 @@ defmodule LiveDebugger.App.Debugger.AsyncJobs.Web.AsyncJobsLive do
137148
def handle_info(_, socket), do: {:noreply, socket}
138149

139150
@impl true
151+
def handle_async(:fetch_async_jobs, {:ok, {:ok, []}}, socket) do
152+
socket
153+
|> assign(:async_jobs, AsyncResult.ok([]))
154+
|> noreply()
155+
end
156+
140157
def handle_async(:fetch_async_jobs, {:ok, {:ok, async_jobs}}, socket)
141158
when is_list(async_jobs) do
142159
Enum.each(async_jobs, fn async_job ->
@@ -145,6 +162,7 @@ defmodule LiveDebugger.App.Debugger.AsyncJobs.Web.AsyncJobsLive do
145162

146163
socket
147164
|> assign(:async_jobs, AsyncResult.ok(async_jobs))
165+
|> push_event("#{@async_jobs_sectinon_id}-summary-pulse", %{})
148166
|> noreply()
149167
end
150168

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

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,22 @@ defmodule LiveDebugger.App.Debugger.Streams.Web.Components do
2626
"""
2727
end
2828

29+
attr(:id, :string, required: true)
2930
slot(:display, required: true)
3031

3132
def streams_section(assigns) do
3233
~H"""
33-
<div id="streams_section-container">
34-
<.section id="streams" class="h-max overflow-y-hidden" title="Streams">
35-
<:right_panel>
36-
<.streams_info_tooltip id="stream-info" />
37-
</:right_panel>
38-
<%= render_slot(@display) %>
39-
</.section>
40-
</div>
34+
<.collapsible_section
35+
id={@id}
36+
class="h-max overflow-y-hidden"
37+
title="Streams"
38+
save_state_in_browser={true}
39+
>
40+
<:right_panel>
41+
<.streams_info_tooltip id="stream-info" />
42+
</:right_panel>
43+
<%= render_slot(@display) %>
44+
</.collapsible_section>
4145
"""
4246
end
4347

lib/live_debugger/app/debugger/streams/web/streams_live.ex

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ defmodule LiveDebugger.App.Debugger.Streams.Web.StreamsLive do
1414
alias LiveDebugger.Services.CallbackTracer.Events.StreamUpdated
1515
alias LiveDebugger.App.Debugger.Events.NodeIdParamChanged
1616

17+
@streams_section_id "streams-section-container"
18+
1719
@doc """
1820
Renders the `StreamsLive` as a nested LiveView component.
1921
@@ -67,9 +69,11 @@ defmodule LiveDebugger.App.Debugger.Streams.Web.StreamsLive do
6769

6870
@impl true
6971
def render(assigns) do
72+
assigns = assign(assigns, id: @streams_section_id)
73+
7074
~H"""
7175
<div class="flex-1 max-w-full flex flex-col gap-4">
72-
<StreamsComponents.streams_section>
76+
<StreamsComponents.streams_section id={@id}>
7377
<:display>
7478
<.async_result :let={stream_names} assign={@stream_names}>
7579
<:loading>
@@ -112,6 +116,7 @@ defmodule LiveDebugger.App.Debugger.Streams.Web.StreamsLive do
112116
) do
113117
socket
114118
|> Hooks.Streams.assign_async_streams(stream, dom_id_fun)
119+
|> push_event("#{@streams_section_id}-summary-pulse", %{})
115120
|> noreply()
116121
end
117122

lib/live_debugger/app/web/components.ex

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ defmodule LiveDebugger.App.Web.Components do
129129
attr(:chevron_class, :any, default: nil, doc: "CSS class for the chevron icon")
130130
attr(:open, :boolean, default: false, doc: "Whether the collapsible is open by default")
131131

132+
attr(:save_state_in_browser, :boolean,
133+
default: false,
134+
doc: "Whether to save the open/closed state in the browser's local storage"
135+
)
136+
132137
attr(:icon, :string,
133138
default: "icon-chevron-right",
134139
doc: "Icon for chevron. It will be rotated 90 degrees when the collapsible is open"
@@ -140,13 +145,17 @@ defmodule LiveDebugger.App.Web.Components do
140145
slot(:inner_block, required: true)
141146

142147
def collapsible(assigns) do
143-
assigns = assign(assigns, :open, to_string(assigns.open))
148+
assigns =
149+
assigns
150+
|> assign(:open, to_string(assigns.open))
151+
|> assign(:save_state_in_browser, to_string(assigns.save_state_in_browser))
144152

145153
~H"""
146154
<details
147155
id={@id}
148156
phx-hook="Collapsible"
149157
data-open={@open}
158+
data-save-state-in-browser={@save_state_in_browser}
150159
class={[
151160
"block"
152161
| List.wrap(@class)
@@ -327,6 +336,50 @@ defmodule LiveDebugger.App.Web.Components do
327336
"""
328337
end
329338

339+
attr(:id, :string, required: true)
340+
attr(:title, :string, required: true)
341+
342+
attr(:save_state_in_browser, :boolean,
343+
default: false,
344+
doc: "Whether to save the open/closed state in the browser's local storage"
345+
)
346+
347+
attr(:class, :any, default: nil)
348+
attr(:title_class, :any, default: nil)
349+
attr(:inner_class, :any, default: nil)
350+
351+
slot(:right_panel)
352+
slot(:inner_block)
353+
354+
def collapsible_section(assigns) do
355+
~H"""
356+
<.collapsible
357+
id={@id}
358+
phx-hook="CollapsedSectionPulse"
359+
class={[
360+
"w-full min-w-[20rem] flex flex-col shadow-custom rounded-sm bg-surface-0-bg border border-default-border"
361+
| List.wrap(@class)
362+
]}
363+
label_class="pr-4 flex items-center h-12 p-2 border-b border-default-border"
364+
save_state_in_browser={@save_state_in_browser}
365+
>
366+
<:label>
367+
<div class="ml-1 flex justify-between items-center w-full gap-2">
368+
<div class={["font-medium text-sm min-w-26" | List.wrap(@title_class)]}><%= @title %></div>
369+
<div class="w-max">
370+
<%= render_slot(@right_panel) %>
371+
</div>
372+
</div>
373+
</:label>
374+
<div class={[
375+
"flex flex-1 overflow-auto rounded-sm bg-surface-0-bg" | List.wrap(@inner_class)
376+
]}>
377+
<%= render_slot(@inner_block) %>
378+
</div>
379+
</.collapsible>
380+
"""
381+
end
382+
330383
@doc """
331384
Typography component to render headings.
332385
"""

priv/static/app.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

priv/static/app.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)