Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule CodexPoolerWeb.Admin.AuditLogsComponents do
use CodexPoolerWeb, :html

alias CodexPoolerWeb.Admin.AuditLogsComponents.Filters
alias CodexPoolerWeb.Admin.LogPagination

import CodexPoolerWeb.Admin.AuditLogsComponents.Presentation,
only: [
Expand All @@ -23,6 +24,7 @@ defmodule CodexPoolerWeb.Admin.AuditLogsComponents do
defdelegate audit_log_filters(assigns), to: Filters

attr :audit_logs, :map, required: true
attr :current_params, :map, required: true
attr :datetime_preferences, :map, required: true

def audit_logs_table(assigns) do
Expand All @@ -31,6 +33,17 @@ defmodule CodexPoolerWeb.Admin.AuditLogsComponents do
id="admin-audit-logs"
class="min-w-0 rounded-box border border-base-300 bg-base-100 shadow-sm"
>
<LogPagination.controls
page={@audit_logs}
base_path="/admin/audit-logs"
current_params={@current_params}
id_prefix="admin-audit-logs-pagination-top"
range_id="admin-audit-logs-range-top"
range_role="audit-logs-range"
label="Audit logs"
placement={:top}
/>

<div class="hidden overflow-x-auto md:block">
<table class="table min-w-[58rem] font-sans">
<colgroup>
Expand Down Expand Up @@ -243,6 +256,17 @@ defmodule CodexPoolerWeb.Admin.AuditLogsComponents do
</caption>
</table>
</div>

<LogPagination.controls
page={@audit_logs}
base_path="/admin/audit-logs"
current_params={@current_params}
id_prefix="admin-audit-logs-pagination-bottom"
range_id="admin-audit-logs-range-bottom"
range_role="audit-logs-range"
label="Audit logs"
placement={:bottom}
/>
</div>
"""
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule CodexPoolerWeb.Admin.RequestLogsPresentation do

alias CodexPoolerWeb.Admin.BadgeComponents, as: AdminBadges
alias CodexPoolerWeb.Admin.Components, as: AdminComponents
alias CodexPoolerWeb.Admin.LogPagination
alias CodexPoolerWeb.Admin.RequestLogsPresentation.Usage

import CodexPoolerWeb.Admin.RequestLogsDisplay,
Expand Down Expand Up @@ -35,6 +36,7 @@ defmodule CodexPoolerWeb.Admin.RequestLogsPresentation do
]

attr :request_logs, :map, required: true
attr :current_params, :map, required: true
attr :datetime_preferences, :map, required: true

def request_logs_table(assigns) do
Expand All @@ -43,7 +45,8 @@ defmodule CodexPoolerWeb.Admin.RequestLogsPresentation do
id="admin-request-logs"
class={[
"min-w-0",
@request_logs.items != [] && "rounded-box border border-base-300 bg-base-100 shadow-sm"
(@request_logs.items != [] or @request_logs.total > 0) &&
"rounded-box border border-base-300 bg-base-100 shadow-sm"
]}
>
<AdminComponents.empty_state
Expand All @@ -54,6 +57,18 @@ defmodule CodexPoolerWeb.Admin.RequestLogsPresentation do
icon="hero-document-magnifying-glass"
/>

<LogPagination.controls
page={@request_logs}
base_path="/admin/request-logs"
current_params={@current_params}
id_prefix="admin-request-logs-pagination-top"
range_id="admin-request-logs-range-top"
range_role="request-logs-range"
label="Request logs"
placement={:top}
show_border={@request_logs.items != []}
/>

<div :if={@request_logs.items != []} class="hidden overflow-x-auto md:block">
<table class="table table-sm admin-log-table min-w-[76rem]">
<colgroup>
Expand Down Expand Up @@ -256,6 +271,18 @@ defmodule CodexPoolerWeb.Admin.RequestLogsPresentation do
</caption>
</table>
</div>

<LogPagination.controls
page={@request_logs}
base_path="/admin/request-logs"
current_params={@current_params}
id_prefix="admin-request-logs-pagination"
range_id="admin-request-logs-range"
range_role="request-logs-range"
label="Request logs"
placement={:bottom}
show_border={@request_logs.items != []}
/>
</div>
"""
end
Expand Down
194 changes: 194 additions & 0 deletions lib/codex_pooler_web/live/admin/log_pagination.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
defmodule CodexPoolerWeb.Admin.LogPagination do
@moduledoc false

use CodexPoolerWeb, :html

@max_page 10_000

@type page_error :: %{required(:field) => :page, required(:message) => String.t()}
@type page_projection :: %{
required(:total) => non_neg_integer(),
required(:limit) => pos_integer(),
required(:offset) => non_neg_integer()
}

attr :page, :map, required: true
attr :base_path, :string, required: true
attr :current_params, :map, required: true
attr :id_prefix, :string, required: true
attr :range_id, :string, required: true
attr :range_role, :string, required: true
attr :label, :string, required: true
attr :placement, :atom, required: true, values: [:top, :bottom]
attr :show_border, :boolean, default: true

def controls(assigns) do
assigns = assign(assigns, metadata(assigns.page))

~H"""
<div
:if={@page.total > 0}
class={[
"border-base-300/70 py-3",
@show_border && @placement == :top && "border-b px-3",
@show_border && @placement == :bottom && "border-t px-3",
!@show_border && "px-3"
]}
>
<nav
id={@id_prefix}
class="grid gap-3 text-sm sm:grid-cols-[auto_minmax(0,1fr)_auto] sm:items-center"
aria-label={pagination_label(@label, @placement)}
>
<p data-role="pagination-status" class="text-base-content/60">
Page {@current_page} of {@total_pages}
</p>
<p
id={@range_id}
data-role={@range_role}
class="text-center tabular-nums text-base-content/70"
>
{@range}
</p>
<div class="join">
<.pagination_link
id={"#{@id_prefix}-prev"}
label="Previous"
enabled={@has_previous_page}
path={__MODULE__.path(@base_path, @current_params, @previous_page)}
/>
<.pagination_link
id={"#{@id_prefix}-next"}
label="Next"
enabled={@has_next_page}
path={__MODULE__.path(@base_path, @current_params, @next_page)}
/>
</div>
</nav>
</div>
"""
end

attr :id, :string, required: true
attr :label, :string, required: true
attr :enabled, :boolean, required: true
attr :path, :string, required: true

defp pagination_link(assigns) do
~H"""
<.link
:if={@enabled}
id={@id}
data-role="pagination-link"
patch={@path}
class="btn btn-sm join-item"
>
{@label}
</.link>
<span
:if={!@enabled}
id={@id}
data-role="pagination-link"
aria-disabled="true"
class="btn btn-sm join-item btn-disabled"
>
{@label}
</span>
"""
end

@spec parse_page(map()) :: {pos_integer(), page_error() | nil}
def parse_page(params) when is_map(params) do
params
|> Map.get("page")
|> normalize_page_value()
|> parse_page_value()
end

@spec offset(pos_integer(), pos_integer()) :: non_neg_integer()
def offset(page, page_size) when page > 0 and page_size > 0, do: (page - 1) * page_size

@spec clamp_page(pos_integer(), page_projection()) :: pos_integer()
def clamp_page(page, %{total: total, limit: limit})
when is_integer(page) and page > 0 and is_integer(total) and total >= 0 and
is_integer(limit) and limit > 0 do
min(page, total_pages(total, limit))
end

@spec put_page(map(), integer()) :: map()
def put_page(params, page) when is_map(params) and page <= 1 do
params
|> stringify_keys()
|> Map.delete("page")
end

def put_page(params, page) when is_map(params) do
params
|> stringify_keys()
|> Map.put("page", Integer.to_string(page))
end

@spec metadata(page_projection()) :: map()
def metadata(%{total: total, limit: limit, offset: offset})
when is_integer(total) and total >= 0 and is_integer(limit) and limit > 0 and
is_integer(offset) and offset >= 0 do
current_page = div(offset, limit) + 1

%{
current_page: current_page,
total_pages: total_pages(total, limit),
previous_page: current_page - 1,
next_page: current_page + 1,
has_previous_page: offset > 0,
has_next_page: offset + limit < total,
range: range(total, limit, offset)
}
end

@spec path(String.t(), map(), integer()) :: String.t()
def path(base_path, params, page) when is_binary(base_path) and is_map(params) do
query =
params
|> put_page(page)
|> Enum.reject(fn {_key, value} -> is_nil(value) or value == "" end)
|> Enum.sort_by(fn {key, _value} -> key end)
|> URI.encode_query()

if query == "", do: base_path, else: base_path <> "?" <> query
end

defp normalize_page_value(nil), do: nil
defp normalize_page_value(value) when is_binary(value), do: String.trim(value)
defp normalize_page_value(value) when is_integer(value), do: Integer.to_string(value)
defp normalize_page_value(_value), do: :invalid

defp parse_page_value(value) when value in [nil, ""], do: {1, nil}

defp parse_page_value(value) when is_binary(value) do
case Integer.parse(value) do
{page, ""} when page > 0 and page <= @max_page -> {page, nil}
_other -> invalid_page()
end
end

defp parse_page_value(_value), do: invalid_page()

defp invalid_page,
do: {1, %{field: :page, message: "Page must be an integer between 1 and 10,000"}}

defp stringify_keys(params),
do: Map.new(params, fn {key, value} -> {to_string(key), value} end)

defp total_pages(total, limit), do: max(div(total + limit - 1, limit), 1)

defp range(0, _limit, _offset), do: "Showing 0 of 0"

defp range(total, limit, offset) do
first = offset + 1
last = min(offset + limit, total)
"Showing #{first}-#{last} of #{total}"
end

defp pagination_label(label, :top), do: "#{label} pagination (top)"
defp pagination_label(label, :bottom), do: "#{label} pagination"
end
Loading