diff --git a/config/dev.exs b/config/dev.exs
index 2b0b9d3be..e022eaad1 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -100,3 +100,9 @@ config :ex_aws, :s3,
# Define the base host to use
config :admin, :base_host, "localhost:3114"
+
+# Define the backend host to use
+config :admin, :backend_host, "localhost:3000"
+
+# Publication index (development defaults)
+config :admin, :publication_reindex_headers, [{"meilisearch-rebuild", "secret"}]
diff --git a/config/prod.exs b/config/prod.exs
index c785a6bfc..c520d2b65 100644
--- a/config/prod.exs
+++ b/config/prod.exs
@@ -44,3 +44,6 @@ config :admin, :logger, [
# Runtime production configuration, including reading
# of environment variables, is done on config/runtime.exs.
+
+config :admin,
+ publication_reindex_headers: {"meilisearch-rebuild", System.get_env("PUBLICATION_INDEX_HEADER_VALUE")}
diff --git a/config/runtime.exs b/config/runtime.exs
index e1b7aa37b..6e62bf599 100644
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -144,6 +144,7 @@ if config_env() == :prod do
end
config :admin, base_host: base_host
+ config :admin, backend_host: base_host
# Config the File Items bucket name
config :admin, :file_items_bucket, System.get_env("FILE_ITEMS_BUCKET_NAME", "file-items")
diff --git a/lib/admin/publications/search_index.ex b/lib/admin/publications/search_index.ex
new file mode 100644
index 000000000..30393f07d
--- /dev/null
+++ b/lib/admin/publications/search_index.ex
@@ -0,0 +1,76 @@
+defmodule Admin.Publications.SearchIndex do
+ @moduledoc """
+ Utilities to trigger a publication reindex on an external indexing endpoint.
+
+ It expects the following application config in `:admin`:
+
+ config :admin, :publications,
+ publication_index_url
+ publication_index_header_value
+
+ """
+
+ require Logger
+
+ defp http_client, do: Application.get_env(:admin, :publication_index_http_client, Req)
+
+ @doc """
+ Trigger a reindex by calling the configured endpoint with the configured header.
+
+ Returns `{:ok, %Req.Response{}}` on success or `{:error, error_code}` on failure.
+ """
+ def reindex do
+ with {:ok, client, url, headers} <- build_reindex_request() do
+
+ req = client.new(method: :get, url: url, headers: headers)
+
+ IO.puts("Reindex request: #{inspect(req)}")
+
+ case client.request(req) do
+ %Req.Response{} = resp ->
+ if resp.status in 200..299 do
+ {:ok, resp}
+ else
+ Logger.error("SearchIndex.reindex failed: #{inspect(resp)}")
+ {:error, resp.status}
+ end
+
+ {:ok, %Req.Response{} = resp} ->
+ if resp.status in 200..299 do
+ {:ok, resp}
+ else
+ Logger.error("SearchIndex.reindex failed: #{inspect(resp)}")
+ {:error, resp.status}
+ end
+
+ {:error, reason} ->
+ Logger.error("SearchIndex.reindex failed: #{inspect(reason)}")
+ {:error, 500}
+
+ other ->
+ Logger.error("SearchIndex.reindex unexpected response: #{inspect(other)}")
+ {:error, :unexpected_response}
+ end
+ end
+ end
+
+ @doc false
+ # Build the HTTP client, url, and headers for the reindex request.
+ defp build_reindex_request do
+
+ case Application.get_env(:admin, :backend_host) do
+ nil ->
+ {:error, :missing_publication_index_url}
+
+ url ->
+ case Application.get_env(:admin, :publication_reindex_headers) do
+ :error ->
+ {:error, :missing_publication_index_header_value}
+
+ headers_value ->
+ client = http_client()
+ {:ok, client, "http://#{url}/items/collections/search/rebuild", headers_value}
+ end
+ end
+ end
+end
diff --git a/lib/admin_web/components/layouts.ex b/lib/admin_web/components/layouts.ex
index 21df18475..df17863d4 100644
--- a/lib/admin_web/components/layouts.ex
+++ b/lib/admin_web/components/layouts.ex
@@ -165,6 +165,7 @@ defmodule AdminWeb.Layouts do
- <.link navigate={~p"/published_items"}>Recent
- <.link navigate={~p"/published_items/featured"}>Featured
+ - <.link navigate={~p"/published_items/search_index"}>Search Index
<.link navigate={~p"/publishers"}>Apps
@@ -212,6 +213,9 @@ defmodule AdminWeb.Layouts do
<.link navigate={~p"/published_items/featured"}>Featured
+
+ <.link navigate={~p"/published_items/search_index"}>Search Index
+
diff --git a/lib/admin_web/live/publication_search_index_live.ex b/lib/admin_web/live/publication_search_index_live.ex
new file mode 100644
index 000000000..8b25d2a8f
--- /dev/null
+++ b/lib/admin_web/live/publication_search_index_live.ex
@@ -0,0 +1,59 @@
+defmodule AdminWeb.PublicationSearchIndexLive do
+ use AdminWeb, :live_view
+
+ alias Admin.Publications.SearchIndex
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+ <.header>
+ Index Status
+ <:subtitle>
+ You can start a reindex of the library index. This is useful if your search engine is not up to date.
+
+
+
+ <.icon name="hero-exclamation-triangle" />
+ This operation is heavy on the database and might take some time to complete.
+
+
{@status || ""}
+
+
+
+
+
+ """
+ end
+
+ @impl true
+ def mount(_params, _session, socket) do
+ {:ok, assign(socket, status: nil, indexing: false)}
+ end
+
+ @impl true
+ def handle_event("reindex", _params, socket) do
+ case SearchIndex.reindex() do
+ {:ok, _resp} ->
+ {:noreply,
+ assign(socket,
+ indexing: true,
+ status: "Reindex has started. It might take some time to complete."
+ )}
+
+ {:error, reason} ->
+ {:noreply,
+ assign(socket,
+ indexing: false,
+ status: "Reindex failed with error #{inspect(reason)}"
+ )}
+ end
+ end
+end
diff --git a/lib/admin_web/router.ex b/lib/admin_web/router.ex
index 7e0da8273..1e50b535f 100644
--- a/lib/admin_web/router.ex
+++ b/lib/admin_web/router.ex
@@ -78,6 +78,7 @@ defmodule AdminWeb.Router do
# published_items
live "/published_items/:id/unpublish", PublishedItemLive.Unpublish, :unpublish
+ live "/published_items/search_index", PublicationSearchIndexLive, :index
# apps
scope "/apps" do
diff --git a/test/admin/publications/publication_search_index_test.exs b/test/admin/publications/publication_search_index_test.exs
new file mode 100644
index 000000000..14254a2ef
--- /dev/null
+++ b/test/admin/publications/publication_search_index_test.exs
@@ -0,0 +1,84 @@
+defmodule Admin.Publications.PublicationSearchIndexTest do
+ use ExUnit.Case, async: true
+
+ alias Admin.Publications.SearchIndex
+
+ setup do
+ # ensure we start with a clean client config
+ Application.delete_env(:admin, :publication_index_http_client)
+ :ok
+ end
+
+ test "returns error when url is missing" do
+ Application.delete_env(:admin, :publication_index_url)
+ Application.put_env(:admin, :publication_index_header_value, "token")
+
+ assert {:error, :missing_publication_index_url} = SearchIndex.reindex()
+ end
+
+ test "returns error when header value is missing" do
+ Application.put_env(:admin, :publication_index_url, "http://example")
+ Application.delete_env(:admin, :publication_index_header_value)
+
+ assert {:error, :missing_publication_index_header_value} = SearchIndex.reindex()
+ end
+
+ test "returns ok on 2xx response" do
+ Application.put_env(:admin, :publication_index_url, "http://example")
+ Application.put_env(:admin, :publication_index_header_value, "token")
+
+ client =
+ Module.concat([__MODULE__, :SuccessClient])
+
+ defmodule client do
+ def new(_opts), do: :req_request
+
+ def request(_req) do
+ resp = %Req.Response{status: 200, body: "ok"}
+ {:ok, resp}
+ end
+ end
+
+ Application.put_env(:admin, :publication_index_http_client, client)
+
+ assert {:ok, resp} = SearchIndex.reindex()
+ assert resp.status == 200
+ end
+
+ test "returns error on non-2xx response" do
+ Application.put_env(:admin, :publication_index_url, "http://example")
+ Application.put_env(:admin, :publication_index_header_value, "token")
+
+ client = Module.concat([__MODULE__, :BadResponseClient])
+
+ defmodule client do
+ def new(_opts), do: :req_request
+
+ def request(_req) do
+ resp = %Req.Response{status: 500, body: "nope"}
+ {:ok, resp}
+ end
+ end
+
+ Application.put_env(:admin, :publication_index_http_client, client)
+
+ assert {:error, 500} = SearchIndex.reindex()
+ end
+
+ test "returns error tuple when client errors" do
+ Application.put_env(:admin, :publication_index_url, "http://example")
+ Application.put_env(:admin, :publication_index_header_value, "token")
+
+ client = Module.concat([__MODULE__, :ErrClient])
+
+ defmodule client do
+ def new(_opts), do: :req_request
+
+ def request(_req), do: {:error, :econnrefused}
+ end
+
+ Application.put_env(:admin, :publication_index_http_client, client)
+
+ assert {:error, :econnrefused} = SearchIndex.reindex()
+ end
+end
diff --git a/test/admin_web/live/publication_index_live_test.exs b/test/admin_web/live/publication_index_live_test.exs
new file mode 100644
index 000000000..fd1d97d4d
--- /dev/null
+++ b/test/admin_web/live/publication_index_live_test.exs
@@ -0,0 +1,56 @@
+defmodule AdminWeb.PublicationIndexLiveTest do
+ use AdminWeb.ConnCase, async: true
+
+ import Phoenix.LiveViewTest
+
+ setup %{conn: conn} do
+ %{conn: conn} = register_and_log_in_user(%{conn: conn})
+ :ok
+ {:ok, conn: conn}
+ end
+
+ test "shows error when reindex fails", %{conn: conn} do
+ Application.put_env(:admin, :publication_index_url, "http://example")
+ Application.put_env(:admin, :publication_index_header_value, "token")
+
+ client = Module.concat([__MODULE__, :FailClient])
+
+ defmodule client do
+ def new(_opts), do: :req_request
+ def request(_req), do: {:error, :econnrefused}
+ end
+
+ Application.put_env(:admin, :publication_index_http_client, client)
+
+ {:ok, view, _html} = live(conn, "/publications/reindex")
+
+ view |> element("button", "Start Reindex") |> render_click()
+
+ assert render(view) =~ "Reindex failed"
+ end
+
+ test "show waitingstatus on success", %{conn: conn} do
+ Application.put_env(:admin, :publication_index_url, "http://example")
+ Application.put_env(:admin, :publication_index_header_value, "token")
+
+ client = Module.concat([__MODULE__, :OkClient])
+
+ defmodule client do
+ def new(_opts), do: :req_request
+ def request(_req) do
+ resp = %Req.Response{status: 200, body: "ok"}
+ {:ok, resp}
+ end
+ end
+
+ Application.put_env(:admin, :publication_index_http_client, client)
+
+ {:ok, view, _html} = live(conn, "/publications/reindex")
+
+ view |> element("button", "Start Reindex") |> render_click()
+
+ refute render(view) =~ "Reindex failed"
+
+ assert view |> element("button[disabled]", "Start Reindex") |> has_element?()
+ end
+end