Skip to content

Commit 0d65edd

Browse files
committed
Extract channel request show page components into separate modules (#4541)
Split the 1158-line show.ex into four focused files to improve maintainability: helpers.ex (shared pure functions), components.ex (reusable display components), timing.ex (timing visualization), and show.ex (LiveView lifecycle + section composition).
1 parent 98dbb1d commit 0d65edd

4 files changed

Lines changed: 860 additions & 806 deletions

File tree

Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
defmodule LightningWeb.ChannelRequestLive.Components do
2+
@moduledoc """
3+
Reusable function components for the channel request detail page.
4+
5+
Provides layout primitives (disclosure sections), HTTP display atoms
6+
(method badges, status codes), and content viewers (headers, body)
7+
used across multiple sections.
8+
"""
9+
10+
use LightningWeb, :component
11+
12+
import LightningWeb.RunLive.Components, only: [channel_state_pill: 1]
13+
14+
alias LightningWeb.ChannelRequestLive.Helpers
15+
alias Phoenix.LiveView.JS
16+
17+
# --- Layout primitives ---
18+
19+
def disclosure_section(assigns) do
20+
assigns =
21+
assigns
22+
|> assign_new(:title_right, fn -> [] end)
23+
|> assign_new(:padded, fn -> true end)
24+
25+
~H"""
26+
<div class="bg-white rounded-lg shadow-sm border border-secondary-200">
27+
<button
28+
type="button"
29+
class="w-full flex items-center justify-between p-4 text-left cursor-pointer"
30+
phx-click={
31+
JS.toggle(to: "##{@id}-content")
32+
|> JS.toggle_class("rotate-180", to: "##{@id}-chevron")
33+
}
34+
>
35+
<div class="flex items-center gap-3">
36+
<h3 class="text-sm font-semibold text-secondary-900">{@title}</h3>
37+
{render_slot(@title_right)}
38+
</div>
39+
<.icon
40+
id={"#{@id}-chevron"}
41+
name="hero-chevron-down-mini"
42+
class={[
43+
"h-5 w-5 text-secondary-400 transition-transform",
44+
unless(@open, do: "rotate-180")
45+
]}
46+
/>
47+
</button>
48+
<div
49+
id={"#{@id}-content"}
50+
class={[
51+
if(@padded, do: "px-4 pb-4"),
52+
unless(@open, do: "hidden")
53+
]}
54+
>
55+
{render_slot(@inner_block)}
56+
</div>
57+
</div>
58+
"""
59+
end
60+
61+
def sub_section(assigns) do
62+
assigns = assign_new(assigns, :title_right, fn -> [] end)
63+
64+
~H"""
65+
<div class="border-t border-secondary-100">
66+
<button
67+
type="button"
68+
class="w-full px-4 py-2.5 flex items-center justify-between cursor-pointer"
69+
phx-click={
70+
JS.toggle(to: "##{@id}-content")
71+
|> JS.toggle_class("rotate-180", to: "##{@id}-chevron")
72+
}
73+
>
74+
<div class="flex items-center gap-2">
75+
<span class="text-xs font-medium text-secondary-500 uppercase tracking-wider">
76+
{@title}
77+
</span>
78+
{render_slot(@title_right)}
79+
</div>
80+
<.icon
81+
id={"#{@id}-chevron"}
82+
name="hero-chevron-down-mini"
83+
class={[
84+
"h-4 w-4 text-secondary-400 transition-transform",
85+
unless(@open, do: "rotate-180")
86+
]}
87+
/>
88+
</button>
89+
<div id={"#{@id}-content"} class={["px-4 pb-3", unless(@open, do: "hidden")]}>
90+
{render_slot(@inner_block)}
91+
</div>
92+
</div>
93+
"""
94+
end
95+
96+
# --- HTTP display atoms ---
97+
98+
def method_badge(assigns) do
99+
color_class =
100+
case assigns.method do
101+
"GET" -> "bg-blue-100 text-blue-800"
102+
"POST" -> "bg-green-100 text-green-800"
103+
"PUT" -> "bg-amber-100 text-amber-800"
104+
"PATCH" -> "bg-amber-100 text-amber-800"
105+
"DELETE" -> "bg-red-100 text-red-800"
106+
_ -> "bg-secondary-100 text-secondary-800"
107+
end
108+
109+
assigns = assign(assigns, color_class: color_class)
110+
111+
~H"""
112+
<span
113+
id="method-badge"
114+
class={[
115+
"inline-flex items-center px-2.5 py-0.5 rounded text-sm font-bold font-mono uppercase",
116+
@color_class
117+
]}
118+
>
119+
{@method || "—"}
120+
</span>
121+
"""
122+
end
123+
124+
def request_path_display(assigns) do
125+
~H"""
126+
<span class="font-mono text-sm break-all">
127+
<span class="text-secondary-900">{@event && @event.request_path}</span>
128+
<span
129+
:if={
130+
@event && @event.request_query_string && @event.request_query_string != ""
131+
}
132+
class="text-secondary-400"
133+
>
134+
?{@event.request_query_string}
135+
</span>
136+
</span>
137+
"""
138+
end
139+
140+
def status_code_display(assigns) do
141+
color_class =
142+
case assigns.status do
143+
s when is_integer(s) and s >= 200 and s < 300 ->
144+
"text-green-700 bg-green-50"
145+
146+
s when is_integer(s) and s >= 300 and s < 400 ->
147+
"text-blue-700 bg-blue-50"
148+
149+
s when is_integer(s) and s >= 400 and s < 500 ->
150+
"text-amber-700 bg-amber-50"
151+
152+
s when is_integer(s) and s >= 500 ->
153+
"text-red-700 bg-red-50"
154+
155+
_ ->
156+
"text-secondary-400"
157+
end
158+
159+
assigns = assign(assigns, color_class: color_class)
160+
161+
~H"""
162+
<span class={["font-mono text-sm font-bold px-1.5 py-0.5 rounded", @color_class]}>
163+
{if @status, do: to_string(@status), else: "—"}
164+
</span>
165+
"""
166+
end
167+
168+
def status_code_badge(assigns) do
169+
color_class =
170+
case assigns.status do
171+
s when s >= 200 and s < 300 -> "bg-green-100 text-green-700"
172+
s when s >= 300 and s < 400 -> "bg-blue-100 text-blue-700"
173+
s when s >= 400 and s < 500 -> "bg-amber-100 text-amber-700"
174+
s when s >= 500 -> "bg-red-100 text-red-700"
175+
_ -> "bg-secondary-100 text-secondary-700"
176+
end
177+
178+
assigns = assign(assigns, color_class: color_class)
179+
180+
~H"""
181+
<span class={[
182+
"inline-flex items-center rounded px-1.5 py-0.5 text-xs font-mono font-bold",
183+
@color_class
184+
]}>
185+
{@status}
186+
</span>
187+
"""
188+
end
189+
190+
def state_pill_with_tooltip(assigns) do
191+
~H"""
192+
<%= if @state == :timeout and @error_message do %>
193+
<Common.wrapper_tooltip
194+
id="state-pill-tooltip"
195+
tooltip={Helpers.humanize_error(@error_message)}
196+
>
197+
<.channel_state_pill state={@state} />
198+
</Common.wrapper_tooltip>
199+
<% else %>
200+
<.channel_state_pill state={@state} />
201+
<% end %>
202+
"""
203+
end
204+
205+
def response_empty(assigns) do
206+
{icon, label} =
207+
case assigns.type do
208+
:transport ->
209+
{"hero-exclamation-triangle", "No response received"}
210+
211+
:credential ->
212+
{"hero-lock-closed", "Request not sent — credential error"}
213+
end
214+
215+
assigns = assign(assigns, icon: icon, label: label)
216+
217+
~H"""
218+
<div class="border-t border-secondary-100">
219+
<div class="flex flex-col items-center justify-center px-4 py-8 text-secondary-500">
220+
<.icon name={@icon} class="h-8 w-8 mb-3 text-secondary-400" />
221+
<p class="font-medium mb-1">{@label}</p>
222+
<p class="text-sm mb-2">{@human_message}</p>
223+
<code class="text-xs font-mono bg-secondary-100 px-2 py-1 rounded">
224+
{@error_code}
225+
</code>
226+
</div>
227+
</div>
228+
"""
229+
end
230+
231+
# --- Content display ---
232+
233+
def headers_table(assigns) do
234+
~H"""
235+
<table class="w-full text-xs">
236+
<tbody class="divide-y divide-secondary-50">
237+
<tr :for={[name, value] <- @headers}>
238+
<td class="py-1.5 pr-3 text-secondary-500 font-medium whitespace-nowrap align-top w-1/3">
239+
{name}
240+
</td>
241+
<td class={[
242+
"py-1.5 font-mono break-all",
243+
if(value == "[REDACTED]",
244+
do: "italic text-secondary-400",
245+
else: "text-secondary-700"
246+
)
247+
]}>
248+
{value}
249+
</td>
250+
</tr>
251+
</tbody>
252+
</table>
253+
"""
254+
end
255+
256+
def body_viewer(assigns) do
257+
content_type = Helpers.extract_content_type(assigns.headers)
258+
is_binary_content = content_type && !Helpers.text_content_type?(content_type)
259+
260+
no_body =
261+
assigns.body_size == 0 and
262+
(is_nil(assigns.body_preview) or assigns.body_preview == "")
263+
264+
assigns =
265+
assign(assigns,
266+
content_type: content_type,
267+
is_binary_content: is_binary_content,
268+
no_body: no_body
269+
)
270+
271+
~H"""
272+
<%= cond do %>
273+
<% @no_body -> %>
274+
<div id={@id} class="py-6 flex flex-col items-center text-secondary-400">
275+
<.icon name="hero-document" class="h-6 w-6 mb-1 text-secondary-300" />
276+
<span class="text-xs">No body</span>
277+
</div>
278+
<% @is_binary_content -> %>
279+
<div id={@id} class="py-4 text-sm text-secondary-500">
280+
<span class="text-xs font-mono bg-secondary-100 px-1.5 py-0.5 rounded">
281+
{Helpers.format_content_type_label(@content_type)}
282+
</span>
283+
<span :if={@body_size} class="ml-2">
284+
{Helpers.format_bytes(@body_size)}
285+
</span>
286+
<span :if={@body_hash} class="ml-2 font-mono text-xs text-secondary-400">
287+
SHA256: {@body_hash}
288+
</span>
289+
</div>
290+
<% is_nil(@body_preview) -> %>
291+
<div id={@id} class="py-4 text-sm text-secondary-500">
292+
Body not captured
293+
<span :if={@body_size} class="text-xs text-secondary-400 ml-1">
294+
({Helpers.format_bytes(@body_size)})
295+
</span>
296+
</div>
297+
<% true -> %>
298+
<div id={@id}>
299+
<div class="relative rounded-md bg-secondary-50 border border-secondary-200">
300+
<div class="absolute top-2 right-2 flex items-center gap-1.5">
301+
<span
302+
:if={@content_type}
303+
class="text-[10px] font-mono text-secondary-400 bg-white/80 rounded px-1.5 py-0.5"
304+
>
305+
{Helpers.format_content_type_label(@content_type)}
306+
</span>
307+
<.copy_icon_button
308+
id={"#{@id}-copy"}
309+
value={@body_preview}
310+
title="Copy body"
311+
size={3}
312+
class="p-1 bg-white/80 rounded"
313+
/>
314+
</div>
315+
<pre class="text-xs font-mono p-3 pr-20 max-h-80 overflow-auto text-secondary-700 whitespace-pre-wrap break-all">{@body_preview}</pre>
316+
</div>
317+
<div
318+
:if={@body_hash}
319+
class="mt-2 flex items-center gap-2 text-[11px] text-secondary-400"
320+
>
321+
<span class="font-mono">
322+
SHA256: {String.slice(@body_hash, 0..15)}...
323+
</span>
324+
<.copy_icon_button
325+
id={"#{@id}-hash-copy"}
326+
value={@body_hash}
327+
title="Copy hash"
328+
size={3}
329+
/>
330+
</div>
331+
<div
332+
:if={
333+
@body_size && @body_preview && @body_size > byte_size(@body_preview)
334+
}
335+
class="mt-1 text-[11px] text-secondary-400"
336+
>
337+
Preview: {Helpers.format_bytes(byte_size(@body_preview))} of {Helpers.format_bytes(
338+
@body_size
339+
)}
340+
</div>
341+
</div>
342+
<% end %>
343+
"""
344+
end
345+
346+
attr :id, :string, required: true
347+
attr :value, :string, required: true
348+
attr :title, :string, default: "Copy"
349+
attr :size, :integer, default: 4
350+
attr :class, :string, default: nil
351+
352+
def copy_icon_button(assigns) do
353+
~H"""
354+
<button
355+
id={@id}
356+
phx-hook="Copy"
357+
data-content={@value}
358+
class={[
359+
"copy-btn text-secondary-400 hover:text-secondary-600 transition-colors shrink-0 cursor-pointer",
360+
@class
361+
]}
362+
title={@title}
363+
>
364+
<.icon name="hero-clipboard" class={"h-#{@size} w-#{@size}"} />
365+
</button>
366+
"""
367+
end
368+
369+
def section_size_badge(assigns) do
370+
~H"""
371+
<span id={@id} class="text-xs text-secondary-400 font-mono">
372+
{Helpers.format_bytes(@size)}
373+
</span>
374+
"""
375+
end
376+
end

0 commit comments

Comments
 (0)