Skip to content

Commit

Permalink
Implement Admin Search (#359)
Browse files Browse the repository at this point in the history
* Update locale dev basic_auth credentials
* Disallow admin routes to be crawled
* Implement admin search
  • Loading branch information
tomkonidas committed Aug 19, 2024
1 parent eb6c91c commit dedc787
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 62 deletions.
4 changes: 2 additions & 2 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ config :plexus,
config :plexus, :generators, api_prefix: "/api/v1"

config :plexus, :basic_auth,
username: "plexus",
password: "plexus"
username: "admin",
password: "admin"

config :plexus, Plexus.Repo,
migration_primary_key: [id: :uuid, type: :binary_id],
Expand Down
6 changes: 6 additions & 0 deletions lib/plexus_web/components/core_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,9 @@ defmodule PlexusWeb.CoreComponents do
default: &Function.identity/1,
doc: "the function for mapping each row before calling the :col and :action slots"

attr :viewport_top, :string
attr :viewport_bottom, :string

slot :col, required: true do
attr :label, :string
end
Expand All @@ -485,6 +488,9 @@ defmodule PlexusWeb.CoreComponents do
<tbody
id={@id}
phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
phx-viewport-top={match?(%Phoenix.LiveView.LiveStream{}, @rows) && @viewport_top}
phx-viewport-bottom={match?(%Phoenix.LiveView.LiveStream{}, @rows) && @viewport_bottom}
phx-page-loading
class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700"
>
<tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="group hover:bg-zinc-50">
Expand Down
103 changes: 86 additions & 17 deletions lib/plexus_web/live/admin/app_live/index.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,60 @@ defmodule PlexusWeb.Admin.AppLive.Index do
def mount(_params, _session, socket) do
if connected?(socket), do: Apps.subscribe()

{entries, page_metadata} =
[scores: true, order_by: :name, page_size: 9999]
|> Apps.list_apps()
|> Map.pop(:entries)

{:ok,
socket
|> assign(:page_metadata, page_metadata)
|> stream_configure(:apps, dom_id: &"apps-#{&1.package}")
|> stream(:apps, entries)}
|> assign(:page, 1)
|> assign(:form, to_form(changeset(), as: :form))
|> assign(:no_results?, false)
|> assign(:end_of_timeline?, false)
|> stream_configure(:apps, dom_id: &"apps-#{&1.package}")}
end

defp changeset(params \\ %{}) do
types = %{search: :string}
data = %{}
Ecto.Changeset.cast({data, types}, params, Map.keys(types))
end

defp paginate_apps(socket, new_page) when new_page >= 1 do
%Scrivener.Page{
total_entries: total_entries,
total_pages: total_pages,
entries: apps
} =
Apps.list_apps(
search_term: socket.assigns.search_term,
page: new_page,
scores: true,
order_by: :name,
page_size: 50
)

case {apps, new_page} do
{[], page} when page != 1 ->
assign(socket, end_of_timeline?: total_pages == new_page)

{apps, _} ->
opts = if new_page == 1, do: [reset: true], else: []
end_of_timeline? = new_page >= total_pages

socket
|> assign(end_of_timeline?: end_of_timeline?)
|> assign(no_results?: apps == [])
|> assign(:page, new_page)
|> assign(:total_entries, total_entries)
|> stream(:apps, apps, opts)
end
end

@impl Phoenix.LiveView
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
{:noreply,
socket
|> assign(:search_term, params["q"])
|> assign(:form, to_form(changeset(%{search: params["q"]}), as: :form))
|> paginate_apps(1)
|> apply_action(socket.assigns.live_action, params)}
end

defp apply_action(socket, :edit, %{"package" => package}) do
Expand All @@ -43,6 +82,44 @@ defmodule PlexusWeb.Admin.AppLive.Index do
|> assign(:app, nil)
end

@impl Phoenix.LiveView
def handle_event("search", %{"form" => form}, socket) do
params =
form
|> Map.get("search", "")
|> String.trim()
|> case do
"" -> %{}
"*" -> %{}
term -> %{q: term}
end

{:noreply, push_patch(socket, to: ~p"/admin/apps?#{params}")}
end

def handle_event("next-page", _, socket) do
{:noreply, paginate_apps(socket, socket.assigns.page + 1)}
end

def handle_event("prev-page", %{"_overran" => true}, socket) do
{:noreply, paginate_apps(socket, 1)}
end

def handle_event("prev-page", _, socket) do
if socket.assigns.page > 1 do
{:noreply, paginate_apps(socket, socket.assigns.page - 1)}
else
{:noreply, socket}
end
end

def handle_event("delete", %{"package" => package}, socket) do
app = Apps.get_app!(package)
{:ok, _} = Apps.delete_app(app)

{:noreply, stream_delete(socket, :apps, app)}
end

