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"/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. + + + +

    {@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