diff --git a/app/controllers/mission_control/jobs/application_controller.rb b/app/controllers/mission_control/jobs/application_controller.rb index d1c56c09..6f5d1af2 100644 --- a/app/controllers/mission_control/jobs/application_controller.rb +++ b/app/controllers/mission_control/jobs/application_controller.rb @@ -13,8 +13,19 @@ class MissionControl::Jobs::ApplicationController < MissionControl::Jobs.base_co include MissionControl::Jobs::ApplicationScoped, MissionControl::Jobs::NotFoundRedirections include MissionControl::Jobs::AdapterFeatures + around_action :set_current_locale + private def default_url_options { server_id: MissionControl::Jobs::Current.server } end + + def set_current_locale(&block) + @previous_config = I18n.config + I18n.config = MissionControl::Jobs::I18nConfig.new + I18n.with_locale(:en, &block) + ensure + I18n.config = @previous_config + @previous_config = nil + end end diff --git a/app/helpers/mission_control/jobs/dates_helper.rb b/app/helpers/mission_control/jobs/dates_helper.rb index f559ea59..ad8a63ad 100644 --- a/app/helpers/mission_control/jobs/dates_helper.rb +++ b/app/helpers/mission_control/jobs/dates_helper.rb @@ -1,5 +1,19 @@ module MissionControl::Jobs::DatesHelper - def formatted_time(time) - time.in_time_zone.strftime("%Y-%m-%d %H:%M:%S.%3N %Z") + def time_distance_in_words_with_title(time) + tag.span time_ago_in_words_with_default_options(time), title: "Since #{time.to_fs(:long)}" + end + + def bidirectional_time_distance_in_words_with_title(time) + time_distance = if time.past? + "#{time_ago_in_words_with_default_options(time)} ago" + else + "in #{time_ago_in_words_with_default_options(time)}" + end + + tag.span time_distance, title: time.to_fs(:long) + end + + def time_ago_in_words_with_default_options(time) + time_ago_in_words(time, include_seconds: true, locale: :en) end end diff --git a/app/helpers/mission_control/jobs/jobs_helper.rb b/app/helpers/mission_control/jobs/jobs_helper.rb index 146504bc..f55f02d7 100644 --- a/app/helpers/mission_control/jobs/jobs_helper.rb +++ b/app/helpers/mission_control/jobs/jobs_helper.rb @@ -29,7 +29,7 @@ def attribute_names_for_job_status(status) when "blocked" then [ "Queue", "Blocked by", "" ] when "finished" then [ "Queue", "Finished" ] when "scheduled" then [ "Queue", "Scheduled", "" ] - when "in_progress" then [ "Queue", "Run by", "Running since" ] + when "in_progress" then [ "Queue", "Run by", "Running for" ] else [] end end diff --git a/app/views/mission_control/jobs/jobs/_general_information.html.erb b/app/views/mission_control/jobs/jobs/_general_information.html.erb index 0b15d7f5..e962e763 100644 --- a/app/views/mission_control/jobs/jobs/_general_information.html.erb +++ b/app/views/mission_control/jobs/jobs/_general_information.html.erb @@ -23,14 +23,14 @@ Enqueued - <%= formatted_time(job.enqueued_at.to_datetime) %> + <%= time_distance_in_words_with_title(job.enqueued_at.to_datetime) %> ago <% if job.scheduled? %> Scheduled - <%= formatted_time(job.scheduled_at) %> + <%= bidirectional_time_distance_in_words_with_title(job.scheduled_at) %> <% if job_delayed?(job) %>
delayed
<% end %> @@ -41,7 +41,7 @@ Failed - <%= formatted_time(job.failed_at) %> + <%= time_distance_in_words_with_title(job.failed_at) %> ago <% end %> @@ -49,7 +49,7 @@ Finished at - <%= formatted_time(job.finished_at) %> + <%= time_distance_in_words_with_title(job.finished_at) %> ago <% end %> diff --git a/app/views/mission_control/jobs/jobs/_job.html.erb b/app/views/mission_control/jobs/jobs/_job.html.erb index bdf8ba54..716dfdbc 100644 --- a/app/views/mission_control/jobs/jobs/_job.html.erb +++ b/app/views/mission_control/jobs/jobs/_job.html.erb @@ -6,7 +6,7 @@
<%= job_arguments(job) %>
<% end %> -
Enqueued <%= formatted_time(job.enqueued_at.to_datetime) %>
+
Enqueued <%= time_distance_in_words_with_title(job.enqueued_at.to_datetime) %> ago
<%= render "mission_control/jobs/jobs/#{jobs_status}/job", job: job %> diff --git a/app/views/mission_control/jobs/jobs/blocked/_job.html.erb b/app/views/mission_control/jobs/jobs/blocked/_job.html.erb index 64f00af4..4521ea91 100644 --- a/app/views/mission_control/jobs/jobs/blocked/_job.html.erb +++ b/app/views/mission_control/jobs/jobs/blocked/_job.html.erb @@ -1,6 +1,6 @@ <%= link_to job.queue_name, application_queue_path(@application, job.queue) %>
<%= job.blocked_by %>
-
Until <%= formatted_time(job.blocked_until) %>
+
Expires <%= bidirectional_time_distance_in_words_with_title(job.blocked_until) %>
<%= render "mission_control/jobs/jobs/blocked/actions", job: job %> diff --git a/app/views/mission_control/jobs/jobs/failed/_job.html.erb b/app/views/mission_control/jobs/jobs/failed/_job.html.erb index 5291336c..22bcd146 100644 --- a/app/views/mission_control/jobs/jobs/failed/_job.html.erb +++ b/app/views/mission_control/jobs/jobs/failed/_job.html.erb @@ -1,6 +1,6 @@ <%= link_to failed_job_error(job), application_job_path(@application, job.job_id, anchor: "error") %> -
<%= formatted_time(job.failed_at) %>
+
<%= time_distance_in_words_with_title(job.failed_at) %> ago
<%= render "mission_control/jobs/jobs/failed/actions", job: job %> diff --git a/app/views/mission_control/jobs/jobs/finished/_job.html.erb b/app/views/mission_control/jobs/jobs/finished/_job.html.erb index b0e9f201..5488bed7 100644 --- a/app/views/mission_control/jobs/jobs/finished/_job.html.erb +++ b/app/views/mission_control/jobs/jobs/finished/_job.html.erb @@ -1,2 +1,2 @@ <%= link_to job.queue_name, application_queue_path(@application, job.queue) %> -
<%= formatted_time(job.finished_at) %>
+
<%= time_distance_in_words_with_title(job.finished_at) %> ago
diff --git a/app/views/mission_control/jobs/jobs/in_progress/_job.html.erb b/app/views/mission_control/jobs/jobs/in_progress/_job.html.erb index f428b28f..48491725 100644 --- a/app/views/mission_control/jobs/jobs/in_progress/_job.html.erb +++ b/app/views/mission_control/jobs/jobs/in_progress/_job.html.erb @@ -6,4 +6,4 @@ — <% end %> -
<%= job.started_at ? formatted_time(job.started_at) : "(Finished)" %>
+
<%= job.started_at ? time_distance_in_words_with_title(job.started_at) : "(Finished)" %>
diff --git a/app/views/mission_control/jobs/jobs/scheduled/_job.html.erb b/app/views/mission_control/jobs/jobs/scheduled/_job.html.erb index b337a04e..78b5a366 100644 --- a/app/views/mission_control/jobs/jobs/scheduled/_job.html.erb +++ b/app/views/mission_control/jobs/jobs/scheduled/_job.html.erb @@ -1,6 +1,6 @@ <%= link_to job.queue_name, application_queue_path(@application, job.queue) %> - <%= formatted_time(job.scheduled_at) %> + <%= bidirectional_time_distance_in_words_with_title(job.scheduled_at) %> <% if job_delayed?(job) %>
delayed
<% end %> diff --git a/app/views/mission_control/jobs/queues/_job.html.erb b/app/views/mission_control/jobs/queues/_job.html.erb index 34c0ae8e..5635abec 100644 --- a/app/views/mission_control/jobs/queues/_job.html.erb +++ b/app/views/mission_control/jobs/queues/_job.html.erb @@ -3,7 +3,7 @@ <%= link_to application_job_path(@application, job.job_id, filter: { queue_name: job.queue }) do %> <%= job_title(job) %> <% end %> -
Enqueued on <%= formatted_time(job.enqueued_at.to_datetime) %>
+
Enqueued <%= time_distance_in_words_with_title(job.enqueued_at.to_datetime) %> ago
<% if job.serialized_arguments.present? %> diff --git a/app/views/mission_control/jobs/recurring_tasks/_recurring_task.html.erb b/app/views/mission_control/jobs/recurring_tasks/_recurring_task.html.erb index f3c40e60..3a40f396 100644 --- a/app/views/mission_control/jobs/recurring_tasks/_recurring_task.html.erb +++ b/app/views/mission_control/jobs/recurring_tasks/_recurring_task.html.erb @@ -14,8 +14,8 @@ <% end %> <%= recurring_task.schedule %> -
<%= recurring_task.last_enqueued_at ? formatted_time(recurring_task.last_enqueued_at) : "Never" %>
-
<%= formatted_time(recurring_task.next_time) %>
+
<%= recurring_task.last_enqueued_at ? bidirectional_time_distance_in_words_with_title(recurring_task.last_enqueued_at) : "Never" %>
+
<%= bidirectional_time_distance_in_words_with_title(recurring_task.next_time) %>
<%= render "mission_control/jobs/recurring_tasks/actions", recurring_task: recurring_task %> diff --git a/app/views/mission_control/jobs/shared/_job.html.erb b/app/views/mission_control/jobs/shared/_job.html.erb index 52075dba..c42a3851 100644 --- a/app/views/mission_control/jobs/shared/_job.html.erb +++ b/app/views/mission_control/jobs/shared/_job.html.erb @@ -3,7 +3,7 @@ <%= link_to application_job_path(@application, job.job_id, filter: { queue_name: job.queue }) do %> <%= job_title(job) %> <% end %> -
Enqueued on <%= formatted_time(job.enqueued_at.to_datetime) %>
+
Enqueued <%= time_distance_in_words_with_title(job.enqueued_at.to_datetime) %> ago
<% if job.serialized_arguments.present? %> @@ -16,9 +16,9 @@
<% if job.started_at %> - Running since <%= formatted_time(job.started_at) %> + Running for <%= time_distance_in_words_with_title(job.started_at) %> <% elsif job.finished_at %> - Finished on <%= formatted_time(job.finished_at) %> + Finished <%= time_distance_in_words_with_title(job.finished_at) %> ago <% else %> Pending <% end %> diff --git a/app/views/mission_control/jobs/workers/_worker.html.erb b/app/views/mission_control/jobs/workers/_worker.html.erb index a10a3d07..3d1aee48 100644 --- a/app/views/mission_control/jobs/workers/_worker.html.erb +++ b/app/views/mission_control/jobs/workers/_worker.html.erb @@ -17,5 +17,5 @@ <% end %> -
<%= formatted_time(worker.last_heartbeat_at) %>
+
<%= time_distance_in_words_with_title(worker.last_heartbeat_at) %> ago
diff --git a/lib/mission_control/jobs/i18n_config.rb b/lib/mission_control/jobs/i18n_config.rb new file mode 100644 index 00000000..442d61f6 --- /dev/null +++ b/lib/mission_control/jobs/i18n_config.rb @@ -0,0 +1,5 @@ +class MissionControl::Jobs::I18nConfig < ::I18n::Config + def available_locales + [ :en ] + end +end diff --git a/test/controllers/jobs_controller_test.rb b/test/controllers/jobs_controller_test.rb index 4d79fc1d..54a36120 100644 --- a/test/controllers/jobs_controller_test.rb +++ b/test/controllers/jobs_controller_test.rb @@ -24,7 +24,6 @@ class MissionControl::Jobs::JobsControllerTest < ActionDispatch::IntegrationTest end test "get jobs and job details when there are multiple instances of the same job due to automatic retries" do - time = Time.now job = AutoRetryingJob.perform_later perform_enqueued_jobs_async @@ -33,7 +32,7 @@ class MissionControl::Jobs::JobsControllerTest < ActionDispatch::IntegrationTest assert_response :ok assert_select "tr.job", 2 - assert_select "tr.job", /AutoRetryingJob\s+Enqueued #{time_pattern(time)}\s+default/ + assert_select "tr.job", /AutoRetryingJob\s+Enqueued less than 5 seconds ago\s+default/ get mission_control_jobs.application_job_url(@application, job.job_id) assert_response :ok @@ -72,16 +71,17 @@ class MissionControl::Jobs::JobsControllerTest < ActionDispatch::IntegrationTest end test "get scheduled jobs" do - time = Time.now DummyJob.set(wait: 3.minutes).perform_later DummyJob.set(wait: 1.minute).perform_later + travel_to 2.minutes.from_now + get mission_control_jobs.application_jobs_url(@application, :scheduled) assert_response :ok assert_select "tr.job", 2 - assert_select "tr.job", /DummyJob\s+Enqueued #{time_pattern(time)}\s+queue_1\s+#{time_pattern(time + 3.minute)}/ - assert_select "tr.job", /DummyJob\s+Enqueued #{time_pattern(time)}\s+queue_1\s+#{time_pattern(time + 1.minute)}/ + assert_select "tr.job", /DummyJob\s+Enqueued 2 minutes ago\s+queue_1\s+in 1 minute/ + assert_select "tr.job", /DummyJob\s+Enqueued 2 minutes ago\s+queue_1\s+(1 minute ago|less than a minute ago)/ assert_select "tr.job", /Discard/ end @@ -104,8 +104,31 @@ class MissionControl::Jobs::JobsControllerTest < ActionDispatch::IntegrationTest end end - private - def time_pattern(time) - /#{time.utc.strftime("%Y-%m-%d %H:%M")}:\d{2}\.\d{3} UTC/ + test "get jobs and job details when the default locale is set to another language than English" do + previous_locales, I18n.available_locales = I18n.available_locales, %i[ en nl ] + + DummyJob.set(wait: 3.minutes).perform_later + + I18n.with_locale(:nl) do + get mission_control_jobs.application_jobs_url(@application, :scheduled) + assert_response :ok + + assert_select "tr.job", /DummyJob\s+Enqueued less than 5 seconds ago\s+queue_1\s+in 3 minutes/ end + ensure + I18n.available_locales = previous_locales + end + + test "get jobs and job details when English is not included among the locales" do + previous_locales, I18n.available_locales = I18n.available_locales, %i[ es nl ] + + DummyJob.set(wait: 3.minutes).perform_later + + get mission_control_jobs.application_jobs_url(@application, :scheduled) + assert_response :ok + + assert_select "tr.job", /DummyJob\s+Enqueued less than 5 seconds ago\s+queue_1\s+in 3 minutes/ + ensure + I18n.available_locales = previous_locales + end end diff --git a/test/controllers/recurring_tasks_controller_test.rb b/test/controllers/recurring_tasks_controller_test.rb index 77f2dcbb..21102b75 100644 --- a/test/controllers/recurring_tasks_controller_test.rb +++ b/test/controllers/recurring_tasks_controller_test.rb @@ -13,33 +13,27 @@ class MissionControl::Jobs::RecurringTasksControllerTest < ActionDispatch::Integ end test "get recurring task list" do - travel_to Time.parse("2024-10-30 19:07:10 UTC") do - schedule_recurring_tasks_async(wait: 2.seconds) do - get mission_control_jobs.application_recurring_tasks_url(@application) - assert_response :ok - - assert_select "tr.recurring_task", 1 - assert_select "td a", "periodic_pause_job" - assert_select "td", "PauseJob" - assert_select "td", "every second" - assert_select "td", /2024-10-30 19:07:1\d\.\d{3}/ - assert_select "button", "Run now" - end + schedule_recurring_tasks_async(wait: 2.seconds) do + get mission_control_jobs.application_recurring_tasks_url(@application) + assert_response :ok + + assert_select "tr.recurring_task", 1 + assert_select "td a", "periodic_pause_job" + assert_select "td", "PauseJob" + assert_select "td", "every second" + assert_select "td", /less than \d+ seconds ago/ end end test "get recurring task details and job list" do - travel_to Time.parse("2024-10-30 19:07:10 UTC") do - schedule_recurring_tasks_async(wait: 1.seconds) do - get mission_control_jobs.application_recurring_task_url(@application, "periodic_pause_job") - assert_response :ok - - assert_select "h1", /periodic_pause_job/ - assert_select "h2", "1 job" - assert_select "tr.job", 1 - assert_select "td a", "PauseJob" - assert_select "td", /2024-10-30 19:07:1\d\.\d{3}/ - end + schedule_recurring_tasks_async(wait: 1.seconds) do + get mission_control_jobs.application_recurring_task_url(@application, "periodic_pause_job") + assert_response :ok + assert_select "h1", /periodic_pause_job/ + assert_select "h2", "1 job" + assert_select "tr.job", 1 + assert_select "td a", "PauseJob" + assert_select "td", /less than \d+ seconds ago/ end end @@ -53,43 +47,4 @@ class MissionControl::Jobs::RecurringTasksControllerTest < ActionDispatch::Integ assert_select "article.is-danger", /Recurring task with id 'invalid_key' not found/ end end - - test "get recurring task with undefined class" do - # simulate recurring task inserted from another app, no validations or callbacks - SolidQueue::RecurringTask.insert({ key: "missing_class_task", class_name: "MissingJob", schedule: "every minute" }) - get mission_control_jobs.application_recurring_tasks_url(@application) - assert_response :ok - - assert_select "tr.recurring_task", 1 - assert_select "td a", "missing_class_task" - assert_select "td", "MissingJob" - assert_select "td", "every minute" - assert_select "button", text: "Run now", count: 0 # Can't be run because the class doesn't exist - end - - test "enqueue recurring task successfully" do - schedule_recurring_tasks_async(wait: 0.1.seconds) - - assert_difference -> { ActiveJob.jobs.pending.count } do - put mission_control_jobs.application_recurring_task_url(@application, "periodic_pause_job") - assert_response :redirect - end - - job = ActiveJob.jobs.pending.last - assert_equal "PauseJob", job.job_class_name - assert_match /jobs\/#{job.job_id}\?server_id=solid_queue\z/, response.location - end - - test "fail to enqueue recurring task with undefined class" do - # simulate recurring task inserted from another app, no validations or callbacks - SolidQueue::RecurringTask.insert({ key: "missing_class_task", class_name: "MissingJob", schedule: "every minute" }) - - assert_no_difference -> { ActiveJob.jobs.pending.count } do - put mission_control_jobs.application_recurring_task_url(@application, "missing_class_task") - assert_response :redirect - - follow_redirect! - assert_select "article.is-danger", /This task can.t be enqueued/ - end - end end diff --git a/test/controllers/retries_controller_test.rb b/test/controllers/retries_controller_test.rb index c7f24a21..ea02f662 100644 --- a/test/controllers/retries_controller_test.rb +++ b/test/controllers/retries_controller_test.rb @@ -10,23 +10,21 @@ class MissionControl::Jobs::JobsControllerTest < ActionDispatch::IntegrationTest end test "retry jobs when there are multiple instances of the same job due to automatic retries" do - travel_to Time.parse("2024-10-30 19:07:10 UTC") do - job = AutoRetryingJob.perform_later + job = AutoRetryingJob.perform_later - perform_enqueued_jobs_async + perform_enqueued_jobs_async - get mission_control_jobs.application_jobs_url(@application, :failed) - assert_response :ok + get mission_control_jobs.application_jobs_url(@application, :failed) + assert_response :ok - assert_select "tr.job", 1 - assert_select "tr.job", /AutoRetryingJob\s+Enqueued 2024-10-30 19:07:1\d\.\d{3} UTC\s+AutoRetryingJob::RandomError/ + assert_select "tr.job", 1 + assert_select "tr.job", /AutoRetryingJob\s+Enqueued less than 5 seconds ago\s+AutoRetryingJob::RandomError/ - post mission_control_jobs.application_job_retry_url(@application, job.job_id) - assert_redirected_to mission_control_jobs.application_jobs_url(@application, :failed) - follow_redirect! + post mission_control_jobs.application_job_retry_url(@application, job.job_id) + assert_redirected_to mission_control_jobs.application_jobs_url(@application, :failed) + follow_redirect! - assert_select "article.is-danger", text: /Job with id '#{job.job_id}' not found/, count: 0 - assert_select "tr.job", 0 - end + assert_select "article.is-danger", text: /Job with id '#{job.job_id}' not found/, count: 0 + assert_select "tr.job", 0 end end