Compare commits

...

2 Commits

Author SHA1 Message Date
Dmitry Popov
cee545cfd9 feat(Audit): updated audit page pagination 2025-03-12 22:32:08 +01:00
Dmitry Popov
9612cda72b feat(Audit): updated audit page pagination 2025-03-12 22:32:00 +01:00
12 changed files with 1562 additions and 80 deletions

View File

@@ -75,6 +75,17 @@ config :phoenix_ddos,
request_paths: ["/auth/eve"], allowed: 20, period: {1, :minute}}
]
config :ash_pagify,
default_limit: 50,
max_limit: 1000,
scopes: %{
role: []
},
reset_on_filter?: true,
replace_invalid_params?: true,
pagination: [opts: {WandererAppWeb.CoreComponents, :pagination_opts}],
table: [opts: {WandererAppWeb.CoreComponents, :table_opts}]
# Configures Elixir's Logger
config :logger, :console,
format: "$time $metadata[$level] $message\n",

View File

@@ -0,0 +1,11 @@
defmodule WandererApp.Api.Preparations.LoadCharacter do
@moduledoc false
use Ash.Resource.Preparation
require Ash.Query
def prepare(query, _params, _) do
query
|> Ash.Query.load([:character])
end
end

View File

@@ -5,6 +5,16 @@ defmodule WandererApp.Api.UserActivity do
domain: WandererApp.Api,
data_layer: AshPostgres.DataLayer
require Ash.Expr
@ash_pagify_options %{
default_limit: 15,
scopes: %{
role: []
}
}
def ash_pagify_options, do: @ash_pagify_options
postgres do
repo(WandererApp.Repo)
table("user_activity_v1")
@@ -31,7 +41,13 @@ defmodule WandererApp.Api.UserActivity do
read :read do
primary?(true)
pagination(offset?: true, keyset?: true)
pagination offset?: true,
default_limit: @ash_pagify_options.default_limit,
countable: true,
required?: false
prepare WandererApp.Api.Preparations.LoadCharacter
end
create :new do

View File

