paginate login history and improve user settings

This commit is contained in:
Graham McIntire 2026-02-01 09:08:48 -06:00
parent feed25080c
commit b08daf9a88
No known key found for this signature in database

View file

@ -19,7 +19,6 @@ defmodule ToweropsWeb.UserSettingsLive do
socket =
socket
|> assign(:page_title, "Account Settings")
|> assign(:active_tab, "personal")
|> assign(:current_token, current_token)
|> assign_changesets()
|> assign_profile_form()
@ -50,8 +49,20 @@ defmodule ToweropsWeb.UserSettingsLive do
end
@impl true
def handle_event("switch_tab", %{"tab" => tab}, socket) do
{:noreply, assign(socket, :active_tab, tab)}
def handle_params(params, _url, socket) do
# Default to "personal" tab if no tab param is provided
tab = Map.get(params, "tab", "personal")
# Get page from params, default to 1
page = params |> Map.get("page", "1") |> String.to_integer()
socket =
socket
|> assign(:active_tab, tab)
|> assign(:login_history_page, page)
|> assign_login_history()
{:noreply, socket}
end
@impl true
@ -302,18 +313,6 @@ defmodule ToweropsWeb.UserSettingsLive do
{:noreply, socket}
end
@impl true
def handle_event("load_more_history", _params, socket) do
next_page = socket.assigns.login_history_page + 1
socket =
socket
|> assign(:login_history_page, next_page)
|> assign_login_history()
{:noreply, socket}
end
# Security tab - TOTP Device Management
@impl true
@ -497,15 +496,24 @@ defmodule ToweropsWeb.UserSettingsLive do
defp assign_login_history(socket) do
user = socket.assigns.current_scope.user
page = socket.assigns[:login_history_page] || 1
limit = 20
offset = (page - 1) * limit
per_page = 5
offset = (page - 1) * per_page
history = Accounts.list_user_login_history(user.id, limit: limit, offset: offset)
has_more = length(history) == limit
# Get total count and history for current page
total_count = Accounts.count_user_login_attempts(user.id)
history = Accounts.list_user_login_history(user.id, limit: per_page, offset: offset)
total_pages = ceil(total_count / per_page)
page = max(1, min(page, max(1, total_pages)))
socket
|> assign(:login_history, history)
|> assign(:has_more_history, has_more)
|> assign(:login_history_pagination, %{
page: page,
per_page: per_page,
total_count: total_count,
total_pages: total_pages
})
end
defp assign_security_alerts(socket) do
@ -599,6 +607,28 @@ defmodule ToweropsWeb.UserSettingsLive do
end
end
# Generate pagination range with ellipsis for large page counts
# Shows: [1] ... [4] [5] [6] ... [20] when on page 5 of 20
defp pagination_range(current_page, total_pages) do
cond do
total_pages <= 7 ->
# Show all pages if 7 or fewer
Enum.to_list(1..total_pages)
current_page <= 4 ->
# Near the start
[1, 2, 3, 4, 5, :ellipsis, total_pages]
current_page >= total_pages - 3 ->
# Near the end
[1, :ellipsis, total_pages - 4, total_pages - 3, total_pages - 2, total_pages - 1, total_pages]
true ->
# In the middle
[1, :ellipsis, current_page - 1, current_page, current_page + 1, :ellipsis, total_pages]
end
end
@impl true
def render(assigns) do
~H"""
@ -622,10 +652,8 @@ defmodule ToweropsWeb.UserSettingsLive do
class="flex min-w-full flex-none gap-x-6 px-4 text-sm/6 font-semibold text-gray-500 sm:px-6 lg:px-8 dark:text-gray-400"
>
<li>
<a
href="#"
phx-click="switch_tab"
phx-value-tab="personal"
<.link
patch={~p"/users/settings?tab=personal"}
class={
if @active_tab == "personal",
do: "text-indigo-600 dark:text-indigo-400",
@ -633,13 +661,11 @@ defmodule ToweropsWeb.UserSettingsLive do
}
>
Personal
</a>
</.link>
</li>
<li>
<a
href="#"
phx-click="switch_tab"
phx-value-tab="account"
<.link
patch={~p"/users/settings?tab=account"}
class={
if @active_tab == "account",
do: "text-indigo-600 dark:text-indigo-400",
@ -647,13 +673,11 @@ defmodule ToweropsWeb.UserSettingsLive do
}
>
Account
</a>
</.link>
</li>
<li>
<a
href="#"
phx-click="switch_tab"
phx-value-tab="sessions"
<.link
patch={~p"/users/settings?tab=sessions"}
class={
if @active_tab == "sessions",
do: "text-indigo-600 dark:text-indigo-400",
@ -661,13 +685,11 @@ defmodule ToweropsWeb.UserSettingsLive do
}
>
Sessions
</a>
</.link>
</li>
<li>
<a
href="#"
phx-click="switch_tab"
phx-value-tab="api"
<.link
patch={~p"/users/settings?tab=api"}
class={
if @active_tab == "api",
do: "text-indigo-600 dark:text-indigo-400",
@ -675,13 +697,11 @@ defmodule ToweropsWeb.UserSettingsLive do
}
>
API
</a>
</.link>
</li>
<li>
<a
href="#"
phx-click="switch_tab"
phx-value-tab="notifications"
<.link
patch={~p"/users/settings?tab=notifications"}
class={
if @active_tab == "notifications",
do: "text-indigo-600 dark:text-indigo-400",
@ -689,13 +709,11 @@ defmodule ToweropsWeb.UserSettingsLive do
}
>
Notifications
</a>
</.link>
</li>
<li>
<a
href="#"
phx-click="switch_tab"
phx-value-tab="security"
<.link
patch={~p"/users/settings?tab=security"}
class={
if @active_tab == "security",
do: "text-indigo-600 dark:text-indigo-400",
@ -703,7 +721,7 @@ defmodule ToweropsWeb.UserSettingsLive do
}
>
Security
</a>
</.link>
</li>
<li>
<.link
@ -1389,15 +1407,104 @@ defmodule ToweropsWeb.UserSettingsLive do
</table>
</div>
<%= if @has_more_history do %>
<div class="mt-6 flex">
<button
type="button"
phx-click="load_more_history"
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring-1 inset-ring-gray-300 hover:bg-gray-100 dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/10 dark:hover:bg-white/20"
>
Load More
</button>
<%= if @login_history_pagination.total_pages > 1 do %>
<div class="mt-6 flex items-center justify-between border-t border-gray-200 dark:border-white/10 pt-6">
<!-- Mobile: Previous/Next only -->
<div class="flex flex-1 justify-between sm:hidden">
<.link
:if={@login_history_pagination.page > 1}
patch={
~p"/users/settings?tab=#{@active_tab}&page=#{@login_history_pagination.page - 1}"
}
class="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
Previous
</.link>
<.link
:if={@login_history_pagination.page < @login_history_pagination.total_pages}
patch={
~p"/users/settings?tab=#{@active_tab}&page=#{@login_history_pagination.page + 1}"
}
class="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
Next
</.link>
</div>
<!-- Desktop: Full pagination with page numbers -->
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700 dark:text-gray-400">
Showing
<span class="font-medium">
{(@login_history_pagination.page - 1) * @login_history_pagination.per_page +
1}
</span>
to
<span class="font-medium">
{min(
@login_history_pagination.page * @login_history_pagination.per_page,
@login_history_pagination.total_count
)}
</span>
of <span class="font-medium">{@login_history_pagination.total_count}</span>
results
</p>
</div>
<div>
<nav
class="isolate inline-flex -space-x-px rounded-md shadow-sm"
aria-label="Pagination"
>
<!-- Previous button -->
<.link
:if={@login_history_pagination.page > 1}
patch={
~p"/users/settings?tab=#{@active_tab}&page=#{@login_history_pagination.page - 1}"
}
class="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:ring-white/10 dark:hover:bg-gray-800"
>
<span class="sr-only">Previous</span>
<.icon name="hero-chevron-left" class="h-5 w-5" />
</.link>
<!-- Page numbers with ellipsis -->
<%= for page_num <- pagination_range(@login_history_pagination.page, @login_history_pagination.total_pages) do %>
<%= if page_num == :ellipsis do %>
<span class="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-700 ring-1 ring-inset ring-gray-300 dark:text-gray-400 dark:ring-white/10">
...
</span>
<% else %>
<.link
patch={~p"/users/settings?tab=#{@active_tab}&page=#{page_num}"}
class={[
"relative inline-flex items-center px-4 py-2 text-sm font-semibold ring-1 ring-inset ring-gray-300 dark:ring-white/10",
if @login_history_pagination.page == page_num do
"z-10 bg-blue-600 text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
else
"text-gray-900 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
end
]}
>
{page_num}
</.link>
<% end %>
<% end %>
<!-- Next button -->
<.link
:if={
@login_history_pagination.page < @login_history_pagination.total_pages
}
patch={
~p"/users/settings?tab=#{@active_tab}&page=#{@login_history_pagination.page + 1}"
}
class="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:ring-white/10 dark:hover:bg-gray-800"
>
<span class="sr-only">Next</span>
<.icon name="hero-chevron-right" class="h-5 w-5" />
</.link>
</nav>
</div>
</div>
</div>
<% end %>
<% end %>