Skip to content

Commit e59a5ca

Browse files
authored
Improve web dashboard navigation (#141)
This commit contains three improvements to the web dashboard navigation: - Current filters are persisted when navigating between the error list and detail pages - In the error detail page there are new navigation links to the prev and next occurrence - Navigating back to the dashboard page now happens over the existing websocket connection instead of doing a full page reload
1 parent 4c9f309 commit e59a5ca

File tree

8 files changed

+170
-49
lines changed

8 files changed

+170
-49
lines changed

lib/error_tracker/web/components/core_components.ex

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ defmodule ErrorTracker.Web.CoreComponents do
131131
"""
132132
end
133133

134-
attr :name, :string, values: ~w[bell bell-slash]
134+
attr :name, :string, values: ~w[bell bell-slash arrow-left arrow-right]
135135

136136
def icon(assigns = %{name: "bell"}) do
137137
~H"""
@@ -167,4 +167,38 @@ defmodule ErrorTracker.Web.CoreComponents do
167167
</svg>
168168
"""
169169
end
170+
171+
def icon(assigns = %{name: "arrow-left"}) do
172+
~H"""
173+
<svg
174+
xmlns="http://www.w3.org/2000/svg"
175+
viewBox="0 0 16 16"
176+
fill="currentColor"
177+
class="!h-4 !w-4 inline-block"
178+
>
179+
<path
180+
fill-rule="evenodd"
181+
d="M14 8a.75.75 0 0 1-.75.75H4.56l3.22 3.22a.75.75 0 1 1-1.06 1.06l-4.5-4.5a.75.75 0 0 1 0-1.06l4.5-4.5a.75.75 0 0 1 1.06 1.06L4.56 7.25h8.69A.75.75 0 0 1 14 8Z"
182+
clip-rule="evenodd"
183+
/>
184+
</svg>
185+
"""
186+
end
187+
188+
def icon(assigns = %{name: "arrow-right"}) do
189+
~H"""
190+
<svg
191+
xmlns="http://www.w3.org/2000/svg"
192+
viewBox="0 0 16 16"
193+
fill="currentColor"
194+
class="!h-4 !w-4 inline-block"
195+
>
196+
<path
197+
fill-rule="evenodd"
198+
d="M2 8a.75.75 0 0 1 .75-.75h8.69L8.22 4.03a.75.75 0 0 1 1.06-1.06l4.5 4.5a.75.75 0 0 1 0 1.06l-4.5 4.5a.75.75 0 0 1-1.06-1.06l3.22-3.22H2.75A.75.75 0 0 1 2 8Z"
199+
clip-rule="evenodd"
200+
/>
201+
</svg>
202+
"""
203+
end
170204
end

lib/error_tracker/web/live/dashboard.ex

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,28 @@ defmodule ErrorTracker.Web.Live.Dashboard do
77

88
alias ErrorTracker.Error
99
alias ErrorTracker.Repo
10+
alias ErrorTracker.Web.Search
1011

1112
@per_page 10
1213

1314
@impl Phoenix.LiveView
1415
def handle_params(params, uri, socket) do
15-
{search, search_form} = search_terms(params)
16-
1716
path = struct(URI, uri |> URI.parse() |> Map.take([:path, :query]))
1817

1918
{:noreply,
2019
socket
21-
|> assign(path: path, search: search, page: 1, search_form: search_form)
20+
|> assign(
21+
path: path,
22+
search: Search.from_params(params),
23+
page: 1,
24+
search_form: Search.to_form(params)
25+
)
2226
|> paginate_errors()}
2327
end
2428

2529
@impl Phoenix.LiveView
2630
def handle_event("search", params, socket) do
27-
{search, _search_form} = search_terms(params["search"] || %{})
31+
search = Search.from_params(params["search"] || %{})
2832

2933
path_w_filters = %URI{socket.assigns.path | query: URI.encode_query(search)}
3034

@@ -109,15 +113,6 @@ defmodule ErrorTracker.Web.Live.Dashboard do
109113
)
110114
end
111115

112-
defp search_terms(params) do
113-
data = %{}
114-
types = %{reason: :string, source_line: :string, source_function: :string, status: :string}
115-
116-
changeset = Ecto.Changeset.cast({data, types}, params, Map.keys(types))
117-
118-
{Ecto.Changeset.apply_changes(changeset), to_form(changeset, as: :search)}
119-
end
120-
121116
defp filter(query, search) do
122117
Enum.reduce(search, query, &do_filter/2)
123118
end

lib/error_tracker/web/live/dashboard.html.heex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
class="border-b bg-gray-400/10 border-y border-gray-900 hover:bg-gray-800/60 last-of-type:border-b-0"
6464
>
6565
<td scope="row" class="px-4 py-4 font-medium text-white relative">
66-
<.link navigate={error_path(@socket, error)} class="absolute inset-1">
66+
<.link navigate={error_path(@socket, error, @search)} class="absolute inset-1">
6767
<span class="sr-only">(<%= sanitize_module(error.kind) %>) <%= error.reason %></span>
6868
</.link>
6969
<p class="whitespace-nowrap text-ellipsis overflow-hidden">

lib/error_tracker/web/live/show.ex

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,52 +7,55 @@ defmodule ErrorTracker.Web.Live.Show do
77
alias ErrorTracker.Error
88
alias ErrorTracker.Occurrence
99
alias ErrorTracker.Repo
10+
alias ErrorTracker.Web.Search
1011

1112
@occurrences_to_navigate 50
1213

1314
@impl Phoenix.LiveView
14-
def mount(%{"id" => id}, _session, socket) do
15+
def mount(params = %{"id" => id}, _session, socket) do
1516
error = Repo.get!(Error, id)
16-
{:ok, assign(socket, error: error, app: Application.fetch_env!(:error_tracker, :otp_app))}
17+
18+
{:ok,
19+
assign(socket,
20+
error: error,
21+
app: Application.fetch_env!(:error_tracker, :otp_app),
22+
search: Search.from_params(params)
23+
)}
1724
end
1825

1926
@impl Phoenix.LiveView
20-
def handle_params(%{"occurrence_id" => occurrence_id}, _uri, socket) do
27+
def handle_params(params, _uri, socket) do
2128
occurrence =
22-
socket.assigns.error
23-
|> Ecto.assoc(:occurrences)
24-
|> Repo.get!(occurrence_id)
25-
26-
socket =
27-
socket
28-
|> assign(:occurrence, occurrence)
29-
|> load_related_occurrences()
30-
31-
{:noreply, socket}
32-
end
33-
34-
def handle_params(_, _uri, socket) do
35-
[occurrence] =
36-
socket.assigns.error
37-
|> Ecto.assoc(:occurrences)
38-
|> order_by([o], desc: o.id)
39-
|> limit(1)
40-
|> Repo.all()
29+
if occurrence_id = params["occurrence_id"] do
30+
socket.assigns.error
31+
|> Ecto.assoc(:occurrences)
32+
|> Repo.get!(occurrence_id)
33+
else
34+
socket.assigns.error
35+
|> Ecto.assoc(:occurrences)
36+
|> order_by([o], desc: o.id)
37+
|> limit(1)
38+
|> Repo.one()
39+
end
4140

4241
socket =
4342
socket
44-
|> assign(:occurrence, occurrence)
43+
|> assign(occurrence: occurrence)
4544
|> load_related_occurrences()
4645

4746
{:noreply, socket}
4847
end
4948

5049
@impl Phoenix.LiveView
5150
def handle_event("occurrence_navigation", %{"occurrence_id" => id}, socket) do
52-
{:noreply,
53-
push_patch(socket,
54-
to: occurrence_path(socket, %Occurrence{error_id: socket.assigns.error.id, id: id})
55-
)}
51+
occurrence_path =
52+
occurrence_path(
53+
socket,
54+
%Occurrence{error_id: socket.assigns.error.id, id: id},
55+
socket.assigns.search
56+
)
57+
58+
{:noreply, push_patch(socket, to: occurrence_path)}
5659
end
5760

5861
@impl Phoenix.LiveView
@@ -123,9 +126,27 @@ defmodule ErrorTracker.Web.Live.Show do
123126
|> Ecto.assoc(:occurrences)
124127
|> Repo.aggregate(:count)
125128

129+
next_occurrence =
130+
base_query
131+
|> where([o], o.id > ^current_occurrence.id)
132+
|> order_by([o], asc: o.id)
133+
|> limit(1)
134+
|> select([:id, :error_id, :inserted_at])
135+
|> Repo.one()
136+
137+
prev_occurrence =
138+
base_query
139+
|> where([o], o.id < ^current_occurrence.id)
140+
|> order_by([o], desc: o.id)
141+
|> limit(1)
142+
|> select([:id, :error_id, :inserted_at])
143+
|> Repo.one()
144+
126145
socket
127146
|> assign(:occurrences, occurrences)
128147
|> assign(:total_occurrences, total_occurrences)
148+
|> assign(:next, next_occurrence)
149+
|> assign(:prev, prev_occurrence)
129150
end
130151

131152
defp related_occurrences(query, num_results) do

lib/error_tracker/web/live/show.html.heex

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<div class="my-6">
2-
<.button type="link" href={dashboard_path(@socket)}>« Back to the dashboard</.button>
2+
<.link navigate={dashboard_path(@socket, @search)}>
3+
<.icon name="arrow-left" /> Back to the dashboard
4+
</.link>
35
</div>
46

57
<div id="header">
@@ -97,6 +99,19 @@
9799
</option>
98100
</select>
99101
</form>
102+
103+
<nav class="grid grid-cols-2 gap-2 mt-2">
104+
<div class="text-left">
105+
<.link :if={@prev} patch={occurrence_path(@socket, @prev, @search)}>
106+
<.icon name="arrow-left" /> Prev
107+
</.link>
108+
</div>
109+
<div class="text-right">
110+
<.link :if={@next} patch={occurrence_path(@socket, @next, @search)}>
111+
Next <.icon name="arrow-right" />
112+
</.link>
113+
</div>
114+
</nav>
100115
</.section>
101116

102117
<.section title="Error kind">

lib/error_tracker/web/router/routes.ex

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,36 @@ defmodule ErrorTracker.Web.Router.Routes do
88
@doc """
99
Returns the dashboard path
1010
"""
11-
def dashboard_path(socket = %Socket{}) do
12-
socket.private[:dashboard_path]
11+
def dashboard_path(socket = %Socket{}, params \\ %{}) do
12+
socket
13+
|> dashboard_uri(params)
14+
|> URI.to_string()
1315
end
1416

1517
@doc """
1618
Returns the path to see the details of an error
1719
"""
18-
def error_path(socket = %Socket{}, %Error{id: id}) do
19-
dashboard_path(socket) <> "/#{id}"
20+
def error_path(socket = %Socket{}, %Error{id: id}, params \\ %{}) do
21+
socket
22+
|> dashboard_uri(params)
23+
|> URI.append_path("/#{id}")
24+
|> URI.to_string()
2025
end
2126

2227
@doc """
2328
Returns the path to see the details of an occurrence
2429
"""
25-
def occurrence_path(socket = %Socket{}, %Occurrence{id: id, error_id: error_id}) do
26-
dashboard_path(socket) <> "/#{error_id}/#{id}"
30+
def occurrence_path(socket = %Socket{}, %Occurrence{id: id, error_id: error_id}, params \\ %{}) do
31+
socket
32+
|> dashboard_uri(params)
33+
|> URI.append_path("/#{error_id}/#{id}")
34+
|> URI.to_string()
35+
end
36+
37+
defp dashboard_uri(socket = %Socket{}, params) do
38+
%URI{
39+
path: socket.private[:dashboard_path],
40+
query: if(Enum.any?(params), do: URI.encode_query(params))
41+
}
2742
end
2843
end

lib/error_tracker/web/search.ex

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
defmodule ErrorTracker.Web.Search do
2+
@moduledoc false
3+
4+
@types %{
5+
reason: :string,
6+
source_line: :string,
7+
source_function: :string,
8+
status: :string
9+
}
10+
11+
defp changeset(params) do
12+
Ecto.Changeset.cast({%{}, @types}, params, Map.keys(@types))
13+
end
14+
15+
@spec from_params(map()) :: %{atom() => String.t()}
16+
def from_params(params) do
17+
params |> changeset() |> Ecto.Changeset.apply_changes()
18+
end
19+
20+
@spec to_form(map()) :: Phoenix.HTML.Form.t()
21+
def to_form(params) do
22+
params |> changeset() |> Phoenix.Component.to_form(as: :search)
23+
end
24+
end

priv/static/app.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,10 @@ select {
766766
border-width: 0;
767767
}
768768

769+
.static {
770+
position: static;
771+
}
772+
769773
.absolute {
770774
position: absolute;
771775
}
@@ -841,6 +845,10 @@ select {
841845
margin-top: 1.5rem;
842846
}
843847

848+
.mt-2 {
849+
margin-top: 0.5rem;
850+
}
851+
844852
.block {
845853
display: block;
846854
}
@@ -873,6 +881,11 @@ select {
873881
display: none;
874882
}
875883

884+
.size-4 {
885+
width: 1rem;
886+
height: 1rem;
887+
}
888+
876889
.\!h-4 {
877890
height: 1rem !important;
878891
}
@@ -925,6 +938,10 @@ select {
925938
grid-template-columns: repeat(2, minmax(0, 1fr));
926939
}
927940

941+
.grid-cols-4 {
942+
grid-template-columns: repeat(4, minmax(0, 1fr));
943+
}
944+
928945
.flex-col {
929946
flex-direction: column;
930947
}

0 commit comments

Comments
 (0)