lib/telemetry_ui/web/web.ex

defmodule TelemetryUI.Web do
  use Plug.Builder

  alias Ecto.Changeset
  alias TelemetryUI.Web.Filter
  alias TelemetryUI.Web.View

  plug(:assign_telemetry_name)
  plug(:assign_theme)
  plug(:fetch_query_params)
  plug(:ensure_allowed)
  plug(:assign_filters)
  plug(:fetch_pages)
  plug(:fetch_current_page)
  plug(:assign_share)
  plug(:index)

  def index(conn = %{params: %{"metric-data" => id}}, _) do
    data =
      with metric when is_struct(metric) <- TelemetryUI.metric_by_id(conn.assigns.telemetry_ui_name, id),
           {:async, async} <- fetch_component_metric_data(conn, metric) do
        async.()
      else
        {:ok, data} -> data
        _ -> []
      end

    conn
    |> put_resp_header("content-type", "application/json")
    |> send_resp(200, Jason.encode!(data))
  end

  def index(conn, _opts) do
    conn = assign(conn, :shared, false)
    content = Phoenix.HTML.Safe.to_iodata(View.render("index.html", Map.put(conn.assigns, :conn, conn)))

    conn
    |> put_resp_header("content-type", "text/html")
    |> delete_resp_header("content-security-policy")
    |> send_resp(200, content)
  end

  defp assign_telemetry_name(conn, _) do
    if conn.assigns[:telemetry_ui_name] do
      conn
    else
      assign(conn, :telemetry_ui_name, :default)
    end
  end

  defp assign_filters(conn, _) do
    params = conn.params["filter"]

    filters =
      params
      |> Filter.cast(conn.assigns.theme.frame_options)
      |> Map.from_struct()

    filter_form =
      %Filter{}
      |> Changeset.cast(filters, ~w(frame)a)
      |> Changeset.apply_changes()

    conn = assign(conn, :filters, filters)
    conn = assign(conn, :filter_form, Changeset.change(filter_form))

    conn
  end

  defp assign_theme(conn, _) do
    assign(conn, :theme, TelemetryUI.theme(conn.assigns.telemetry_ui_name))
  end

  defp assign_share(conn, _) do
    if TelemetryUI.valid_share_key?(conn.assigns.theme.share_key) do
      filters = %{conn.assigns.filters | page: conn.assigns.current_page.id}
      share = Filter.encrypt(filters, conn.assigns.theme.share_key)
      assign(conn, :share, share)
    else
      assign(conn, :share, nil)
    end
  end

  defp ensure_allowed(conn, _) do
    if conn.assigns[:telemetry_ui_allowed] do
      conn
    else
      halt(send_resp(conn, 404, "Not found"))
    end
  end

  defp fetch_current_page(conn, _) do
    page =
      with %{"page" => page_id} when not is_nil(page_id) <- conn.params["filter"],
           page when not is_nil(page) <- TelemetryUI.page_by_id(conn.assigns.telemetry_ui_name, page_id) do
        fetch_metric_data(conn, page)
      else
        _ -> fetch_metric_data(conn, hd(conn.assigns.pages))
      end

    assign(conn, :current_page, page)
  end

  defp fetch_pages(conn, _) do
    pages = TelemetryUI.pages(conn.assigns.telemetry_ui_name)
    assign(conn, :pages, pages)
  end

  defp fetch_metric_data(conn, page) do
    metrics =
      Enum.map(page.metrics, fn metric ->
        case fetch_component_metric_data(conn, metric) do
          {:ok, data} -> %{metric | data: data}
          {:async, _} -> metric
          _ -> metric
        end
      end)

    %{page | metrics: metrics}
  end

  defp fetch_component_metric_data(conn, metric) do
    case Function.info(metric.data_resolver, :arity) do
      {:arity, 0} -> metric.data_resolver.()
      {:arity, 1} -> metric.data_resolver.(conn.assigns.filters)
      {:arity, 2} -> metric.data_resolver.(conn.assigns.telemetry_ui_name, conn.assigns.filters)
    end
  end
end