lib/oban/live_dashboard.ex

defmodule Oban.LiveDashboard do
  use Phoenix.LiveDashboard.PageBuilder, refresher?: true

  import Phoenix.LiveDashboard.Helpers, only: [format_value: 2]
  import Ecto.Query

  @impl true
  def menu_link(_, _) do
    {:ok, "Oban"}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <.live_table
      id="oban_jobs"
      dom_id="oban-jobs"
      page={@page}
      row_attrs={&row_attrs/1}
      row_fetcher={&fetch_jobs/2}
      title="Oban Jobs"
      search={false}
    >
      <:col field={:id} header="ID" sortable={:desc} />
      <:col field={:state} sortable={:desc} />
      <:col field={:queue} sortable={:desc} />
      <:col field={:worker} sortable={:desc} />
      <:col :let={job} field={:attempt} header="Attempts" sortable={:desc}>
        <%= job.attempt %>/<%= job.max_attempts %>
      </:col>
      <:col :let={job} field={:inserted_at} sortable={:desc}>
        <%= format_value(job.inserted_at) %>
      </:col>
      <:col :let={job} field={:scheduled_at} sortable={:desc}>
        <%= format_value(job.scheduled_at) %>
      </:col>
    </.live_table>
    <.live_modal
      :if={@job != nil}
      id="modal"
      title="Job"
      return_to={live_dashboard_path(@socket, @page, params: %{})}
    >
      <.label_value_list>
        <:elem label="ID"><%= @job.id %></:elem>
        <:elem label="State"><%= @job.state %></:elem>
        <:elem label="Queue"><%= @job.queue %></:elem>
        <:elem label="Worker"><%= @job.worker %></:elem>
        <:elem label="Args"><%= format_value(@job.args, nil) %></:elem>
        <:elem :if={@job.meta != %{}} label="Meta"><%= format_value(@job.meta, nil) %></:elem>
        <:elem :if={@job.tags != []} label="Tags"><%= format_value(@job.tags, nil) %></:elem>
        <:elem :if={@job.errors != []} label="Errors"><%= format_errors(@job.errors) %></:elem>
        <:elem label="Attempts"><%= @job.attempt %>/<%= @job.max_attempts %></:elem>
        <:elem label="Priority"><%= @job.priority %></:elem>
        <:elem label="Attempted at"><%= format_value(@job.attempted_at) %></:elem>
        <:elem :if={@job.cancelled_at} label="Cancelled at">
          <%= format_value(@job.cancelled_at) %>
        </:elem>
        <:elem :if={@job.completed_at} label="Completed at">
          <%= format_value(@job.completed_at) %>
        </:elem>
        <:elem :if={@job.discarded_at} label="Discarded at">
          <%= format_value(@job.discarded_at) %>
        </:elem>
        <:elem label="Inserted at"><%= format_value(@job.inserted_at) %></:elem>
        <:elem label="Scheduled at"><%= format_value(@job.scheduled_at) %></:elem>
      </.label_value_list>
    </.live_modal>
    """
  end

  @impl true
  def handle_params(%{"params" => %{"job" => job_id}}, _url, socket) do
    case fetch_job(job_id) do
      {:ok, job} ->
        {:noreply, assign(socket, job: job)}

      :error ->
        to = live_dashboard_path(socket, socket.assigns.page, params: %{})
        {:noreply, push_patch(socket, to: to)}
    end
  end

  def handle_params(_params, _url, socket) do
    {:noreply, assign(socket, job: nil)}
  end

  @impl true
  def handle_event("show_job", params, socket) do
    to = live_dashboard_path(socket, socket.assigns.page, params: params)
    {:noreply, push_patch(socket, to: to)}
  end

  defp fetch_jobs(params, _node) do
    total_jobs = Oban.Repo.aggregate(Oban.config(), Oban.Job, :count)
    jobs = Oban.Repo.all(Oban.config(), jobs_query(params)) |> Enum.map(&Map.from_struct/1)
    {jobs, total_jobs}
  end

  defp fetch_job(id) do
    case Oban.Repo.get(Oban.config(), Oban.Job, id) do
      nil ->
        :error

      job ->
        {:ok, job}
    end
  end

  defp jobs_query(%{sort_by: sort_by, sort_dir: sort_dir, limit: l}) do
    Oban.Job
    |> limit(^l)
    |> order_by({^sort_dir, ^sort_by})
  end

  defp row_attrs(job) do
    [
      {"phx-click", "show_job"},
      {"phx-value-job", job[:id]},
      {"phx-page-loading", true}
    ]
  end

  defp format_errors(errors) do
    Enum.map(errors, &Map.get(&1, "error"))
  end

  def format_value(%DateTime{} = datetime) do
    DateTime.to_string(datetime)
  end

  def format_value(nil), do: nil
end