diff --git a/README.md b/README.md index 49fcc5e8..45ca788e 100644 --- a/README.md +++ b/README.md @@ -278,6 +278,14 @@ ActiveJob.jobs.finished.where(job_class_name: "SomeJob") # For adapters that support filtering by worker: # All jobs in progress being run by a given worker ActiveJob.jobs.in_progress.where(worker_id: 42) + +# Using date filters +# You can filter by: enqueued_at, scheduled_at or finished_at +ActiveJob.jobs.pending.where(enqueued_at: 2.days.ago) +ActiveJob.jobs.pending.where(scheduled_at: Date.today) + +date_range = (Time.parse("2024-11-01")..Time.parse("2024-12-01")) +ActiveJob.jobs.finished.where(finished_at: date_range) ``` Some examples of bulk operations: diff --git a/app/assets/stylesheets/mission_control/jobs/jobs.css b/app/assets/stylesheets/mission_control/jobs/jobs.css index 31f13fd1..f48d813e 100644 --- a/app/assets/stylesheets/mission_control/jobs/jobs.css +++ b/app/assets/stylesheets/mission_control/jobs/jobs.css @@ -1,3 +1,7 @@ +.filter { + max-width: 100%; +} + .filter input { width: 15rem; } diff --git a/app/controllers/concerns/mission_control/jobs/job_filters.rb b/app/controllers/concerns/mission_control/jobs/job_filters.rb index f3956138..0d83eb60 100644 --- a/app/controllers/concerns/mission_control/jobs/job_filters.rb +++ b/app/controllers/concerns/mission_control/jobs/job_filters.rb @@ -12,7 +12,9 @@ def set_filters @job_filters = { job_class_name: params.dig(:filter, :job_class_name).presence, queue_name: params.dig(:filter, :queue_name).presence, - finished_at: finished_at_range_params + finished_at: date_range_params(:finished_at), + enqueued_at: date_range_params(:enqueued_at), + scheduled_at: date_range_params(:scheduled_at) }.compact end @@ -20,8 +22,8 @@ def active_filters? @job_filters.any? end - def finished_at_range_params - range_start, range_end = params.dig(:filter, :finished_at_start), params.dig(:filter, :finished_at_end) + def date_range_params(date_type) + range_start, range_end = params.dig(:filter, :"#{date_type}_start"), params.dig(:filter, :"#{date_type}_end") if range_start || range_end (parse_with_time_zone(range_start)..parse_with_time_zone(range_end)) end diff --git a/app/views/mission_control/jobs/jobs/_filters.html.erb b/app/views/mission_control/jobs/jobs/_filters.html.erb index cd6ec5ac..9d405684 100644 --- a/app/views/mission_control/jobs/jobs/_filters.html.erb +++ b/app/views/mission_control/jobs/jobs/_filters.html.erb @@ -1,45 +1,86 @@ -
-
-
- <%= form_for :filter, url: application_jobs_path(MissionControl::Jobs::Current.application, jobs_status), method: :get, - data: { controller: "form", action: "input->form#debouncedSubmit" } do |form| %> +
+ <%= form_for :filter, url: application_jobs_path(MissionControl::Jobs::Current.application, jobs_status), method: :get, + data: { controller: "form", action: "input->form#debouncedSubmit" } do |form| %> +
+
+ <%= form.label :job_class_name, class: "label" %>
<%= form.text_field :job_class_name, value: @job_filters[:job_class_name], class: "input", list: "job-classes", placeholder: "Filter by job class...", autocomplete: "off" %>
+
+
+ <%= form.label :queue_name, class: "label" %>
<%= form.text_field :queue_name, value: @job_filters[:queue_name], class: "input", list: "queue-names", placeholder: "Filter by queue name...", autocomplete: "off" %>
+
- <% if jobs_status == "finished" %> + <% if jobs_status == "finished" %> +
+ <%= form.label :finished_at_start, class: "label" %>
<%= form.datetime_field :finished_at_start, value: @job_filters[:finished_at]&.begin, class: "input", placeholder: "Finished from" %>
+
+
+ <%= form.label :finished_at_end, class: "label" %>
+ <%# TODO: add max date of today? %> <%= form.datetime_field :finished_at_end, value: @job_filters[:finished_at]&.end, class: "input", placeholder: "Finished to" %>
- <% end %> +
+ <% end %> - <%= hidden_field_tag :server_id, MissionControl::Jobs::Current.server.id %> +
+ <%= form.label :enqueued_at_start, class: "label" %> +
+ <%= form.datetime_field :enqueued_at_start, value: @job_filters[:enqueued_at]&.begin, class: "input", placeholder: "Enqueued from" %> +
+
- +
+ <%= form.label :enqueued_at_end, class: "label" %> +
+ <%# TODO: add max date of today? %> + <%= form.datetime_field :enqueued_at_end, value: @job_filters[:enqueued_at]&.end, class: "input", placeholder: "Enqueued to" %> +
+
- - <% end %> -
+
+ <%= form.label :scheduled_at_start, class: "label" %> +
+ <%= form.datetime_field :scheduled_at_start, value: @job_filters[:scheduled_at]&.begin, class: "input", placeholder: "Scheduled from" %> +
+
+ +
+ <%= form.label :scheduled_at_start, class: "label" %> +
+ <%# TODO: add max date of today? %> + <%= form.datetime_field :scheduled_at_end, value: @job_filters[:scheduled_at]&.end, class: "input", placeholder: "Scheduled to" %> +
+
+ + <%= hidden_field_tag :server_id, MissionControl::Jobs::Current.server.id %> + + + + -
- <%= link_to "Clear", application_jobs_path(MissionControl::Jobs::Current.application, jobs_status, job_class_name: nil, queue_name: nil, finished_at: nil..nil), class: "button" %> +
+ <%= link_to "Clear", application_jobs_path(MissionControl::Jobs::Current.application, jobs_status, job_class_name: nil, queue_name: nil, finished_at: nil..nil), class: "button" %> +
-
+ <% end %>
diff --git a/app/views/mission_control/jobs/jobs/pending/_job.html.erb b/app/views/mission_control/jobs/jobs/pending/_job.html.erb new file mode 100644 index 00000000..06c4efed --- /dev/null +++ b/app/views/mission_control/jobs/jobs/pending/_job.html.erb @@ -0,0 +1,2 @@ +<%= link_to job.queue_name, application_queue_path(@application, job.queue) %> +
Ready to run
\ No newline at end of file diff --git a/lib/active_job/jobs_relation.rb b/lib/active_job/jobs_relation.rb index 0d71e5e4..246d15a7 100644 --- a/lib/active_job/jobs_relation.rb +++ b/lib/active_job/jobs_relation.rb @@ -23,9 +23,9 @@ class ActiveJob::JobsRelation include Enumerable STATUSES = %i[ pending failed in_progress blocked scheduled finished ] - FILTERS = %i[ queue_name job_class_name ] + FILTERS = %i[ queue_name job_class_name finished_at scheduled_at enqueued_at ] - PROPERTIES = %i[ queue_name status offset_value limit_value job_class_name worker_id recurring_task_id finished_at ] + PROPERTIES = %i[ queue_name status offset_value limit_value job_class_name worker_id recurring_task_id finished_at scheduled_at enqueued_at ] attr_reader *PROPERTIES, :default_page_size delegate :last, :[], :reverse, to: :to_a @@ -52,15 +52,21 @@ def initialize(queue_adapter: ActiveJob::Base.queue_adapter, default_page_size: # * :worker_id - To only include the jobs processed by the provided worker. # * :recurring_task_id - To only include the jobs corresponding to runs of a recurring task. # * :finished_at - (Range) To only include the jobs finished between the provided range - def where(job_class_name: nil, queue_name: nil, worker_id: nil, recurring_task_id: nil, finished_at: nil) + # * :scheduled_at - (Range) To only include the jobs scheduled between the provided range + # * :enqueued_at - (Range) To only include the jobs enqueued between the provided range + def where(job_class_name: nil, queue_name: nil, worker_id: nil, recurring_task_id: nil, finished_at: nil, scheduled_at: nil, enqueued_at: nil) # Remove nil arguments to avoid overriding parameters when concatenating +where+ clauses arguments = { job_class_name: job_class_name, queue_name: queue_name&.to_s, worker_id: worker_id, recurring_task_id: recurring_task_id, - finished_at: finished_at + finished_at: finished_at, + scheduled_at: scheduled_at, + enqueued_at: enqueued_at }.compact + # TODO: is this collect needed? .collect { |key, value| [ key, value.to_s ] }.to_h + clone_with **arguments end @@ -268,11 +274,22 @@ def loaded? # Filtering for not natively supported filters is performed in memory def filter(jobs) - jobs.filter { |job| satisfy_filter?(job) } + jobs.filter { |job| satisfies_filters?(job) } + end + + def satisfies_filter?(filter_value, job_value) + return filter_value.cover?(job_value) if filter_value.is_a?(Range) # TODO: needed? && job_value.is_a?(ActiveSupport::TimeWithZone) + + filter_value == job_value end - def satisfy_filter?(job) - filters.all? { |property| public_send(property) == job.public_send(property) } + def satisfies_filters?(job) + filters.all? do |property| + filter_value = public_send(property) + job_value = job.public_send(property) + + satisfies_filter?(filter_value, job_value) + end end def filters diff --git a/test/controllers/jobs_controller_test.rb b/test/controllers/jobs_controller_test.rb index 54a36120..fedebb27 100644 --- a/test/controllers/jobs_controller_test.rb +++ b/test/controllers/jobs_controller_test.rb @@ -63,6 +63,48 @@ class MissionControl::Jobs::JobsControllerTest < ActionDispatch::IntegrationTest assert_select "tr.job", 1 end + test "get pending jobs filtered by enqueued_at date" do + job = DummyJob.perform_later(42) + + get mission_control_jobs.application_jobs_url(@application, :pending) + assert_response :ok + assert_select "tr.job", 1 + + get mission_control_jobs.application_jobs_url(@application, :pending, filter: { enqueued_at_start: 1.hour.from_now.to_s }) + assert_response :ok + assert_select "tr.job", 0 + + get mission_control_jobs.application_jobs_url(@application, :pending, filter: { enqueued_at_start: 1.hour.ago.to_s, enqueued_at_end: 1.hour.from_now }) + assert_response :ok + assert_select "tr.job", 1 + + get mission_control_jobs.application_jobs_url(@application, :pending, filter: { enqueued_at_end: 1.hour.from_now }) + assert_response :ok + assert_select "tr.job", 1 + end + + test "get scheduled jobs filtered by scheduled_at date" do + job = DummyJob.set(wait: 30.minutes).perform_later(42) + + get mission_control_jobs.application_jobs_url(@application, :scheduled) + assert_response :ok + assert_select "tr.job", 1 + + get mission_control_jobs.application_jobs_url(@application, :scheduled, filter: { scheduled_at_start: 1.hour.from_now.to_s }) + assert_response :ok + assert_select "tr.job", 0 + + get mission_control_jobs.application_jobs_url(@application, :scheduled, filter: { scheduled_at_start: 15.minutes.from_now.to_s, scheduled_at_end: 45.minutes.from_now }) + assert_response :ok + assert_select "tr.job", 1 + + get mission_control_jobs.application_jobs_url(@application, :scheduled, filter: { scheduled_at_end: 45.minutes.from_now }) + assert_response :ok + assert_select "tr.job", 1 + end + + # TODO: hitting clear on the date input should refresh the list? + test "redirect to queue when job doesn't exist" do job = DummyJob.perform_later(42)