@@ -39,7 +39,7 @@ defmodule WandererApp.Map.Audit do
:ok
end
def get_activity_page(map_id, page, per_page, period, activity) do
def get_activity_query(map_id, period, activity) do
{from, to} = period |> get_period()
query =
@@ -65,10 +65,6 @@ defmodule WandererApp.Map.Audit do
query
|> Ash.Query.sort(inserted_at: :desc)
|> WandererApp.Api.read(
page: [limit: per_page, offset: (page - 1) * per_page],
load: [:character]
)
end
def track_acl_event(

File diff suppressed because it is too large Load Diff

View File

@@ -951,4 +951,64 @@ defmodule WandererAppWeb.CoreComponents do
when is_binary(eve_alliance_id) or is_integer(eve_alliance_id) do
"#{@image_base_url}/alliances/#{eve_alliance_id}/logo?size=32"
end
def pagination_opts do
[
ellipsis_attrs: [class: "ellipsis"],
ellipsis_content: "",
next_link_content: next_icon(),
page_links: {:ellipsis, 7},
previous_link_content: previous_icon(),
current_link_attrs: [
class:
"relative z-10 inline-flex items-center bg-indigo-600 px-4 py-2 text-sm font-semibold text-white focus:z-20 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600",
aria: [current: "page"]
],
next_link_attrs: [
aria: [label: "Go to next page"],
class: ""
],
pagination_link_attrs: [
class:
"relative z-10 inline-flex items-center px-4 py-2 text-sm font-semibold text-white focus:z-20 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
],
previous_link_attrs: [
aria: [label: "Go to previous page"],
class: ""
]
]
end
defp next_icon do
assigns = %{}
~H"""
<.icon name="hero-chevron-right" class="h-5 w-5" />
"""
end
defp previous_icon do
assigns = %{}
~H"""
<.icon name="hero-chevron-left" class="h-5 w-5" />
"""
end
def table_opts do
[
container: true,
container_attrs: [class: "table-container"],
no_results_content: no_results_content(),
table_attrs: [class: "table"]
]
end
defp no_results_content do
assigns = %{}
~H"""
<p>Nothing found.</p>
"""
end
end

View File

@@ -0,0 +1,115 @@
defmodule WandererAppWeb.Components.Pagination do
@moduledoc """
Pagination component for AshPagify.
"""
alias WandererAppWeb.Components
alias AshPagify.Meta
alias AshPagify.Misc
@spec default_opts() :: [Components.pagination_option()]
def default_opts do
[
current_link_attrs: [
class: "pagination-link is-current",
aria: [current: "page"]
],
disabled_class: "disabled",
ellipsis_attrs: [class: "pagination-ellipsis"],
ellipsis_content: Phoenix.HTML.raw("&hellip;"),
next_link_attrs: [
aria: [label: "Go to next page"],
class: "pagination-next"
],
next_link_content: "Next",
page_links: :all,
pagination_link_aria_label: &"Go to page #{&1}",
pagination_link_attrs: [class: "pagination-link"],
previous_link_attrs: [
aria: [label: "Go to previous page"],
class: "pagination-previous"
],
previous_link_content: "Previous",
wrapper_attrs: [
class: "pagination",
role: "navigation",
aria: [label: "pagination"]
]
]
end
def merge_opts(opts) do
default_opts()
|> Misc.list_merge(Misc.global_option(:pagination) || [])
|> Misc.list_merge(opts)
end
def max_pages(:all, total_pages), do: total_pages
def max_pages(:hide, _), do: 0
def max_pages({:ellipsis, max_pages}, _), do: max_pages
def show_pagination(nil), do: false
def show_pagination?(%Meta{errors: [], total_pages: total_pages}) do
total_pages > 1
end
def show_pagination?(_), do: false
def get_page_link_range(current_page, max_pages, total_pages) do
# number of additional pages to show before or after current page
additional = ceil(max_pages / 2)
cond do
max_pages >= total_pages ->
1..total_pages
current_page + additional > total_pages ->
(total_pages - max_pages + 1)..total_pages
true ->
first = max(current_page - additional + 1, 1)
last = min(first + max_pages - 1, total_pages)
first..last
end
end
@spec build_page_link_helper(Meta.t(), Components.pagination_path()) ::
(integer() -> String.t() | nil)
def build_page_link_helper(_meta, nil), do: fn _offset -> nil end
def build_page_link_helper(%Meta{} = meta, path) do
query_params = build_query_params(meta)
fn offset ->
params = maybe_put_offset(query_params, offset)
Components.build_path(path, params)
end
end
defp build_query_params(%Meta{} = meta) do
Components.to_query(meta.ash_pagify, for: meta.resource, default_scopes: meta.default_scopes)
end
defp maybe_put_offset(params, 0), do: Keyword.delete(params, :offset)
defp maybe_put_offset(params, offset), do: Keyword.put(params, :offset, offset)
def attrs_for_page_link(page, %{current_page: page}, opts) do
add_page_link_aria_label(opts[:current_link_attrs], page, opts)
end
def attrs_for_page_link(page, _meta, opts) do
add_page_link_aria_label(opts[:pagination_link_attrs], page, opts)
end
defp add_page_link_aria_label(attrs, page, opts) do
aria_label = opts[:pagination_link_aria_label].(page)
Keyword.update(
attrs,
:aria,
[label: aria_label],
&Keyword.put(&1, :label, aria_label)
)
end
end

View File

@@ -25,31 +25,11 @@ defmodule WandererAppWeb.UserActivity do
def render(assigns) do
~H"""
<div id={@id}>
<span
:if={@page > 1}
class="text-1xl fixed bottom-10 right-10 bg-zinc-700 text-white rounded-lg p-1 text-center min-w-[65px] z-50 opacity-70"
>
<%= @page %>
</span>
<ul
id="events"
class="space-y-4"
phx-update="stream"
phx-viewport-top={@page > 1 && "prev-page"}
phx-viewport-bottom={!@end_of_stream? && "next-page"}
phx-page-loading
class={[
if(@end_of_stream?, do: "pb-10", else: "pb-[calc(200vh)]"),
if(@page == 1, do: "pt-10", else: "pt-[calc(200vh)]")
]}
>
<ul id="events" class="space-y-4" phx-update="stream" phx-page-loading class={["pt-10"]}>
<li :for={{dom_id, activity} <- @stream} id={dom_id}>
<.activity_entry activity={activity} can_undo_types={@can_undo_types} />
</li>
</ul>
<div :if={@end_of_stream?} class="mt-5 text-center">
No more activity
</div>
</div>
"""
end

View File

@@ -52,7 +52,7 @@ defmodule WandererAppWeb.MapAuditLive do
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
apply_action(socket, socket.assigns.live_action, params)
end
@impl true
@@ -83,26 +83,6 @@ defmodule WandererAppWeb.MapAuditLive do
|> push_navigate(to: ~p"/#{map_slug}/audit?period=#{period}&activity=#{activity}")}
end
def handle_event("top", _, socket) do
{:noreply, socket |> load_activity(1)}
end
def handle_event("next-page", _, socket) do
{:noreply, load_activity(socket, socket.assigns.page + 1)}
end
def handle_event("prev-page", %{"_overran" => true}, socket) do
{:noreply, load_activity(socket, 1)}
end
def handle_event("prev-page", _, socket) do
if socket.assigns.page > 1 do
{:noreply, load_activity(socket, socket.assigns.page - 1)}
else
{:noreply, socket}
end
end
def handle_event(
"undo",
%{"event-data" => event_data, "event-type" => "systems_removed"},
@@ -138,7 +118,7 @@ defmodule WandererAppWeb.MapAuditLive do
{:noreply, socket}
end
defp apply_action(socket, :index, _params) do
defp apply_action(socket, :index, params) do
socket
|> assign(:active_page, :audit)
|> assign(:page_title, "Map - Audit")
@@ -158,45 +138,29 @@ defmodule WandererAppWeb.MapAuditLive do
{"Signatures Added", :signatures_added},
{"Signatures Removed", :signatures_removed}
])
|> load_activity(1)
|> list_activity(params)
end
defp load_activity(socket, new_page) when new_page >= 1 do
defp list_activity(socket, params, opts \\ []) do
%{
activity: activity,
per_page: per_page,
page: cur_page,
map_id: map_id,
map_slug: map_slug,
map_subscription_active: map_subscription_active,
period: period
} =
socket.assigns
period = get_valid_period(period, map_subscription_active)
query = WandererApp.Map.Audit.get_activity_query(map_id, period, activity)
with {:ok, page} <-
WandererApp.Map.Audit.get_activity_page(map_id, new_page, per_page, period, activity) do
{activity, at, limit} =
if new_page >= cur_page do
{page.results, -1, per_page * 3 * -1}
else
{Enum.reverse(page.results), 0, per_page * 3}
end
AshPagify.validate_and_run(query, params, opts)
|> case do
{:ok, {activity, meta}} ->
{:noreply, socket |> assign(:meta, meta) |> stream(:activity, activity, reset: true)}
case activity do
[] ->
socket
|> assign(end_of_stream?: at == -1)
|> stream(:activity, [])
[_ | _] = _ ->
socket
|> assign(end_of_stream?: false)
|> assign(page: if(activity == [], do: cur_page, else: new_page))
|> stream(:activity, activity, at: at, limit: limit)
end
else
_ -> socket
{:error, meta} ->
valid_path = AshPagify.Components.build_path(~p"/#{map_slug}/audit", meta.params)
{:noreply, socket |> push_navigate(to: valid_path)}
end
end