@impl Phoenix.LiveView
def handle_info({:app_created, app}, socket) do
app = Apps.get_app!(app.package, scores: true)
Expand Down Expand Up @@ -79,12 +156,4 @@ defmodule PlexusWeb.Admin.AppLive.Index do
|> put_flash(:info, "'#{app.name}' Rating Updated")
|> stream_insert(:apps, app)}
end

@impl Phoenix.LiveView
def handle_event("delete", %{"package" => package}, socket) do
app = Apps.get_app!(package)
{:ok, _} = Apps.delete_app(app)

{:noreply, stream_delete(socket, :apps, app)}
end
end
106 changes: 65 additions & 41 deletions lib/plexus_web/live/admin/app_live/index.html.heex
Original file line number Diff line number Diff line change
@@ -1,53 +1,77 @@
<.header>
Apps (<%= @page_metadata.total_entries %> entries)
Apps (<%= @total_entries %> entries)
<:actions>
<.link patch={~p"/admin/apps/new"}>
<.button>New App</.button>
</.link>
</:actions>
</.header>

<.table
id="apps"
rows={@streams.apps}
row_click={fn {_id, app} -> JS.navigate(~p"/admin/apps/#{app}") end}
<.simple_form for={@form} id="search-form" phx-change="search" phx-submit="search">
<.focus_wrap id="focus-first-search">
<.input field={@form[:search]} label="Search" phx-debounce="300" />
</.focus_wrap>
</.simple_form>

<div
id="apps-container"
class={[
if(@end_of_timeline?, do: "pb-10", else: "pb-[calc(200vh)]"),
if(@page == 1, do: "pt-10", else: "pt-[calc(200vh)]")
]}
>
<:col :let={{_id, app}} label="Icon">
<div class="aspect-h-1 aspect-w-1 w-16 h-16 overflow-hidden">
<img
src={app.icon_url}
alt={app.name <> " Icon"}
class="h-full w-full object-cover object-center"
/>
</div>
</:col>
<:col :let={{_dom_id, app}} label="Name">
<h3 class="text-sm text-gray-700"><%= app.name %></h3>
</:col>
<:col :let={{_dom_id, app}} label="Package">
<p class="text-sm text-gray-700"><%= app.package %></p>
</:col>
<:col :let={{_dom_id, app}} label="Native">
<.badge score={app.scores.native} />
</:col>
<:col :let={{_dom_id, app}} label="MicroG">
<.badge score={app.scores.micro_g} />
</:col>
<:action :let={{_dom_id, app}}>
<div class="sr-only">
<.link navigate={~p"/admin/apps/#{app}"}>Show</.link>
</div>
<.link patch={~p"/admin/apps/#{app}/edit"}>Edit</.link>
</:action>
<:action :let={{dom_id, app}}>
<.link
phx-click={JS.push("delete", value: %{package: app.package}) |> hide("##{dom_id}")}
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>
<.table
id="apps"
viewport_top={@page > 1 && "prev-page"}
viewport_bottom={!@end_of_timeline? && "next-page"}
rows={@streams.apps}
row_click={fn {_id, app} -> JS.navigate(~p"/admin/apps/#{app}") end}
>
<:col :let={{_id, app}} label="Icon">
<div class="aspect-h-1 aspect-w-1 w-16 h-16 overflow-hidden">
<img
src={app.icon_url}
alt={app.name <> " Icon"}
class="h-full w-full object-cover object-center"
/>
</div>
</:col>
<:col :let={{_dom_id, app}} label="Name">
<h3 class="text-sm text-gray-700"><%= app.name %></h3>
</:col>
<:col :let={{_dom_id, app}} label="Package">
<p class="text-sm text-gray-700"><%= app.package %></p>
</:col>
<:col :let={{_dom_id, app}} label="De-Googled">
<.badge score={app.scores.native} />
</:col>
<:col :let={{_dom_id, app}} label="MicroG">
<.badge score={app.scores.micro_g} />
</:col>
<:action :let={{_dom_id, app}}>
<div class="sr-only">
<.link navigate={~p"/admin/apps/#{app}"}>Show</.link>
</div>
<.link patch={~p"/admin/apps/#{app}/edit"}>Edit</.link>
</:action>
<:action :let={{dom_id, app}}>
<.link
phx-click={JS.push("delete", value: %{package: app.package}) |> hide("##{dom_id}")}
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>

<div :if={@no_results?} class="mt-5 text-[50px] text-center">
No apps found<br />😭
</div>

<div :if={@end_of_timeline? and not @no_results?} class="mt-5 text-[50px] text-center">
End of list<br />🤭
</div>
</div>

<.modal
:if={@live_action in [:new, :edit]}
Expand Down
4 changes: 2 additions & 2 deletions priv/static/robots.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
#
# To ban all spiders from the entire site uncomment the next two lines:
# User-agent: *
# Disallow: /
User-agent: *
Disallow: /admin/

0 comments on commit dedc787

Please sign in to comment.