Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

buttons to retry, cancel, delete, etc #5

Merged
merged 1 commit into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 56 additions & 20 deletions lib/obanalyze/dashboard.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule Obanalyze.Dashboard do
use Phoenix.LiveDashboard.PageBuilder, refresher?: true

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

alias Obanalyze.ObanJobs
alias Obanalyze.NavItem
Expand Down Expand Up @@ -43,24 +44,40 @@ defmodule Obanalyze.Dashboard do
</.live_nav_bar>

<.live_modal :if={@job != nil} id="job-modal" title={"Job - #{@job.id}"} 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>
<div class="mb-4 btn-toolbar" role="toolbar" aria-label="Oban Job actions">
<div :if={can_cancel_job?(@job)} class="btn-group" role="group">
<button type="button" class="btn btn-primary btn-sm mr-2" phx-click="cancel_job" phx-value-job={@job.id} data-disable-with="Cancelling...">Cancel</button>
</div>
<div :if={can_run_job?(@job)} class="btn-group" role="group">
<button type="button" class="btn btn-primary btn-sm mr-2" phx-click="retry_job" phx-value-job={@job.id} data-disable-with="Running...">Run now</button>
</div>
<div :if={can_retry_job?(@job)} class="btn-group" role="group">
<button type="button" class="btn btn-primary btn-sm mr-2" phx-click="retry_job" phx-value-job={@job.id} data-disable-with="Retrying...">Retry</button>
</div>
<div :if={can_delete_job?(@job)} class="btn-group" role="group">
<button type="button" class="btn btn-primary btn-sm mr-2" phx-click="delete_job" phx-value-job={@job.id} data-disable-with="Deleting..." data-confirm="Are you sure you want to delete this job?">Delete</button>
</div>
</div>
<div class="tabular-info">
<.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>
</div>
</.live_modal>
"""
end
Expand Down Expand Up @@ -92,6 +109,25 @@ defmodule Obanalyze.Dashboard do
{:noreply, push_patch(socket, to: to)}
end

def handle_event("cancel_job", %{"job" => job_id}, socket) do
with {:ok, job} <- ObanJobs.cancel_oban_job(job_id) do
{:noreply, assign(socket, job: job)}
end
end

def handle_event("retry_job", %{"job" => job_id}, socket) do
with {:ok, job} <- ObanJobs.retry_oban_job(job_id) do
{:noreply, assign(socket, job: job)}
end
end

def handle_event("delete_job", %{"job" => job_id}, socket) do
with :ok <- ObanJobs.delete_oban_job(job_id) do
to = live_dashboard_path(socket, socket.assigns.page, params: %{})
{:noreply, push_patch(socket, to: to)}
end
end

@impl true
def handle_refresh(socket) do
socket =
Expand All @@ -107,8 +143,8 @@ defmodule Obanalyze.Dashboard do

defp assign_job(socket, job_id) do
if job_id do
case ObanJobs.get_oban_job(job_id) do
%Oban.Job{} = job ->
case ObanJobs.fetch_oban_job(job_id) do
{:ok, job} ->
assign(socket, job: job)

_ ->
Expand Down
19 changes: 19 additions & 0 deletions lib/obanalyze/helpers.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule Obanalyze.Helpers do
alias Oban.Job

def can_cancel_job?(%Job{} = job) do
job.state in ["available", "executing", "inserted", "retryable", "scheduled"]
end

def can_delete_job?(%Job{} = job) do
job.state not in ["executing"]
end

def can_retry_job?(%Job{} = job) do
job.state in ["cancelled", "completed", "discarded", "retryable"]
end

def can_run_job?(%Job{} = job) do
job.state in ["inserted", "scheduled"]
end
end
36 changes: 33 additions & 3 deletions lib/obanalyze/oban_jobs.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,38 @@
defmodule Obanalyze.ObanJobs do
import Ecto.Query, only: [group_by: 3, order_by: 2, order_by: 3, select: 3, limit: 2, where: 3]
import Ecto.Query,
only: [from: 2, group_by: 3, order_by: 2, order_by: 3, select: 3, limit: 2, where: 3]

def get_oban_job(id) do
Oban.Repo.get(Oban.config(), Oban.Job, id)
def get_oban_job(job_id) do
Oban.Repo.get(Oban.config(), Oban.Job, job_id)
end

def fetch_oban_job(job_id) do
case get_oban_job(job_id) do
%Oban.Job{} = job -> {:ok, job}
_ -> {:error, :not_found}
end
end

def delete_oban_job(job_id) do
query = from(oj in Oban.Job, where: oj.id in [^job_id])
Oban.Repo.delete_all(Oban.config(), query)
:ok
end

def retry_oban_job(job_id) do
with {:ok, job} <- fetch_oban_job(job_id),
:ok <- Oban.Engine.retry_job(Oban.config(), job),
{:ok, job} <- fetch_oban_job(job_id) do
{:ok, job}
end
end

def cancel_oban_job(job_id) do
with {:ok, job} <- fetch_oban_job(job_id),
:ok <- Oban.Engine.cancel_job(Oban.config(), job),
{:ok, job} <- fetch_oban_job(job_id) do
{:ok, job}
end
end

def list_jobs_with_total(params, job_state) do
Expand Down
36 changes: 36 additions & 0 deletions test/obanalyze/dashboard_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,42 @@ defmodule Obanalyze.DashboardTest do
|> length() == 0
end

test "run now job" do
job = job_fixture(%{foo: "bar"}, schedule_in: 1000)
{:ok, live, _rendered} = live(build_conn(), "/dashboard/obanalyze?params[job]=#{job.id}")

assert has_element?(live, "pre", "scheduled")
element(live, "button", "Run now") |> render_click()
assert has_element?(live, "pre", "available")
end

test "retry job" do
job = job_fixture(%{foo: "bar"}, state: "cancelled")
{:ok, live, _rendered} = live(build_conn(), "/dashboard/obanalyze?params[job]=#{job.id}")

assert has_element?(live, "pre", "cancelled")
element(live, "button", "Retry") |> render_click()
assert has_element?(live, "pre", "available")
end

test "cancel job" do
job = job_fixture(%{foo: "bar"}, schedule_in: 1000)
{:ok, live, _rendered} = live(build_conn(), "/dashboard/obanalyze?params[job]=#{job.id}")

assert has_element?(live, "pre", "scheduled")
element(live, "button", "Cancel") |> render_click()
assert has_element?(live, "pre", "cancelled")
end

test "delete job" do
job = job_fixture(%{foo: "bar"}, schedule_in: 1000)
{:ok, live, _rendered} = live(build_conn(), "/dashboard/obanalyze?params[job]=#{job.id}")

assert has_element?(live, "pre", "scheduled")
element(live, "button", "Delete") |> render_click()
assert_patched(live, "/dashboard/obanalyze?")
end

defp job_fixture(args, opts) do
opts = Keyword.put_new(opts, :worker, "FakeWorker")
{:ok, job} = Oban.Job.new(args, opts) |> Oban.insert()
Expand Down
Loading