View File

@@ -101,6 +101,10 @@
class="pt-20 w-full h-full col-span-2 lg:col-span-1 p-4 pl-20 pb-20 overflow-auto"
>
<div class="flex flex-col gap-4 w-full">
<div class="flex justify-between w-full">
<div />
<WandererAppWeb.Components.pagination meta={@meta} path={~p"/#{@map_slug}/audit?period=#{@period}&activity=#{@activity}"} />
</div>
<.live_component
module={UserActivity}
id="user-activity"
@@ -111,5 +115,10 @@
end_of_stream?={@end_of_stream?}
event_name="activity_event"
/>
<div class="flex justify-between w-full">
<div />
<WandererAppWeb.Components.pagination meta={@meta} path={~p"/#{@map_slug}/audit?period=#{@period}&activity=#{@activity}"} />
</div>
</div>
</main>

View File

@@ -50,7 +50,7 @@ defmodule WandererApp.MixProject do
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:dialyxir, ">= 0.0.0", only: [:dev], runtime: false},
{:doctor, ">= 0.0.0", only: [:dev], runtime: false},
{:ex_doc, ">= 0.0.0", only: [:dev], runtime: false},
{:ex_doc, "~> 0.37", runtime: false},
{:sobelow, ">= 0.0.0", only: [:dev], runtime: false},
{:mix_audit, ">= 0.0.0", only: [:dev], runtime: false},
{:ex_check, "~> 0.14.0", only: [:dev], runtime: false},
@@ -117,7 +117,8 @@ defmodule WandererApp.MixProject do
{:version_tasks, "~> 0.12.0"},
{:error_tracker, "~> 0.2"},
{:ddrt, "~> 0.2.1"},
{:live_view_events, "~> 0.1.0"}
{:live_view_events, "~> 0.1.0"},
{:ash_pagify, "~> 1.4.1"}
]
end

View File

@@ -1,6 +1,7 @@
%{
"ash": {:hex, :ash, "3.4.15", "0b8a0ae9bc543267380ffdacfeb1bc8d1bc831c1acb58b923ac0285464d5badd", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.3.36 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.9", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3647184d23c40a8d4d381c3616b5c5c783d4d2e969918b6fd36aa171fede9cfa"},
"ash_cloak": {:hex, :ash_cloak, "0.1.2", "d70338491ad8b6a18c691c25a2a236e18bb726c551642f56d996d25a9f1e779b", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}], "hexpm", "8b13dc44d8c58a7a876e537b3eab03672ac04f442568b4f9c1d70ccd9522812f"},
"ash_pagify": {:hex, :ash_pagify, "1.4.1", "af25d5f68b6df84ed5388dd4688658fd08fa59e99f70361a0497c376b50ac115", [:mix], [{:ash, "~> 3.3", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, "~> 2.1", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:ash_postgres, "~> 2.1", [hex: :ash_postgres, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.37", [hex: :ex_doc, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}], "hexpm", "5b7f771c5a76f92d120536cd87fb25b7321a681482aeaf127b7202bd18552c84"},
"ash_phoenix": {:hex, :ash_phoenix, "2.1.2", "7215cf3a1ebc82ca0e5317a8449e1725fa753354674a0e8cd7fc1c8ffd1181c7", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5.6 or ~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.3 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "b591bd731a0855f670b5bc3f48c364b1694d508071f44d57bcd508c82817c51e"},
"ash_postgres": {:hex, :ash_postgres, "2.4.1", "6fa9bbb40e9d4a73bcdd2403e036874421e8c919dc57338eb6476cc8a82fa112", [:mix], [{:ash, ">= 3.4.9 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.2.30 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, ">= 3.12.1 and < 4.0.0-0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, ">= 0.3.36 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:inflex, "~> 2.1", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "9419993fe7f200db7230c372f5aa280f8bebb175501c9e8d58703c9054006c7b"},
"ash_sql": {:hex, :ash_sql, "0.2.32", "de99255becfb9daa7991c18c870e9f276bb372acda7eda3e05c3e2ff2ca8922e", [:mix], [{:ash, ">= 3.1.7 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "43773bcd33d21319c11804d76fe11f1a1b7c8faba7aaedeab6f55fde3d2405db"},
@@ -27,7 +28,7 @@
"dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
"doctor": {:hex, :doctor, "0.21.0", "20ef89355c67778e206225fe74913e96141c4d001cb04efdeba1a2a9704f1ab5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "a227831daa79784eb24cdeedfa403c46a4cb7d0eab0e31232ec654314447e4e0"},
"earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"},
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
"earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"},
"ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"},
"ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"},
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
@@ -36,7 +37,7 @@
"ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"},
"ex2ms": {:hex, :ex2ms, "1.7.0", "45b9f523d0b777667ded60070d82d871a37e294f0b6c5b8eca86771f00f82ee1", [:mix], [], "hexpm", "2589eee51f81f1b1caa6d08c990b1ad409215fe6f64c73f73c67d36ed10be827"},
"ex_check": {:hex, :ex_check, "0.14.0", "d6fbe0bcc51cf38fea276f5bc2af0c9ae0a2bb059f602f8de88709421dae4f0e", [:mix], [], "hexpm", "8a602e98c66e6a4be3a639321f1f545292042f290f91fa942a285888c6868af0"},
"ex_doc": {:hex, :ex_doc, "0.34.1", "9751a0419bc15bc7580c73fde506b17b07f6402a1e5243be9e0f05a68c723368", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d441f1a86a235f59088978eff870de2e815e290e44a8bd976fe5d64470a4c9d2"},
"ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"},
"ex_rated": {:hex, :ex_rated, "2.1.0", "d40e6fe35097b10222df2db7bb5dd801d57211bac65f29063de5f201c2a6aebc", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm", "936c155337253ed6474f06d941999dd3a9cf0fe767ec99a59f2d2989dc2cc13f"},
"expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"},
"exsync": {:hex, :exsync, "0.4.1", "0a14fe4bfcb80a509d8a0856be3dd070fffe619b9ba90fec13c58b316c176594", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "cefb22aa805ec97ffc5b75a4e1dc54bcaf781e8b32564bf74abbe5803d1b5178"},